mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(javascript): preserve loader classification and exact source targets
This commit is contained in:
@@ -844,3 +844,12 @@ truncated under the `html_inline_script_locations_per_file` limit, and the
|
||||
scope's evidence counts the omitted locations. The graph names the
|
||||
file and up to 10 starting lines in its limitations, and coverage is partial. Import maps, speculation rules, data blocks, and blank
|
||||
bodies are not JavaScript and are not reported.
|
||||
|
||||
For static source relationships, an explicitly requested declaration filename
|
||||
(`.d.ts`, `.d.mts`, or `.d.cts`) retains its inventoried identity through both
|
||||
`import` and `require`. A declaration found only as an emitted-extension fallback
|
||||
remains unavailable as a runtime implementation. Package URL targets decode
|
||||
filename escapes once, including dots within filenames; forbidden whole segments
|
||||
and encoded separators remain rejected. ESM local-path classification uses the
|
||||
authored request before URL whitespace preprocessing, so leading whitespace
|
||||
cannot turn a bare request into a relative file dependency.
|
||||
|
||||
@@ -648,8 +648,8 @@ const contextualCandidate = (
|
||||
"URL and Node builtin schemes are outside static artifact module resolution.",
|
||||
]);
|
||||
if (
|
||||
!isRelativeJavaScriptModuleSpecifier(declared) &&
|
||||
!declared.startsWith("/")
|
||||
!isRelativeJavaScriptModuleSpecifier(input.declaredPath) &&
|
||||
!input.declaredPath.startsWith("/")
|
||||
)
|
||||
return bareModuleCandidate(input);
|
||||
if (input.moduleKind !== "require") {
|
||||
@@ -1088,7 +1088,7 @@ const resolveLegacyEsmSubpath = (
|
||||
subpath: string,
|
||||
): CandidateResolution => {
|
||||
const subpathPath = stripQueryAndFragment(htmlUrlText(subpath));
|
||||
const declarationRejection = rejectDeclaration(subpathPath);
|
||||
const declarationRejection = rejectDeclaration(subpathPath, true);
|
||||
if (declarationRejection !== null) return declarationRejection;
|
||||
let depth = directory === "." ? 0 : directory.split("/").length;
|
||||
for (const segment of subpathPath.split("/")) {
|
||||
@@ -1115,7 +1115,7 @@ const resolveLegacyEsmSubpath = (
|
||||
const encodedPath = url.pathname.slice(
|
||||
PACKAGE_ARTIFACT_ROOT_URL.pathname.length,
|
||||
);
|
||||
const rejected = rejectDeclaration(encodedPath);
|
||||
const rejected = rejectDeclaration(encodedPath, true);
|
||||
if (rejected !== null) return rejected;
|
||||
const decoded = decodeModuleUrlPath(encodedPath);
|
||||
if (typeof decoded !== "string") return decoded;
|
||||
@@ -1201,7 +1201,7 @@ const resolveExportTarget = (
|
||||
PACKAGE_ARTIFACT_ROOT_URL.pathname.length,
|
||||
);
|
||||
// Package targets are URLs for both loaders, so their percent bytes encode.
|
||||
const rejected = rejectDeclaration(encodedPath, false);
|
||||
const rejected = rejectDeclaration(encodedPath, true);
|
||||
if (rejected !== null) return withTargetSource(rejected);
|
||||
const decoded = decodeModuleUrlPath(encodedPath);
|
||||
if (typeof decoded !== "string") return withTargetSource(decoded);
|
||||
@@ -1483,7 +1483,15 @@ const resolveCandidate = (
|
||||
emittedSourceCandidates: readonly TypeScriptEmittedSourceCandidate[] = [],
|
||||
): CandidateResolution => {
|
||||
if (!commonJsDirectoryRequest(input)) {
|
||||
// An explicitly selected source path owns its identity independently of
|
||||
// whether its declaration bytes can implement a runtime module.
|
||||
const exactSource =
|
||||
input.context === "module-specifier" &&
|
||||
input.runtimePathKind === undefined
|
||||
? resolveRuntimeSourceCandidates(input, candidate, [])
|
||||
: null;
|
||||
const file =
|
||||
exactSource ??
|
||||
resolveFileCandidates(
|
||||
input,
|
||||
fileCandidates(candidate, input.moduleKind),
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { join } from "node:path";
|
||||
|
||||
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.each(["CLI", "stdio MCP"] as const)(
|
||||
"preserves module classification and explicit source identities through %s",
|
||||
async (surface, { cli }) => {
|
||||
const root = await createTestTempDirectory("rea-module-owner-semantics-");
|
||||
const valid = {
|
||||
"legacy/x%2Ey.js": "node_modules/legacy/x.y.js",
|
||||
"mapped/encoded": "node_modules/mapped/x.y.js",
|
||||
"mapped/double": "node_modules/mapped/x%2Ey.js",
|
||||
"#encoded": "local.file.mjs",
|
||||
"./dep.mjs ": "dep.mjs",
|
||||
"./de\np.mjs": "dep.mjs",
|
||||
};
|
||||
const invalid = [" ./dep.mjs", "\n./dep.mjs", "\t./dep.mjs", ".\n/dep.mjs"];
|
||||
await writeFixtureFiles(root, {
|
||||
"package.json": JSON.stringify({
|
||||
type: "module",
|
||||
imports: { "#encoded": "./local%2Efile.mjs" },
|
||||
}),
|
||||
"main.mjs": [
|
||||
...Object.keys(valid),
|
||||
...invalid,
|
||||
"mapped/missing",
|
||||
"mapped/invalid",
|
||||
"./dep.d.ts",
|
||||
"./cfg.d.mts",
|
||||
"./legacy.d.cts",
|
||||
]
|
||||
.map((specifier) => `import ${JSON.stringify(specifier)};`)
|
||||
.join("\n"),
|
||||
"main.cjs": [
|
||||
"./dep.d.ts",
|
||||
"./cfg.d.mts",
|
||||
"./legacy.d.cts",
|
||||
"mapped/encoded",
|
||||
"mapped/double",
|
||||
]
|
||||
.map((specifier) => `require(${JSON.stringify(specifier)});`)
|
||||
.join("\n"),
|
||||
"fallback.cts": 'require("./only.js");',
|
||||
"dep.d.ts": "export declare const value: number;",
|
||||
"cfg.d.mts": "export declare const value: number;",
|
||||
"legacy.d.cts": "export declare const value: number;",
|
||||
"only.d.ts": "export declare const value: number;",
|
||||
"dep.mjs": 'throw new Error("must never load");',
|
||||
"local.file.mjs": 'throw new Error("must never load");',
|
||||
"node_modules/legacy/package.json": '{"type":"module"}',
|
||||
"node_modules/legacy/x.y.js": 'throw new Error("must never load");',
|
||||
"node_modules/mapped/package.json": JSON.stringify({
|
||||
type: "module",
|
||||
exports: {
|
||||
"./encoded": "./x%2Ey.js",
|
||||
"./double": "./x%252Ey.js",
|
||||
"./missing": ["./absent%2Efile.js", "./x.y.js"],
|
||||
"./invalid": "./%2e%2e/escape.js",
|
||||
},
|
||||
}),
|
||||
"node_modules/mapped/x.y.js": 'throw new Error("must never load");',
|
||||
"node_modules/mapped/x%2Ey.js": 'throw new Error("must never load");',
|
||||
"oracle.mjs": `import {fileURLToPath} from "node:url"; import {relative} from "node:path"; const valid = ${JSON.stringify(Object.keys(valid))}; const invalid = ${JSON.stringify(invalid)}; console.log(JSON.stringify({ valid: valid.map(request => relative(process.cwd(), fileURLToPath(import.meta.resolve(request)))), invalid: invalid.map(request => { try { import.meta.resolve(request); return false; } catch { return true; } }) }));`,
|
||||
});
|
||||
expect(await runNodeJsonControl(root, join(root, "oracle.mjs"))).toEqual({
|
||||
valid: Object.values(valid),
|
||||
invalid: invalid.map(() => true),
|
||||
});
|
||||
const result = await analyzeJavaScriptDirectory(surface, cli, root);
|
||||
expect(result.statistics.parse_failures).toBe(0);
|
||||
const edges = (specifier: string) =>
|
||||
result.graph.edges.filter(
|
||||
(edge) =>
|
||||
edge.relation === "imports" &&
|
||||
edge.properties.specifier === specifier,
|
||||
);
|
||||
for (const [specifier, target] of Object.entries(valid)) {
|
||||
const matches = edges(specifier);
|
||||
expect(matches.length, specifier).toBeGreaterThan(0);
|
||||
for (const edge of matches)
|
||||
expect(edge.properties).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: target,
|
||||
});
|
||||
}
|
||||
for (const specifier of invalid) {
|
||||
const matches = edges(specifier);
|
||||
expect(matches.length, specifier).toBeGreaterThan(0);
|
||||
for (const edge of matches)
|
||||
expect(edge.properties.resolved_path).toBeNull();
|
||||
}
|
||||
for (const suffix of ["dep.d.ts", "cfg.d.mts", "legacy.d.cts"]) {
|
||||
const matches = edges(`./${suffix}`);
|
||||
expect(matches.length, suffix).toBeGreaterThan(0);
|
||||
for (const edge of matches)
|
||||
expect(edge.properties).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: suffix,
|
||||
});
|
||||
}
|
||||
for (const [specifier, status] of [
|
||||
["./only.js", "unavailable"],
|
||||
["mapped/missing", "not-found"],
|
||||
["mapped/invalid", "rejected"],
|
||||
] as const) {
|
||||
const matches = edges(specifier);
|
||||
expect(matches.length, specifier).toBeGreaterThan(0);
|
||||
for (const edge of matches)
|
||||
expect(edge.properties).toMatchObject({
|
||||
resolution_status: status,
|
||||
resolved_path: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
-1
@@ -342,7 +342,6 @@ describe("package exports URL path rejections", () => {
|
||||
"./actual%00file.cjs",
|
||||
"./actual%2Ffile.cjs",
|
||||
"./actual%5Cfile.cjs",
|
||||
"./actual%2Efile.cjs",
|
||||
"./../../../../escape.cjs",
|
||||
].map((declaredPath) => ({ moduleKind, declaredPath })),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user