Skip to content
Merged
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 @@ -40,5 +40,7 @@ test-results/
# generated files
shared/types/lexicons

**/__screenshots__/**

# output
.vercel
18 changes: 9 additions & 9 deletions app/composables/npm/useNpmSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import type {
NpmDownloadCount,
MinimalPackument,
} from '#shared/types'
import { NPM_REGISTRY, NPM_API } from '~/utils/npm/common'

/**
* Convert packument to search result format for display
Expand Down Expand Up @@ -55,7 +54,8 @@ export function useNpmSearch(
query: MaybeRefOrGetter<string>,
options: MaybeRefOrGetter<NpmSearchOptions> = {},
) {
const cachedFetch = useCachedFetch()
const { $npmRegistry } = useNuxtApp()

// Client-side cache
const cache = shallowRef<{
query: string
Expand All @@ -70,7 +70,7 @@ export function useNpmSearch(

const asyncData = useLazyAsyncData(
() => `search:incremental:${toValue(query)}`,
async (_nuxtApp, { signal }) => {
async ({ $npmRegistry, $npmApi }, { signal }) => {
const q = toValue(query)

if (!q.trim()) {
Expand All @@ -91,8 +91,8 @@ export function useNpmSearch(
if (q.length === 1) {
const encodedName = encodePackageName(q)
const [{ data: pkg, isStale }, { data: downloads }] = await Promise.all([
cachedFetch<Packument>(`${NPM_REGISTRY}/${encodedName}`, { signal }),
cachedFetch<NpmDownloadCount>(`${NPM_API}/downloads/point/last-week/${encodedName}`, {
$npmRegistry<Packument>(`/${encodedName}`, { signal }),
$npmApi<NpmDownloadCount>(`/downloads/point/last-week/${encodedName}`, {
signal,
}),
])
Expand Down Expand Up @@ -122,8 +122,8 @@ export function useNpmSearch(
}
}

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 @@ -179,8 +179,8 @@ export function useNpmSearch(
params.set('size', String(size))
params.set('from', String(from))

const { data: response } = await cachedFetch<NpmSearchResponse>(
`${NPM_REGISTRY}/-/v1/search?${params.toString()}`,
const { data: response } = await $npmRegistry<NpmSearchResponse>(
`/-/v1/search?${params.toString()}`,
{},
60,
)
Expand Down
30 changes: 14 additions & 16 deletions app/composables/npm/useOrgPackages.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { NuxtApp } from '#app'
import type { NpmSearchResponse, NpmSearchResult, MinimalPackument } from '#shared/types'
import { emptySearchResponse, packumentToSearchResult } from './useNpmSearch'
import { NPM_REGISTRY, NPM_API } from '~/utils/npm/common'
import { mapWithConcurrency } from '#shared/utils/async'

/**
Expand All @@ -10,6 +10,7 @@ import { mapWithConcurrency } from '#shared/utils/async'
* Note: npm bulk downloads API does not support scoped packages.
*/
async function fetchBulkDownloads(
$npmApi: NuxtApp['$npmApi'],
packageNames: string[],
options: Parameters<typeof $fetch>[1] = {},
): Promise<Map<string, number>> {
Expand All @@ -28,11 +29,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 @@ -54,8 +55,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 }
}),
Comment on lines 56 to 62
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing options parameter for scoped package downloads.

The scoped package individual fetches do not receive the options parameter (which includes signal for request cancellation). This is inconsistent with the unscoped bulk fetch on line 34, which correctly passes options. If the parent request is aborted, these requests will continue executing.

🔧 Proposed fix to pass options
           batch.map(async name => {
             const encoded = encodePackageName(name)
             const { data } = await $npmApi<{ downloads: number }>(
               `/downloads/point/last-week/${encoded}`,
+              options,
             )
             return { name, downloads: data.downloads }
           }),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 }
}),
batch.map(async name => {
const encoded = encodePackageName(name)
const { data } = await $npmApi<{ downloads: number }>(
`/downloads/point/last-week/${encoded}`,
options,
)
return { name, downloads: data.downloads }
}),

Expand All @@ -80,11 +81,9 @@ async function fetchBulkDownloads(
* 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, $npmApi }, { signal }) => {
const org = toValue(orgName)
if (!org) {
return emptySearchResponse
Expand All @@ -93,8 +92,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 @@ -124,10 +123,9 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
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 All @@ -141,7 +139,7 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
)
})(),
// Fetch downloads in bulk
fetchBulkDownloads(packageNames, { signal }),
fetchBulkDownloads($npmApi, packageNames, { signal }),
])

// Convert to search results with download data
Expand Down
8 changes: 5 additions & 3 deletions app/composables/npm/useOutdatedDependencies.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { NuxtApp } from '#app'
import { maxSatisfying, prerelease, major, minor, diff, gt } from 'semver'
import type { Packument } from '#shared/types'
import { mapWithConcurrency } from '#shared/utils/async'
Expand All @@ -7,7 +8,6 @@ import {
isNonSemverConstraint,
constraintIncludesPrerelease,
} from '~/utils/npm/outdated-dependencies'
import { NPM_REGISTRY } from '~/utils/npm/common'

// Cache for packument fetches to avoid duplicate requests across components
const packumentCache = new Map<string, Promise<Packument | null>>()
Expand All @@ -18,6 +18,7 @@ const packumentCache = new Map<string, Promise<Packument | null>>()
*/
async function checkDependencyOutdated(
cachedFetch: CachedFetchFunction,
$npmRegistry: NuxtApp['$npmRegistry'],
packageName: string,
constraint: string,
): Promise<OutdatedDependencyInfo | null> {
Expand All @@ -31,7 +32,7 @@ async function checkDependencyOutdated(
if (cached) {
packument = await cached
} else {
const promise = cachedFetch<Packument>(`${NPM_REGISTRY}/${encodePackageName(packageName)}`)
const promise = $npmRegistry<Packument>(`/${encodePackageName(packageName)}`)
.then(({ data }) => data)
.catch(() => null)
packumentCache.set(packageName, promise)
Expand Down Expand Up @@ -92,6 +93,7 @@ async function checkDependencyOutdated(
export function useOutdatedDependencies(
dependencies: MaybeRefOrGetter<Record<string, string> | undefined>,
) {
const { $npmRegistry } = useNuxtApp()
const cachedFetch = useCachedFetch()
const outdated = shallowRef<Record<string, OutdatedDependencyInfo>>({})

Expand All @@ -105,7 +107,7 @@ export function useOutdatedDependencies(
const batchResults = await mapWithConcurrency(
entries,
async ([name, constraint]) => {
const info = await checkDependencyOutdated(cachedFetch, name, constraint)
const info = await checkDependencyOutdated(cachedFetch, $npmRegistry, name, constraint)
return [name, info] as const
},
5,
Expand Down
7 changes: 2 additions & 5 deletions app/composables/npm/usePackage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { Packument, SlimPackument, SlimVersion, SlimPackumentVersion } from '#shared/types'
import { NPM_REGISTRY } from '~/utils/npm/common'
import { extractInstallScriptsInfo } from '~/utils/install-scripts'

/** Number of recent versions to include in initial payload */
Expand Down Expand Up @@ -98,13 +97,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
9 changes: 3 additions & 6 deletions app/composables/npm/usePackageDownloads.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import type { NpmDownloadCount } from '#shared/types'
import { NPM_API } from '~/utils/npm/common'

export function usePackageDownloads(
name: MaybeRefOrGetter<string>,
period: MaybeRefOrGetter<'last-day' | 'last-week' | 'last-month' | 'last-year'> = 'last-week',
) {
const cachedFetch = useCachedFetch()

const asyncData = useLazyAsyncData(
() => `downloads:${toValue(name)}:${toValue(period)}`,
async (_nuxtApp, { signal }) => {
async ({ $npmApi }, { 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
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 }
}
}
2 changes: 0 additions & 2 deletions app/pages/search.vue
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,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)
},
},
}
})
8 changes: 4 additions & 4 deletions app/utils/npm/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { PackageVersionInfo } from '#shared/types'
import { getVersions } from 'fast-npm-meta'
import { compare } from 'semver'
import { NPM_API } from './common'

type NpmDownloadsRangeResponse = {
start: string
Expand All @@ -19,10 +18,11 @@ export async function fetchNpmDownloadsRange(
start: string,
end: string,
): Promise<NpmDownloadsRangeResponse> {
const { $npmApi } = useNuxtApp()
const encodedName = encodePackageName(packageName)
return await $fetch<NpmDownloadsRangeResponse>(
`${NPM_API}/downloads/range/${start}:${end}/${encodedName}`,
)
return (
await $npmApi<NpmDownloadsRangeResponse>(`/downloads/range/${start}:${end}/${encodedName}`)
).data
}

// ============================================================================
Expand Down
3 changes: 0 additions & 3 deletions app/utils/npm/common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
export const NPM_REGISTRY = 'https://registry.npmjs.org'
export const NPM_API = 'https://api.npmjs.org'

/**
* Constructs a scope:team string in the format expected by npm.
* npm operations require the format @scope:team (with @ prefix).
Expand Down
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'
import { encodePackageName } from '#shared/utils/npm'

/**
Expand Down Expand Up @@ -71,8 +72,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 @@ -119,6 +119,7 @@
"@vitest/coverage-v8": "4.0.18",
"@vue/test-utils": "2.4.6",
"axe-core": "4.11.1",
"defu": "6.1.4",
"eslint-plugin-regexp": "3.0.0",
"fast-check": "4.5.3",
"h3": "1.15.5",
Expand Down
1 change: 1 addition & 0 deletions shared/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const NPMX_SITE = 'https://npmx.dev'
export const BLUESKY_API = 'https://public.api.bsky.app/xrpc/'
export const BLUESKY_COMMENTS_REQUEST = '/api/atproto/bluesky-comments'
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
Loading
Loading