Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ test-results/

# generated files
shared/types/lexicons

**/__screenshots__/**
15 changes: 11 additions & 4 deletions app/composables/useCachedFetch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { CachedFetchResult } from '#shared/utils/fetch-cache-config'
import defu from 'defu'

/**
* Get the cachedFetch function from the current request context.
Expand Down Expand Up @@ -34,9 +35,12 @@ export function useCachedFetch(): CachedFetchFunction {
return async <T = unknown>(
url: string,
options: Parameters<typeof $fetch>[1] = {},
_ttl?: number,
_ttl: number = FETCH_CACHE_DEFAULT_TTL,
): Promise<CachedFetchResult<T>> => {
const data = (await $fetch<T>(url, options)) as T
const defaultFetchOptions: Parameters<typeof $fetch>[1] = {
cache: 'force-cache',
}
const data = (await $fetch<T>(url, defu(options, defaultFetchOptions))) as T
return { data, isStale: false, cachedAt: null }
}
}
Expand All @@ -55,9 +59,12 @@ export function useCachedFetch(): CachedFetchFunction {
return async <T = unknown>(
url: string,
options: Parameters<typeof $fetch>[1] = {},
_ttl?: number,
_ttl: number = FETCH_CACHE_DEFAULT_TTL,
): Promise<CachedFetchResult<T>> => {
const data = (await $fetch<T>(url, options)) as T
const defaultFetchOptions: Parameters<typeof $fetch>[1] = {
cache: 'force-cache',
}
const data = (await $fetch<T>(url, defu(options, defaultFetchOptions))) as T
return { data, isStale: false, cachedAt: null }
}
}
62 changes: 30 additions & 32 deletions app/composables/useNpmRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ import { isExactVersion } from '~/utils/versions'
import { extractInstallScriptsInfo } from '~/utils/install-scripts'
import type { CachedFetchFunction } from '#shared/utils/fetch-cache-config'

const NPM_REGISTRY = 'https://registry.npmjs.org'
const NPM_API = 'https://api.npmjs.org'

// Cache for packument fetches to avoid duplicate requests across components
const packumentCache = new Map<string, Promise<Packument | null>>()

Expand All @@ -33,6 +30,8 @@ async function fetchBulkDownloads(
const downloads = new Map<string, number>()
if (packageNames.length === 0) return downloads

const { $npmApi } = useNuxtApp()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danielroe this line seems to be the problem.

replacing it by a simple mock "fixes" the issue. Any ideas 😅

 const $npmApi = ()=>{
  return Promise.resolve({data: null,})
 }

// Separate scoped and unscoped packages
const scopedPackages = packageNames.filter(n => n.startsWith('@'))
const unscopedPackages = packageNames.filter(n => !n.startsWith('@'))
Expand All @@ -45,11 +44,11 @@ async function fetchBulkDownloads(
bulkPromises.push(
(async () => {
try {
const response = await $fetch<Record<string, { downloads: number } | null>>(
`${NPM_API}/downloads/point/last-week/${chunk.join(',')}`,
const response = await $npmApi<Record<string, { downloads: number } | null>>(
`/downloads/point/last-week/${chunk.join(',')}`,
options,
)
for (const [name, data] of Object.entries(response)) {
for (const [name, data] of Object.entries(response.data)) {
if (data?.downloads !== undefined) {
downloads.set(name, data.downloads)
}
Expand All @@ -71,8 +70,8 @@ async function fetchBulkDownloads(
const results = await Promise.allSettled(
batch.map(async name => {
const encoded = encodePackageName(name)
const data = await $fetch<{ downloads: number }>(
`${NPM_API}/downloads/point/last-week/${encoded}`,
const { data } = await $npmApi<{ downloads: number }>(
`/downloads/point/last-week/${encoded}`,
)
return { name, downloads: data.downloads }
}),
Expand Down Expand Up @@ -184,13 +183,11 @@ export function usePackage(
name: MaybeRefOrGetter<string>,
requestedVersion?: MaybeRefOrGetter<string | null>,
) {
const cachedFetch = useCachedFetch()

const asyncData = useLazyAsyncData(
() => `package:${toValue(name)}:${toValue(requestedVersion) ?? ''}`,
async (_nuxtApp, { signal }) => {
async ({ $npmRegistry }, { signal }) => {
const encodedName = encodePackageName(toValue(name))
const { data: r, isStale } = await cachedFetch<Packument>(`${NPM_REGISTRY}/${encodedName}`, {
const { data: r, isStale } = await $npmRegistry<Packument>(`/${encodedName}`, {
signal,
})
const reqVer = toValue(requestedVersion)
Expand Down Expand Up @@ -233,14 +230,14 @@ export function usePackageDownloads(
name: MaybeRefOrGetter<string>,
period: MaybeRefOrGetter<'last-day' | 'last-week' | 'last-month' | 'last-year'> = 'last-week',
) {
const cachedFetch = useCachedFetch()
const { $npmApi } = useNuxtApp()

const asyncData = useLazyAsyncData(
() => `downloads:${toValue(name)}:${toValue(period)}`,
async (_nuxtApp, { signal }) => {
const encodedName = encodePackageName(toValue(name))
const { data, isStale } = await cachedFetch<NpmDownloadCount>(
`${NPM_API}/downloads/point/${toValue(period)}/${encodedName}`,
const { data, isStale } = await $npmApi<NpmDownloadCount>(
`/downloads/point/${toValue(period)}/${encodedName}`,
{ signal },
)
return { ...data, isStale }
Expand Down Expand Up @@ -273,9 +270,11 @@ export async function fetchNpmDownloadsRange(
end: string,
): Promise<NpmDownloadsRangeResponse> {
const encodedName = encodePackageName(packageName)
return await $fetch<NpmDownloadsRangeResponse>(
`${NPM_API}/downloads/range/${start}:${end}/${encodedName}`,
)
const { $npmApi } = useNuxtApp()

return (
await $npmApi<NpmDownloadsRangeResponse>(`/downloads/range/${start}:${end}/${encodedName}`)
).data
}

const emptySearchResponse = {
Expand All @@ -294,7 +293,6 @@ export function useNpmSearch(
query: MaybeRefOrGetter<string>,
options: MaybeRefOrGetter<NpmSearchOptions> = {},
) {
const cachedFetch = useCachedFetch()
// Client-side cache
const cache = shallowRef<{
query: string
Expand All @@ -309,7 +307,7 @@ export function useNpmSearch(

const asyncData = useLazyAsyncData(
() => `search:incremental:${toValue(query)}`,
async (_nuxtApp, { signal }) => {
async ({ $npmRegistry }, { signal }) => {
const q = toValue(query)
if (!q.trim()) {
return emptySearchResponse
Expand All @@ -326,8 +324,8 @@ export function useNpmSearch(
// Use requested size for initial fetch
params.set('size', String(opts.size ?? 25))

const { data: response, isStale } = await cachedFetch<NpmSearchResponse>(
`${NPM_REGISTRY}/-/v1/search?${params.toString()}`,
const { data: response, isStale } = await $npmRegistry<NpmSearchResponse>(
`/-/v1/search?${params.toString()}`,
{ signal },
60,
)
Expand Down Expand Up @@ -368,6 +366,8 @@ export function useNpmSearch(

isLoadingMore.value = true

const { $npmRegistry } = useNuxtApp()

try {
// Fetch from where we left off - calculate size needed
const from = currentCount
Expand All @@ -378,7 +378,7 @@ export function useNpmSearch(
params.set('size', String(size))
params.set('from', String(from))

const { data: response } = await cachedFetch<NpmSearchResponse>(
const { data: response } = await $npmRegistry<NpmSearchResponse>(
`${NPM_REGISTRY}/-/v1/search?${params.toString()}`,
{},
60,
Expand Down Expand Up @@ -509,11 +509,9 @@ function packumentToSearchResult(pkg: MinimalPackument, weeklyDownloads?: number
* Returns search-result-like objects for compatibility with PackageList
*/
export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
const cachedFetch = useCachedFetch()

const asyncData = useLazyAsyncData(
() => `org-packages:${toValue(orgName)}`,
async (_nuxtApp, { signal }) => {
async ({ $npmRegistry }, { signal }) => {
const org = toValue(orgName)
if (!org) {
return emptySearchResponse
Expand All @@ -522,8 +520,8 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
// Get all package names in the org
let packageNames: string[]
try {
const { data } = await cachedFetch<Record<string, string>>(
`${NPM_REGISTRY}/-/org/${encodeURIComponent(org)}/package`,
const { data } = await $npmRegistry<Record<string, string>>(
`/-/org/${encodeURIComponent(org)}/package`,
{ signal },
)
packageNames = Object.keys(data)
Expand Down Expand Up @@ -556,10 +554,9 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
batch.map(async name => {
try {
const encoded = encodePackageName(name)
const { data: pkg } = await cachedFetch<MinimalPackument>(
`${NPM_REGISTRY}/${encoded}`,
{ signal },
)
const { data: pkg } = await $npmRegistry<MinimalPackument>(`/${encoded}`, {
signal,
})
return pkg
} catch {
return null
Expand Down Expand Up @@ -702,6 +699,7 @@ async function checkDependencyOutdated(
if (cached) {
packument = await cached
} else {
// todo: use $npmRegistry here
const promise = cachedFetch<Packument>(`${NPM_REGISTRY}/${encodePackageName(packageName)}`)
.then(({ data }) => data)
.catch(() => null)
Expand Down
2 changes: 0 additions & 2 deletions app/pages/search.vue
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,6 @@ interface ValidatedSuggestion {
/** Cache for existence checks to avoid repeated API calls */
const existenceCache = ref<Record<string, boolean | 'pending'>>({})

const NPM_REGISTRY = 'https://registry.npmjs.org'

interface NpmSearchResponse {
total: number
objects: Array<{ package: { name: string } }>
Expand Down
22 changes: 22 additions & 0 deletions app/plugins/npm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export default defineNuxtPlugin(() => {
const cachedFetch = useCachedFetch()

return {
provide: {
npmRegistry: <T>(
url: Parameters<CachedFetchFunction>[0],
options?: Parameters<CachedFetchFunction>[1],
ttl?: Parameters<CachedFetchFunction>[2],
) => {
return cachedFetch<T>(url, { baseURL: NPM_REGISTRY, ...options }, ttl)
},
npmApi: <T>(
url: Parameters<CachedFetchFunction>[0],
options?: Parameters<CachedFetchFunction>[1],
ttl?: Parameters<CachedFetchFunction>[2],
) => {
return cachedFetch<T>(url, { baseURL: NPM_API, ...options }, ttl)
},
},
}
})
3 changes: 1 addition & 2 deletions app/utils/package-name.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import validatePackageName from 'validate-npm-package-name'
import { NPM_REGISTRY } from '#shared/utils/constants'

/**
* Normalize a package name for comparison by removing common variations.
Expand Down Expand Up @@ -70,8 +71,6 @@ export interface CheckNameResult {
similarPackages?: SimilarPackage[]
}

const NPM_REGISTRY = 'https://registry.npmjs.org'

export async function checkPackageExists(
name: string,
options: Parameters<typeof $fetch>[1] = {},
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"@vitest/coverage-v8": "4.0.18",
"@vue/test-utils": "2.4.6",
"axe-core": "4.11.1",
"defu": "6.1.4",
"knip": "5.82.1",
"lint-staged": "16.2.7",
"playwright-core": "1.58.0",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions shared/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const CACHE_MAX_AGE_ONE_YEAR = 60 * 60 * 24 * 365

// API Strings
export const NPM_REGISTRY = 'https://registry.npmjs.org'
export const NPM_API = 'https://api.npmjs.org'
export const ERROR_PACKAGE_ANALYSIS_FAILED = 'Failed to analyze package.'
export const ERROR_PACKAGE_VERSION_AND_FILE_FAILED = 'Version and file path are required.'
export const ERROR_PACKAGE_REQUIREMENTS_FAILED =
Expand Down
8 changes: 3 additions & 5 deletions test/nuxt/composables/use-npm-registry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('usePackageDownloads', () => {

// Check that fetch was called with the correct URL (first argument)
expect(fetchSpy).toHaveBeenCalled()
expect(fetchSpy.mock.calls[0]?.[0]).toBe('https://api.npmjs.org/downloads/point/last-week/vue')
expect(fetchSpy.mock.calls[0]?.[0]).toBe('/downloads/point/last-week/vue')
expect(data.value?.downloads).toBe(1234567)
})

Expand All @@ -40,7 +40,7 @@ describe('usePackageDownloads', () => {

// Check that fetch was called with the correct URL (first argument)
expect(fetchSpy).toHaveBeenCalled()
expect(fetchSpy.mock.calls[0]?.[0]).toBe('https://api.npmjs.org/downloads/point/last-month/vue')
expect(fetchSpy.mock.calls[0]?.[0]).toBe('/downloads/point/last-month/vue')
})

it('should encode scoped package names', async () => {
Expand All @@ -54,8 +54,6 @@ describe('usePackageDownloads', () => {

// Check that fetch was called with the correct URL (first argument)
expect(fetchSpy).toHaveBeenCalled()
expect(fetchSpy.mock.calls[0]?.[0]).toBe(
'https://api.npmjs.org/downloads/point/last-week/@vue%2Fcore',
)
expect(fetchSpy.mock.calls[0]?.[0]).toBe('/downloads/point/last-week/@vue%2Fcore')
})
})
1 change: 1 addition & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export default defineConfig({
},
browser: {
enabled: true,
headless: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
Expand Down
Loading