Skip to content

Commit 527a263

Browse files
committed
refactor: avoid re-overriding per request
1 parent 1668d18 commit 527a263

1 file changed

Lines changed: 176 additions & 147 deletions

File tree

modules/runtime/server/cache.ts

Lines changed: 176 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -316,171 +316,182 @@ function logUnmockedRequest(type: string, detail: string, url: string): void {
316316
)
317317
}
318318

319-
export default defineNitroPlugin(nitroApp => {
320-
const storage = useStorage('fixtures')
319+
/**
320+
* Shared fixture-backed fetch implementation.
321+
* This is used by both cachedFetch and the global $fetch override.
322+
*/
323+
async function fetchFromFixtures<T>(
324+
url: string,
325+
storage: ReturnType<typeof useStorage>,
326+
): Promise<CachedFetchResult<T>> {
327+
// Check for mock responses (OSV, JSR)
328+
const mockResult = getMockForUrl(url)
329+
if (mockResult) {
330+
if (VERBOSE) process.stdout.write(`[test-fixtures] Mock: ${url}\n`)
331+
return { data: mockResult.data as T, isStale: false, cachedAt: Date.now() }
332+
}
321333

322-
if (VERBOSE) {
323-
process.stdout.write('[test-fixtures] Test mode active (verbose logging enabled)\n')
334+
// Check for fast-npm-meta
335+
const fastNpmMetaResult = await handleFastNpmMeta(url, storage)
336+
if (fastNpmMetaResult) {
337+
if (VERBOSE) process.stdout.write(`[test-fixtures] Fast-npm-meta: ${url}\n`)
338+
return { data: fastNpmMetaResult.data as T, isStale: false, cachedAt: Date.now() }
324339
}
325340

326-
nitroApp.hooks.hook('request', event => {
327-
event.context.cachedFetch = async <T = unknown>(
328-
url: string,
329-
_options?: Parameters<typeof $fetch>[1],
330-
_ttl?: number,
331-
): Promise<CachedFetchResult<T>> => {
332-
// Check for mock responses (OSV, JSR)
333-
const mockResult = getMockForUrl(url)
334-
if (mockResult) {
335-
if (VERBOSE) process.stdout.write(`[test-fixtures] Mock: ${url}\n`)
336-
return { data: mockResult.data as T, isStale: false, cachedAt: Date.now() }
337-
}
341+
const match = matchUrlToFixture(url)
338342

339-
// Check for fast-npm-meta
340-
const fastNpmMetaResult = await handleFastNpmMeta(url, storage)
341-
if (fastNpmMetaResult) {
342-
if (VERBOSE) process.stdout.write(`[test-fixtures] Fast-npm-meta: ${url}\n`)
343-
return { data: fastNpmMetaResult.data as T, isStale: false, cachedAt: Date.now() }
344-
}
343+
if (!match) {
344+
logUnmockedRequest('NO FIXTURE PATTERN', 'URL does not match any known fixture pattern', url)
345+
throw createError({
346+
statusCode: 404,
347+
statusMessage: 'No test fixture available',
348+
message: `No fixture pattern matches URL: ${url}`,
349+
})
350+
}
345351

346-
const match = matchUrlToFixture(url)
352+
const fixturePath = getFixturePath(match.type, match.name)
353+
const data = await storage.getItem<T>(fixturePath)
347354

348-
if (!match) {
349-
logUnmockedRequest(
350-
'NO FIXTURE PATTERN',
351-
'URL does not match any known fixture pattern',
352-
url,
353-
)
354-
throw createError({
355-
statusCode: 404,
356-
statusMessage: 'No test fixture available',
357-
message: `No fixture pattern matches URL: ${url}`,
358-
})
355+
if (data === null) {
356+
// For user searches or search queries without fixtures, return empty results
357+
if (match.type === 'user' || match.type === 'search') {
358+
if (VERBOSE) process.stdout.write(`[test-fixtures] Empty ${match.type}: ${match.name}\n`)
359+
return {
360+
data: { objects: [], total: 0, time: new Date().toISOString() } as T,
361+
isStale: false,
362+
cachedAt: Date.now(),
359363
}
364+
}
360365

361-
const fixturePath = getFixturePath(match.type, match.name)
362-
const data = await storage.getItem<T>(fixturePath)
363-
364-
if (data === null) {
365-
// For user searches or search queries without fixtures, return empty results
366-
if (match.type === 'user' || match.type === 'search') {
367-
if (VERBOSE) process.stdout.write(`[test-fixtures] Empty ${match.type}: ${match.name}\n`)
368-
return {
369-
data: { objects: [], total: 0, time: new Date().toISOString() } as T,
370-
isStale: false,
371-
cachedAt: Date.now(),
372-
}
373-
}
366+
// Log missing fixture (but don't spam - these are often expected for dependencies)
367+
if (VERBOSE) {
368+
process.stderr.write(`[test-fixtures] Missing: ${fixturePath}\n`)
369+
}
374370

375-
// Log missing fixture (but don't spam - these are often expected for dependencies)
376-
if (VERBOSE) {
377-
process.stderr.write(`[test-fixtures] Missing: ${fixturePath}\n`)
378-
}
371+
throw createError({
372+
statusCode: 404,
373+
statusMessage: 'Package not found',
374+
message: `No fixture for ${match.type}: ${match.name}`,
375+
})
376+
}
379377

380-
throw createError({
381-
statusCode: 404,
382-
statusMessage: 'Package not found',
383-
message: `No fixture for ${match.type}: ${match.name}`,
384-
})
385-
}
378+
if (VERBOSE) process.stdout.write(`[test-fixtures] Served: ${match.type}/${match.name}\n`)
386379

387-
if (VERBOSE) process.stdout.write(`[test-fixtures] Served: ${match.type}/${match.name}\n`)
380+
return { data, isStale: false, cachedAt: Date.now() }
381+
}
388382

389-
return { data, isStale: false, cachedAt: Date.now() }
390-
}
383+
/**
384+
* Handle native fetch for esm.sh URLs.
385+
*/
386+
async function handleEsmShFetch(
387+
urlStr: string,
388+
init: RequestInit | undefined,
389+
storage: ReturnType<typeof useStorage>,
390+
): Promise<Response | null> {
391+
if (!urlStr.startsWith('https://esm.sh/')) {
392+
return null
393+
}
391394

392-
const original$Fetch = globalThis.$fetch
393-
const originalFetch = globalThis.fetch
395+
const method = init?.method?.toUpperCase() || 'GET'
396+
const urlObj = new URL(urlStr)
397+
const pathname = urlObj.pathname.slice(1) // Remove leading /
394398

395-
globalThis.fetch = async (url: URL | RequestInfo, init?: RequestInit): Promise<Response> => {
396-
const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url
399+
// HEAD request - return headers with x-typescript-types if fixture exists
400+
if (method === 'HEAD') {
401+
// Extract package@version from pathname
402+
let pkgVersion = pathname
403+
const slashIndex = pkgVersion.indexOf(
404+
'/',
405+
pkgVersion.includes('@') ? pkgVersion.lastIndexOf('@') + 1 : 0,
406+
)
407+
if (slashIndex !== -1) {
408+
pkgVersion = pkgVersion.slice(0, slashIndex)
409+
}
397410

398-
// Handle esm.sh requests specially (used by docs feature)
399-
if (urlStr.startsWith('https://esm.sh/')) {
400-
const method = init?.method?.toUpperCase() || 'GET'
401-
const urlObj = new URL(urlStr)
402-
const pathname = urlObj.pathname.slice(1) // Remove leading /
403-
404-
// HEAD request - return headers with x-typescript-types if fixture exists
405-
if (method === 'HEAD') {
406-
// Extract package@version from pathname
407-
let pkgVersion = pathname
408-
const slashIndex = pkgVersion.indexOf(
409-
'/',
410-
pkgVersion.includes('@') ? pkgVersion.lastIndexOf('@') + 1 : 0,
411-
)
412-
if (slashIndex !== -1) {
413-
pkgVersion = pkgVersion.slice(0, slashIndex)
414-
}
415-
416-
const fixturePath = `${FIXTURE_PATHS.esmHeaders}:${pkgVersion.replace(/\//g, ':')}.json`
417-
const headerData = await storage.getItem<{ 'x-typescript-types': string }>(fixturePath)
418-
419-
if (headerData) {
420-
if (VERBOSE) process.stdout.write(`[test-fixtures] fetch HEAD esm.sh: ${pkgVersion}\n`)
421-
return new Response(null, {
422-
status: 200,
423-
headers: {
424-
'x-typescript-types': headerData['x-typescript-types'],
425-
'content-type': 'application/javascript',
426-
},
427-
})
428-
}
429-
430-
// No fixture - return 200 without x-typescript-types header (types not available)
431-
if (VERBOSE)
432-
process.stdout.write(`[test-fixtures] fetch HEAD esm.sh (no fixture): ${pkgVersion}\n`)
433-
return new Response(null, {
434-
status: 200,
435-
headers: { 'content-type': 'application/javascript' },
436-
})
437-
}
411+
const fixturePath = `${FIXTURE_PATHS.esmHeaders}:${pkgVersion.replace(/\//g, ':')}.json`
412+
const headerData = await storage.getItem<{ 'x-typescript-types': string }>(fixturePath)
438413

439-
// GET request - return .d.ts content if fixture exists
440-
if (method === 'GET' && pathname.endsWith('.d.ts')) {
441-
const fixturePath = `${FIXTURE_PATHS.esmTypes}:${pathname.replace(/\//g, ':')}`
442-
const content = await storage.getItem<string>(fixturePath)
443-
444-
if (content) {
445-
if (VERBOSE) process.stdout.write(`[test-fixtures] fetch GET esm.sh: ${pathname}\n`)
446-
return new Response(content, {
447-
status: 200,
448-
headers: { 'content-type': 'application/typescript' },
449-
})
450-
}
451-
452-
// No fixture - return 404 for missing .d.ts files
453-
if (VERBOSE)
454-
process.stdout.write(`[test-fixtures] fetch GET esm.sh (no fixture): ${pathname}\n`)
455-
return new Response('Not Found', {
456-
status: 404,
457-
headers: { 'content-type': 'text/plain' },
458-
})
459-
}
414+
if (headerData) {
415+
if (VERBOSE) process.stdout.write(`[test-fixtures] fetch HEAD esm.sh: ${pkgVersion}\n`)
416+
return new Response(null, {
417+
status: 200,
418+
headers: {
419+
'x-typescript-types': headerData['x-typescript-types'],
420+
'content-type': 'application/javascript',
421+
},
422+
})
423+
}
460424

461-
// Other esm.sh requests - return empty response
462-
return new Response(null, { status: 200 })
463-
}
425+
// No fixture - return 200 without x-typescript-types header (types not available)
426+
if (VERBOSE)
427+
process.stdout.write(`[test-fixtures] fetch HEAD esm.sh (no fixture): ${pkgVersion}\n`)
428+
return new Response(null, {
429+
status: 200,
430+
headers: { 'content-type': 'application/javascript' },
431+
})
432+
}
464433

465-
return originalFetch(url, init)
434+
// GET request - return .d.ts content if fixture exists
435+
if (method === 'GET' && pathname.endsWith('.d.ts')) {
436+
const fixturePath = `${FIXTURE_PATHS.esmTypes}:${pathname.replace(/\//g, ':')}`
437+
const content = await storage.getItem<string>(fixturePath)
438+
439+
if (content) {
440+
if (VERBOSE) process.stdout.write(`[test-fixtures] fetch GET esm.sh: ${pathname}\n`)
441+
return new Response(content, {
442+
status: 200,
443+
headers: { 'content-type': 'application/typescript' },
444+
})
466445
}
467446

468-
// @ts-expect-error invalid global augmentation
469-
globalThis.$fetch = async (url, options) => {
470-
if (typeof url === 'string' && url.startsWith('/')) {
471-
return original$Fetch(url, options)
447+
// No fixture - return 404 for missing .d.ts files
448+
if (VERBOSE)
449+
process.stdout.write(`[test-fixtures] fetch GET esm.sh (no fixture): ${pathname}\n`)
450+
return new Response('Not Found', {
451+
status: 404,
452+
headers: { 'content-type': 'text/plain' },
453+
})
454+
}
455+
456+
// Other esm.sh requests - return empty response
457+
return new Response(null, { status: 200 })
458+
}
459+
460+
// Flag to ensure we only patch globals once
461+
let globalsPatched = false
462+
463+
export default defineNitroPlugin(nitroApp => {
464+
const storage = useStorage('fixtures')
465+
466+
if (VERBOSE) {
467+
process.stdout.write('[test-fixtures] Test mode active (verbose logging enabled)\n')
468+
}
469+
470+
// Patch global fetch/$fetch once, not per-request
471+
if (!globalsPatched) {
472+
globalsPatched = true
473+
474+
const originalFetch = globalThis.fetch
475+
const original$FetchRaw = globalThis.$fetch.raw
476+
477+
// Override native fetch for esm.sh requests
478+
globalThis.fetch = async (input: URL | RequestInfo, init?: RequestInit): Promise<Response> => {
479+
const urlStr =
480+
typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url
481+
482+
const esmResponse = await handleEsmShFetch(urlStr, init, storage)
483+
if (esmResponse) {
484+
return esmResponse
472485
}
473486

474-
const { data } = await event.context.cachedFetch!<any>(url as string, options)
475-
return data
487+
return originalFetch(input, init)
476488
}
477489

478-
// Also override $fetch.raw for esm.sh
479-
const originalFetchRaw = globalThis.$fetch.raw
480-
// @ts-expect-error invalid global augmentation
481-
globalThis.$fetch.raw = async (url, options) => {
490+
// Override $fetch.raw for esm.sh requests
491+
// @ts-expect-error - modifying global
492+
globalThis.$fetch.raw = async (url: string, options?: any) => {
482493
if (typeof url === 'string' && url.startsWith('/')) {
483-
return originalFetchRaw(url, options)
494+
return original$FetchRaw(url, options)
484495
}
485496

486497
// Handle esm.sh requests specially (used by docs feature)
@@ -489,15 +500,33 @@ export default defineNitroPlugin(nitroApp => {
489500
if (esmResult !== null) {
490501
return esmResult
491502
}
492-
// If no fixture found, throw an error in test mode
493-
throw createError({
494-
statusCode: 404,
495-
statusMessage: 'No esm.sh fixture available',
496-
message: `No fixture for esm.sh URL: ${url}`,
497-
})
503+
// No fixture - return response without types (graceful degradation)
504+
if (VERBOSE)
505+
process.stdout.write(`[test-fixtures] $fetch.raw esm.sh (no fixture): ${url}\n`)
506+
return {
507+
status: 200,
508+
statusText: 'OK',
509+
url,
510+
headers: new Headers({ 'content-type': 'application/javascript' }),
511+
_data: null,
512+
}
498513
}
499514

500-
return originalFetchRaw(url, options)
515+
return original$FetchRaw(url, options)
516+
}
517+
518+
// Note: We don't override $fetch itself globally because it needs
519+
// access to event.context.cachedFetch which is per-request
520+
}
521+
522+
// Per-request: set up cachedFetch on the event context
523+
nitroApp.hooks.hook('request', event => {
524+
event.context.cachedFetch = <T = unknown>(
525+
url: string,
526+
_options?: Parameters<typeof $fetch>[1],
527+
_ttl?: number,
528+
): Promise<CachedFetchResult<T>> => {
529+
return fetchFromFixtures<T>(url, storage)
501530
}
502531
})
503532
})

0 commit comments

Comments
 (0)