Files
rea/src/domain/javascript/javascriptSourceMapPaths.ts
T
morluto b58adafa4f fix(javascript): align module lookup and source evidence
Preserve CommonJS literal paths and confined ESM URL semantics, including package main fallback, directory intent, Unicode identity, and present opaque modules. Parse extraction inputs at their AST/source owner, retain skipped HTML and source-map coverage, and distinguish endpoint candidates from proven receiver facts. Version structural fingerprints with computed literal contents and propagate them through worker contracts.

Reviewed fixes include #1810, #1840, #2642, #2700, #2702, #2714, #2803, #2960, #2968, #2986, #3035, and #3036.

Validation: 158 resolver tests passed; public CLI/MCP module lookup controls, package-main/Unicode workflows, and six builtin workflows passed in the current compiled lane. Worker schema/fingerprint regressions passed. Large public pdf.js completion and real Electron analysis remain unverified.
2026-10-14 08:46:50 +08:00

184 lines
6.1 KiB
TypeScript

import { posix } from "node:path";
import { domainToASCII, fileURLToPath, pathToFileURL } from "node:url";
import { TraceMap } from "@jridgewell/trace-mapping";
import { z } from "zod";
/** Map-scoped source declaration and its path interpretation for matching. */
export const javascriptSourceMapReferenceSchema = z.strictObject({
source_name: z.string(),
source_root: z.string().nullable(),
map_path: z.string().min(1),
resolution: z.discriminatedUnion("kind", [
z.strictObject({
kind: z.enum(["artifact-relative", "suffix"]),
path: z.string().min(1),
}),
z.strictObject({
kind: z.literal("unresolved"),
reason: z.string().min(1),
}),
]),
});
/** Typed source-map context retained through graph serialization. */
export type JavaScriptSourceMapReference = z.infer<
typeof javascriptSourceMapReferenceSchema
>;
/** Interpret declarations together once at their source-map ingestion boundary. */
export const resolveJavaScriptSourceMapReferences = (
sourceNames: readonly (string | null)[],
sourceRoot: string | null,
mapPath: string,
): (JavaScriptSourceMapReference | null)[] => {
if (sourceNames.length === 0) return [];
const resolvedSources = resolveSourceMapUrls(
sourceNames.map((sourceName) => sourceName ?? ""),
sourceRoot,
"",
);
return sourceNames.map((sourceName, index) => {
if (sourceName === null) return null;
const reference = resolvedSources[index];
if (reference === undefined)
throw new TypeError(
`TraceMap did not resolve source declaration ${String(index)}`,
);
return {
source_name: sourceName,
source_root: sourceRoot,
map_path: mapPath,
resolution: resolveJavaScriptSourceMapPath(reference, mapPath),
};
});
};
/** Resolve a source declaration using the same pinned format implementation as map consumers. */
export const resolveJavaScriptSourceMapUrl = (
source: string | null,
sourceRoot: string | null | undefined,
mapUrl: string,
): string => {
const resolved = resolveSourceMapUrls([source ?? ""], sourceRoot, mapUrl)[0];
if (resolved === undefined)
throw new TypeError(
"TraceMap did not resolve its single source declaration",
);
return resolved;
};
const resolveSourceMapUrls = (
sources: string[],
sourceRoot: string | null | undefined,
mapUrl: string,
): readonly string[] =>
new TraceMap(
{
version: 3,
names: [],
sources,
...(sourceRoot == null ? {} : { sourceRoot }),
mappings: "",
},
mapUrl,
).resolvedSources;
/** Resolve a source-map URL reference without changing its recorded identity. */
const resolveJavaScriptSourceMapPath = (
source: string,
mapPath: string,
): JavaScriptSourceMapReference["resolution"] => {
const portable = source.replaceAll("\\", "/");
const windowsPath = /^[a-z]:\//iu.test(portable) || source.startsWith("\\\\");
// Preserve parent traversal rather than letting URL resolution clamp at '/'.
const anchor = `/${"source/".repeat(portable.split("/").length + 1)}`;
const directory = posix.join(anchor, posix.dirname(mapPath));
const base = pathToFileURL(`${directory}/`, { windows: false });
const absolute = windowsPath ? windowsFileUrl(source) : URL.parse(source);
const resolved = windowsPath ? absolute : URL.parse(source, base);
if (resolved === null)
return {
kind: "unresolved",
reason: "Source reference is not a valid URL or filesystem path.",
};
if (!resolved.pathname.startsWith("/"))
return {
kind: "unresolved",
reason:
"Source reference uses a non-hierarchical URL without a file pathname.",
};
if (resolved.pathname.endsWith("/"))
return {
kind: "unresolved",
reason: "Source reference names a directory rather than a file.",
};
// Reject separators in declarations, not escaped literal names in the owned base.
const declaredPath = windowsPath
? (absolute?.pathname ?? "")
: (URL.parse(source, "file:///")?.pathname ?? "");
if (/%(?:2f|5c)/iu.test(declaredPath))
return {
kind: "unresolved",
reason: "Source reference contains an encoded path separator.",
};
const relative =
absolute === null
? !portable.startsWith("/")
: absolute.href !== resolved.href;
const path = decodedUrlPath(resolved);
if (path === null)
return {
kind: "unresolved",
reason:
"Source reference has malformed URL encoding or an invalid file URL.",
};
const value = relative
? posix.relative(anchor, path)
: path.replace(/^\/+/, "");
if (value === "" || value === ".." || value.startsWith("../"))
return {
kind: "unresolved",
reason: "Source reference resolves outside the inventoried artifact.",
};
return { path: value, kind: relative ? "artifact-relative" : "suffix" };
};
// Reject URL delimiters/escapes before IDNA can truncate or decode the host.
const unusableHost = /[\0\t\n\r #/:<>?@[\\\]^|%]/u;
// URL/IDNA validation also rejects Unicode hosts that normalize to empty.
const carriesUsableHost = (host: string): boolean =>
!unusableHost.test(host) && domainToASCII(host) !== "";
const windowsFileUrl = (path: string): URL | null => {
// A malformed UNC host aborts pathToFileURL() on the Node builds this
// package admits, so decline the reference before handing the path over
// rather than trusting a TypeError that never arrives.
if (
path.startsWith("\\\\") &&
!carriesUsableHost(path.slice(2).split("\\")[0] ?? "")
)
return null;
try {
return pathToFileURL(path, { windows: true });
} catch (cause: unknown) {
if (cause instanceof TypeError) return null;
throw cause;
}
};
const decodedUrlPath = (url: URL): string | null => {
try {
if (url.protocol === "file:") {
const windows = url.hostname !== "" && url.hostname !== "localhost";
const path = fileURLToPath(url, { windows });
return windows ? path.replaceAll("\\", "/") : path;
}
return `${url.host}${decodeURIComponent(url.pathname)}`;
} catch (cause: unknown) {
if (cause instanceof URIError || cause instanceof TypeError) return null;
throw cause;
}
};