Skip to content
Open
Show file tree
Hide file tree
Changes from all 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()

Comment on lines +33 to +34
Copy link
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