mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 03:02:56 +00:00
feat(javascript): resolve exact package import aliases (#1862)
* feat(javascript): resolve exact package import aliases Resolve exact #alias specifiers against the importer's enclosing package.json imports map using the shared condition, array, and null target selection. Nested packages keep their own maps; missing keys, explicit nulls, and out-of-scope aliases report a precise reason. Wildcard, recursive, and external-package import targets remain unresolved. * fix(javascript): keep nested import aliases in CLI graphs Bound nested #alias requires produce application-graph require edges from the enclosing package. Node reports MODULE_NOT_FOUND when a nested package.json has no imports field, without inheriting the ancestor map. * fix(javascript): resolve literal package import keys before path admission * fix(javascript): preserve import keys and invalid-target fallback * fix(javascript): preserve unmatched package target arrays * fix(javascript): retain package target refusal evidence * test(browser): match real verifier startup budget --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
1 parent
e8491e023a
commit
d28b41ea4e
10 files changed
+1838
-83
No files matched your search
@@ -0,0 +1,251 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
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";
|
||||
|
||||
const literalAliases = ["#/x", "#x%2Fy", "#x%2Ey", "#x\\y", "#x\0y"];
|
||||
const fallbackAliases = {
|
||||
"#inactive-array": {
|
||||
node: [{ browser: "./browser.cjs" }],
|
||||
default: "./config.cjs",
|
||||
},
|
||||
"#nested-inactive-array": {
|
||||
node: [[{ browser: "./browser.cjs" }]],
|
||||
default: "./config.cjs",
|
||||
},
|
||||
"#parent": ["../outside.cjs", "./config.cjs"],
|
||||
"#absolute": { node: [["/outside.cjs"], "./config.cjs"] },
|
||||
"#url": {
|
||||
import: ["https://example.invalid/outside.cjs", "./config.cjs"],
|
||||
require: ["file:///outside.cjs", "./config.cjs"],
|
||||
},
|
||||
};
|
||||
const resolvedAliases = [...literalAliases, ...Object.keys(fallbackAliases)];
|
||||
const refusedAliases = {
|
||||
"#empty": { target: [], limitation: "an empty imports target array" },
|
||||
"#empty-active": {
|
||||
target: { node: [] },
|
||||
limitation: "an empty imports target array",
|
||||
},
|
||||
"#null-last": {
|
||||
target: [[], null],
|
||||
limitation: "an explicit null imports target",
|
||||
},
|
||||
"#empty-last": {
|
||||
target: [null, []],
|
||||
limitation: "an empty imports target array",
|
||||
},
|
||||
"#inactive": {
|
||||
target: [{ browser: "./browser.cjs" }],
|
||||
limitation: "it declares browser.",
|
||||
},
|
||||
"#nested-inactive": {
|
||||
target: [
|
||||
[{ browser: "./browser.cjs" }, { "rea-inactive": "./inactive.cjs" }],
|
||||
],
|
||||
limitation: "it declares browser, rea-inactive.",
|
||||
},
|
||||
};
|
||||
const allAliases = [...resolvedAliases, ...Object.keys(refusedAliases)];
|
||||
|
||||
const createPackageImportAliasFixture = async () => {
|
||||
const root = await createTestTempDirectory("rea-package-import-aliases-");
|
||||
const mainSource = [
|
||||
'const config = require("#config");',
|
||||
'const nested = require("./node_modules/fixture/index.cjs");',
|
||||
'const missing = require("#missing");',
|
||||
...allAliases.map((specifier) => `require(${JSON.stringify(specifier)});`),
|
||||
].join("\n");
|
||||
const moduleSource = allAliases
|
||||
.map((specifier) => `import ${JSON.stringify(specifier)};`)
|
||||
.join("\n");
|
||||
const nestedSource = [
|
||||
'const config = require("#config");',
|
||||
"module.exports = config;",
|
||||
].join("\n");
|
||||
const files = {
|
||||
"package.json": JSON.stringify({
|
||||
name: "app",
|
||||
imports: {
|
||||
"#config": "./config.cjs",
|
||||
...Object.fromEntries(
|
||||
literalAliases.map((specifier) => [specifier, "./config.cjs"]),
|
||||
),
|
||||
...fallbackAliases,
|
||||
...Object.fromEntries(
|
||||
Object.entries(refusedAliases).map(([specifier, { target }]) => [
|
||||
specifier,
|
||||
target,
|
||||
]),
|
||||
),
|
||||
},
|
||||
}),
|
||||
"main.cjs": mainSource,
|
||||
"main.mjs": moduleSource,
|
||||
"config.cjs": "module.exports = 'app-config';",
|
||||
"node_modules/fixture/package.json": JSON.stringify({
|
||||
name: "fixture",
|
||||
imports: { "#config": "./local.cjs" },
|
||||
}),
|
||||
"node_modules/fixture/index.cjs": nestedSource,
|
||||
"node_modules/fixture/local.cjs": "module.exports = 'fixture-config';",
|
||||
};
|
||||
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, mainSource, moduleSource, nestedSource };
|
||||
};
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"resolves exact package import aliases with package scope through %s",
|
||||
async (surface, { cli }) => {
|
||||
const { root, mainSource, moduleSource, nestedSource } =
|
||||
await createPackageImportAliasFixture();
|
||||
const nativeRequire = createRequire(join(root, "main.cjs"));
|
||||
expect(
|
||||
relative(root, nativeRequire.resolve("#config")).replaceAll("\\", "/"),
|
||||
).toBe("config.cjs");
|
||||
expect(
|
||||
relative(
|
||||
root,
|
||||
createRequire(join(root, "node_modules/fixture/index.cjs")).resolve(
|
||||
"#config",
|
||||
),
|
||||
).replaceAll("\\", "/"),
|
||||
).toBe("node_modules/fixture/local.cjs");
|
||||
expect(() => nativeRequire.resolve("#missing")).toThrow(
|
||||
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;
|
||||
for (const specifier of resolvedAliases)
|
||||
for (const [moduleKind, sourcePath, source] of [
|
||||
["require", "main.cjs", mainSource],
|
||||
["import", "main.mjs", moduleSource],
|
||||
] as const)
|
||||
expect(graph.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
module_link_kind: moduleKind,
|
||||
specifier,
|
||||
resolved_path: "config.cjs",
|
||||
resolution_status: "resolved",
|
||||
}),
|
||||
evidence: sourceEvidence(sourcePath, source),
|
||||
}),
|
||||
);
|
||||
const imports = graph.edges.filter(
|
||||
({ properties }) => properties.module_link_kind === "require",
|
||||
);
|
||||
for (const [specifier, { limitation }] of Object.entries(refusedAliases))
|
||||
for (const [moduleKind, sourcePath, source] of [
|
||||
["require", "main.cjs", mainSource],
|
||||
["import", "main.mjs", moduleSource],
|
||||
] as const)
|
||||
expect(graph.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
module_link_kind: moduleKind,
|
||||
specifier,
|
||||
resolved_path: null,
|
||||
resolution_status: "external",
|
||||
}),
|
||||
evidence: sourceEvidence(sourcePath, source, limitation),
|
||||
}),
|
||||
);
|
||||
expect(imports).toContainEqual(
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
specifier: "#config",
|
||||
resolved_path: "config.cjs",
|
||||
resolution_status: "resolved",
|
||||
}),
|
||||
evidence: sourceEvidence("main.cjs", mainSource),
|
||||
}),
|
||||
);
|
||||
expect(imports).toContainEqual(
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
specifier: "#config",
|
||||
resolved_path: "node_modules/fixture/local.cjs",
|
||||
resolution_status: "resolved",
|
||||
}),
|
||||
evidence: sourceEvidence(
|
||||
"node_modules/fixture/index.cjs",
|
||||
nestedSource,
|
||||
),
|
||||
}),
|
||||
);
|
||||
expect(imports).toContainEqual(
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
specifier: "#missing",
|
||||
resolved_path: null,
|
||||
resolution_status: "external",
|
||||
}),
|
||||
evidence: expect.objectContaining({
|
||||
limitations: expect.arrayContaining([
|
||||
expect.stringContaining("does not define package import alias"),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const sourceEvidence = (
|
||||
sourcePath: string,
|
||||
source: string,
|
||||
limitation?: string,
|
||||
): unknown =>
|
||||
expect.objectContaining({
|
||||
artifact: expect.objectContaining({
|
||||
available: true,
|
||||
sha256: createHash("sha256").update(source).digest("hex"),
|
||||
}),
|
||||
location: expect.objectContaining({
|
||||
available: true,
|
||||
value: expect.objectContaining({
|
||||
kind: "source-range",
|
||||
source: sourcePath,
|
||||
}),
|
||||
}),
|
||||
...(limitation === undefined
|
||||
? {}
|
||||
: {
|
||||
limitations: expect.arrayContaining([
|
||||
expect.stringContaining(limitation),
|
||||
]),
|
||||
}),
|
||||
});
|
||||
@@ -29,6 +29,11 @@ const chromeExecutable = [
|
||||
existsSync(candidate),
|
||||
);
|
||||
|
||||
// Match the real-browser verifier: a cold Chrome launch on a loaded coverage
|
||||
// runner can exceed 20 seconds before publishing DevToolsActivePort.
|
||||
const CHROME_STARTUP_TIMEOUT_MS = 60_000;
|
||||
const CHROME_TEST_TIMEOUT_MS = CHROME_STARTUP_TIMEOUT_MS + 30_000;
|
||||
|
||||
// Issue 1586's page, plus the HTML, image, use, and foreign-origin cases the
|
||||
// real snapshot must distinguish. The XHTML page is the prefixed-svg gap
|
||||
// reported on PR 1602. The tests are skipped when no Chrome-family executable
|
||||
@@ -90,7 +95,7 @@ it.skipIf(chromeExecutable === undefined)(
|
||||
executable,
|
||||
activePortPath: join(profile, "DevToolsActivePort"),
|
||||
stderr: () => stderr,
|
||||
timeoutMs: 20_000,
|
||||
timeoutMs: CHROME_STARTUP_TIMEOUT_MS,
|
||||
});
|
||||
const endpoint = `http://127.0.0.1:${String(port)}`;
|
||||
const provider = new CdpBrowserProvider();
|
||||
@@ -158,6 +163,7 @@ it.skipIf(chromeExecutable === undefined)(
|
||||
});
|
||||
}
|
||||
},
|
||||
CHROME_TEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
const xhtmlPage = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -261,7 +267,7 @@ for (const { format, page, contentType, nodeNames, link, destinations } of [
|
||||
executable,
|
||||
activePortPath: join(profile, "DevToolsActivePort"),
|
||||
stderr: () => stderr,
|
||||
timeoutMs: 20_000,
|
||||
timeoutMs: CHROME_STARTUP_TIMEOUT_MS,
|
||||
});
|
||||
const endpoint = `http://127.0.0.1:${String(port)}`;
|
||||
const provider = new CdpBrowserProvider();
|
||||
@@ -312,6 +318,7 @@ for (const { format, page, contentType, nodeNames, link, destinations } of [
|
||||
});
|
||||
}
|
||||
},
|
||||
CHROME_TEST_TIMEOUT_MS,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -742,6 +742,30 @@ describe("nested and array package export targets", () => {
|
||||
);
|
||||
});
|
||||
describe("unmatched package export conditions", () => {
|
||||
it.each([
|
||||
[[{ browser: "./default.cjs" }], "browser"],
|
||||
[
|
||||
[[{ browser: "./default.cjs" }, { "rea-inactive": "./node.cjs" }]],
|
||||
"browser, rea-inactive",
|
||||
],
|
||||
[{ node: [{ browser: "./default.cjs" }] }, "node, browser"],
|
||||
] as const)(
|
||||
"preserves condition names in unmatched target %j",
|
||||
async (target, declared) => {
|
||||
for (const moduleKind of ["import", "require"] as const) {
|
||||
const { node_error, outcome } = await compareWithNode(
|
||||
target,
|
||||
moduleKind,
|
||||
);
|
||||
expect(node_error).toBe("ERR_PACKAGE_PATH_NOT_EXPORTED");
|
||||
expect(outcome).toMatchObject({
|
||||
resolution_status: "external",
|
||||
resolved_path: null,
|
||||
});
|
||||
expect(outcome.limitations[0]).toContain(`It declares ${declared}.`);
|
||||
}
|
||||
},
|
||||
);
|
||||
it.each(["import", "require"] as const)(
|
||||
"names the declared conditions for %s",
|
||||
async (moduleKind) => {
|
||||
@@ -769,6 +793,31 @@ describe("unmatched package export conditions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("blocked package export provenance", () => {
|
||||
it.each([
|
||||
[[], "an empty exports target array"],
|
||||
[{ node: [] }, "an empty exports target array"],
|
||||
[[null, []], "an empty exports target array"],
|
||||
[[[], null], "an explicit null exports target"],
|
||||
] as const)(
|
||||
"preserves the blocking source value in %j",
|
||||
async (target, limitation) => {
|
||||
for (const moduleKind of ["import", "require"] as const) {
|
||||
const { node_error, outcome } = await compareWithNode(
|
||||
target,
|
||||
moduleKind,
|
||||
);
|
||||
expect(node_error).toBe("ERR_PACKAGE_PATH_NOT_EXPORTED");
|
||||
expect(outcome).toMatchObject({
|
||||
resolution_status: "external",
|
||||
resolved_path: null,
|
||||
});
|
||||
expect(outcome.limitations[0]).toContain(limitation);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("package exports fallback refusal reasons", () => {
|
||||
it.each([
|
||||
[{ import: [42, 7] }, "ERR_INVALID_PACKAGE_TARGET", "rejected"],
|
||||
|
||||
@@ -0,0 +1,654 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join, relative } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
resolveArtifactPathByContext,
|
||||
type ArtifactPathResolution,
|
||||
} from "../../../src/application/javascript/JavaScriptArtifactPathResolution.js";
|
||||
import type { JavaScriptArtifactFile } from "../../../src/domain/javascript/javascriptArtifactFiles.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
const nodeOutcomeSchema = z.strictObject({
|
||||
target: z.string().nullable(),
|
||||
error_code: z.string().nullable(),
|
||||
});
|
||||
|
||||
const posixRelative = (root: string, absolute: string): string =>
|
||||
relative(root, absolute).replaceAll("\\", "/");
|
||||
|
||||
const inventory = (
|
||||
contents: ReadonlyMap<string, string>,
|
||||
): Map<string, JavaScriptArtifactFile> =>
|
||||
new Map(
|
||||
[...contents].map(([path, value]) => [
|
||||
path,
|
||||
{
|
||||
path,
|
||||
container_sha256: "a".repeat(64),
|
||||
sha256: "b".repeat(64),
|
||||
bytes: Buffer.byteLength(value),
|
||||
inventory_artifact_id: path,
|
||||
kind: path.endsWith("package.json") ? "package-json" : "javascript",
|
||||
unpacked: false,
|
||||
text: { included: true, value },
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const writeTree = async (
|
||||
root: string,
|
||||
files: Readonly<Record<string, string>>,
|
||||
): Promise<Map<string, string>> => {
|
||||
const contents = new Map<string, string>();
|
||||
await Promise.all(
|
||||
Object.entries(files).map(async ([path, value]) => {
|
||||
const absolute = join(root, path);
|
||||
await mkdir(dirname(absolute), { recursive: true });
|
||||
await writeFile(absolute, value);
|
||||
contents.set(path, value);
|
||||
}),
|
||||
);
|
||||
return contents;
|
||||
};
|
||||
|
||||
const nodeRequireTarget = async (
|
||||
importer: string,
|
||||
specifier: string,
|
||||
): Promise<z.infer<typeof nodeOutcomeSchema>> => {
|
||||
try {
|
||||
return {
|
||||
target: createRequire(importer).resolve(specifier),
|
||||
error_code: null,
|
||||
};
|
||||
} catch (error) {
|
||||
const code =
|
||||
error instanceof Error && "code" in error
|
||||
? z.string().parse(Reflect.get(error, "code"))
|
||||
: null;
|
||||
return { target: null, error_code: code };
|
||||
}
|
||||
};
|
||||
|
||||
const nodeImportTarget = async (
|
||||
cwd: string,
|
||||
specifier: string,
|
||||
): Promise<z.infer<typeof nodeOutcomeSchema>> => {
|
||||
const { stdout } = await execute(
|
||||
process.execPath,
|
||||
[
|
||||
"--input-type=module",
|
||||
"-e",
|
||||
`
|
||||
try {
|
||||
const url = await import.meta.resolve(${JSON.stringify(specifier)});
|
||||
console.log(JSON.stringify({ target: url, error_code: null }));
|
||||
} catch (error) {
|
||||
console.log(JSON.stringify({
|
||||
target: null,
|
||||
error_code: error.code ?? null,
|
||||
}));
|
||||
}
|
||||
`,
|
||||
],
|
||||
{ cwd },
|
||||
);
|
||||
return nodeOutcomeSchema.parse(JSON.parse(stdout));
|
||||
};
|
||||
|
||||
const resolveAlias = (
|
||||
files: Map<string, JavaScriptArtifactFile>,
|
||||
declaredPath: string,
|
||||
sourcePath: string,
|
||||
moduleKind: "import" | "require",
|
||||
): ArtifactPathResolution =>
|
||||
resolveArtifactPathByContext({
|
||||
declaredPath,
|
||||
sourcePath,
|
||||
context: "module-specifier",
|
||||
moduleKind,
|
||||
files,
|
||||
});
|
||||
|
||||
describe("literal package import keys compared with Node", () => {
|
||||
it("matches literal imports keys containing path-like bytes in both Node loaders", async () => {
|
||||
const root = await createTestTempDirectory("rea-import-alias-literal-");
|
||||
const specifiers = ["#/x", "#x%2Fy", "#x%2Ey", "#x\\y", "#x\0y"];
|
||||
const contents = await writeTree(root, {
|
||||
"package.json": JSON.stringify({
|
||||
imports: Object.fromEntries(
|
||||
specifiers.map((specifier) => [specifier, "./target.cjs"]),
|
||||
),
|
||||
}),
|
||||
"main.cjs": "module.exports = 1;",
|
||||
"target.cjs": "module.exports = 1;",
|
||||
});
|
||||
const inventoried = inventory(contents);
|
||||
for (const specifier of specifiers) {
|
||||
const required = await nodeRequireTarget(
|
||||
join(root, "main.cjs"),
|
||||
specifier,
|
||||
);
|
||||
expect(required.error_code).toBeNull();
|
||||
expect(posixRelative(root, required.target ?? "")).toBe("target.cjs");
|
||||
const imported = await nodeImportTarget(root, specifier);
|
||||
expect(imported.error_code).toBeNull();
|
||||
expect(posixRelative(root, fileURLToPath(imported.target ?? ""))).toBe(
|
||||
"target.cjs",
|
||||
);
|
||||
for (const moduleKind of ["require", "import"] as const)
|
||||
expect(
|
||||
resolveAlias(inventoried, specifier, "main.cjs", moduleKind),
|
||||
JSON.stringify({ specifier, moduleKind }),
|
||||
).toMatchObject({
|
||||
declared_path: specifier,
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "target.cjs",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("still rejects unsafe selected targets for literal imports keys", async () => {
|
||||
const root = await createTestTempDirectory("rea-import-alias-target-");
|
||||
const contents = await writeTree(root, {
|
||||
"package.json": JSON.stringify({
|
||||
imports: { "#x%2Fy": "./nested%2Ftarget.cjs" },
|
||||
}),
|
||||
"main.cjs": "module.exports = 1;",
|
||||
"nested/target.cjs": "module.exports = 1;",
|
||||
});
|
||||
const inventoried = inventory(contents);
|
||||
const required = await nodeRequireTarget(join(root, "main.cjs"), "#x%2Fy");
|
||||
expect(required.error_code).toBe("ERR_INVALID_MODULE_SPECIFIER");
|
||||
const imported = await nodeImportTarget(root, "#x%2Fy");
|
||||
expect(imported.error_code).toBe("ERR_INVALID_MODULE_SPECIFIER");
|
||||
for (const moduleKind of ["require", "import"] as const)
|
||||
expect(
|
||||
resolveAlias(inventoried, "#x%2Fy", "main.cjs", moduleKind),
|
||||
).toMatchObject({ resolution_status: "rejected", resolved_path: null });
|
||||
});
|
||||
});
|
||||
|
||||
describe("inactive package target arrays compared with Node", () => {
|
||||
it("continues past inactive arrays in enclosing imports and exports conditions", async () => {
|
||||
const root = await createTestTempDirectory("rea-inactive-package-array-");
|
||||
const conditional = {
|
||||
node: [{ browser: "./browser.cjs" }],
|
||||
default: "./fallback.cjs",
|
||||
};
|
||||
const nested = {
|
||||
node: [
|
||||
[{ browser: "./browser.cjs" }],
|
||||
{ "rea-inactive": "./inactive.cjs" },
|
||||
],
|
||||
default: "./fallback.cjs",
|
||||
};
|
||||
const contents = await writeTree(root, {
|
||||
"package.json": JSON.stringify({
|
||||
imports: { "#conditional": conditional, "#nested": nested },
|
||||
}),
|
||||
"main.cjs": "module.exports = 1;",
|
||||
"fallback.cjs": "module.exports = 1;",
|
||||
"node_modules/fixture/package.json": JSON.stringify({
|
||||
name: "fixture",
|
||||
exports: { ".": conditional, "./nested": nested },
|
||||
}),
|
||||
"node_modules/fixture/fallback.cjs": "module.exports = 2;",
|
||||
});
|
||||
const inventoried = inventory(contents);
|
||||
for (const [specifier, expected] of [
|
||||
["#conditional", "fallback.cjs"],
|
||||
["#nested", "fallback.cjs"],
|
||||
["fixture", "node_modules/fixture/fallback.cjs"],
|
||||
["fixture/nested", "node_modules/fixture/fallback.cjs"],
|
||||
] as const) {
|
||||
const required = await nodeRequireTarget(
|
||||
join(root, "main.cjs"),
|
||||
specifier,
|
||||
);
|
||||
expect(required.error_code, specifier).toBeNull();
|
||||
expect(posixRelative(root, required.target ?? "")).toBe(expected);
|
||||
const imported = await nodeImportTarget(root, specifier);
|
||||
expect(imported.error_code).toBeNull();
|
||||
expect(posixRelative(root, fileURLToPath(imported.target ?? ""))).toBe(
|
||||
expected,
|
||||
);
|
||||
for (const moduleKind of ["require", "import"] as const)
|
||||
expect(
|
||||
resolveAlias(inventoried, specifier, "main.cjs", moduleKind),
|
||||
).toMatchObject({
|
||||
declared_path: specifier,
|
||||
resolution_status: "resolved",
|
||||
resolved_path: expected,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("package target refusal evidence compared with Node", () => {
|
||||
it("keeps empty, null, invalid, and unmatched array outcomes distinct", async () => {
|
||||
const root = await createTestTempDirectory("rea-inactive-package-refusal-");
|
||||
const inactive = { browser: "./browser.cjs" };
|
||||
const contents = await writeTree(root, {
|
||||
"package.json": JSON.stringify({
|
||||
imports: {
|
||||
"#empty-direct": [],
|
||||
"#empty": { node: [], default: "./fallback.cjs" },
|
||||
"#null": { node: [null, inactive], default: "./fallback.cjs" },
|
||||
"#null-last": [[], null],
|
||||
"#empty-last": [null, []],
|
||||
"#invalid": {
|
||||
node: ["../outside.cjs", inactive],
|
||||
default: "./fallback.cjs",
|
||||
},
|
||||
"#unmatched": [inactive],
|
||||
"#unmatched-nested": [
|
||||
[inactive, { "rea-inactive": "./inactive.cjs" }],
|
||||
],
|
||||
"#unmatched-active": { node: [inactive] },
|
||||
},
|
||||
}),
|
||||
"main.cjs": "module.exports = 1;",
|
||||
"fallback.cjs": "module.exports = 1;",
|
||||
});
|
||||
const inventoried = inventory(contents);
|
||||
for (const [specifier, errorCode, status, limitation] of [
|
||||
[
|
||||
"#empty-direct",
|
||||
"ERR_PACKAGE_IMPORT_NOT_DEFINED",
|
||||
"external",
|
||||
"an empty imports target array",
|
||||
],
|
||||
[
|
||||
"#empty",
|
||||
"ERR_PACKAGE_IMPORT_NOT_DEFINED",
|
||||
"external",
|
||||
"an empty imports target array",
|
||||
],
|
||||
[
|
||||
"#null",
|
||||
"ERR_PACKAGE_IMPORT_NOT_DEFINED",
|
||||
"external",
|
||||
"an explicit null imports target",
|
||||
],
|
||||
[
|
||||
"#null-last",
|
||||
"ERR_PACKAGE_IMPORT_NOT_DEFINED",
|
||||
"external",
|
||||
"an explicit null imports target",
|
||||
],
|
||||
[
|
||||
"#empty-last",
|
||||
"ERR_PACKAGE_IMPORT_NOT_DEFINED",
|
||||
"external",
|
||||
"an empty imports target array",
|
||||
],
|
||||
["#invalid", "ERR_INVALID_PACKAGE_TARGET", "rejected", "../outside.cjs"],
|
||||
[
|
||||
"#unmatched",
|
||||
"ERR_PACKAGE_IMPORT_NOT_DEFINED",
|
||||
"external",
|
||||
"it declares browser.",
|
||||
],
|
||||
[
|
||||
"#unmatched-nested",
|
||||
"ERR_PACKAGE_IMPORT_NOT_DEFINED",
|
||||
"external",
|
||||
"it declares browser, rea-inactive.",
|
||||
],
|
||||
[
|
||||
"#unmatched-active",
|
||||
"ERR_PACKAGE_IMPORT_NOT_DEFINED",
|
||||
"external",
|
||||
"it declares node, browser.",
|
||||
],
|
||||
] as const) {
|
||||
expect(
|
||||
(await nodeRequireTarget(join(root, "main.cjs"), specifier)).error_code,
|
||||
).toBe(errorCode);
|
||||
expect((await nodeImportTarget(root, specifier)).error_code).toBe(
|
||||
errorCode,
|
||||
);
|
||||
for (const moduleKind of ["require", "import"] as const) {
|
||||
const result = resolveAlias(
|
||||
inventoried,
|
||||
specifier,
|
||||
"main.cjs",
|
||||
moduleKind,
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
resolution_status: status,
|
||||
resolved_path: null,
|
||||
});
|
||||
expect(result.limitations.join(" "), specifier).toContain(limitation);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("package import target arrays compared with Node", () => {
|
||||
it("skips invalid parent, absolute, and URL targets before an exact fallback", async () => {
|
||||
const root = await createTestTempDirectory("rea-import-alias-fallback-");
|
||||
const imports = {
|
||||
"#parent": ["../outside.cjs", "./fallback.cjs"],
|
||||
"#absolute": { node: [["/outside.cjs"], "./fallback.cjs"] },
|
||||
"#url": {
|
||||
import: ["https://example.invalid/outside.cjs", "./fallback.cjs"],
|
||||
require: ["file:///outside.cjs", "./fallback.cjs"],
|
||||
},
|
||||
};
|
||||
const contents = await writeTree(root, {
|
||||
"package.json": JSON.stringify({ imports }),
|
||||
"main.cjs": "module.exports = 1;",
|
||||
"fallback.cjs": "module.exports = 1;",
|
||||
});
|
||||
const inventoried = inventory(contents);
|
||||
for (const specifier of Object.keys(imports)) {
|
||||
const required = await nodeRequireTarget(
|
||||
join(root, "main.cjs"),
|
||||
specifier,
|
||||
);
|
||||
expect(required.error_code).toBeNull();
|
||||
expect(posixRelative(root, required.target ?? "")).toBe("fallback.cjs");
|
||||
const imported = await nodeImportTarget(root, specifier);
|
||||
expect(imported.error_code).toBeNull();
|
||||
expect(posixRelative(root, fileURLToPath(imported.target ?? ""))).toBe(
|
||||
"fallback.cjs",
|
||||
);
|
||||
for (const moduleKind of ["require", "import"] as const)
|
||||
expect(
|
||||
resolveAlias(inventoried, specifier, "main.cjs", moduleKind),
|
||||
).toMatchObject({
|
||||
declared_path: specifier,
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "fallback.cjs",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves invalid, null, and unsupported-package outcomes in arrays", async () => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-import-alias-fallback-refuse-",
|
||||
);
|
||||
const contents = await writeTree(root, {
|
||||
"package.json": JSON.stringify({
|
||||
imports: {
|
||||
"#invalid": [null, "../outside.cjs"],
|
||||
"#blocked": ["../outside.cjs", null],
|
||||
"#package": ["fixture", "./fallback.cjs"],
|
||||
},
|
||||
}),
|
||||
"main.cjs": "module.exports = 1;",
|
||||
"fallback.cjs": "module.exports = 1;",
|
||||
"node_modules/fixture/package.json": JSON.stringify({
|
||||
name: "fixture",
|
||||
main: "index.cjs",
|
||||
}),
|
||||
"node_modules/fixture/index.cjs": "module.exports = 2;",
|
||||
});
|
||||
const inventoried = inventory(contents);
|
||||
for (const [specifier, errorCode, resolutionStatus, reason] of [
|
||||
["#invalid", "ERR_INVALID_PACKAGE_TARGET", "rejected", "../outside.cjs"],
|
||||
[
|
||||
"#blocked",
|
||||
"ERR_PACKAGE_IMPORT_NOT_DEFINED",
|
||||
"external",
|
||||
"explicit null",
|
||||
],
|
||||
] as const) {
|
||||
const required = await nodeRequireTarget(
|
||||
join(root, "main.cjs"),
|
||||
specifier,
|
||||
);
|
||||
expect(required.error_code).toBe(errorCode);
|
||||
expect((await nodeImportTarget(root, specifier)).error_code).toBe(
|
||||
errorCode,
|
||||
);
|
||||
for (const moduleKind of ["require", "import"] as const) {
|
||||
const result = resolveAlias(
|
||||
inventoried,
|
||||
specifier,
|
||||
"main.cjs",
|
||||
moduleKind,
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
resolution_status: resolutionStatus,
|
||||
resolved_path: null,
|
||||
});
|
||||
expect(result.limitations.join(" ")).toContain(reason);
|
||||
}
|
||||
}
|
||||
const required = await nodeRequireTarget(
|
||||
join(root, "main.cjs"),
|
||||
"#package",
|
||||
);
|
||||
expect(posixRelative(root, required.target ?? "")).toBe(
|
||||
"node_modules/fixture/index.cjs",
|
||||
);
|
||||
const imported = await nodeImportTarget(root, "#package");
|
||||
expect(imported.error_code).toBeNull();
|
||||
expect(posixRelative(root, fileURLToPath(imported.target ?? ""))).toBe(
|
||||
"node_modules/fixture/index.cjs",
|
||||
);
|
||||
for (const moduleKind of ["require", "import"] as const) {
|
||||
const result = resolveAlias(
|
||||
inventoried,
|
||||
"#package",
|
||||
"main.cjs",
|
||||
moduleKind,
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
resolution_status: "external",
|
||||
resolved_path: null,
|
||||
});
|
||||
expect(result.limitations.join(" ")).toContain(
|
||||
"External-package import targets remain unresolved",
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("package import aliases compared with Node", () => {
|
||||
it("matches Node require and import for an exact root alias", async () => {
|
||||
const root = await createTestTempDirectory("rea-import-alias-root-");
|
||||
const files = {
|
||||
"package.json": JSON.stringify({
|
||||
name: "app",
|
||||
imports: { "#config": "./config.cjs" },
|
||||
}),
|
||||
"main.cjs": 'module.exports = require("#config");',
|
||||
"config.cjs": "module.exports = 'config.cjs';",
|
||||
};
|
||||
const contents = await writeTree(root, files);
|
||||
const inventoried = inventory(contents);
|
||||
const required = await nodeRequireTarget(join(root, "main.cjs"), "#config");
|
||||
expect(posixRelative(root, required.target ?? "")).toBe("config.cjs");
|
||||
expect(
|
||||
resolveAlias(inventoried, "#config", "main.cjs", "require"),
|
||||
).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "config.cjs",
|
||||
});
|
||||
const imported = await nodeImportTarget(root, "#config");
|
||||
expect(imported.error_code).toBeNull();
|
||||
expect(
|
||||
resolveAlias(inventoried, "#config", "main.cjs", "import"),
|
||||
).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "config.cjs",
|
||||
});
|
||||
});
|
||||
|
||||
it("matches Node when a nested package has its own imports map", async () => {
|
||||
const root = await createTestTempDirectory("rea-import-alias-nested-");
|
||||
const files = {
|
||||
"package.json": JSON.stringify({
|
||||
name: "app",
|
||||
imports: { "#config": "./root.cjs" },
|
||||
}),
|
||||
"root.cjs": "module.exports = 'root';",
|
||||
"node_modules/fixture/package.json": JSON.stringify({
|
||||
name: "fixture",
|
||||
imports: { "#config": "./local.cjs" },
|
||||
}),
|
||||
"node_modules/fixture/index.cjs": 'module.exports = require("#config");',
|
||||
"node_modules/fixture/local.cjs": "module.exports = 'local';",
|
||||
};
|
||||
const contents = await writeTree(root, files);
|
||||
const inventoried = inventory(contents);
|
||||
const nested = await nodeRequireTarget(
|
||||
join(root, "node_modules/fixture/index.cjs"),
|
||||
"#config",
|
||||
);
|
||||
expect(posixRelative(root, nested.target ?? "")).toBe(
|
||||
"node_modules/fixture/local.cjs",
|
||||
);
|
||||
expect(
|
||||
resolveAlias(
|
||||
inventoried,
|
||||
"#config",
|
||||
"node_modules/fixture/index.cjs",
|
||||
"require",
|
||||
),
|
||||
).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "node_modules/fixture/local.cjs",
|
||||
});
|
||||
const fromRoot = await nodeRequireTarget(join(root, "root.cjs"), "#config");
|
||||
expect(posixRelative(root, fromRoot.target ?? "")).toBe("root.cjs");
|
||||
expect(
|
||||
resolveAlias(inventoried, "#config", "root.cjs", "require"),
|
||||
).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "root.cjs",
|
||||
});
|
||||
});
|
||||
|
||||
it("matches Node when a nested package does not define the alias", async () => {
|
||||
const root = await createTestTempDirectory("rea-import-alias-scope-");
|
||||
const files = {
|
||||
"package.json": JSON.stringify({
|
||||
name: "app",
|
||||
imports: { "#config": "./root.cjs" },
|
||||
}),
|
||||
"root.cjs": "module.exports = 'root';",
|
||||
"node_modules/fixture/package.json": JSON.stringify({
|
||||
name: "fixture",
|
||||
}),
|
||||
"node_modules/fixture/index.cjs": 'module.exports = require("#config");',
|
||||
};
|
||||
await writeTree(root, files);
|
||||
const nested = await nodeRequireTarget(
|
||||
join(root, "node_modules/fixture/index.cjs"),
|
||||
"#config",
|
||||
);
|
||||
expect(nested.target).toBeNull();
|
||||
// CJS reports MODULE_NOT_FOUND when the nested package.json has no
|
||||
// `imports` field at all; it still does not inherit the ancestor map.
|
||||
expect(nested.error_code).toBe("MODULE_NOT_FOUND");
|
||||
const outcome = resolveAlias(
|
||||
inventory(new Map(Object.entries(files))),
|
||||
"#config",
|
||||
"node_modules/fixture/index.cjs",
|
||||
"require",
|
||||
);
|
||||
expect(outcome).toMatchObject({
|
||||
resolution_status: "external",
|
||||
resolved_path: null,
|
||||
});
|
||||
expect(outcome.limitations.join(" ")).toContain(
|
||||
"node_modules/fixture/package.json",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("package import alias refusals compared with Node", () => {
|
||||
it("matches Node refusal for a missing key, explicit null, and reserved specifier", async () => {
|
||||
const root = await createTestTempDirectory("rea-import-alias-refuse-");
|
||||
const files = {
|
||||
"package.json": JSON.stringify({
|
||||
name: "app",
|
||||
imports: {
|
||||
"#config": "./config.cjs",
|
||||
"#blocked": null,
|
||||
"#": "./config.cjs",
|
||||
"#/": "./config.cjs",
|
||||
"#config/": "./config.cjs",
|
||||
},
|
||||
}),
|
||||
"main.cjs": "module.exports = 1;",
|
||||
"config.cjs": "module.exports = 1;",
|
||||
};
|
||||
await writeTree(root, files);
|
||||
const inventoried = inventory(new Map(Object.entries(files)));
|
||||
const missing = await nodeRequireTarget(join(root, "main.cjs"), "#missing");
|
||||
expect(missing.error_code).toBe("ERR_PACKAGE_IMPORT_NOT_DEFINED");
|
||||
expect(
|
||||
resolveAlias(inventoried, "#missing", "main.cjs", "require"),
|
||||
).toMatchObject({ resolution_status: "external", resolved_path: null });
|
||||
const blocked = await nodeRequireTarget(join(root, "main.cjs"), "#blocked");
|
||||
expect(blocked.error_code).toBe("ERR_PACKAGE_IMPORT_NOT_DEFINED");
|
||||
expect(
|
||||
resolveAlias(inventoried, "#blocked", "main.cjs", "require"),
|
||||
).toMatchObject({ resolution_status: "external", resolved_path: null });
|
||||
for (const specifier of ["#", "#/", "#config/"]) {
|
||||
const reserved = await nodeRequireTarget(
|
||||
join(root, "main.cjs"),
|
||||
specifier,
|
||||
);
|
||||
expect(reserved.error_code).toBe("ERR_INVALID_MODULE_SPECIFIER");
|
||||
expect((await nodeImportTarget(root, specifier)).error_code).toBe(
|
||||
"ERR_INVALID_MODULE_SPECIFIER",
|
||||
);
|
||||
for (const moduleKind of ["require", "import"] as const)
|
||||
expect(
|
||||
resolveAlias(inventoried, specifier, "main.cjs", moduleKind),
|
||||
).toMatchObject({ resolution_status: "rejected", resolved_path: null });
|
||||
}
|
||||
});
|
||||
|
||||
it("matches Node when an import condition selects a different exact file", async () => {
|
||||
const root = await createTestTempDirectory("rea-import-alias-cond-");
|
||||
const files = {
|
||||
"package.json": JSON.stringify({
|
||||
name: "app",
|
||||
type: "module",
|
||||
imports: {
|
||||
"#config": {
|
||||
import: "./config.mjs",
|
||||
require: "./config.cjs",
|
||||
},
|
||||
},
|
||||
}),
|
||||
"main.cjs": 'module.exports = require("#config");',
|
||||
"config.cjs": "export default 'cjs'; module.exports = 'cjs';",
|
||||
"config.mjs": "export default 'mjs';",
|
||||
};
|
||||
await writeTree(root, files);
|
||||
const inventoried = inventory(new Map(Object.entries(files)));
|
||||
const required = await nodeRequireTarget(join(root, "main.cjs"), "#config");
|
||||
expect(posixRelative(root, required.target ?? "")).toBe("config.cjs");
|
||||
expect(
|
||||
resolveAlias(inventoried, "#config", "main.cjs", "require"),
|
||||
).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "config.cjs",
|
||||
});
|
||||
const imported = await nodeImportTarget(root, "#config");
|
||||
expect(imported.error_code).toBeNull();
|
||||
expect(imported.target ?? "").toContain("config.mjs");
|
||||
expect(
|
||||
resolveAlias(inventoried, "#config", "main.cjs", "import"),
|
||||
).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "config.mjs",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user