Something went wrong. Try again.
[READ-ONLY]
a fast, modern browser for the npm registry forked from npmx.dev/npmx.dev
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866import process from 'node:process'import type { CachedFetchResult } from '#shared/utils/fetch-cache-config'import { createFetch } from 'ofetch'
/** * Test fixtures plugin for CI environments. * * This plugin intercepts all cachedFetch calls and serves pre-recorded fixture data * instead of hitting the real npm API. * * This ensures: * - Tests are deterministic and don't depend on external API availability * - We don't hammer the npm registry during CI runs * - Tests run faster with no network latency * * Set NUXT_TEST_FIXTURES_VERBOSE=true for detailed logging. */
const VERBOSE = process.env.NUXT_TEST_FIXTURES_VERBOSE === 'true'
const FIXTURE_PATHS = { packument: 'npm-registry:packuments', search: 'npm-registry:search', org: 'npm-registry:orgs', downloads: 'npm-api:downloads', user: 'users', esmHeaders: 'esm-sh:headers', esmTypes: 'esm-sh:types', githubContributors: 'github:contributors.json', githubContributorsStats: 'github:contributors-stats.json',} as const
type FixtureType = keyof typeof FIXTURE_PATHS
interface FixtureMatch { type: FixtureType name: string}
interface MockResult { data: unknown}
function getFixturePath(type: FixtureType, name: string): string { const dir = FIXTURE_PATHS[type] let filename: string
switch (type) { case 'packument': case 'downloads': filename = `${name}.json` break case 'search': filename = `${name.replace(/:/g, '-')}.json` break case 'org': case 'user': filename = `${name}.json` break default: filename = `${name}.json` }
return `${dir}:${filename.replace(/\//g, ':')}`}
/** * Parse a scoped package name with optional version. * Handles formats like: @scope/name, @scope/name@version, name, name@version */function parseScopedPackageWithVersion(input: string): { name: string; version?: string } { if (input.startsWith('@')) { // Scoped package: @scope/name or @scope/name@version const slashIndex = input.indexOf('/') if (slashIndex === -1) { // Invalid format like just "@scope" return { name: input } } const afterSlash = input.slice(slashIndex + 1) const atIndex = afterSlash.indexOf('@') if (atIndex === -1) { // @scope/name (no version) return { name: input } } // @scope/name@version return { name: input.slice(0, slashIndex + 1 + atIndex), version: afterSlash.slice(atIndex + 1), } }
// Unscoped package: name or name@version const atIndex = input.indexOf('@') if (atIndex === -1) { return { name: input } } return { name: input.slice(0, atIndex), version: input.slice(atIndex + 1), }}
function getMockForUrl(url: string): MockResult | null { let urlObj: URL try { urlObj = new URL(url) } catch { return null }
const { host, pathname, searchParams } = urlObj
// OSV API - return empty vulnerability results if (host === 'api.osv.dev') { if (pathname === '/v1/querybatch') { return { data: { results: [] } } } if (pathname.startsWith('/v1/query')) { return { data: { vulns: [] } } } }
// JSR registry - return null (npm packages aren't on JSR) if (host === 'jsr.io' && pathname.endsWith('/meta.json')) { return { data: null } }
// Bundlephobia API - return mock size data if (host === 'bundlephobia.com' && pathname === '/api/size') { const packageSpec = searchParams.get('package') if (packageSpec) { return { data: { name: packageSpec.split('@')[0], size: 12345, gzip: 4567, dependencyCount: 3, }, } } }
// npms.io API - return mock package score data if (host === 'api.npms.io') { const packageMatch = decodeURIComponent(pathname).match(/^\/v2\/package\/(.+)$/) if (packageMatch?.[1]) { return { data: { analyzedAt: new Date().toISOString(), collected: { metadata: { name: packageMatch[1] }, }, score: { final: 0.75, detail: { quality: 0.8, popularity: 0.7, maintenance: 0.75, }, }, }, } } }
// jsdelivr CDN - return 404 for README files, etc. if (host === 'cdn.jsdelivr.net') { // Return null data which will cause a 404 - README files are optional return { data: null } }
// jsdelivr data API - return mock file listing if (host === 'data.jsdelivr.com') { const packageMatch = decodeURIComponent(pathname).match(/^\/v1\/packages\/npm\/(.+)$/) if (packageMatch?.[1]) { const pkgWithVersion = packageMatch[1] const parsed = parseScopedPackageWithVersion(pkgWithVersion) return { data: { type: 'npm', name: parsed.name, version: parsed.version || 'latest', files: [ { name: 'package.json', hash: 'abc123', size: 1000 }, { name: 'index.js', hash: 'def456', size: 500 }, { name: 'README.md', hash: 'ghi789', size: 2000 }, ], }, } } }
// Gravatar API - return 404 (avatars not needed in tests) if (host === 'www.gravatar.com') { return { data: null } }
// GitHub API - handled via fixtures, return null to use fixture system // Note: The actual fixture loading is handled in fetchFromFixtures via special case if (host === 'api.github.com') { // Return null here so it goes through fetchFromFixtures which handles the fixture loading return null }
// esm.sh is handled specially via $fetch.raw override, not here // Return null to indicate no mock available at the cachedFetch level
return null}
/** * Process a single package query for fast-npm-meta. * Returns the metadata for a single package or null/error result. */async function processSingleFastNpmMeta( packageQuery: string, storage: ReturnType<typeof useStorage>, metadata: boolean,): Promise<Record<string, unknown>> { let packageName = packageQuery let specifier = 'latest'
if (packageName.startsWith('@')) { const atIndex = packageName.indexOf('@', 1) if (atIndex !== -1) { specifier = packageName.slice(atIndex + 1) packageName = packageName.slice(0, atIndex) } } else { const atIndex = packageName.indexOf('@') if (atIndex !== -1) { specifier = packageName.slice(atIndex + 1) packageName = packageName.slice(0, atIndex) } }
// Special case: packages with "does-not-exist" in the name should 404 if (packageName.includes('does-not-exist') || packageName.includes('nonexistent')) { return { error: 'not_found' } }
const fixturePath = getFixturePath('packument', packageName) const packument = await storage.getItem<any>(fixturePath)
if (!packument) { // For unknown packages without the special markers, try to return stub data // This is handled elsewhere - returning error here for fast-npm-meta return { error: 'not_found' } }
let version: string | undefined if (specifier === 'latest' || !specifier) { version = packument['dist-tags']?.latest } else if (packument['dist-tags']?.[specifier]) { version = packument['dist-tags'][specifier] } else if (packument.versions?.[specifier]) { version = specifier } else { version = packument['dist-tags']?.latest }
if (!version) { return { error: 'version_not_found' } }
const result: Record<string, unknown> = { name: packageName, specifier, version, publishedAt: packument.time?.[version] || new Date().toISOString(), lastSynced: Date.now(), }
// Include metadata if requested if (metadata) { const versionData = packument.versions?.[version] if (versionData?.deprecated) { result.deprecated = versionData.deprecated } }
return result}
/** * Process a single package for the /versions/ endpoint. * Returns PackageVersionsInfo shape: { name, distTags, versions, specifier, time, lastSynced } */async function processSingleVersionsMeta( packageQuery: string, storage: ReturnType<typeof useStorage>, metadata: boolean,): Promise<Record<string, unknown>> { let packageName = packageQuery let specifier = '*'
if (packageName.startsWith('@')) { const atIndex = packageName.indexOf('@', 1) if (atIndex !== -1) { specifier = packageName.slice(atIndex + 1) packageName = packageName.slice(0, atIndex) } } else { const atIndex = packageName.indexOf('@') if (atIndex !== -1) { specifier = packageName.slice(atIndex + 1) packageName = packageName.slice(0, atIndex) } }
if (packageName.includes('does-not-exist') || packageName.includes('nonexistent')) { return { name: packageName, error: 'not_found' } }
const fixturePath = getFixturePath('packument', packageName) const packument = await storage.getItem<any>(fixturePath)
if (!packument) { return { name: packageName, error: 'not_found' } }
const result: Record<string, unknown> = { name: packageName, specifier, distTags: packument['dist-tags'] || {}, versions: Object.keys(packument.versions || {}), time: packument.time || {}, lastSynced: Date.now(), }
if (metadata) { const versionsMeta: Record<string, Record<string, unknown>> = {} for (const [ver, data] of Object.entries(packument.versions || {})) { const meta: Record<string, unknown> = { version: ver } const vData = data as Record<string, unknown> if (vData.deprecated) meta.deprecated = vData.deprecated if (packument.time?.[ver]) meta.time = packument.time[ver] versionsMeta[ver] = meta } result.versionsMeta = versionsMeta }
return result}
async function handleFastNpmMeta( url: string, storage: ReturnType<typeof useStorage>,): Promise<MockResult | null> { let urlObj: URL try { urlObj = new URL(url) } catch { return null }
const { host, pathname, searchParams } = urlObj
if (host !== 'npm.antfu.dev') return null
const rawPath = decodeURIComponent(pathname.slice(1)) if (!rawPath) return null
const metadata = searchParams.get('metadata') === 'true'
// Determine if this is a /versions/ request const isVersions = rawPath.startsWith('versions/') const pathPart = isVersions ? rawPath.slice('versions/'.length) : rawPath const processFn = isVersions ? (pkg: string) => processSingleVersionsMeta(pkg, storage, metadata) : (pkg: string) => processSingleFastNpmMeta(pkg, storage, metadata)
// Handle batch requests (package1+package2+...) if (pathPart.includes('+')) { const packages = pathPart.split('+') const results = await Promise.all(packages.map(processFn)) return { data: results } }
// Handle single package request const result = await processFn(pathPart) if ('error' in result) { return { data: null } } return { data: result }}
/** * Handle GitHub API requests using fixtures. */async function handleGitHubApi( url: string, storage: ReturnType<typeof useStorage>,): Promise<MockResult | null> { let urlObj: URL try { urlObj = new URL(url) } catch { return null }
const { host, pathname } = urlObj
if (host !== 'api.github.com') return null
// Contributors stats endpoint: /repos/{owner}/{repo}/stats/contributors const contributorsStatsMatch = pathname.match(/^\/repos\/([^/]+)\/([^/]+)\/stats\/contributors$/) if (contributorsStatsMatch) { const contributorsStats = await storage.getItem<unknown[]>( FIXTURE_PATHS.githubContributorsStats, ) if (contributorsStats) { return { data: contributorsStats } } return { data: [] } }
// Contributors endpoint: /repos/{owner}/{repo}/contributors const contributorsMatch = pathname.match(/^\/repos\/([^/]+)\/([^/]+)\/contributors$/) if (contributorsMatch) { const contributors = await storage.getItem<unknown[]>(FIXTURE_PATHS.githubContributors) if (contributors) { return { data: contributors } } // Return empty array if no fixture exists return { data: [] } }
// Other GitHub API endpoints can be added here as needed return null}
interface FixtureMatchWithVersion extends FixtureMatch { version?: string // 'latest', a semver version, or undefined for full packument}
function matchUrlToFixture(url: string): FixtureMatchWithVersion | null { let urlObj: URL try { urlObj = new URL(url) } catch { return null }
const { host, pathname, searchParams } = urlObj
// npm registry (registry.npmjs.org) if (host === 'registry.npmjs.org') { // Search endpoint if (pathname === '/-/v1/search') { const query = searchParams.get('text') if (query) { const maintainerMatch = query.match(/^maintainer:(.+)$/) if (maintainerMatch?.[1]) { return { type: 'user', name: maintainerMatch[1] } } return { type: 'search', name: query } } return { type: 'search', name: '' } }
// Org packages const orgMatch = pathname.match(/^\/-\/org\/([^/]+)\/package$/) if (orgMatch?.[1]) { return { type: 'org', name: orgMatch[1] } }
// Packument - handle both full packument and version manifest requests let packagePath = decodeURIComponent(pathname.slice(1)) if (packagePath && !packagePath.startsWith('-/')) { let version: string | undefined
if (packagePath.startsWith('@')) { const parts = packagePath.split('/') if (parts.length > 2) { // @scope/name/version or @scope/name/latest version = parts[2] packagePath = `${parts[0]}/${parts[1]}` } // else just @scope/name - full packument } else { const slashIndex = packagePath.indexOf('/') if (slashIndex !== -1) { // name/version or name/latest version = packagePath.slice(slashIndex + 1) packagePath = packagePath.slice(0, slashIndex) } // else just name - full packument }
return { type: 'packument', name: packagePath, version } } }
// npm API (api.npmjs.org) if (host === 'api.npmjs.org') { const downloadsMatch = pathname.match(/^\/downloads\/point\/[^/]+\/(.+)$/) if (downloadsMatch?.[1]) { return { type: 'downloads', name: decodeURIComponent(downloadsMatch[1]) } } }
return null}
/** * Log a message to stderr with clear formatting for unmocked requests. */function logUnmockedRequest(type: string, detail: string, url: string): void { process.stderr.write( `\n` + `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` + `[test-fixtures] ${type}\n` + `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` + `${detail}\n` + `URL: ${url}\n` + `\n` + `To fix: Add a fixture file or update test/e2e/test-utils.ts\n` + `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`, )}
/** * Shared fixture-backed fetch implementation. * This is used by both cachedFetch and the global $fetch override. */async function fetchFromFixtures<T>( url: string, storage: ReturnType<typeof useStorage>,): Promise<CachedFetchResult<T>> { // Check for mock responses (OSV, JSR) const mockResult = getMockForUrl(url) if (mockResult) { if (VERBOSE) process.stdout.write(`[test-fixtures] Mock: ${url}\n`) return { data: mockResult.data as T, isStale: false, cachedAt: Date.now() } }
// Check for fast-npm-meta const fastNpmMetaResult = await handleFastNpmMeta(url, storage) if (fastNpmMetaResult) { if (VERBOSE) process.stdout.write(`[test-fixtures] Fast-npm-meta: ${url}\n`) return { data: fastNpmMetaResult.data as T, isStale: false, cachedAt: Date.now() } }
// Check for GitHub API const githubResult = await handleGitHubApi(url, storage) if (githubResult) { if (VERBOSE) process.stdout.write(`[test-fixtures] GitHub API: ${url}\n`) return { data: githubResult.data as T, isStale: false, cachedAt: Date.now() } }
const match = matchUrlToFixture(url)
if (!match) { logUnmockedRequest('NO FIXTURE PATTERN', 'URL does not match any known fixture pattern', url) throw createError({ statusCode: 404, statusMessage: 'No test fixture available', message: `No fixture pattern matches URL: ${url}`, }) }
const fixturePath = getFixturePath(match.type, match.name) const rawData = await storage.getItem<any>(fixturePath)
if (rawData === null) { // For user searches or search queries without fixtures, return empty results if (match.type === 'user' || match.type === 'search') { if (VERBOSE) process.stdout.write(`[test-fixtures] Empty ${match.type}: ${match.name}\n`) return { data: { objects: [], total: 0, time: new Date().toISOString() } as T, isStale: false, cachedAt: Date.now(), } }
// For org packages without fixtures, return 404 if (match.type === 'org') { throw createError({ statusCode: 404, statusMessage: 'Org not found', message: `No fixture for org: ${match.name}`, }) }
// For packuments without fixtures, return a stub packument // This allows tests to work without needing fixtures for every dependency if (match.type === 'packument') { // Special case: packages with "does-not-exist" in the name should 404 // This allows tests to verify 404 behavior for nonexistent packages if (match.name.includes('does-not-exist') || match.name.includes('nonexistent')) { throw createError({ statusCode: 404, statusMessage: 'Package not found', message: `Package ${match.name} does not exist`, }) }
if (VERBOSE) process.stderr.write(`[test-fixtures] Stub packument: ${match.name}\n`) const stubVersion = '1.0.0' const stubPackument = { 'name': match.name, 'dist-tags': { latest: stubVersion }, 'versions': { [stubVersion]: { name: match.name, version: stubVersion, description: `Stub fixture for ${match.name}`, dependencies: {}, }, }, 'time': { created: new Date().toISOString(), modified: new Date().toISOString(), [stubVersion]: new Date().toISOString(), }, 'maintainers': [], }
// If a specific version was requested, return just that version manifest if (match.version) { return { data: stubPackument.versions[stubVersion] as T, isStale: false, cachedAt: Date.now(), } }
return { data: stubPackument as T, isStale: false, cachedAt: Date.now(), } }
// For downloads without fixtures, return zero downloads if (match.type === 'downloads') { if (VERBOSE) process.stderr.write(`[test-fixtures] Stub downloads: ${match.name}\n`) return { data: { downloads: 0, start: '2025-01-01', end: '2025-01-31', package: match.name, } as T, isStale: false, cachedAt: Date.now(), } }
// Log missing fixture for unknown types if (VERBOSE) { process.stderr.write(`[test-fixtures] Missing: ${fixturePath}\n`) }
throw createError({ statusCode: 404, statusMessage: 'Not found', message: `No fixture for ${match.type}: ${match.name}`, }) }
// Handle version-specific requests for packuments (e.g., /create-vite/latest) let data: T = rawData if (match.type === 'packument' && match.version) { const packument = rawData as any let resolvedVersion = match.version
// Resolve 'latest' or dist-tags to actual version if (packument['dist-tags']?.[resolvedVersion]) { resolvedVersion = packument['dist-tags'][resolvedVersion] }
// Return the version manifest instead of full packument const versionData = packument.versions?.[resolvedVersion] if (versionData) { data = versionData as T if (VERBOSE) process.stdout.write( `[test-fixtures] Served: ${match.type}/${match.name}@${resolvedVersion}\n`, ) } else { if (VERBOSE) process.stderr.write( `[test-fixtures] Version not found: ${match.name}@${resolvedVersion}\n`, ) throw createError({ statusCode: 404, statusMessage: 'Version not found', message: `No version ${resolvedVersion} in fixture for ${match.name}`, }) } } else { if (VERBOSE) process.stdout.write(`[test-fixtures] Served: ${match.type}/${match.name}\n`) }
return { data, isStale: false, cachedAt: Date.now() }}
/** * Handle native fetch for esm.sh URLs. */async function handleEsmShFetch( urlStr: string, init: RequestInit | undefined, storage: ReturnType<typeof useStorage>,): Promise<Response> { const method = init?.method?.toUpperCase() || 'GET' const urlObj = new URL(urlStr) const pathname = urlObj.pathname.slice(1) // Remove leading /
// HEAD request - return headers with x-typescript-types if fixture exists if (method === 'HEAD') { // Extract package@version from pathname let pkgVersion = pathname const slashIndex = pkgVersion.indexOf( '/', pkgVersion.includes('@') ? pkgVersion.lastIndexOf('@') + 1 : 0, ) if (slashIndex !== -1) { pkgVersion = pkgVersion.slice(0, slashIndex) }
const fixturePath = `${FIXTURE_PATHS.esmHeaders}:${pkgVersion.replace(/\//g, ':')}.json` const headerData = await storage.getItem<{ 'x-typescript-types': string }>(fixturePath)
if (headerData) { if (VERBOSE) process.stdout.write(`[test-fixtures] fetch HEAD esm.sh: ${pkgVersion}\n`) return new Response(null, { status: 200, headers: { 'x-typescript-types': headerData['x-typescript-types'], 'content-type': 'application/javascript', }, }) }
// No fixture - return 200 without x-typescript-types header (types not available) if (VERBOSE) process.stdout.write(`[test-fixtures] fetch HEAD esm.sh (no fixture): ${pkgVersion}\n`) return new Response(null, { status: 200, headers: { 'content-type': 'application/javascript' }, }) }
// GET request - return .d.ts content if fixture exists if (method === 'GET' && pathname.endsWith('.d.ts')) { const fixturePath = `${FIXTURE_PATHS.esmTypes}:${pathname.replace(/\//g, ':')}` const content = await storage.getItem<string>(fixturePath)
if (content) { if (VERBOSE) process.stdout.write(`[test-fixtures] fetch GET esm.sh: ${pathname}\n`) return new Response(content, { status: 200, headers: { 'content-type': 'application/typescript' }, }) }
// Return a minimal stub .d.ts file instead of 404 // This allows docs tests to work without real type definition fixtures if (VERBOSE) process.stdout.write(`[test-fixtures] fetch GET esm.sh (stub types): ${pathname}\n`) const stubTypes = `// Stub types for ${pathname}export declare function stubFunction(): void;export declare const stubConstant: string;export type StubType = string | number;export interface StubInterface { value: string;}` return new Response(stubTypes, { status: 200, headers: { 'content-type': 'application/typescript' }, }) }
// Other esm.sh requests - return empty response return new Response(null, { status: 200 })}
export default defineNitroPlugin(nitroApp => { const storage = useStorage('fixtures')
if (VERBOSE) { process.stdout.write('[test-fixtures] Test mode active (verbose logging enabled)\n') }
const originalFetch = globalThis.fetch const original$fetch = globalThis.$fetch
// Override native fetch for esm.sh requests and to inject test fixture responses globalThis.fetch = async (input: URL | RequestInfo, init?: RequestInit): Promise<Response> => { const urlStr = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url
if ( urlStr.startsWith('/') || urlStr.startsWith('data:') || urlStr.includes('woff') || urlStr.includes('fonts') ) { return await originalFetch(input, init) }
if (urlStr.startsWith('https://esm.sh/')) { return await handleEsmShFetch(urlStr, init, storage) }
try { const res = await fetchFromFixtures(urlStr, storage) if (res.data) { return new Response(JSON.stringify(res.data), { status: 200, headers: { 'content-type': 'application/json' }, }) } return new Response('Not Found', { status: 404 }) } catch (err: any) { // Convert createError exceptions to proper HTTP responses const statusCode = err?.statusCode || err?.status || 404 const message = err?.message || 'Not Found' return new Response(JSON.stringify({ error: message }), { status: statusCode, headers: { 'content-type': 'application/json' }, }) } }
const $fetch = createFetch({ fetch: globalThis.fetch, })
// Create the wrapper function for globalThis.$fetch const fetchWrapper = async <T = unknown>( url: string, options?: Parameters<typeof $fetch>[1], ): Promise<T> => { if (typeof url === 'string' && !url.startsWith('/')) { return $fetch<T>(url, options as any) } return original$fetch<T>(url, options as any) as any }
// Copy .raw and .create from the created $fetch instance to the wrapper Object.assign(fetchWrapper, { raw: $fetch.raw, create: $fetch.create, })
// Replace globalThis.$fetch with our wrapper (must be done AFTER setting .raw/.create) // @ts-expect-error - wrapper function types don't fully match Nitro's $fetch types globalThis.$fetch = fetchWrapper
// Per-request: set up cachedFetch on the event context nitroApp.hooks.hook('request', event => { event.context.cachedFetch = async (url: string, options?: any) => { return { data: await globalThis.$fetch(url, options), isStale: false, cachedAt: null, } } })})