From a76dea052c407d8635b8c3fd5902607ef9828c11 Mon Sep 17 00:00:00 2001 From: golden-fox07 Date: Mon, 31 Aug 2026 17:26:14 +0530 Subject: [PATCH 1/4] fix: prevent source map references from being resolved --- server/utils/docs/client.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/utils/docs/client.ts b/server/utils/docs/client.ts index ff3d56d863..e8786a474b 100644 --- a/server/utils/docs/client.ts +++ b/server/utils/docs/client.ts @@ -130,8 +130,14 @@ function createLoader(): ( * * Handles resolving relative imports and esm.sh redirects. */ -function createResolver(): (specifier: string, referrer: string) => string { +export function createResolver(): (specifier: string, referrer: string) => string { return (specifier: string, referrer: string) => { + // Source map references are not modules. Resolving bare source map + // filenames can incorrectly treat them as package specifiers + if (specifier.endsWith('.map')) { + return specifier + } + // Handle relative imports if (specifier.startsWith('.') || specifier.startsWith('/')) { return new URL(specifier, referrer).toString() From 25156819ccc88ed434c7ad25e2fc49591ebb9c3f Mon Sep 17 00:00:00 2001 From: golden-fox07 Date: Mon, 31 Aug 2026 18:35:11 +0530 Subject: [PATCH 2/4] test: cover source map references in resolver --- test/unit/server/utils/docs/client.spec.ts | 47 ++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 test/unit/server/utils/docs/client.spec.ts diff --git a/test/unit/server/utils/docs/client.spec.ts b/test/unit/server/utils/docs/client.spec.ts new file mode 100644 index 0000000000..f2cb1837f8 --- /dev/null +++ b/test/unit/server/utils/docs/client.spec.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest' +import { createResolver } from '#server/utils/docs/client' + +// ============================================================================= +// Issue #2739: sourceMappingURL specifiers resolved as package names +// https://github.com/npmx-dev/npmx.dev/issues/2739 +// ============================================================================= + +describe('issue #2739 - source map specifiers', () => { + const resolve = createResolver() + const referrer = 'https://esm.sh/effect-web-midi@0.2.1/dist-types/index.d.ts' + + it('does not resolve source map references', () => { + expect(resolve('index.d.ts.map', referrer)).toBe('index.d.ts.map') + }) + + it('does not resolve relative source map references', () => { + expect(resolve('./index.d.ts.map', referrer)).toBe('./index.d.ts.map') + }) +}) + +describe('createResolver', () => { + const resolve = createResolver() + const referrer = 'https://esm.sh/effect-web-midi@0.2.1/dist-types/index.d.ts' + + it('resolves relative imports', () => { + expect(resolve('./src/index.d.ts', referrer)).toBe( + 'https://esm.sh/effect-web-midi@0.2.1/dist-types/src/index.d.ts', + ) + }) + + it('resolves absolute paths', () => { + expect(resolve('/foo.d.ts', referrer)).toBe('https://esm.sh/foo.d.ts') + }) + + it('resolves bare specifiers', () => { + expect(resolve('effect', referrer)).toBe('https://esm.sh/effect') + }) + + it('leaves absolute URLs unchanged', () => { + expect(resolve('https://esm.sh/effect', referrer)).toBe('https://esm.sh/effect') + }) + + it('leaves node builtins unchanged', () => { + expect(resolve('node:fs', referrer)).toBe('node:fs') + }) +}) From 9e1a4e8310a3f2a68376abdcff1934035383e355 Mon Sep 17 00:00:00 2001 From: Felix Schneider <99918022+trueberryless@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:14:10 +0200 Subject: [PATCH 3/4] feat: tighten source map check with regex; add loader early exit --- server/utils/docs/client.ts | 10 ++- test/unit/server/utils/docs/client.spec.ts | 80 ++++++++++++++++------ 2 files changed, 68 insertions(+), 22 deletions(-) diff --git a/server/utils/docs/client.ts b/server/utils/docs/client.ts index e8786a474b..855ce1a06e 100644 --- a/server/utils/docs/client.ts +++ b/server/utils/docs/client.ts @@ -18,6 +18,8 @@ import { isBuiltin } from 'node:module' /** Timeout for fetching modules in milliseconds */ const FETCH_TIMEOUT_MS = 30 * 1000 +const IS_SOURCE_MAP = /\.(?:js|mjs|cjs|d\.[mc]?ts)\.map$/ + // ============================================================================= // Main Export // ============================================================================= @@ -70,7 +72,7 @@ interface LoadResponse { * * Fetches modules from URLs using fetch(), with proper timeout handling. */ -function createLoader(): ( +export function createLoader(): ( specifier: string, isDynamic?: boolean, cacheSetting?: string, @@ -82,6 +84,10 @@ function createLoader(): ( _cacheSetting?: string, _checksum?: string, ) => { + if (IS_SOURCE_MAP.test(specifier)) { + return undefined + } + const url = URL.parse(specifier) if (url === null) { @@ -134,7 +140,7 @@ export function createResolver(): (specifier: string, referrer: string) => strin return (specifier: string, referrer: string) => { // Source map references are not modules. Resolving bare source map // filenames can incorrectly treat them as package specifiers - if (specifier.endsWith('.map')) { + if (IS_SOURCE_MAP.test(specifier)) { return specifier } diff --git a/test/unit/server/utils/docs/client.spec.ts b/test/unit/server/utils/docs/client.spec.ts index f2cb1837f8..c329d386dc 100644 --- a/test/unit/server/utils/docs/client.spec.ts +++ b/test/unit/server/utils/docs/client.spec.ts @@ -1,23 +1,5 @@ -import { describe, expect, it } from 'vitest' -import { createResolver } from '#server/utils/docs/client' - -// ============================================================================= -// Issue #2739: sourceMappingURL specifiers resolved as package names -// https://github.com/npmx-dev/npmx.dev/issues/2739 -// ============================================================================= - -describe('issue #2739 - source map specifiers', () => { - const resolve = createResolver() - const referrer = 'https://esm.sh/effect-web-midi@0.2.1/dist-types/index.d.ts' - - it('does not resolve source map references', () => { - expect(resolve('index.d.ts.map', referrer)).toBe('index.d.ts.map') - }) - - it('does not resolve relative source map references', () => { - expect(resolve('./index.d.ts.map', referrer)).toBe('./index.d.ts.map') - }) -}) +import { describe, expect, it, vi, afterEach } from 'vitest' +import { createResolver, createLoader } from '#server/utils/docs/client' describe('createResolver', () => { const resolve = createResolver() @@ -44,4 +26,62 @@ describe('createResolver', () => { it('leaves node builtins unchanged', () => { expect(resolve('node:fs', referrer)).toBe('node:fs') }) + + // Issue #2739: Prevent sourceMappingURL specifiers from being resolved as package names + // https://github.com/npmx-dev/npmx.dev/issues/2739 + it('does not resolve source map references', () => { + expect(resolve('index.d.ts.map', referrer)).toBe('index.d.ts.map') + expect(resolve('./index.d.ts.map', referrer)).toBe('./index.d.ts.map') + }) + + it('does not falsely flag packages containing the word map', () => { + expect(resolve('source-map', referrer)).toBe('https://esm.sh/source-map') + expect(resolve('my.package.map', referrer)).toBe('https://esm.sh/my.package.map') + }) +}) + +describe('createLoader', () => { + const load = createLoader() + + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('returns undefined for source map files', async () => { + expect(await load('index.d.ts.map')).toBeUndefined() + expect(await load('https://esm.sh/foo.js.map')).toBeUndefined() + }) + + it('returns undefined for invalid or non-HTTP URLs', async () => { + expect(await load('not-a-valid-url')).toBeUndefined() + expect(await load('node:fs')).toBeUndefined() + expect(await load('file:///etc/passwd')).toBeUndefined() + }) + + it('fetches and returns module content for valid URLs', async () => { + const mockFetch = vi.fn().mockResolvedValue({ + status: 200, + url: 'https://esm.sh/foo@1.0.0/index.d.ts', + headers: new Headers({ 'content-type': 'application/typescript' }), + _data: { text: async () => 'export const foo = 1;' }, + }) + vi.stubGlobal('$fetch', { raw: mockFetch }) + + const result = await load('https://esm.sh/foo@1.0.0/index.d.ts') + + expect(result).toEqual({ + kind: 'module', + specifier: 'https://esm.sh/foo@1.0.0/index.d.ts', + headers: { 'content-type': 'application/typescript' }, + content: 'export const foo = 1;', + }) + expect(mockFetch).toHaveBeenCalledTimes(1) + }) + + it('returns undefined if the fetch fails (non-200 status)', async () => { + const mockFetch = vi.fn().mockResolvedValue({ status: 404 }) + vi.stubGlobal('$fetch', { raw: mockFetch }) + + expect(await load('https://esm.sh/foo@1.0.0/missing.d.ts')).toBeUndefined() + }) }) From 055563c72a901b4211edac7283b8cd7f73436c0b Mon Sep 17 00:00:00 2001 From: Felix Schneider <99918022+trueberryless@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:28:08 +0200 Subject: [PATCH 4/4] feat: allow fragments and params --- server/utils/docs/client.ts | 2 +- test/unit/server/utils/docs/client.spec.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/server/utils/docs/client.ts b/server/utils/docs/client.ts index 855ce1a06e..f6a277d17f 100644 --- a/server/utils/docs/client.ts +++ b/server/utils/docs/client.ts @@ -18,7 +18,7 @@ import { isBuiltin } from 'node:module' /** Timeout for fetching modules in milliseconds */ const FETCH_TIMEOUT_MS = 30 * 1000 -const IS_SOURCE_MAP = /\.(?:js|mjs|cjs|d\.[mc]?ts)\.map$/ +const IS_SOURCE_MAP = /\.(?:js|mjs|cjs|d\.[mc]?ts)\.map(?:[?#].*)?$/ // ============================================================================= // Main Export diff --git a/test/unit/server/utils/docs/client.spec.ts b/test/unit/server/utils/docs/client.spec.ts index c329d386dc..96c0e9bc39 100644 --- a/test/unit/server/utils/docs/client.spec.ts +++ b/test/unit/server/utils/docs/client.spec.ts @@ -50,6 +50,8 @@ describe('createLoader', () => { it('returns undefined for source map files', async () => { expect(await load('index.d.ts.map')).toBeUndefined() expect(await load('https://esm.sh/foo.js.map')).toBeUndefined() + expect(await load('index.d.ts.map?v=123')).toBeUndefined() + expect(await load('https://esm.sh/foo.js.map#hash')).toBeUndefined() }) it('returns undefined for invalid or non-HTTP URLs', async () => {