Skip to content

Commit a81dbe0

Browse files
OrbisK43081jserhalpdanielroe
authored
feat: unifying npm registry requests with caching (#641)
Co-authored-by: James Garbutt <43081j@users.noreply.github.com> Co-authored-by: Philippe Serhal <philippe.serhal@gmail.com> Co-authored-by: Daniel Roe <daniel@roe.dev>
1 parent 7af8c65 commit a81dbe0

File tree

15 files changed

+78
-59
lines changed

15 files changed

+78
-59
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,7 @@ test-results/
4040
# generated files
4141
shared/types/lexicons
4242

43+
**/__screenshots__/**
44+
4345
# output
4446
.vercel

app/composables/npm/useNpmSearch.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type {
55
NpmDownloadCount,
66
MinimalPackument,
77
} from '#shared/types'
8-
import { NPM_REGISTRY, NPM_API } from '~/utils/npm/common'
98

109
/**
1110
* Convert packument to search result format for display
@@ -55,7 +54,8 @@ export function useNpmSearch(
5554
query: MaybeRefOrGetter<string>,
5655
options: MaybeRefOrGetter<NpmSearchOptions> = {},
5756
) {
58-
const cachedFetch = useCachedFetch()
57+
const { $npmRegistry } = useNuxtApp()
58+
5959
// Client-side cache
6060
const cache = shallowRef<{
6161
query: string
@@ -70,7 +70,7 @@ export function useNpmSearch(
7070

7171
const asyncData = useLazyAsyncData(
7272
() => `search:incremental:${toValue(query)}`,
73-
async (_nuxtApp, { signal }) => {
73+
async ({ $npmRegistry, $npmApi }, { signal }) => {
7474
const q = toValue(query)
7575

7676
if (!q.trim()) {
@@ -91,8 +91,8 @@ export function useNpmSearch(
9191
if (q.length === 1) {
9292
const encodedName = encodePackageName(q)
9393
const [{ data: pkg, isStale }, { data: downloads }] = await Promise.all([
94-
cachedFetch<Packument>(`${NPM_REGISTRY}/${encodedName}`, { signal }),
95-
cachedFetch<NpmDownloadCount>(`${NPM_API}/downloads/point/last-week/${encodedName}`, {
94+
$npmRegistry<Packument>(`/${encodedName}`, { signal }),
95+
$npmApi<NpmDownloadCount>(`/downloads/point/last-week/${encodedName}`, {
9696
signal,
9797
}),
9898
])
@@ -122,8 +122,8 @@ export function useNpmSearch(
122122
}
123123
}
124124

125-
const { data: response, isStale } = await cachedFetch<NpmSearchResponse>(
126-
`${NPM_REGISTRY}/-/v1/search?${params.toString()}`,
125+
const { data: response, isStale } = await $npmRegistry<NpmSearchResponse>(
126+
`/-/v1/search?${params.toString()}`,
127127
{ signal },
128128
60,
129129
)
@@ -179,8 +179,8 @@ export function useNpmSearch(
179179
params.set('size', String(size))
180180
params.set('from', String(from))
181181

182-
const { data: response } = await cachedFetch<NpmSearchResponse>(
183-
`${NPM_REGISTRY}/-/v1/search?${params.toString()}`,
182+
const { data: response } = await $npmRegistry<NpmSearchResponse>(
183+
`/-/v1/search?${params.toString()}`,
184184
{},
185185
60,
186186
)

app/composables/npm/useOrgPackages.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import type { NuxtApp } from '#app'
12
import type { NpmSearchResponse, NpmSearchResult, MinimalPackument } from '#shared/types'
23
import { emptySearchResponse, packumentToSearchResult } from './useNpmSearch'
3-
import { NPM_REGISTRY, NPM_API } from '~/utils/npm/common'
44
import { mapWithConcurrency } from '#shared/utils/async'
55

66
/**
@@ -10,6 +10,7 @@ import { mapWithConcurrency } from '#shared/utils/async'
1010
* Note: npm bulk downloads API does not support scoped packages.
1111
*/
1212
async function fetchBulkDownloads(
13+
$npmApi: NuxtApp['$npmApi'],
1314
packageNames: string[],
1415
options: Parameters<typeof $fetch>[1] = {},
1516
): Promise<Map<string, number>> {
@@ -28,11 +29,11 @@ async function fetchBulkDownloads(
2829
bulkPromises.push(
2930
(async () => {
3031
try {
31-
const response = await $fetch<Record<string, { downloads: number } | null>>(
32-
`${NPM_API}/downloads/point/last-week/${chunk.join(',')}`,
32+
const response = await $npmApi<Record<string, { downloads: number } | null>>(
33+
`/downloads/point/last-week/${chunk.join(',')}`,
3334
options,
3435
)
35-
for (const [name, data] of Object.entries(response)) {
36+
for (const [name, data] of Object.entries(response.data)) {
3637
if (data?.downloads !== undefined) {
3738
downloads.set(name, data.downloads)
3839
}
@@ -54,8 +55,8 @@ async function fetchBulkDownloads(
5455
const results = await Promise.allSettled(
5556
batch.map(async name => {
5657
const encoded = encodePackageName(name)
57-
const data = await $fetch<{ downloads: number }>(
58-
`${NPM_API}/downloads/point/last-week/${encoded}`,
58+
const { data } = await $npmApi<{ downloads: number }>(
59+
`/downloads/point/last-week/${encoded}`,
5960
)
6061
return { name, downloads: data.downloads }
6162
}),
@@ -80,11 +81,9 @@ async function fetchBulkDownloads(
8081
* Returns search-result-like objects for compatibility with PackageList
8182
*/
8283
export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
83-
const cachedFetch = useCachedFetch()
84-
8584
const asyncData = useLazyAsyncData(
8685
() => `org-packages:${toValue(orgName)}`,
87-
async (_nuxtApp, { signal }) => {
86+
async ({ $npmRegistry, $npmApi }, { signal }) => {
8887
const org = toValue(orgName)
8988
if (!org) {
9089
return emptySearchResponse
@@ -93,8 +92,8 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
9392
// Get all package names in the org
9493
let packageNames: string[]
9594
try {
96-
const { data } = await cachedFetch<Record<string, string>>(
97-
`${NPM_REGISTRY}/-/org/${encodeURIComponent(org)}/package`,
95+
const { data } = await $npmRegistry<Record<string, string>>(
96+
`/-/org/${encodeURIComponent(org)}/package`,
9897
{ signal },
9998
)
10099
packageNames = Object.keys(data)
@@ -124,10 +123,9 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
124123
async name => {
125124
try {
126125
const encoded = encodePackageName(name)
127-
const { data: pkg } = await cachedFetch<MinimalPackument>(
128-
`${NPM_REGISTRY}/${encoded}`,
129-
{ signal },
130-
)
126+
const { data: pkg } = await $npmRegistry<MinimalPackument>(`/${encoded}`, {
127+
signal,
128+
})
131129
return pkg
132130
} catch {
133131
return null
@@ -141,7 +139,7 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
141139
)
142140
})(),
143141
// Fetch downloads in bulk
144-
fetchBulkDownloads(packageNames, { signal }),
142+
fetchBulkDownloads($npmApi, packageNames, { signal }),
145143
])
146144

147145
// Convert to search results with download data

app/composables/npm/useOutdatedDependencies.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { NuxtApp } from '#app'
12
import { maxSatisfying, prerelease, major, minor, diff, gt } from 'semver'
23
import type { Packument } from '#shared/types'
34
import { mapWithConcurrency } from '#shared/utils/async'
@@ -7,7 +8,6 @@ import {
78
isNonSemverConstraint,
89
constraintIncludesPrerelease,
910
} from '~/utils/npm/outdated-dependencies'
10-
import { NPM_REGISTRY } from '~/utils/npm/common'
1111

1212
// Cache for packument fetches to avoid duplicate requests across components
1313
const packumentCache = new Map<string, Promise<Packument | null>>()
@@ -18,6 +18,7 @@ const packumentCache = new Map<string, Promise<Packument | null>>()
1818
*/
1919
async function checkDependencyOutdated(
2020
cachedFetch: CachedFetchFunction,
21+
$npmRegistry: NuxtApp['$npmRegistry'],
2122
packageName: string,
2223
constraint: string,
2324
): Promise<OutdatedDependencyInfo | null> {
@@ -31,7 +32,7 @@ async function checkDependencyOutdated(
3132
if (cached) {
3233
packument = await cached
3334
} else {
34-
const promise = cachedFetch<Packument>(`${NPM_REGISTRY}/${encodePackageName(packageName)}`)
35+
const promise = $npmRegistry<Packument>(`/${encodePackageName(packageName)}`)
3536
.then(({ data }) => data)
3637
.catch(() => null)
3738
packumentCache.set(packageName, promise)
@@ -92,6 +93,7 @@ async function checkDependencyOutdated(
9293
export function useOutdatedDependencies(
9394
dependencies: MaybeRefOrGetter<Record<string, string> | undefined>,
9495
) {
96+
const { $npmRegistry } = useNuxtApp()
9597
const cachedFetch = useCachedFetch()
9698
const outdated = shallowRef<Record<string, OutdatedDependencyInfo>>({})
9799

@@ -105,7 +107,7 @@ export function useOutdatedDependencies(
105107
const batchResults = await mapWithConcurrency(
106108
entries,
107109
async ([name, constraint]) => {
108-
const info = await checkDependencyOutdated(cachedFetch, name, constraint)
110+
const info = await checkDependencyOutdated(cachedFetch, $npmRegistry, name, constraint)
109111
return [name, info] as const
110112
},
111113
5,

app/composables/npm/usePackage.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { Packument, SlimPackument, SlimVersion, SlimPackumentVersion } from '#shared/types'
2-
import { NPM_REGISTRY } from '~/utils/npm/common'
32
import { extractInstallScriptsInfo } from '~/utils/install-scripts'
43

54
/** Number of recent versions to include in initial payload */
@@ -98,13 +97,11 @@ export function usePackage(
9897
name: MaybeRefOrGetter<string>,
9998
requestedVersion?: MaybeRefOrGetter<string | null>,
10099
) {
101-
const cachedFetch = useCachedFetch()
102-
103100
const asyncData = useLazyAsyncData(
104101
() => `package:${toValue(name)}:${toValue(requestedVersion) ?? ''}`,
105-
async (_nuxtApp, { signal }) => {
102+
async ({ $npmRegistry }, { signal }) => {
106103
const encodedName = encodePackageName(toValue(name))
107-
const { data: r, isStale } = await cachedFetch<Packument>(`${NPM_REGISTRY}/${encodedName}`, {
104+
const { data: r, isStale } = await $npmRegistry<Packument>(`/${encodedName}`, {
108105
signal,
109106
})
110107
const reqVer = toValue(requestedVersion)

app/composables/npm/usePackageDownloads.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
import type { NpmDownloadCount } from '#shared/types'
2-
import { NPM_API } from '~/utils/npm/common'
32

43
export function usePackageDownloads(
54
name: MaybeRefOrGetter<string>,
65
period: MaybeRefOrGetter<'last-day' | 'last-week' | 'last-month' | 'last-year'> = 'last-week',
76
) {
8-
const cachedFetch = useCachedFetch()
9-
107
const asyncData = useLazyAsyncData(
118
() => `downloads:${toValue(name)}:${toValue(period)}`,
12-
async (_nuxtApp, { signal }) => {
9+
async ({ $npmApi }, { signal }) => {
1310
const encodedName = encodePackageName(toValue(name))
14-
const { data, isStale } = await cachedFetch<NpmDownloadCount>(
15-
`${NPM_API}/downloads/point/${toValue(period)}/${encodedName}`,
11+
const { data, isStale } = await $npmApi<NpmDownloadCount>(
12+
`/downloads/point/${toValue(period)}/${encodedName}`,
1613
{ signal },
1714
)
1815
return { ...data, isStale }

app/composables/useCachedFetch.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { CachedFetchResult } from '#shared/utils/fetch-cache-config'
2+
import { defu } from 'defu'
23

34
/**
45
* Get the cachedFetch function from the current request context.
@@ -34,9 +35,12 @@ export function useCachedFetch(): CachedFetchFunction {
3435
return async <T = unknown>(
3536
url: string,
3637
options: Parameters<typeof $fetch>[1] = {},
37-
_ttl?: number,
38+
_ttl: number = FETCH_CACHE_DEFAULT_TTL,
3839
): Promise<CachedFetchResult<T>> => {
39-
const data = (await $fetch<T>(url, options)) as T
40+
const defaultFetchOptions: Parameters<typeof $fetch>[1] = {
41+
cache: 'force-cache',
42+
}
43+
const data = (await $fetch<T>(url, defu(options, defaultFetchOptions))) as T
4044
return { data, isStale: false, cachedAt: null }
4145
}
4246
}
@@ -55,9 +59,12 @@ export function useCachedFetch(): CachedFetchFunction {
5559
return async <T = unknown>(
5660
url: string,
5761
options: Parameters<typeof $fetch>[1] = {},
58-
_ttl?: number,
62+
_ttl: number = FETCH_CACHE_DEFAULT_TTL,
5963
): Promise<CachedFetchResult<T>> => {
60-
const data = (await $fetch<T>(url, options)) as T
64+
const defaultFetchOptions: Parameters<typeof $fetch>[1] = {
65+
cache: 'force-cache',
66+
}
67+
const data = (await $fetch<T>(url, defu(options, defaultFetchOptions))) as T
6168
return { data, isStale: false, cachedAt: null }
6269
}
6370
}

app/pages/search.vue

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,8 +316,6 @@ interface ValidatedSuggestion {
316316
/** Cache for existence checks to avoid repeated API calls */
317317
const existenceCache = ref<Record<string, boolean | 'pending'>>({})
318318
319-
const NPM_REGISTRY = 'https://registry.npmjs.org'
320-
321319
interface NpmSearchResponse {
322320
total: number
323321
objects: Array<{ package: { name: string } }>

app/plugins/npm.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export default defineNuxtPlugin(() => {
2+
const cachedFetch = useCachedFetch()
3+
4+
return {
5+
provide: {
6+
npmRegistry: <T>(
7+
url: Parameters<CachedFetchFunction>[0],
8+
options?: Parameters<CachedFetchFunction>[1],
9+
ttl?: Parameters<CachedFetchFunction>[2],
10+
) => {
11+
return cachedFetch<T>(url, { baseURL: NPM_REGISTRY, ...options }, ttl)
12+
},
13+
npmApi: <T>(
14+
url: Parameters<CachedFetchFunction>[0],
15+
options?: Parameters<CachedFetchFunction>[1],
16+
ttl?: Parameters<CachedFetchFunction>[2],
17+
) => {
18+
return cachedFetch<T>(url, { baseURL: NPM_API, ...options }, ttl)
19+
},
20+
},
21+
}
22+
})

app/utils/npm/api.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { PackageVersionInfo } from '#shared/types'
22
import { getVersions } from 'fast-npm-meta'
33
import { compare } from 'semver'
4-
import { NPM_API } from './common'
54

65
type NpmDownloadsRangeResponse = {
76
start: string
@@ -19,10 +18,11 @@ export async function fetchNpmDownloadsRange(
1918
start: string,
2019
end: string,
2120
): Promise<NpmDownloadsRangeResponse> {
21+
const { $npmApi } = useNuxtApp()
2222
const encodedName = encodePackageName(packageName)
23-
return await $fetch<NpmDownloadsRangeResponse>(
24-
`${NPM_API}/downloads/range/${start}:${end}/${encodedName}`,
25-
)
23+
return (
24+
await $npmApi<NpmDownloadsRangeResponse>(`/downloads/range/${start}:${end}/${encodedName}`)
25+
).data
2626
}
2727

2828
// ============================================================================

0 commit comments

Comments
 (0)