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
@@ -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