From 45fcd28b4018723bdb508a9229b74ab9eb749f75 Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:34:15 +0530 Subject: [PATCH] fix(javascript): resolve bare source-map and worker URLs next to the script (#890) * fix(javascript): resolve bare source-map and worker URLs next to the script A sourceMappingURL like `main.js.map` (the default of webpack, esbuild, rollup, Vite and rolldown) and `new Worker("worker.js")` were resolved as module specifiers, so the bare name was looked up as an npm package and the map or worker script was never linked. Resolve them as URLs relative to the declaring script. * fix(javascript): tighten url-reference resolution edge cases Use looksExternal for non-module contexts so protocol-relative URLs cannot be misread as root-absolute paths, cover url-reference unit cases, and assert bare service-worker script resolution. --------- Co-authored-by: Cursor Agent --- .../JavaScriptArtifactGraphFindings.ts | 42 +++++++++----- .../JavaScriptArtifactPathResolution.test.ts | 55 ++++++++++++++++++ .../JavaScriptArtifactPathResolution.ts | 8 ++- .../javascriptSourceMapLocations.test.ts | 58 ++++++++++++++++++- 4 files changed, 143 insertions(+), 20 deletions(-) diff --git a/src/application/javascript/JavaScriptArtifactGraphFindings.ts b/src/application/javascript/JavaScriptArtifactGraphFindings.ts index e3bf4c6e81..9158ff906f 100644 --- a/src/application/javascript/JavaScriptArtifactGraphFindings.ts +++ b/src/application/javascript/JavaScriptArtifactGraphFindings.ts @@ -268,11 +268,12 @@ const addSourceMapEdge = ( context: JavaScriptArtifactGraphContext, input: FindingInput, ): void => { - const resolvedPath = resolveArtifactPath( - input.value.declared_url, - input.file.path, - context.filesByPath, - ); + const resolvedPath = resolveArtifactPathByContext({ + declaredPath: input.value.declared_url, + sourcePath: input.file.path, + context: "url-reference", + files: context.filesByPath, + }).resolved_path; const resolvedFile = resolvedPath === null ? undefined : context.filesByPath.get(resolvedPath); const resolvedNode = @@ -341,17 +342,26 @@ const resolveReference = ( ? null : { path: `${sourcePath}#module:${specifier}`, file, node: module }; } - const path = resolveArtifactPath( - specifier, - sourcePath, - context.filesByPath, - reference.kind === "require" - ? "require" - : reference.kind === "static-import" || - reference.kind === "dynamic-import" - ? "import" - : undefined, - ); + // Worker scripts are URLs relative to the script, not module specifiers. + const path = + reference.kind === "worker" || reference.kind === "service-worker" + ? resolveArtifactPathByContext({ + declaredPath: specifier, + sourcePath, + context: "url-reference", + files: context.filesByPath, + }).resolved_path + : resolveArtifactPath( + specifier, + sourcePath, + context.filesByPath, + reference.kind === "require" + ? "require" + : reference.kind === "static-import" || + reference.kind === "dynamic-import" + ? "import" + : undefined, + ); if (path === null) return null; const file = context.filesByPath.get(path); const node = context.fileNodes.get(path); diff --git a/src/application/javascript/JavaScriptArtifactPathResolution.test.ts b/src/application/javascript/JavaScriptArtifactPathResolution.test.ts index 3f6232def5..5223926e83 100644 --- a/src/application/javascript/JavaScriptArtifactPathResolution.test.ts +++ b/src/application/javascript/JavaScriptArtifactPathResolution.test.ts @@ -562,6 +562,61 @@ describe("contextual JavaScript module identity", () => { }); }); +describe("url-reference JavaScript artifact path resolution", () => { + it("resolves sibling paths and rejects external URL forms", () => { + const files = fileMap([ + file("dist/app.js", "root"), + file("dist/app.js.map", "root"), + file("dist/worker.js", "root"), + ]); + + expect( + resolve({ + declaredPath: "app.js.map", + sourcePath: "dist/app.js", + context: "url-reference", + files, + }), + ).toMatchObject({ + resolution_status: "resolved", + resolved_path: "dist/app.js.map", + }); + expect( + resolve({ + declaredPath: "app.js.map?v=1#section", + sourcePath: "dist/app.js", + context: "url-reference", + files, + }), + ).toMatchObject({ + resolution_status: "resolved", + resolved_path: "dist/app.js.map", + }); + expect( + resolve({ + declaredPath: "//cdn.example.com/app.js.map", + sourcePath: "dist/app.js", + context: "url-reference", + files, + }), + ).toMatchObject({ + resolution_status: "external", + resolved_path: null, + }); + expect( + resolve({ + declaredPath: "https://cdn.example.com/app.js.map", + sourcePath: "dist/app.js", + context: "url-reference", + files, + }), + ).toMatchObject({ + resolution_status: "external", + resolved_path: null, + }); + }); +}); + const resolve = (input: ResolveArtifactPathInput) => resolveArtifactPathByContext(input); diff --git a/src/application/javascript/JavaScriptArtifactPathResolution.ts b/src/application/javascript/JavaScriptArtifactPathResolution.ts index 57a5012b02..3450ffd20b 100644 --- a/src/application/javascript/JavaScriptArtifactPathResolution.ts +++ b/src/application/javascript/JavaScriptArtifactPathResolution.ts @@ -13,7 +13,8 @@ type ArtifactPathResolutionContext = | "package-entrypoint" | "filesystem-expression" | "module-specifier" - | "html-reference"; + | "html-reference" + | "url-reference"; type UnresolvedArtifactPathStatus = | "not-found" @@ -127,6 +128,7 @@ const contextualCandidate = ( const { context } = input; const declared = context === "html-reference" || + context === "url-reference" || (context === "module-specifier" && input.moduleKind !== "require") ? stripQueryAndFragment(input.declaredPath) : input.declaredPath; @@ -140,9 +142,9 @@ const contextualCandidate = ( ]); if (!declared.startsWith(".") && !declared.startsWith("/")) return bareModuleCandidate(input, declared); - } else if (hasScheme(declared)) + } else if (looksExternal(declared)) return unresolvedOutcome(input, "external", [ - "URL schemes are outside this local artifact path context.", + "URL schemes and protocol-relative URLs are outside this local artifact path context.", ]); const relative = declared.startsWith("/") ? declared.slice(1) : declared; return declared.startsWith("/") diff --git a/tests/boundary/filesystem/javascriptSourceMapLocations.test.ts b/tests/boundary/filesystem/javascriptSourceMapLocations.test.ts index 089e89da57..e8be59a4b0 100644 --- a/tests/boundary/filesystem/javascriptSourceMapLocations.test.ts +++ b/tests/boundary/filesystem/javascriptSourceMapLocations.test.ts @@ -1,5 +1,5 @@ import { createHash } from "node:crypto"; -import { writeFile } from "node:fs/promises"; +import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it } from "vitest"; @@ -42,3 +42,59 @@ it.each(["\n", "\r\n", "\r", "\u2028", "\u2029"])( }); }, ); + +it("resolves a bare source-map file name and worker URL next to the script", async () => { + const root = await createTestTempDirectory("rea-source-map-relative-"); + await mkdir(join(root, "dist")); + await writeFile( + join(root, "dist", "app.js"), + [ + 'new Worker("worker.js");', + 'navigator.serviceWorker.register("sw.js");', + "//# sourceMappingURL=app.js.map", + ].join("\n"), + ); + await writeFile( + join(root, "dist", "worker.js"), + "self.onmessage = () => {};", + ); + await writeFile(join(root, "dist", "sw.js"), "self.onfetch = () => {};"); + await writeFile( + join(root, "dist", "app.js.map"), + JSON.stringify({ + version: 3, + sources: ["../src/app.js"], + sourcesContent: ['new Worker("worker.js");'], + names: [], + mappings: "AAAA", + }), + ); + const result = await reconstructJavaScriptArtifact({ input_path: root }); + const nodes = new Map(result.graph.nodes.map((node) => [node.node_id, node])); + const mapEdge = result.graph.edges.find( + ({ relation, properties }) => + relation === "maps_to" && properties.declared_url === "app.js.map", + ); + expect(mapEdge?.properties).toMatchObject({ + declared_url: "app.js.map", + resolved_path: "dist/app.js.map", + }); + expect(nodes.get(mapEdge?.target_node_id ?? "")?.identity.strategy).toBe( + "content-digest", + ); + const workerEdge = result.graph.edges.find( + ({ relation, source_node_id }) => + relation === "maps_to" && nodes.get(source_node_id)?.kind === "worker", + ); + expect(workerEdge?.properties).toMatchObject({ + resolved_path: "dist/worker.js", + }); + const serviceWorkerEdge = result.graph.edges.find( + ({ relation, source_node_id }) => + relation === "maps_to" && + nodes.get(source_node_id)?.kind === "service-worker", + ); + expect(serviceWorkerEdge?.properties).toMatchObject({ + resolved_path: "dist/sw.js", + }); +});