fix(javascript): preserve map boundaries and loader provenance

Share embedded-map version admission and derive indexed overlap from represented mappings. Carry createRequire filename bases through worker facts and graph identity. Select factory export bindings at each bundler owner.

Validated accepted and malformed source maps through real CLI and stdio MCP codec workflows; verified loader and esbuild graph output against Node runtime controls. Rebased consumer checks passed.

Fixes #6803

Fixes #6614

Fixes #6798

Fixes #6797
This commit is contained in:
morluto
2026-10-24 01:00:14 +08:00
parent d78892eb4b
commit ccba355fa9
15 changed files with 373 additions and 32 deletions
+10 -1
View File
@@ -252,7 +252,8 @@ Selected bounded text is then parsed as inert data to recover:
- Electron preload paths and renderer files visible in static syntax;
- local HTML script entrypoints;
- Webpack and Rspack chunk registrations and module factories represented as
AST literals;
AST literals, plus esbuild `__commonJS` module tables; recovered esbuild
CommonJS exports use its factory's `exports`-then-`module` parameter order;
- static imports, dynamic imports, CommonJS `require` calls, workers, and
service workers (only `new Worker(new URL(path, import.meta.url))` resolves
relative to the calling module; string worker and service-worker paths use
@@ -761,6 +762,14 @@ URL targets before selecting a later package-local file; an unsupported bare
package or recursive alias keeps its unresolved outcome rather than selecting
a later fallback.
For a proven `createRequire(new URL(relativeFile, import.meta.url))` call,
relative `require()` edges use the selected filename's directory and retain the
source-relative `resolution_base_path` on the graph edge. This follows Node's
`createRequire(filename)` contract; the path is a loader-resolution fact and
does not change the source file or call-site Evidence identity. If the filename
cannot be proven, REA keeps the require target unresolved rather than silently
resolving it from the caller's directory.
ESM relative module paths, legacy package subpaths without exports, and selected
package exports targets use URL suffix handling and one percent-decoding pass.
Bare specifiers retain their complete query/fragment text for exports key matching.
+8 -1
View File
@@ -67,7 +67,14 @@ The adapter uses the locked upstream
without modifying its code. It validates map/VLQ structure before allocation,
normalizes nested indexed offsets before upstream flattening, checks section
boundaries and indexes, and retains raw declaration identity. It supports regular
maps and inline indexed sections. External section URLs return an actionable
maps and inline indexed sections. Numeric `version: 3` is required on the root
and every embedded map, including indexed ancestors. Invalid embedded headers
report their section path and cannot produce descendant mapping facts. This
uses the rejection option for version errors allowed by
[ECMA-426](https://tc39.es/ecma426/#sec-decodeindexsourcemap).
Equal section starts are allowed when earlier sections contribute no mappings;
actual decoded mappings must stay before the next section boundary.
External section URLs return an actionable
unsupported-profile error. This is a defined decoder profile, not a claim to
implement every current source-map extension.
@@ -1,4 +1,5 @@
import { createHash } from "node:crypto";
import { posix } from "node:path";
import { compactStringIdentityKey } from "../../domain/compactStringIdentity.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
@@ -306,6 +307,7 @@ const addReference = (
kind: input.value.kind,
specifier: input.value.specifier,
expression: input.value.expression,
resolution_base_path: input.value.resolution_base_path ?? null,
resolution_mode:
input.value.kind === "static-import" ||
input.value.kind === "dynamic-import" ||
@@ -634,6 +636,12 @@ const resolveReference = (
if (specifier === null) return null;
const isWorker =
reference.kind === "worker" || reference.kind === "service-worker";
const resolutionSourcePath =
reference.kind === "require" && reference.resolution_base_path !== undefined
? posix.normalize(
posix.join(posix.dirname(sourcePath), reference.resolution_base_path),
)
: sourcePath;
const chunk = isWorker
? null
: resolveBundlerChunkReference(context, sourcePath, reference);
@@ -668,7 +676,7 @@ const resolveReference = (
})
: resolveArtifactPathByContext({
declaredPath: specifier,
sourcePath,
sourcePath: resolutionSourcePath,
context: "module-specifier",
files: context.filesByPath,
jsonDocuments: context.analysis.jsonDocuments,
@@ -679,7 +687,7 @@ const resolveReference = (
"dynamic-import" === reference.kind
? {
moduleKind: moduleResolutionModeForJavaScriptReference(
sourcePath,
resolutionSourcePath,
reference.kind,
),
}
@@ -5,10 +5,7 @@ import * as t from "@babel/types";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import {
runtimeFunctionParameters,
semanticStaticPropertyKey,
} from "./javascriptAstValues.js";
import { semanticStaticPropertyKey } from "./javascriptAstValues.js";
import { stringValue } from "./javascriptStaticAnalysisHelpers.js";
import {
resolveSemanticBindingState,
@@ -36,15 +33,18 @@ export const fingerprintJavaScriptAst = (node: t.Node): string => {
export const collectJavaScriptExports = (
node: t.Node,
bindings?: JavaScriptSemanticAnalysisState,
exportsParameter?: t.Identifier,
): StaticExports => {
const exports = new Set<string>();
const parameter = t.isFunction(node)
? runtimeFunctionParameters(node)[1]
: undefined;
const factoryExports =
bindings !== undefined && t.isFunction(node) && t.isIdentifier(parameter)
? resolveSemanticBindingState(bindings, parameter, parameter.name)
?.bindingId
bindings !== undefined &&
t.isFunction(node) &&
exportsParameter !== undefined
? resolveSemanticBindingState(
bindings,
exportsParameter,
exportsParameter.name,
)?.bindingId
: undefined;
const isExportsAlias = (reference: t.Node | null): boolean => {
if (!t.isIdentifier(reference)) return false;
@@ -256,7 +256,13 @@ const recoverBundlerModule = (
if (key.startsWith("[computed@") || key.startsWith("[unknown@"))
accumulator.unknownFindings += 1;
const fingerprint = fingerprintJavaScriptAst(factory);
const exportsValue = collectJavaScriptExports(factory, bindings);
const exportsParameter =
runtimeFunctionParameters(factory)[runtime === "esbuild-commonjs" ? 0 : 1];
const exportsValue = collectJavaScriptExports(
factory,
bindings,
t.isIdentifier(exportsParameter) ? exportsParameter : undefined,
);
const requireName = factoryRequireName(factory);
const requireParameter = runtimeFunctionParameters(factory)[2];
const requireBindingId = t.isIdentifier(requireParameter)
@@ -91,6 +91,9 @@ export const inspectCall = (
node,
kind: "require",
specifier: requireCall.specifier,
...(requireCall.resolutionBasePath === undefined
? {}
: { resolutionBasePath: requireCall.resolutionBasePath }),
});
const chunkLoad = chunkLoadSpecifier(node, bindings, context);
if (chunkLoad !== null)
@@ -265,7 +268,10 @@ const requireCallSpecifier = (
bindings: JavaScriptSemanticAnalysisState,
context: FindingContext,
resolveMemberChanged = false,
): { readonly specifier: string | undefined } | null => {
): {
readonly specifier: string | undefined;
readonly resolutionBasePath?: string;
} | null => {
const call = semanticRequireCallAt(
node,
bindings,
@@ -280,7 +286,12 @@ const requireCallSpecifier = (
context.accumulator.unknownFindings += 1;
return { specifier: undefined };
}
return { specifier: argumentValue(call.arguments?.[0]) };
return {
specifier: argumentValue(call.arguments?.[0]),
...(call.sourceRelativePath === undefined
? {}
: { resolutionBasePath: call.sourceRelativePath }),
};
};
/** Examine member-loader receivers only after shared alias mutation facts settle. */
@@ -311,6 +322,9 @@ export const finishModuleRequireCalls = (
node,
kind: "require",
specifier: call.specifier,
...(call.resolutionBasePath === undefined
? {}
: { resolutionBasePath: call.resolutionBasePath }),
});
}
context.accumulator.pendingModuleRequireCalls.length = 0;
@@ -933,11 +947,14 @@ export const addReference = (
: { specifier, expression: null };
addLocatedFinding(context, {
collection: context.accumulator.references,
key: `reference\0${input.kind}\0${reference.specifier ?? reference.expression}`,
key: `reference\0${input.kind}\0${reference.specifier ?? reference.expression}\0${input.resolutionBasePath ?? ""}`,
node: input.node,
value: {
kind: input.kind,
...reference,
...(input.resolutionBasePath === undefined
? {}
: { resolution_base_path: input.resolutionBasePath }),
module_key: null,
location: range(input.node),
},
@@ -113,6 +113,7 @@ export interface JavaScriptReferenceInput {
readonly node: t.Node;
readonly kind: JavaScriptStaticReference["kind"];
readonly specifier: string | undefined;
readonly resolutionBasePath?: string;
}
/** Candidate route or network endpoint literal. */
@@ -31,6 +31,8 @@ interface JavaScriptStaticReferenceState {
readonly module_key: string | null;
/** Recovered factory runtime; absent in older or unbundled findings. */
readonly module_runtime?: string;
/** Source-relative filename selected by a proven createRequire factory. */
readonly resolution_base_path?: string;
readonly location: JavaScriptSourceRange;
}
+5 -5
View File
@@ -78,6 +78,11 @@ export const classifySourceMapSection = (
reason:
"Indexed source-map section requires an embedded map or external URL.",
};
if (!isVersion3Map(section.map))
return {
kind: "invalid",
reason: "Indexed source-map section map is not a version 3 map.",
};
return { kind: "embedded", map: section.map };
};
@@ -169,11 +174,6 @@ export const inspectSourceMapEnvelope = (root: unknown): SourceMapEnvelope => {
externalSections.push({ path, url: classification.url });
else if (classification.kind === "invalid")
invalidSections.push({ path, reason: classification.reason });
else if (!isVersion3Map(classification.map))
invalidSections.push({
path,
reason: "Indexed source-map section map is not a version 3 map.",
});
else children.push({ map: classification.map, path });
}
for (let index = children.length - 1; index >= 0; index -= 1) {
@@ -296,6 +296,7 @@ const electron = z
}));
const referenceShape = {
...findingContext,
resolution_base_path: z.string().exactOptional(),
kind: z.enum([
"static-import",
"dynamic-import",
+6 -8
View File
@@ -147,10 +147,10 @@ const addSections = ({
for (const [index, section] of sections.entries()) {
if ((index & 255) === 0) options.check?.();
const previous = sections[index - 1];
if (previous !== undefined && compare(section.offset, previous.offset) <= 0)
if (previous !== undefined && compare(section.offset, previous.offset) < 0)
throw new SourceMapFormatFailure(
"format",
"Indexed section offsets must be strictly increasing.",
"Indexed section offsets must be in nondecreasing order.",
);
const classification = classifySourceMapSection(section);
if (classification.kind === "external")
@@ -159,7 +159,10 @@ const addSections = ({
`External source-map section URL is not fetched: ${classification.url}`,
);
if (classification.kind === "invalid")
throw new SourceMapFormatFailure("format", classification.reason);
throw new SourceMapFormatFailure(
"format",
`${classification.reason} Section path: ${JSON.stringify([...node.path, index])}.`,
);
embeddedMaps.push(classification.map);
}
for (let index = sections.length - 1; index >= 0; index -= 1) {
@@ -181,11 +184,6 @@ const addSections = ({
? nextOffset
: node.stop;
const offset = addOffset(node.offset, section.offset);
if (node.stop !== null && compare(offset, node.stop) >= 0)
throw new SourceMapFormatFailure(
"format",
"Nested source-map section overlaps its enclosing boundary.",
);
pending.push({
value: map,
path: [...node.path, index],
@@ -63,7 +63,8 @@ it("uses the same conservative section classification for URL-plus-map and malfo
expect(malformed).toThrow(
expect.objectContaining({
reason: "format",
message: "External source-map section URL must be a string.",
message:
"External source-map section URL must be a string. Section path: [0].",
}),
);
});
@@ -0,0 +1,52 @@
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import {
analyzeJavaScriptDirectory,
runNodeJsonControl,
} from "../../support/cli/javascriptApplication.js";
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
cliTest.for(["CLI", "stdio MCP"] as const)(
"resolves proven createRequire filenames from their own directory through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory("rea-create-require-base-");
await writeFixtureFiles(root, {
"main.mjs": `import { createRequire } from "node:module";
const nested = createRequire(new URL("./nested/anchor.cjs", import.meta.url));
const current = createRequire(import.meta.url);
console.log(JSON.stringify([nested("./dep.cjs"), current("./dep.cjs")]));`,
"dep.cjs": 'module.exports = "root";',
"nested/anchor.cjs": "",
"nested/dep.cjs": 'module.exports = "nested";',
});
expect(await runNodeJsonControl(root, "main.mjs")).toEqual([
"nested",
"root",
]);
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
const dependencies = graph.edges
.filter(
({ relation, properties }) =>
relation === "imports" &&
properties.specifier === "./dep.cjs" &&
properties.kind === "require",
)
.map(({ properties }) => ({
resolution_base_path: properties.resolution_base_path,
resolved_path: properties.resolved_path,
}));
expect(dependencies).toHaveLength(2);
expect(dependencies).toEqual(
expect.arrayContaining([
{
resolution_base_path: "./nested/anchor.cjs",
resolved_path: "nested/dep.cjs",
},
{ resolution_base_path: null, resolved_path: "dep.cjs" },
]),
);
},
);
@@ -0,0 +1,45 @@
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import {
analyzeJavaScriptDirectory,
runNodeJsonControl,
} from "../../support/cli/javascriptApplication.js";
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
const esbuildBundle = `(() => {
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => () => {
if (!mod) {
mod = { exports: {} };
cb[__getOwnPropNames(cb)[0]](mod.exports, mod);
}
return mod.exports;
};
var require_dep = __commonJS({
"dep.cjs"(exports, module) {
exports.parse = function parse() { return module.id; };
}
});
console.log(JSON.stringify(Object.keys(require_dep())));
})();`;
cliTest.for(["CLI", "stdio MCP"] as const)(
"retains exports from esbuild CommonJS factories that also accept module through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory("rea-esbuild-commonjs-exports-");
await writeFixtureFiles(root, { "bundle.js": esbuildBundle });
expect(await runNodeJsonControl(root, "bundle.js")).toEqual(["parse"]);
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
const module = graph.nodes.find(
({ kind, observations }) =>
kind === "javascript-module" &&
observations[0]?.properties.module_key === "dep.cjs" &&
observations[0]?.properties.bundler === "esbuild",
);
expect(module?.observations[0]?.properties.exports).toEqual(["parse"]);
expect(graph.coverage.status).toBe("complete");
},
);
@@ -1,3 +1,5 @@
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
import { expect, onTestFinished } from "vitest";
import { Client } from "@modelcontextprotocol/client";
import {
@@ -112,3 +114,195 @@ cliTest(
// Allow cold CLI and MCP ownership preparation across the full parity sequence.
180_000,
);
cliTest(
"applies the shared version constraint to nested indexed maps through CLI and stdio MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-indexed-map-version-");
const fixture = webSourceLocationFixture();
await mkdir(join(root, "files", "modules"), { recursive: true });
await writeFile(join(root, "files", "modules", "main.js"), fixture.source);
const manifest = join(root, "manifest.json").replaceAll("\\", "/");
await writeFile(manifest, JSON.stringify(fixture.manifest));
const { call } = await connectLocalToolsMcp({
REA_BROWSER_EXECUTABLE: "relative-unconfigured-browser",
});
const leaf: unknown = JSON.parse(fixture.sourceMap.text);
for (const [index, version] of [3, 2, null, undefined, "3"].entries()) {
const text = JSON.stringify({
version: 3,
sections: [
{
offset: { line: 0, column: 0 },
map: {
version,
sections: [{ offset: { line: 0, column: 0 }, map: leaf }],
},
},
],
});
const path = join(root, `nested-${index}.map`);
await writeFile(path, text);
const result = await cli.run({
arguments: [
"trace-web-source-location",
manifest,
"0",
path,
fixture.sourceMap.url,
"1",
"0",
"--json",
],
environment: {
REA_LOG_LEVEL: "silent",
REA_BROWSER_EXECUTABLE: "relative-unconfigured-browser",
},
timeoutMs: 45_000,
});
const response = await call("trace_web_source_location", {
manifest_path: manifest,
script_index: 0,
source_map: { path, url: fixture.sourceMap.url },
generated_position: { line: 1, column: 0 },
});
if (version === 3) {
expect(result.exitCode, result.stderr).toBe(0);
expect(result.json).toMatchObject({
normalized_result: {
matches: [
{
state: "mapped",
section_path: [0, 0],
content: { text: "original" },
},
],
},
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
expect(response.structuredContent).toEqual(result.json);
} else {
const failure = {
code: "invalid_request",
details: {
issues: [
{
path: ["source_map", "path"],
reason: "invalid_format",
message: expect.stringContaining(
"not a version 3 map. Section path: [0].",
),
},
],
},
};
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject(failure);
expect(result.json).not.toHaveProperty("normalized_result");
expect(parseMcpToolError(response).error).toMatchObject(failure);
expect(response.structuredContent).toBeUndefined();
}
}
},
180_000,
);
cliTest(
"traces empty equal-offset sections while rejecting decreasing offsets and represented overlaps",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-empty-indexed-map-");
const fixture = webSourceLocationFixture();
await mkdir(join(root, "files", "modules"), { recursive: true });
await writeFile(join(root, "files", "modules", "main.js"), fixture.source);
const manifest = join(root, "manifest.json").replaceAll("\\", "/");
await writeFile(manifest, JSON.stringify(fixture.manifest));
const { call } = await connectLocalToolsMcp();
const leaf: unknown = JSON.parse(fixture.sourceMap.text);
const empty = { version: 3, sources: [], names: [], mappings: "" };
const nestedEmpty = {
version: 3,
sections: [{ offset: { line: 0, column: 0 }, map: empty }],
};
for (const [index, sample] of [
{ first: empty, firstColumn: 0, nextColumn: 0, succeeds: true },
{ first: nestedEmpty, firstColumn: 0, nextColumn: 0, succeeds: true },
{ first: empty, firstColumn: 0, nextColumn: 1, succeeds: true },
{ first: leaf, firstColumn: 0, nextColumn: 0, succeeds: false },
{ first: empty, firstColumn: 1, nextColumn: 0, succeeds: false },
].entries()) {
const path = join(root, `empty-${index}.map`);
await writeFile(
path,
JSON.stringify({
version: 3,
sections: [
{
offset: { line: 0, column: sample.firstColumn },
map: sample.first,
},
{ offset: { line: 0, column: sample.nextColumn }, map: leaf },
],
}),
);
const result = await cli.run({
arguments: [
"trace-web-source-location",
manifest,
"0",
path,
fixture.sourceMap.url,
"1",
String(sample.nextColumn),
"--json",
],
environment: { REA_LOG_LEVEL: "silent" },
timeoutMs: 45_000,
});
const response = await call("trace_web_source_location", {
manifest_path: manifest,
script_index: 0,
source_map: { path, url: fixture.sourceMap.url },
generated_position: { line: 1, column: sample.nextColumn },
});
if (sample.succeeds) {
expect(result.exitCode, result.stderr).toBe(0);
expect(result.json).toMatchObject({
normalized_result: {
lookup: {
matched_generated_position: {
line: 1,
column: sample.nextColumn,
},
},
matches: [
{
state: "mapped",
section_path: [1],
content: { text: "original" },
},
],
},
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
expect(response.structuredContent).toEqual(result.json);
} else {
const failure = {
code: "invalid_request",
details: {
issues: [
{
path: ["source_map", "path"],
reason: "invalid_format",
message: expect.stringMatching(/overlap|nondecreasing/u),
},
],
},
};
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject(failure);
expect(parseMcpToolError(response).error).toMatchObject(failure);
}
}
},
180_000,
);