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
@@ -868,6 +868,365 @@ describe("package exports exact targets", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("package import alias resolution", () => {
|
||||
it.each(["import", "require"] as const)(
|
||||
"resolves an exact #%s alias from the enclosing package imports map",
|
||||
(moduleKind) => {
|
||||
const files = aliasFiles({
|
||||
"package.json": JSON.stringify({
|
||||
imports: { "#config": "./config.cjs" },
|
||||
}),
|
||||
"main.js": "",
|
||||
"config.cjs": "",
|
||||
});
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath: "#config",
|
||||
sourcePath: "main.js",
|
||||
context: "module-specifier",
|
||||
moduleKind,
|
||||
files,
|
||||
}),
|
||||
).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "config.cjs",
|
||||
declared_path: "#config",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("resolves an alias from a nested file through the enclosing package root", () => {
|
||||
const files = aliasFiles({
|
||||
"package.json": JSON.stringify({
|
||||
imports: { "#config": "./config.cjs" },
|
||||
}),
|
||||
"src/main.js": "",
|
||||
"config.cjs": "",
|
||||
});
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath: "#config",
|
||||
sourcePath: "src/main.js",
|
||||
context: "module-specifier",
|
||||
moduleKind: "require",
|
||||
files,
|
||||
}),
|
||||
).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "config.cjs",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses a nested package imports map rather than the application root", () => {
|
||||
const files = aliasFiles({
|
||||
"package.json": JSON.stringify({
|
||||
imports: { "#config": "./root.cjs" },
|
||||
}),
|
||||
"root.cjs": "",
|
||||
"node_modules/fixture/package.json": JSON.stringify({
|
||||
imports: { "#config": "./local.cjs" },
|
||||
}),
|
||||
"node_modules/fixture/index.js": "",
|
||||
"node_modules/fixture/local.cjs": "",
|
||||
});
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath: "#config",
|
||||
sourcePath: "node_modules/fixture/index.js",
|
||||
context: "module-specifier",
|
||||
moduleKind: "require",
|
||||
files,
|
||||
}),
|
||||
).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "node_modules/fixture/local.cjs",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not inherit an ancestor imports map across a nested package.json", () => {
|
||||
const files = aliasFiles({
|
||||
"package.json": JSON.stringify({
|
||||
imports: { "#config": "./root.cjs" },
|
||||
}),
|
||||
"root.cjs": "",
|
||||
"node_modules/fixture/package.json": "{}",
|
||||
"node_modules/fixture/index.js": "",
|
||||
});
|
||||
const outcome = resolve({
|
||||
declaredPath: "#config",
|
||||
sourcePath: "node_modules/fixture/index.js",
|
||||
context: "module-specifier",
|
||||
moduleKind: "require",
|
||||
files,
|
||||
});
|
||||
expect(outcome).toMatchObject({
|
||||
resolution_status: "external",
|
||||
resolved_path: null,
|
||||
});
|
||||
expect(outcome.limitations.join(" ")).toContain(
|
||||
"node_modules/fixture/package.json",
|
||||
);
|
||||
expect(outcome.limitations.join(" ")).not.toContain("root.cjs");
|
||||
});
|
||||
|
||||
it("does not apply a nested package map to the application root importer", () => {
|
||||
const files = aliasFiles({
|
||||
"package.json": JSON.stringify({
|
||||
imports: { "#config": "./root.cjs" },
|
||||
}),
|
||||
"main.js": "",
|
||||
"root.cjs": "",
|
||||
"node_modules/fixture/package.json": JSON.stringify({
|
||||
imports: { "#config": "./local.cjs" },
|
||||
}),
|
||||
"node_modules/fixture/local.cjs": "",
|
||||
});
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath: "#config",
|
||||
sourcePath: "main.js",
|
||||
context: "module-specifier",
|
||||
moduleKind: "require",
|
||||
files,
|
||||
}),
|
||||
).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "root.cjs",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("package import alias target selection", () => {
|
||||
it("selects import and require conditions from an imports target", () => {
|
||||
const files = aliasFiles({
|
||||
"package.json": JSON.stringify({
|
||||
imports: {
|
||||
"#config": {
|
||||
import: "./config.mjs",
|
||||
require: "./config.cjs",
|
||||
default: "./fallback.js",
|
||||
},
|
||||
},
|
||||
}),
|
||||
"main.js": "",
|
||||
"config.mjs": "",
|
||||
"config.cjs": "",
|
||||
"fallback.js": "",
|
||||
});
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath: "#config",
|
||||
sourcePath: "main.js",
|
||||
context: "module-specifier",
|
||||
moduleKind: "import",
|
||||
files,
|
||||
}).resolved_path,
|
||||
).toBe("config.mjs");
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath: "#config",
|
||||
sourcePath: "main.js",
|
||||
context: "module-specifier",
|
||||
moduleKind: "require",
|
||||
files,
|
||||
}).resolved_path,
|
||||
).toBe("config.cjs");
|
||||
});
|
||||
|
||||
it("resolves array import targets at their first string entry", () => {
|
||||
const files = aliasFiles({
|
||||
"package.json": JSON.stringify({
|
||||
imports: { "#config": [{ import: "./missing.mjs" }, "./config.cjs"] },
|
||||
}),
|
||||
"main.js": "",
|
||||
"config.cjs": "",
|
||||
});
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath: "#config",
|
||||
sourcePath: "main.js",
|
||||
context: "module-specifier",
|
||||
moduleKind: "require",
|
||||
files,
|
||||
}),
|
||||
).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "config.cjs",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not add an extension or index to a selected import target", () => {
|
||||
const files = aliasFiles({
|
||||
"package.json": JSON.stringify({
|
||||
imports: { "#config": "./config" },
|
||||
}),
|
||||
"main.js": "",
|
||||
"config.cjs": "",
|
||||
"config/index.js": "",
|
||||
});
|
||||
const outcome = resolve({
|
||||
declaredPath: "#config",
|
||||
sourcePath: "main.js",
|
||||
context: "module-specifier",
|
||||
moduleKind: "require",
|
||||
files,
|
||||
});
|
||||
expect(outcome).toMatchObject({
|
||||
resolution_status: "not-found",
|
||||
resolved_path: null,
|
||||
});
|
||||
expect(outcome.limitations.join(" ")).toContain(
|
||||
"import targets are exact files",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("package import alias refusals", () => {
|
||||
it("reports a missing exact alias without looking in another package", () => {
|
||||
const files = aliasFiles({
|
||||
"package.json": JSON.stringify({
|
||||
imports: { "#config": "./config.cjs" },
|
||||
}),
|
||||
"main.js": "",
|
||||
"config.cjs": "",
|
||||
});
|
||||
const outcome = resolve({
|
||||
declaredPath: "#other",
|
||||
sourcePath: "main.js",
|
||||
context: "module-specifier",
|
||||
moduleKind: "require",
|
||||
files,
|
||||
});
|
||||
expect(outcome).toMatchObject({
|
||||
resolution_status: "external",
|
||||
resolved_path: null,
|
||||
});
|
||||
expect(outcome.limitations.join(" ")).toContain("#other");
|
||||
expect(outcome.limitations.join(" ")).toContain("#config");
|
||||
});
|
||||
|
||||
it("reports an explicit null import target as blocked", () => {
|
||||
const files = aliasFiles({
|
||||
"package.json": JSON.stringify({
|
||||
imports: { "#config": null },
|
||||
}),
|
||||
"main.js": "",
|
||||
"config.cjs": "",
|
||||
});
|
||||
const outcome = resolve({
|
||||
declaredPath: "#config",
|
||||
sourcePath: "main.js",
|
||||
context: "module-specifier",
|
||||
moduleKind: "require",
|
||||
files,
|
||||
});
|
||||
expect(outcome).toMatchObject({
|
||||
resolution_status: "external",
|
||||
resolved_path: null,
|
||||
});
|
||||
expect(outcome.limitations.join(" ")).toContain("explicit null");
|
||||
});
|
||||
|
||||
it("leaves wildcard import aliases unresolved", () => {
|
||||
const files = aliasFiles({
|
||||
"package.json": JSON.stringify({
|
||||
imports: { "#internal/*": "./src/*.js" },
|
||||
}),
|
||||
"main.js": "",
|
||||
"src/util.js": "",
|
||||
});
|
||||
const outcome = resolve({
|
||||
declaredPath: "#internal/util",
|
||||
sourcePath: "main.js",
|
||||
context: "module-specifier",
|
||||
moduleKind: "import",
|
||||
files,
|
||||
});
|
||||
expect(outcome).toMatchObject({
|
||||
resolution_status: "external",
|
||||
resolved_path: null,
|
||||
});
|
||||
expect(outcome.limitations.join(" ")).toMatch(/wildcard/i);
|
||||
});
|
||||
|
||||
it("leaves recursive and external-package import targets unresolved", () => {
|
||||
const files = aliasFiles({
|
||||
"package.json": JSON.stringify({
|
||||
imports: { "#loop": "#config", "#dep": "fixture" },
|
||||
}),
|
||||
"main.js": "",
|
||||
"node_modules/fixture/index.js": "",
|
||||
});
|
||||
const recursive = resolve({
|
||||
declaredPath: "#loop",
|
||||
sourcePath: "main.js",
|
||||
context: "module-specifier",
|
||||
moduleKind: "require",
|
||||
files,
|
||||
});
|
||||
const external = resolve({
|
||||
declaredPath: "#dep",
|
||||
sourcePath: "main.js",
|
||||
context: "module-specifier",
|
||||
moduleKind: "require",
|
||||
files,
|
||||
});
|
||||
expect(recursive).toMatchObject({
|
||||
resolution_status: "external",
|
||||
resolved_path: null,
|
||||
});
|
||||
expect(recursive.limitations.join(" ")).toMatch(/recursive/i);
|
||||
expect(external).toMatchObject({
|
||||
resolution_status: "external",
|
||||
resolved_path: null,
|
||||
});
|
||||
expect(external.limitations.join(" ")).toMatch(/external-package/i);
|
||||
});
|
||||
|
||||
it("rejects reserved hash specifiers", () => {
|
||||
const files = aliasFiles({
|
||||
"package.json": JSON.stringify({
|
||||
imports: { "#config": "./config.cjs" },
|
||||
}),
|
||||
"main.js": "",
|
||||
"config.cjs": "",
|
||||
});
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath: "#",
|
||||
sourcePath: "main.js",
|
||||
context: "module-specifier",
|
||||
moduleKind: "import",
|
||||
files,
|
||||
}),
|
||||
).toMatchObject({ resolution_status: "rejected", resolved_path: null });
|
||||
});
|
||||
|
||||
it("does not use a foreign-container enclosing package.json", () => {
|
||||
const files = fileMap([
|
||||
file(
|
||||
"package.json",
|
||||
"other",
|
||||
JSON.stringify({ imports: { "#config": "./config.cjs" } }),
|
||||
),
|
||||
file("main.js", "root"),
|
||||
file("config.cjs", "root"),
|
||||
]);
|
||||
const outcome = resolve({
|
||||
declaredPath: "#config",
|
||||
sourcePath: "main.js",
|
||||
context: "module-specifier",
|
||||
moduleKind: "require",
|
||||
files,
|
||||
});
|
||||
expect(outcome).toMatchObject({
|
||||
resolution_status: "external",
|
||||
resolved_path: null,
|
||||
});
|
||||
expect(outcome.limitations.join(" ")).toContain("package scopes");
|
||||
});
|
||||
});
|
||||
|
||||
describe("directory package entrypoint precedence", () => {
|
||||
it.each([
|
||||
["require", '{"main":"actual.cjs"}', "actual.cjs"],
|
||||
@@ -1211,6 +1570,15 @@ const file = (
|
||||
: { included: true, value: text },
|
||||
});
|
||||
|
||||
const aliasFiles = (
|
||||
entries: Readonly<Record<string, string | null>>,
|
||||
): ReturnType<typeof fileMap> =>
|
||||
fileMap(
|
||||
Object.entries(entries).map(([path, text]) =>
|
||||
file(path, "root", text ?? ""),
|
||||
),
|
||||
);
|
||||
|
||||
describe("exact HTML resource resolution", () => {
|
||||
it.each([
|
||||
["extension", ["app.js"]],
|
||||
|
||||
Reference in new issue
Block a user