Skip to content

Commit 1668d18

Browse files
committed
fix: overwrite globalThis.fetch instead
1 parent 99b5962 commit 1668d18

2 files changed

Lines changed: 77 additions & 116 deletions

File tree

modules/runtime/server/cache.ts

Lines changed: 76 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -156,31 +156,6 @@ async function handleFastNpmMeta(
156156
}
157157
}
158158

159-
/**
160-
* Handle esm.sh requests for the standard $fetch (returns data directly).
161-
*/
162-
async function handleEsmShRequest(
163-
url: string,
164-
_options: unknown,
165-
storage: ReturnType<typeof useStorage>,
166-
): Promise<unknown | null> {
167-
const urlObj = new URL(url)
168-
if (urlObj.hostname !== 'esm.sh') return null
169-
170-
// For GET requests to .d.ts files, return the file content
171-
const typesPath = urlObj.pathname.slice(1) // Remove leading /
172-
if (typesPath.endsWith('.d.ts')) {
173-
const fixturePath = `${FIXTURE_PATHS.esmTypes}:${typesPath.replace(/\//g, ':')}`
174-
const content = await storage.getItem<string>(fixturePath)
175-
if (content) {
176-
if (VERBOSE) process.stdout.write(`[test-fixtures] esm.sh types: ${typesPath}\n`)
177-
return content
178-
}
179-
}
180-
181-
return null
182-
}
183-
184159
/**
185160
* Handle esm.sh requests for $fetch.raw (returns response object with headers).
186161
*/
@@ -348,80 +323,6 @@ export default defineNitroPlugin(nitroApp => {
348323
process.stdout.write('[test-fixtures] Test mode active (verbose logging enabled)\n')
349324
}
350325

351-
// Override native fetch globally to intercept esm.sh requests from ofetch
352-
// This is needed because server/utils/docs/client.ts imports $fetch from ofetch
353-
// directly, bypassing our globalThis.$fetch override
354-
const originalNativeFetch = globalThis.fetch
355-
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
356-
const url =
357-
typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url
358-
if (url.includes('esm.sh')) {
359-
const method = init?.method || 'GET'
360-
361-
// HEAD request - return headers with x-typescript-types
362-
if (method === 'HEAD') {
363-
const urlObj = new URL(url)
364-
const pathname = urlObj.pathname.slice(1)
365-
let pkgVersion = pathname
366-
const slashIndex = pkgVersion.indexOf(
367-
'/',
368-
pkgVersion.includes('@') ? pkgVersion.lastIndexOf('@') + 1 : 0,
369-
)
370-
if (slashIndex !== -1) {
371-
pkgVersion = pkgVersion.slice(0, slashIndex)
372-
}
373-
374-
const fixturePath = `${FIXTURE_PATHS.esmHeaders}:${pkgVersion.replace(/\//g, ':')}.json`
375-
const headerData = await storage.getItem<{ 'x-typescript-types': string }>(fixturePath)
376-
377-
if (headerData) {
378-
if (VERBOSE) process.stdout.write(`[test-fixtures] fetch HEAD esm.sh: ${pkgVersion}\n`)
379-
return new Response(null, {
380-
status: 200,
381-
headers: new Headers({
382-
'x-typescript-types': headerData['x-typescript-types'],
383-
'content-type': 'application/javascript',
384-
}),
385-
})
386-
}
387-
388-
// No fixture - return response without types header
389-
if (VERBOSE)
390-
process.stdout.write(`[test-fixtures] fetch HEAD esm.sh (no fixture): ${pkgVersion}\n`)
391-
return new Response(null, {
392-
status: 200,
393-
headers: new Headers({ 'content-type': 'application/javascript' }),
394-
})
395-
}
396-
397-
// GET request for .d.ts files
398-
if (method === 'GET') {
399-
const urlObj = new URL(url)
400-
const pathname = urlObj.pathname.slice(1)
401-
const fixturePath = `${FIXTURE_PATHS.esmTypes}:${pathname.replace(/\//g, ':')}`
402-
const content = await storage.getItem<string>(fixturePath)
403-
404-
if (content) {
405-
if (VERBOSE) process.stdout.write(`[test-fixtures] fetch GET esm.sh: ${pathname}\n`)
406-
return new Response(content, {
407-
status: 200,
408-
headers: new Headers({ 'content-type': 'application/typescript' }),
409-
})
410-
}
411-
412-
// No fixture - return 404 so the loader skips this file
413-
if (VERBOSE)
414-
process.stdout.write(`[test-fixtures] fetch GET esm.sh (no fixture): ${pathname}\n`)
415-
return new Response('Not Found', {
416-
status: 404,
417-
headers: new Headers({ 'content-type': 'text/plain' }),
418-
})
419-
}
420-
}
421-
422-
return originalNativeFetch(input, init)
423-
}
424-
425326
nitroApp.hooks.hook('request', event => {
426327
event.context.cachedFetch = async <T = unknown>(
427328
url: string,
@@ -488,26 +389,86 @@ export default defineNitroPlugin(nitroApp => {
488389
return { data, isStale: false, cachedAt: Date.now() }
489390
}
490391

491-
const originalFetch = globalThis.$fetch
392+
const original$Fetch = globalThis.$fetch
393+
const originalFetch = globalThis.fetch
492394

493-
// @ts-expect-error invalid global augmentation
494-
globalThis.$fetch = async (url, options) => {
495-
if (typeof url === 'string' && url.startsWith('/')) {
496-
return originalFetch(url, options)
497-
}
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
498397

499398
// Handle esm.sh requests specially (used by docs feature)
500-
if (typeof url === 'string' && url.includes('esm.sh')) {
501-
const esmResult = await handleEsmShRequest(url, options, storage)
502-
if (esmResult !== null) {
503-
return esmResult
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+
})
504437
}
505-
// If no fixture found, throw an error in test mode
506-
throw createError({
507-
statusCode: 404,
508-
statusMessage: 'No esm.sh fixture available',
509-
message: `No fixture for esm.sh URL: ${url}`,
510-
})
438+
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+
}
460+
461+
// Other esm.sh requests - return empty response
462+
return new Response(null, { status: 200 })
463+
}
464+
465+
return originalFetch(url, init)
466+
}
467+
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)
511472
}
512473

513474
const { data } = await event.context.cachedFetch!<any>(url as string, options)

server/utils/docs/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ async function getTypesUrl(packageName: string, version: string): Promise<string
164164

165165
try {
166166
const response = await $fetch.raw(url, {
167-
method: 'HEAD' as 'GET', // Cast to satisfy Nitro's typed $fetch (external URL, any method is fine)
167+
method: 'HEAD',
168168
timeout: FETCH_TIMEOUT_MS,
169169
})
170170
return response.headers.get('x-typescript-types')

0 commit comments

Comments
 (0)