mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 06:15:59 +00:00
fix(reference): preserve module syntax and loader request identity
This commit is contained in:
1 parent
9974512600
commit
f2e5fd148a
10 files changed
+487
-190
No files matched your search
@@ -0,0 +1,203 @@
|
||||
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 { historicalSourceGraphSchema } from "../../../src/domain/referenceSourceGraph.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"preserves historical module syntax, loader scope and URL request identity",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-reference-module-semantics-",
|
||||
);
|
||||
const files: Record<string, string> = {
|
||||
"package.json": '{"type":"module"}\n',
|
||||
"main.js": 'require("./dep.cjs"); export {};\n',
|
||||
"dep.cjs": 'throw new Error("must never load");\n',
|
||||
"optional.cjs":
|
||||
'require.resolve?.("./dep.cjs"); require?.resolve("./dep.cjs");\n',
|
||||
"overwritten.cjs":
|
||||
'require.resolve = other; require.resolve?.("./dep.cjs");\n',
|
||||
"shadowed.cjs":
|
||||
'function f(require) { require?.resolve("./dep.cjs"); }\n',
|
||||
"types.d.ts":
|
||||
'declare module "consumer" { import type { Marker } from "dependency"; export type A = import("./dep").Marker; export type B = typeof import("./dep"); }\n',
|
||||
"dep.ts": "export interface Marker { id: string }\n",
|
||||
"nested/package.json": '{"type":"commonjs"}\n',
|
||||
"nested/main.js": 'require("../dep.cjs");\n',
|
||||
"unknown/package.json": "{invalid",
|
||||
"unknown/main.js": 'require("../dep.cjs");\n',
|
||||
"url.mjs":
|
||||
'import "./dep%20name.mjs?v=1#first"; import "./dep%20name.mjs?v=2#second"; import "./dep%2520name.mjs"; import "./bad%2Fname.mjs"; import "./bad%.mjs";\n',
|
||||
"dep name.mjs": "export const actual = 1;\n",
|
||||
"dep%20name.mjs": "export const literalPercent = 2;\n",
|
||||
"literal.cjs":
|
||||
'require.resolve("./dep%20name.mjs"); require.resolve("./dep?literal.cjs");\n',
|
||||
"dep?literal.cjs": 'throw new Error("must never load");\n',
|
||||
"oracle.mjs":
|
||||
'import { fileURLToPath } from "node:url"; import { createRequire } from "node:module"; const require = createRequire(import.meta.url); console.log(JSON.stringify([fileURLToPath(import.meta.resolve("./dep%20name.mjs?v=1#first")), fileURLToPath(import.meta.resolve("./dep%2520name.mjs")), require.resolve("./dep%20name.mjs"), require.resolve("./dep?literal.cjs")]));\n',
|
||||
};
|
||||
for (const directory of ["nested", "unknown"])
|
||||
await mkdir(join(root, directory));
|
||||
await Promise.all(
|
||||
Object.entries(files).map(([path, source]) =>
|
||||
writeFile(join(root, path), source),
|
||||
),
|
||||
);
|
||||
// This self-authored resolver control never loads a dependency.
|
||||
const oracle = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[join(root, "oracle.mjs")],
|
||||
{ timeout: 5_000 },
|
||||
);
|
||||
expect(JSON.parse(oracle.stdout)).toEqual(
|
||||
[
|
||||
"dep name.mjs",
|
||||
"dep%20name.mjs",
|
||||
"dep%20name.mjs",
|
||||
"dep?literal.cjs",
|
||||
].map((path) => join(root, path)),
|
||||
);
|
||||
const result = await cli.run({
|
||||
arguments: ["import-reference-source", root, "--json"],
|
||||
});
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
const graph = historicalSourceGraphSchema.parse(result.json);
|
||||
expect(graph.parse_failures).toEqual([]);
|
||||
const edges = (from: string) =>
|
||||
graph.relationships.filter((edge) => edge.from_path === from);
|
||||
expect(edges("main.js")).toEqual([]);
|
||||
expect(edges("shadowed.cjs")).toEqual([]);
|
||||
for (const from of ["optional.cjs", "nested/main.js"])
|
||||
expect(edges(from)).toEqual([
|
||||
{
|
||||
from_path: from,
|
||||
to: "dep.cjs",
|
||||
kind: "requires",
|
||||
resolution: "internal",
|
||||
parse_state: "parsed",
|
||||
},
|
||||
]);
|
||||
for (const [from, to] of [
|
||||
["overwritten.cjs", "./dep.cjs"],
|
||||
["unknown/main.js", "../dep.cjs"],
|
||||
] as const)
|
||||
expect(edges(from)).toEqual([
|
||||
{
|
||||
from_path: from,
|
||||
to,
|
||||
kind: "requires",
|
||||
resolution: "unknown",
|
||||
parse_state: "partial",
|
||||
},
|
||||
]);
|
||||
expect(edges("types.d.ts")).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
from_path: "types.d.ts",
|
||||
to: "consumer",
|
||||
kind: "declares-module",
|
||||
resolution: "unknown",
|
||||
parse_state: "parsed",
|
||||
},
|
||||
{
|
||||
from_path: "types.d.ts",
|
||||
to: "dependency",
|
||||
kind: "imports",
|
||||
resolution: "external",
|
||||
parse_state: "parsed",
|
||||
},
|
||||
{
|
||||
from_path: "types.d.ts",
|
||||
to: "dep.ts",
|
||||
kind: "imports",
|
||||
resolution: "internal",
|
||||
parse_state: "parsed",
|
||||
},
|
||||
]),
|
||||
);
|
||||
for (const [source_specifier, to] of [
|
||||
["./dep%20name.mjs?v=1#first", "dep name.mjs"],
|
||||
["./dep%20name.mjs?v=2#second", "dep name.mjs"],
|
||||
["./dep%2520name.mjs", "dep%20name.mjs"],
|
||||
])
|
||||
expect(edges("url.mjs")).toContainEqual({
|
||||
from_path: "url.mjs",
|
||||
to,
|
||||
source_specifier,
|
||||
kind: "imports",
|
||||
resolution: "internal",
|
||||
parse_state: "parsed",
|
||||
});
|
||||
expect(
|
||||
edges("url.mjs").filter((edge) => edge.resolution === "unresolved"),
|
||||
).toHaveLength(2);
|
||||
expect(
|
||||
graph.entries.find((entry) => entry.path === "url.mjs")?.limitations,
|
||||
).toHaveLength(2);
|
||||
expect(
|
||||
edges("literal.cjs")
|
||||
.map((edge) => edge.to)
|
||||
.sort(),
|
||||
).toEqual(["dep%20name.mjs", "dep?literal.cjs"]);
|
||||
for (const [path, source] of Object.entries(files))
|
||||
expect(graph.entries).toContainEqual(
|
||||
expect.objectContaining({
|
||||
path,
|
||||
sha256: createHash("sha256").update(source).digest("hex"),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"preserves case-sensitive compiler suffix languages in full graphs and summaries",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-reference-native-suffixes-",
|
||||
);
|
||||
const languages = {
|
||||
"upper.C": "C++",
|
||||
"lower.c": "C",
|
||||
"upper.M": "Objective-C++",
|
||||
"lower.m": "Objective-C",
|
||||
};
|
||||
await Promise.all(
|
||||
Object.keys(languages).map((path) =>
|
||||
writeFile(join(root, path), "// inert source\n"),
|
||||
),
|
||||
);
|
||||
const full = await cli.run({
|
||||
arguments: ["import-reference-source", root, "--json"],
|
||||
});
|
||||
expect(full.exitCode, full.stderr).toBe(0);
|
||||
const graph = historicalSourceGraphSchema.parse(full.json);
|
||||
expect(
|
||||
Object.fromEntries(
|
||||
graph.entries.map((entry) => [
|
||||
entry.path,
|
||||
entry.kind === "file" ? entry.language : null,
|
||||
]),
|
||||
),
|
||||
).toEqual(languages);
|
||||
const summary = await cli.run({
|
||||
arguments: [
|
||||
"import-reference-source",
|
||||
root,
|
||||
"--json",
|
||||
"--detail",
|
||||
"summary",
|
||||
],
|
||||
});
|
||||
expect(summary.exitCode, summary.stderr).toBe(0);
|
||||
expect(summary.json).toMatchObject({
|
||||
languages: ["C", "C++", "Objective-C", "Objective-C++"],
|
||||
});
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user