fix(javascript): preserve package and source-map boundary identity (#1939)

* fix(javascript): preserve bare package specifier suffixes

* fix(javascript): resolve ESM package self references

* fix(javascript): recover retained prefixed source maps

* test(javascript): cover suffixed ESM self references

* fix(javascript): retain legacy ESM package suffix behavior

* fix(javascript): reject legacy ESM directory and extension fallback

* refactor(javascript): consolidate package resolution and workflow coverage

* fix(javascript): report exact ESM package subpath misses

* fix(javascript): compare parsed names for ESM self references

* refactor(javascript): parse bare package requests once

* fix(javascript): derive ESM dependency lookup from package URLs

---------

Co-authored-by: hhang <[email protected]>
This commit is contained in:
morluto
2026-10-12 04:09:17 +08:00
committed by GitHub
co-authored by hhang
parent 85c1d2587a
commit 14935e4d47
13 changed files with 860 additions and 157 deletions
+15 -2
View File
@@ -508,6 +508,12 @@ from unmatched nested targets rather than reporting array indices.
For imports and requires, the active Node conditions include `module-sync`;
earlier active conditions retain precedence over later ones.
Package self-references use the importer's enclosing package scope before
installed dependencies. ESM requires the parsed package name to equal the
metadata name; a scope-only name such as `@scope` cannot capture `@scope/pkg`.
CommonJS retains Node's raw-request name/prefix matching. Both paths preserve
the original specifier and select exports under their own loader conditions.
Exact package-private `#alias` specifiers resolve against the importer's
enclosing `package.json` `imports` map — the nearest same-container package
scope, stopping at a `node_modules` directory so a nested package cannot inherit
@@ -523,8 +529,15 @@ 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.
ESM relative module paths and selected package exports targets use URL suffix handling
and one percent-decoding pass. CommonJS relative paths and legacy package main
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.
For dependency lookup, ESM constructs a package metadata URL instead of treating
the parsed package name as a literal directory. A root request such as `legacy?x`
does not load either `node_modules/legacy?x` or `node_modules/legacy`; an exact
self-reference with that metadata name can still select its exports first.
Without an exports map, ESM subpaths select exact files; directory imports and
extension fallback are refused. CommonJS relative paths and legacy package main
fields retain literal filename punctuation. Exports targets must start with
`./` and exclude `.`, `..`, and `node_modules` path segments, including their
encoded forms, before URL normalization. Arrays skip invalid exports targets
@@ -11,7 +11,10 @@ import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSem
import { projectJavaScriptModuleSemantics } from "../../domain/javascript/javascriptModuleSemanticIr.js";
import { parseJavaScriptSource } from "../../domain/javascript/javascriptSourceParser.js";
import { hasValidSourceMapContents } from "../../domain/sourceMapContents.js";
import { flattenSourceMapLeaves } from "../../domain/sourceMapEnvelope.js";
import {
flattenSourceMapLeaves,
sourceMapJsonText,
} from "../../domain/sourceMapEnvelope.js";
import type {
JavaScriptSourceRange,
JavaScriptSourcePoint,
@@ -521,7 +524,7 @@ const parseSourceMap = (
if (!file.text.included) return unavailableSourceMap(file);
let value: unknown;
try {
value = JSON.parse(file.text.value);
value = JSON.parse(sourceMapJsonText(file.text.value));
} catch (cause: unknown) {
void cause;
return invalidSourceMap(file, "Source map is not valid JSON.");
@@ -176,9 +176,7 @@ const contextualCandidate = (
): string | CandidateResolution => {
const { context } = input;
let declared =
context === "html-reference" ||
context === "url-reference" ||
(context === "module-specifier" && input.moduleKind !== "require")
context === "html-reference" || context === "url-reference"
? stripQueryAndFragment(input.declaredPath)
: input.declaredPath;
if (context === "html-reference") return htmlCandidate(input);
@@ -190,9 +188,9 @@ const contextualCandidate = (
"URL and Node builtin schemes are outside static artifact module resolution.",
]);
if (!declared.startsWith(".") && !declared.startsWith("/"))
return bareModuleCandidate(input, declared);
return bareModuleCandidate(input);
if (input.moduleKind !== "require") {
const decoded = decodeModuleUrlPath(declared);
const decoded = decodeModuleUrlPath(stripQueryAndFragment(declared));
if (typeof decoded !== "string") return decoded;
declared = decoded;
}
@@ -237,34 +235,51 @@ const relativeCandidate = (sourcePath: string, declared: string): string =>
const bareModuleCandidate = (
input: ResolveArtifactPathInput,
declared: string,
): CandidateResolution => {
const packageName = barePackageName(declared);
if (packageName === null || isBuiltin(declared))
const request = parseBarePackageRequest(input.declaredPath, input.moduleKind);
if (request === null || isBuiltin(input.declaredPath))
return unresolvedCandidate("external", [
"The bare specifier is a Node builtin or invalid package name.",
]);
if (input.moduleKind === "import" && packageName.includes("%"))
if (input.moduleKind === "import" && request.packageName.includes("%"))
return unresolvedCandidate("rejected", [
"Percent signs are not admitted in ES module package names.",
]);
const self = commonJsSelfCandidate(input, declared);
const self = packageSelfCandidate(input, request);
if (self !== null) return self;
const subpath =
declared === packageName ? "." : `.${declared.slice(packageName.length)}`;
// ESM constructs a package.json URL; its parsed identity is not a literal
// dependency directory. Self resolution above still uses the exact name.
let dependencyDirectory = posix.join("node_modules", request.packageName);
if (input.moduleKind === "import") {
const metadataUrl = new URL(
`./node_modules/${request.packageName}/package.json`,
PACKAGE_ARTIFACT_ROOT_URL,
);
if (!metadataUrl.pathname.endsWith("/package.json"))
return unresolvedCandidate("external", [
`ESM package name ${JSON.stringify(request.packageName)} produces a dependency URL whose pathname does not name package.json; no literal or suffix-stripped package directory was searched.`,
]);
const decoded = decodeModuleUrlPath(
metadataUrl.pathname.slice(PACKAGE_ARTIFACT_ROOT_URL.pathname.length),
);
if (typeof decoded !== "string") return decoded;
const confined = confineCandidate(decoded);
if (typeof confined !== "string") return confined;
dependencyDirectory = posix.dirname(confined);
}
const source = input.files.get(input.sourcePath);
let directory = posix.dirname(input.sourcePath);
while (true) {
const candidate = posix.join(directory, "node_modules", packageName);
const candidate = posix.join(directory, dependencyDirectory);
const metadata = readPackageMetadata(input, candidate);
if (packageExportsEligible(input) && metadata.kind !== "absent")
return resolvePackageSpecifier(input, candidate, subpath, metadata);
if (subpath === ".") {
if (request.dependencyExportsEligible && metadata.kind !== "absent")
return resolvePackageDependency(input, candidate, request, metadata);
if (request.packageSubpath === ".") {
const direct = resolveFileCandidates(input, fileCandidates(candidate));
if (direct !== null) return direct;
}
if (hasContainerDirectory(input.files, candidate, source?.container_sha256))
return resolvePackageSpecifier(input, candidate, subpath, metadata);
return resolvePackageDependency(input, candidate, request, metadata);
if (directory === "." || directory === "") break;
directory = posix.dirname(directory);
}
@@ -330,24 +345,25 @@ const resolvePackageImportAlias = (
return unresolvedCandidate("external", [
`Wildcard package import aliases remain unresolved; package metadata ${metadata.path} has no exact imports key ${specifier}.`,
]);
const selected = selectPackageImportTarget(
const selected = selectExportTarget(
lookup.target,
input.moduleKind,
metadata.path,
packageExportConditions(input.moduleKind),
packageMetadataUrl(metadata.path),
"imports",
);
if (selected.status === "rejected")
if (selected.kind === "invalid" || selected.kind === "invalid-config")
return unresolvedCandidate("rejected", [
`Package metadata ${metadata.path}: ${selected.limitation}`,
]);
if (selected.status === "unmatched")
if (selected.kind === "unmatched")
return unresolvedCandidate("external", [
`Package metadata ${metadata.path} declares no imports target for alias ${specifier} under the active conditions; it declares ${selected.declared.join(", ") || "no conditions"}.`,
]);
if (selected.status === "blocked")
if (selected.kind === "blocked")
return unresolvedCandidate("external", [
`Package metadata ${metadata.path} blocks import alias ${specifier} with ${selected.cause === "null" ? "an explicit null imports target" : "an empty imports target array"}.`,
]);
if (selected.status === "non-relative") {
if (selected.kind === "non-relative") {
const target = classifyPackageImportTargetString(selected.value);
return unresolvedCandidate("external", [
target.kind === "recursive"
@@ -358,77 +374,44 @@ const resolvePackageImportAlias = (
return resolveExportTarget(input, metadata.path, selected, "imports");
};
const selectPackageImportTarget = (
value: unknown,
moduleKind: ResolveArtifactPathInput["moduleKind"],
packagePath: string,
):
| ({ readonly status: "value" } & PackageFileTarget)
| { readonly status: "non-relative"; readonly value: string }
| { readonly status: "blocked"; readonly cause: PackageTargetBlockCause }
| { readonly status: "unmatched"; readonly declared: readonly string[] }
| { readonly status: "rejected"; readonly limitation: string } => {
const selected = selectExportTarget(
value,
packageExportConditions(moduleKind),
packageMetadataUrl(packagePath),
"imports",
);
if (selected.kind === "invalid" || selected.kind === "invalid-config")
return { status: "rejected", limitation: selected.limitation };
if (selected.kind === "unmatched")
return {
status: "unmatched",
declared: selected.declared,
};
if (selected.kind === "blocked")
return { status: "blocked", cause: selected.cause };
if (selected.kind === "non-relative")
return { status: "non-relative", value: selected.value };
return {
status: "value",
value: selected.value,
url: selected.url,
};
};
/** Self references use the nearest package scope before dependency lookup. */
const commonJsSelfCandidate = (
const packageSelfCandidate = (
input: ResolveArtifactPathInput,
declared: string,
request: BarePackageRequest,
): CandidateResolution | null => {
if (input.moduleKind !== "require" || !input.files.has(input.sourcePath))
if (input.moduleKind === undefined || !input.files.has(input.sourcePath))
return null;
const metadata = enclosingPackageMetadata(input);
if (metadata.kind === "none") return null;
if (metadata.kind === "unavailable")
return unresolvedCandidate("unavailable", [metadata.limitation]);
if (
metadata.name === undefined ||
metadata.exports === undefined ||
(declared !== metadata.name && !declared.startsWith(`${metadata.name}/`))
)
if (metadata.name === undefined || metadata.exports === undefined)
return null;
const subpath =
declared === metadata.name
? "."
: `.${declared.slice(metadata.name.length)}`;
let subpath: string;
if (input.moduleKind === "import") {
if (metadata.name !== request.packageName) return null;
subpath = request.packageSubpath;
} else if (input.declaredPath === metadata.name) {
subpath = ".";
} else if (input.declaredPath.startsWith(`${metadata.name}/`)) {
subpath = `.${input.declaredPath.slice(metadata.name.length)}`;
} else return null;
// Self resolution does not apply the CommonJS dependency-name matcher.
return resolvePackageExports(input, metadata.path, subpath, metadata.exports);
};
/** Package roots and exact subpaths share metadata and exports selection. */
const resolvePackageSpecifier = (
/** Resolve an installed dependency using one parsed request and its metadata. */
const resolvePackageDependency = (
input: ResolveArtifactPathInput,
directory: string,
subpath: string,
request: BarePackageRequest,
metadata: PackageMetadata,
): CandidateResolution => {
const exportsEligible = packageExportsEligible(input);
if (exportsEligible && metadata.kind === "unavailable")
const subpath = request.packageSubpath;
if (request.dependencyExportsEligible && metadata.kind === "unavailable")
return unresolvedCandidate("unavailable", [metadata.limitation]);
const rawExports =
exportsEligible && metadata.kind === "available"
request.dependencyExportsEligible && metadata.kind === "available"
? metadata.exports
: undefined;
if (rawExports === undefined) {
@@ -437,8 +420,17 @@ const resolvePackageSpecifier = (
resolveFileCandidates(input, fileCandidates(directory)) ??
resolveDirectory(input, directory, metadata, new Set())
);
// Preserve the trailing slash for a package directory request.
const candidate = confineCandidate(`${directory}/${subpath.slice(2)}`);
// Resolve legacy ESM subpaths as URLs: suffixes identify the loaded module
// but do not belong to its filesystem path. Keep the declared specifier on
// the relationship; only derive the inventory lookup path here.
const fileSubpath =
input.moduleKind === "import"
? decodeModuleUrlPath(stripQueryAndFragment(subpath.slice(2)))
: subpath.slice(2);
if (typeof fileSubpath !== "string") return fileSubpath;
const candidate = confineCandidate(`${directory}/${fileSubpath}`);
if (input.moduleKind === "import" && typeof candidate === "string")
return resolveLegacyEsmSubpath(input, candidate);
return typeof candidate === "string"
? resolveCandidate(input, candidate, new Set())
: candidate;
@@ -451,6 +443,21 @@ const resolvePackageSpecifier = (
);
};
/** Legacy ESM package subpaths are exact URLs, never extension or directory lookups. */
const resolveLegacyEsmSubpath = (
input: ResolveArtifactPathInput,
candidate: string,
): CandidateResolution => {
const file = resolveFileCandidates(input, [candidate]);
if (file !== null) return file;
const source = input.files.get(input.sourcePath);
if (hasContainerDirectory(input.files, candidate, source?.container_sha256))
return unresolvedCandidate("rejected", [
`ESM package subpath ${candidate} names a directory; directory imports are unsupported and package main or index lookup was not attempted.`,
]);
return exactPackageTargetNotFound(candidate, "ESM subpath");
};
const resolvePackageExports = (
input: ResolveArtifactPathInput,
packagePath: string,
@@ -517,23 +524,36 @@ const resolveExportTarget = (
);
};
const barePackageName = (specifier: string): string | null => {
const segments = specifier.split("/");
if (specifier.startsWith("@"))
return segments.length >= 2 && segments[0] !== "" && segments[1] !== ""
? `${segments[0]}/${segments[1]}`
: null;
return segments[0] === "" ? null : (segments[0] ?? null);
};
/** Derived package identity and dependency policy; input retains the raw request. */
interface BarePackageRequest {
readonly packageName: string;
readonly packageSubpath: string;
readonly dependencyExportsEligible: boolean;
}
const packageExportsEligible = (input: ResolveArtifactPathInput): boolean => {
if (input.moduleKind !== "require") return true;
const parseBarePackageRequest = (
specifier: string,
moduleKind: ResolveArtifactPathInput["moduleKind"],
): BarePackageRequest | null => {
const segments = specifier.split("/");
let packageName = segments[0] ?? "";
if (packageName === "") return null;
if (specifier.startsWith("@")) {
const packageSegment = segments[1];
if (packageSegment === undefined || packageSegment === "") return null;
packageName = `${packageName}/${packageSegment}`;
}
// Node's CommonJS exports matcher excludes percent-containing package names.
// Compare the captured name so a scoped name cannot fall back to its scope.
const match = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/.exec(
input.declaredPath,
);
return match?.[1] === barePackageName(input.declaredPath);
const dependencyExportsEligible =
moduleKind !== "require" ||
/^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/.exec(specifier)?.[1] ===
packageName;
return {
packageName,
packageSubpath: `.${specifier.slice(packageName.length)}`,
dependencyExportsEligible,
};
};
const hasContainerDirectory = (
@@ -1061,12 +1081,12 @@ const notFoundCandidate = (): CandidateResolution => ({
const exactPackageTargetNotFound = (
path: string,
mapKind: "exports" | "imports",
targetKind: "exports" | "imports" | "ESM subpath",
): CandidateResolution => ({
resolvedPath: null,
status: "not-found",
limitations: [
`The selected package ${mapKind === "imports" ? "import" : "exports"} target ${path} does not exist in the inventoried artifact container; ${mapKind === "imports" ? "import" : "exports"} targets are exact files, so no extension or directory index was tried.`,
`The selected package ${targetKind === "imports" ? "import" : targetKind} target ${path} does not exist in the inventoried artifact container; ${targetKind === "imports" ? "import" : targetKind} targets are exact files, so no extension or directory index was tried.`,
],
});
+1 -1
View File
@@ -20,10 +20,10 @@ import type {
import { webSourceMapsSchema } from "../domain/webBundleAnalysis.js";
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
import { safeParseJson } from "../domain/safeJson.js";
import { sourceMapJsonText } from "../domain/sourceMapEnvelope.js";
import { jsonParts } from "../domain/jsonSerialization.js";
import {
inspectSourceMapValue,
sourceMapJsonText,
SourceMapFormatFailure,
type SourceMapLeaf,
} from "../javascript/sourceMaps/SourceMapFormat.js";
+13
View File
@@ -8,6 +8,19 @@
*/
type SourceMapRecord = Readonly<Record<string, unknown>>;
/**
* Return the JSON parsing view of retained source-map text. ECMA-426 permits
* HTTP responses to start with `)]}'`; the first line is transport framing.
* Keep the original text for artifact identity and Evidence provenance.
* https://tc39.es/ecma426/#sec-fetchsourcemap
*/
export const sourceMapJsonText = (text: string): string => {
const withoutBom = text.replace(/^\uFEFF/u, "");
if (!withoutBom.startsWith(")]}'")) return withoutBom;
const newline = withoutBom.search(/[\r\n]/u);
return newline === -1 ? "" : withoutBom.slice(newline);
};
const isRecord = (value: unknown): value is SourceMapRecord =>
typeof value === "object" && value !== null && !Array.isArray(value);
+1 -14
View File
@@ -1,6 +1,7 @@
import { z } from "zod";
import { WEB_SOURCE_MAP_LIMITS } from "../../domain/webSourceLocation.js";
import { hasValidSourceMapContents } from "../../domain/sourceMapContents.js";
import { sourceMapJsonText } from "../../domain/sourceMapEnvelope.js";
const leafSchema = z.object({
version: z.literal(3),
@@ -279,20 +280,6 @@ const inspectLeaf = (
return leaf;
};
/**
* Return the JSON view of source-map text without changing the source evidence.
* ECMA-426 §11.2.1 permits HTTP(S) responses to start with `)]}'`; the line
* through the first HTTP newline is transport framing, not map content. Local
* point tracing shares this parser and has historically accepted the same
* framing. https://tc39.es/ecma426/#sec-fetchsourcemap
*/
export const sourceMapJsonText = (text: string): string => {
const withoutBom = text.replace(/^\uFEFF/u, "");
if (!withoutBom.startsWith(")]}'")) return withoutBom;
const newline = withoutBom.search(/[\r\n]/u);
return newline === -1 ? "" : withoutBom.slice(newline);
};
/** Validate structure before allocation, retaining raw declarations and section boundaries. */
export const inspectSourceMap = (
text: string,
@@ -4,12 +4,10 @@ import { createRequire } from "node:module";
import { dirname, join, relative } from "node:path";
import { expect } from "vitest";
import { z } from "zod";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
const literalAliases = ["#/x", "#x%2Fy", "#x%2Ey", "#x\\y", "#x\0y"];
const fallbackAliases = {
@@ -158,26 +156,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
expect.objectContaining({ code: "ERR_PACKAGE_IMPORT_NOT_DEFINED" }),
);
let document: unknown;
if (surface === "CLI") {
const response = await cli.run({
arguments: ["analyze-javascript-application", root, "--format", "json"],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(response.exitCode, response.stderr).toBe(0);
document = response.json;
} else {
const { call } = await connectLocalToolsMcp();
const response = await call("analyze_javascript_application", {
input_path: root,
format: "directory",
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
document = response.structuredContent;
}
const { graph } = z
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
.parse(document).normalized_result;
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
for (const specifier of resolvedAliases)
for (const [moduleKind, sourcePath, source] of [
["require", "main.cjs", mainSource],
@@ -0,0 +1,109 @@
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
const requests = [
{ specifier: "legacy?x", plainName: "legacy", exports: false },
{ specifier: "exported#entry", plainName: "exported", exports: true },
{ specifier: "@scope/legacy?x", plainName: "@scope/legacy", exports: false },
{ specifier: "@scope#entry/pkg", plainName: "@scope/pkg", exports: true },
] as const;
// Windows cannot materialize the question-mark directories in this fixture.
cliTest.skipIf(process.platform === "win32").for(["CLI", "stdio MCP"] as const)(
"keeps ESM root URL suffixes out of dependency filesystem lookup through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory("rea-package-root-suffix-");
const sources = {
"main.mjs": requests
.map(({ specifier }) => `import ${JSON.stringify(specifier)};`)
.join("\n"),
"main.cjs": requests
.map(({ specifier }) => `require(${JSON.stringify(specifier)});`)
.join("\n"),
};
const files: Record<string, string> = { ...sources };
for (const item of requests)
for (const name of [item.specifier, item.plainName]) {
const directory = `node_modules/${name}`;
files[`${directory}/package.json`] = JSON.stringify(
item.exports ? { exports: "./entry.cjs" } : { main: "./entry.cjs" },
);
files[`${directory}/entry.cjs`] =
`module.exports = ${JSON.stringify(name)};`;
}
await Promise.all(
Object.entries(files).map(async ([path, text]) => {
const absolute = join(root, path);
await mkdir(dirname(absolute), { recursive: true });
await writeFile(absolute, text);
}),
);
if (surface === "CLI") {
const native = await promisify(execFile)(
process.execPath,
[
"--input-type=module",
"--eval",
`import { createRequire } from "node:module";
const require = createRequire(new URL("./main.cjs", import.meta.url));
const requests = ${JSON.stringify(requests.map(({ specifier }) => specifier))};
const results = await Promise.all(requests.map(async (specifier) => {
let esm;
try { esm = { value: (await import(specifier)).default, error: null }; }
catch (error) { esm = { value: null, error: error.code }; }
return { esm, require: require(specifier) };
}));
console.log(JSON.stringify(results));`,
],
{ cwd: root, timeout: 5_000 },
);
expect(JSON.parse(native.stdout)).toEqual(
requests.map(({ specifier }) => ({
esm: { value: null, error: "ERR_MODULE_NOT_FOUND" },
require: specifier,
})),
);
}
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
for (const { specifier } of requests)
for (const [moduleKind, sourcePath] of [
["import", "main.mjs"],
["require", "main.cjs"],
] as const)
expect(graph.edges).toContainEqual(
expect.objectContaining({
relation: "imports",
properties: expect.objectContaining({
module_link_kind: moduleKind,
specifier,
resolved_path:
moduleKind === "import"
? null
: `node_modules/${specifier}/entry.cjs`,
resolution_status:
moduleKind === "import" ? "external" : "resolved",
}),
evidence: expect.objectContaining({
artifact: expect.objectContaining({
sha256: createHash("sha256")
.update(sources[sourcePath])
.digest("hex"),
}),
location: expect.objectContaining({
value: expect.objectContaining({ source: sourcePath }),
}),
}),
}),
);
},
);
@@ -0,0 +1,207 @@
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
type SelfNameCase = {
readonly directory: string;
readonly packageName: string;
readonly exportKey: string;
readonly specifier: string;
readonly selfValue: string;
readonly dependency?: {
readonly packagePath: string;
readonly exportKey: string;
readonly value: string;
};
readonly esmTarget: string;
readonly requireTarget: string;
readonly esmValue: string;
readonly requireValue: string;
};
const cases = [
{
directory: "scope-prefix",
packageName: "@scope",
exportKey: "./pkg",
specifier: "@scope/pkg",
selfValue: "scope self",
dependency: {
packagePath: "node_modules/@scope/pkg",
exportKey: ".",
value: "scope dependency",
},
esmTarget: "scope-prefix/node_modules/@scope/pkg/dependency.cjs",
requireTarget: "scope-prefix/self.cjs",
esmValue: "scope dependency",
requireValue: "scope self",
},
{
directory: "slash-name",
packageName: "fixture/sub",
exportKey: "./feature",
specifier: "fixture/sub/feature",
selfValue: "slash self",
dependency: {
packagePath: "node_modules/fixture",
exportKey: "./sub/feature",
value: "slash dependency",
},
esmTarget: "slash-name/node_modules/fixture/dependency.cjs",
requireTarget: "slash-name/self.cjs",
esmValue: "slash dependency",
requireValue: "slash self",
},
{
directory: "slash-exact-name",
packageName: "fixture/sub",
exportKey: ".",
specifier: "fixture/sub",
selfValue: "slash exact self",
dependency: {
packagePath: "node_modules/fixture",
exportKey: "./sub",
value: "slash exact dependency",
},
esmTarget: "slash-exact-name/node_modules/fixture/dependency.cjs",
requireTarget: "slash-exact-name/self.cjs",
esmValue: "slash exact dependency",
requireValue: "slash exact self",
},
{
directory: "valid-scoped",
packageName: "@scope/pkg",
exportKey: "./feature",
specifier: "@scope/pkg/feature",
selfValue: "valid scoped self",
esmTarget: "valid-scoped/self.cjs",
requireTarget: "valid-scoped/self.cjs",
esmValue: "valid scoped self",
requireValue: "valid scoped self",
},
{
directory: "query-self-name",
packageName: "self?x",
exportKey: ".",
specifier: "self?x",
selfValue: "query self",
esmTarget: "query-self-name/self.cjs",
requireTarget: "query-self-name/self.cjs",
esmValue: "query self",
requireValue: "query self",
},
{
directory: "fragment-self-name",
packageName: "@scope/self#entry",
exportKey: ".",
specifier: "@scope/self#entry",
selfValue: "fragment self",
esmTarget: "fragment-self-name/self.cjs",
requireTarget: "fragment-self-name/self.cjs",
esmValue: "fragment self",
requireValue: "fragment self",
},
] satisfies readonly SelfNameCase[];
const sourceFor = (specifier: string, moduleKind: "import" | "require") =>
moduleKind === "import"
? `import value from ${JSON.stringify(specifier)};\nexport default value;`
: `const value = require(${JSON.stringify(specifier)});\nmodule.exports = value;`;
const createSelfNameFixture = async () => {
const root = await createTestTempDirectory("rea-package-self-name-boundary-");
const files: Record<string, string> = {};
for (const item of cases) {
const prefix = item.directory;
files[`${prefix}/package.json`] = JSON.stringify({
name: item.packageName,
exports: { [item.exportKey]: "./self.cjs" },
});
files[`${prefix}/main.mjs`] = sourceFor(item.specifier, "import");
files[`${prefix}/main.cjs`] = sourceFor(item.specifier, "require");
files[`${prefix}/self.cjs`] =
`module.exports = ${JSON.stringify(item.selfValue)};`;
if (item.dependency !== undefined) {
const dependencyPrefix = `${prefix}/${item.dependency.packagePath}`;
files[`${dependencyPrefix}/package.json`] = JSON.stringify({
exports: { [item.dependency.exportKey]: "./dependency.cjs" },
});
files[`${dependencyPrefix}/dependency.cjs`] =
`module.exports = ${JSON.stringify(item.dependency.value)};`;
}
}
files["node-control.mjs"] = `import { createRequire } from "node:module";
const cases = ${JSON.stringify(cases.map(({ directory, specifier }) => ({ directory, specifier })))};
const esm = await Promise.all(
cases.map(async ({ directory }) =>
(await import(new URL(directory + "/main.mjs", import.meta.url))).default,
),
);
const require = cases.map(({ directory, specifier }) =>
createRequire(new URL(directory + "/main.cjs", import.meta.url))(specifier),
);
console.log(JSON.stringify({ esm, require }));`;
await Promise.all(
Object.entries(files).map(async ([path, content]) => {
const absolute = join(root, path);
await mkdir(dirname(absolute), { recursive: true });
await writeFile(absolute, content);
}),
);
return root;
};
cliTest.for(["CLI", "stdio MCP"] as const)(
"matches self references by parsed ESM name and preserves CommonJS prefix matching through %s",
async (surface, { cli }) => {
const root = await createSelfNameFixture();
if (surface === "CLI") {
const native = await promisify(execFile)(
process.execPath,
[join(root, "node-control.mjs")],
{ cwd: root, timeout: 5_000 },
);
expect(JSON.parse(native.stdout)).toEqual({
esm: cases.map(({ esmValue }) => esmValue),
require: cases.map(({ requireValue }) => requireValue),
});
}
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
for (const item of cases)
for (const [moduleKind, target] of [
["import", item.esmTarget],
["require", item.requireTarget],
] as const) {
const sourcePath = `${item.directory}/main.${moduleKind === "import" ? "mjs" : "cjs"}`;
const source = sourceFor(item.specifier, moduleKind);
expect(graph.edges).toContainEqual(
expect.objectContaining({
relation: "imports",
properties: expect.objectContaining({
module_link_kind: moduleKind,
specifier: item.specifier,
resolved_path: target,
resolution_status: "resolved",
}),
evidence: expect.objectContaining({
artifact: expect.objectContaining({
sha256: createHash("sha256").update(source).digest("hex"),
}),
location: expect.objectContaining({
value: expect.objectContaining({ source: sourcePath }),
}),
}),
}),
);
}
},
);
@@ -0,0 +1,278 @@
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
type PackageSpecifierCase = {
readonly specifier: string;
readonly target: string | null;
readonly status: "resolved" | "external" | "rejected" | "not-found";
readonly value: string | number | null;
readonly error: string | null;
};
const requests = [
{
specifier: "rea-self-fixture",
target: "root.cjs",
status: "resolved",
value: 11,
error: null,
},
{
specifier: "rea-self-fixture/feature",
target: "feature.cjs",
status: "resolved",
value: 22,
error: null,
},
{
specifier: "rea-self-fixture/feature?cache=1",
target: "punctuated.cjs",
status: "resolved",
value: 33,
error: null,
},
{
specifier: "rea-self-fixture/feature?cache=2",
target: null,
status: "external",
value: null,
error: "ERR_PACKAGE_PATH_NOT_EXPORTED",
},
{
specifier: "rea-self-fixture/feature#entry",
target: null,
status: "external",
value: null,
error: "ERR_PACKAGE_PATH_NOT_EXPORTED",
},
{
specifier: "fixture/feature?cache=1",
target: null,
status: "external",
value: null,
error: "ERR_PACKAGE_PATH_NOT_EXPORTED",
},
{
specifier: "fixture/feature#entry",
target: null,
status: "external",
value: null,
error: "ERR_PACKAGE_PATH_NOT_EXPORTED",
},
{
specifier: "fixture/literal?cache=1",
target: "node_modules/fixture/literal.cjs",
status: "resolved",
value: "literal",
error: null,
},
{
specifier: "legacy/?cache=1",
target: null,
status: "rejected",
value: null,
error: "ERR_UNSUPPORTED_DIR_IMPORT",
},
{
specifier: "legacy/#entry",
target: null,
status: "rejected",
value: null,
error: "ERR_UNSUPPORTED_DIR_IMPORT",
},
{
specifier: "legacy/nested/?cache=1",
target: null,
status: "rejected",
value: null,
error: "ERR_UNSUPPORTED_DIR_IMPORT",
},
{
specifier: "legacy/nested/#entry",
target: null,
status: "rejected",
value: null,
error: "ERR_UNSUPPORTED_DIR_IMPORT",
},
{
specifier: "legacy/feature.js",
target: "node_modules/legacy/feature.js",
status: "resolved",
value: "legacy feature",
error: null,
},
{
specifier: "legacy/feature.js?cache=1",
target: "node_modules/legacy/feature.js",
status: "resolved",
value: "legacy feature",
error: null,
},
{
specifier: "legacy/feature.js#entry",
target: "node_modules/legacy/feature.js",
status: "resolved",
value: "legacy feature",
error: null,
},
{
specifier: "legacy/with%20space.js?cache=1",
target: "node_modules/legacy/with space.js",
status: "resolved",
value: "legacy spaced feature",
error: null,
},
{
specifier: "legacy/feature",
target: null,
status: "not-found",
value: null,
error: "ERR_MODULE_NOT_FOUND",
},
] satisfies readonly PackageSpecifierCase[];
const createPackageSpecifierFixture = async (): Promise<{
readonly root: string;
readonly source: string;
}> => {
const root = await createTestTempDirectory("rea-package-specifier-suffix-");
const packagePath = join(root, "node_modules", "fixture");
const legacyPackagePath = join(root, "node_modules", "legacy");
await Promise.all([
mkdir(packagePath, { recursive: true }),
mkdir(join(legacyPackagePath, "nested"), { recursive: true }),
]);
await writeFile(
join(root, "package.json"),
JSON.stringify({
name: "rea-self-fixture",
exports: {
".": { import: "./root.cjs", require: "./require.cjs" },
"./feature": { import: "./feature.cjs", require: "./require.cjs" },
"./feature?cache=1": {
import: "./punctuated.cjs",
require: "./require.cjs",
},
},
}),
);
const source = requests
.map(
({ specifier }, index) =>
`import value${index} from ${JSON.stringify(specifier)};`,
)
.join("\n");
await Promise.all([
writeFile(join(root, "main.mjs"), source),
writeFile(join(root, "root.cjs"), "module.exports = 11;"),
writeFile(join(root, "feature.cjs"), "module.exports = 22;"),
writeFile(join(root, "punctuated.cjs"), "module.exports = 33;"),
writeFile(join(root, "require.cjs"), "module.exports = -1;"),
]);
await writeFile(
join(packagePath, "package.json"),
JSON.stringify({
exports: {
"./feature": "./feature.cjs",
"./literal?cache=1": "./literal.cjs",
},
}),
);
await Promise.all([
writeFile(join(packagePath, "feature.cjs"), 'module.exports = "feature";'),
writeFile(join(packagePath, "literal.cjs"), 'module.exports = "literal";'),
writeFile(
join(legacyPackagePath, "package.json"),
JSON.stringify({ name: "legacy", main: "./main.cjs" }),
),
writeFile(
join(legacyPackagePath, "main.cjs"),
'module.exports = "legacy root";',
),
writeFile(
join(legacyPackagePath, "feature.js"),
'module.exports = "legacy feature";',
),
writeFile(
join(legacyPackagePath, "with space.js"),
'module.exports = "legacy spaced feature";',
),
writeFile(
join(legacyPackagePath, "nested", "package.json"),
JSON.stringify({ main: "./main.cjs" }),
),
writeFile(
join(legacyPackagePath, "nested", "main.cjs"),
'module.exports = "legacy nested";',
),
]);
return { root, source };
};
cliTest.for(["CLI", "stdio MCP"] as const)(
"resolves package self references and suffix keys through %s",
async (surface, { cli }) => {
const { root, source } = await createPackageSpecifierFixture();
if (surface === "CLI") {
const native = await promisify(execFile)(
process.execPath,
[
"--input-type=module",
"-e",
`const requests = ${JSON.stringify(requests.map(({ specifier }) => specifier))};
const results = await Promise.all(requests.map(async (specifier) => {
try {
const result = await import(specifier);
return { value: result.default, error: null };
} catch (error) {
return { value: null, error: error.code };
}
}));
console.log(JSON.stringify(results));`,
],
{ cwd: root, timeout: 5_000 },
);
expect(JSON.parse(native.stdout)).toEqual(
requests.map(({ value, error }) => ({ value, error })),
);
}
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
for (const { specifier, target, status } of requests)
expect(graph.edges).toContainEqual(
expect.objectContaining({
relation: "imports",
properties: expect.objectContaining({
module_link_kind: "import",
specifier,
resolved_path: target,
resolution_status: status,
}),
evidence: expect.objectContaining({
...(specifier === "legacy/feature"
? {
limitations: expect.arrayContaining([
"The selected package ESM subpath target node_modules/legacy/feature does not exist in the inventoried artifact container; ESM subpath targets are exact files, so no extension or directory index was tried.",
]),
}
: {}),
artifact: expect.objectContaining({
sha256: createHash("sha256").update(source).digest("hex"),
}),
location: expect.objectContaining({
value: expect.objectContaining({ source: "main.mjs" }),
}),
}),
}),
);
},
);
@@ -7,9 +7,9 @@ import { expect } from "vitest";
import { z } from "zod";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
const createPackageSubpathFixture = async () => {
const root = await createTestTempDirectory("rea-package-subpaths-");
@@ -124,26 +124,7 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
).replaceAll("\\", "/"),
).toBe("node_modules/fi%xture/actual.cjs");
let document: unknown;
if (surface === "CLI") {
const response = await cli.run({
arguments: ["analyze-javascript-application", root, "--format", "json"],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(response.exitCode, response.stderr).toBe(0);
document = response.json;
} else {
const { call } = await connectLocalToolsMcp();
const response = await call("analyze_javascript_application", {
input_path: root,
format: "directory",
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
document = response.structuredContent;
}
const { graph } = z
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
.parse(document).normalized_result;
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
const imports = graph.edges.filter(
({ properties }) => properties.module_link_kind === "require",
);
@@ -0,0 +1,79 @@
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { z } from "zod";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest.for(["CLI", "stdio MCP"] as const)(
"recovers retained prefixed source-map originals and byte identity through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory("rea-retained-map-prefix-");
const original = "export const answer: number = 42;";
const mapText = `\uFEFF)]}'retained response\r\n${JSON.stringify({
version: 3,
sources: ["original.ts"],
sourcesContent: [original],
names: [],
mappings: "AAAA",
})}`;
const mapPath = join(root, "bundle.js.map");
await writeFile(
join(root, "bundle.js"),
"export const answer = 42;\n//# sourceMappingURL=bundle.js.map\n",
);
await writeFile(mapPath, mapText);
let document: unknown;
if (surface === "CLI") {
const response = await cli.run({
arguments: ["analyze-javascript-application", root, "--format", "json"],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(response.exitCode, response.stderr).toBe(0);
document = response.json;
} else {
const { call } = await connectLocalToolsMcp();
const response = await call("analyze_javascript_application", {
input_path: root,
format: "directory",
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
document = response.structuredContent;
}
const { graph } = z
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
.parse(document).normalized_result;
const mapDigest = createHash("sha256").update(mapText).digest("hex");
expect(graph.nodes).toContainEqual(
expect.objectContaining({
kind: "source-module",
identity: expect.objectContaining({
strategy: "source-map-original",
source_map_sha256: mapDigest,
original_source: "original.ts",
source_sha256: createHash("sha256").update(original).digest("hex"),
}),
observations: expect.arrayContaining([
expect.objectContaining({
source_map_reference: expect.objectContaining({
source_name: "original.ts",
map_path: "bundle.js.map",
}),
properties: { content_available: true },
evidence: expect.objectContaining({
artifact: expect.objectContaining({ sha256: mapDigest }),
}),
}),
]),
}),
);
expect(graph.coverage.status).toBe("complete");
expect(await readFile(mapPath, "utf8")).toBe(mapText);
},
);
@@ -0,0 +1,34 @@
import { expect } from "vitest";
import { z } from "zod";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import type { TestCli } from "./cliFixture.js";
/** Analyze a directory through a public adapter and validate its result contract. */
export const analyzeJavaScriptDirectory = async (
surface: "CLI" | "stdio MCP",
cli: TestCli,
root: string,
) => {
let document: unknown;
if (surface === "CLI") {
const response = await cli.run({
arguments: ["analyze-javascript-application", root, "--format", "json"],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(response.exitCode, response.stderr).toBe(0);
document = response.json;
} else {
const { call } = await connectLocalToolsMcp();
const response = await call("analyze_javascript_application", {
input_path: root,
format: "directory",
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
document = response.structuredContent;
}
return z
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
.parse(document).normalized_result;
};