Preserve missing ASAR companion occurrence identity, declared size and unknown content in graph nodes and resolution limitations. Apply URL text semantics once for ESM lookup while preserving literal CommonJS paths and original specifiers. Keep CJS wrapper var bindings and assignment history, recognizing only untouched wrapper redeclarations. Validation: real ASAR producer workflow and CLI/MCP path/coverage cases; semantic reassignment controls. Context7 Node documentation and direct Node wrapper execution confirm wrapper behavior. Fixes #3213 Fixes #3214 Fixes #3223 Related: #3278, #3254 Co-authored-by: Geronimo <varungore1@gmail.com> Co-authored-by: Paolo Valletta <paovalletta@hotmail.it>
2849 lines
81 KiB
TypeScript
2849 lines
81 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
resolveArtifactPathByContext,
|
|
type ResolveArtifactPathInput,
|
|
} from "./JavaScriptArtifactPathResolution.js";
|
|
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
|
import { artifactLocalIdentity } from "./JavaScriptArtifactGraphContext.js";
|
|
import { applicationNodeIdentitySchema } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
|
|
import { analyzeJavaScriptStaticSource } from "../../domain/javascript/javascriptStaticAnalysis.js";
|
|
|
|
describe("artifact URI scheme classification", () => {
|
|
it.each(["html-reference", "module-specifier", "url-reference"] as const)(
|
|
"keeps digit-bearing schemes external in %s even when a local file matches",
|
|
(context) => {
|
|
const files = fileMap([
|
|
file("index.html", "root"),
|
|
file("web3:app.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "web3:app.js",
|
|
sourcePath: "index.html",
|
|
context,
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "external", resolved_path: null });
|
|
},
|
|
);
|
|
|
|
it("keeps references under a digit-bearing external base href external", () => {
|
|
const files = fileMap([
|
|
file("index.html", "root"),
|
|
file("web3:assets/app.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "app.js",
|
|
sourcePath: "index.html",
|
|
context: "html-reference",
|
|
htmlBaseHref: "web3:assets/",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "external", resolved_path: null });
|
|
});
|
|
});
|
|
|
|
describe("artifact-local graph identity", () => {
|
|
it("preserves complete long namespace and key values", () => {
|
|
const digest = "a".repeat(64);
|
|
const namespacePrefix = "n".repeat(512);
|
|
const keyPrefix = "k".repeat(4_096);
|
|
const first = artifactLocalIdentity(
|
|
digest,
|
|
`${namespacePrefix}a`,
|
|
`${keyPrefix}a`,
|
|
);
|
|
const second = artifactLocalIdentity(
|
|
digest,
|
|
`${namespacePrefix}b`,
|
|
`${keyPrefix}b`,
|
|
);
|
|
|
|
expect(applicationNodeIdentitySchema.parse(first)).toEqual(first);
|
|
expect(applicationNodeIdentitySchema.parse(second)).toEqual(second);
|
|
expect(first).not.toEqual(second);
|
|
});
|
|
});
|
|
|
|
describe("loader-owned path lookup", () => {
|
|
it("applies URL whitespace handling only to ESM lookup and retains declarations", () => {
|
|
const tab = "\t";
|
|
const files = fileMap([
|
|
file("app/entry.mjs", "root"),
|
|
file("app/feature.js", "root"),
|
|
file(`app/tab${tab}name.js`, "root"),
|
|
]);
|
|
for (const whitespace of ["\t", "\n", "\r"]) {
|
|
const raw = `./fea${whitespace}ture.js`;
|
|
expect(
|
|
resolve({
|
|
declaredPath: raw,
|
|
sourcePath: "app/entry.mjs",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
}),
|
|
).toMatchObject({ declared_path: raw, resolved_path: "app/feature.js" });
|
|
}
|
|
expect(
|
|
resolve({
|
|
declaredPath: `./tab${tab}name.js`,
|
|
sourcePath: "app/entry.mjs",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolved_path: null });
|
|
expect(
|
|
resolve({
|
|
declaredPath: `./tab${tab}name.js`,
|
|
sourcePath: "app/entry.cjs",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolved_path: "app/tab\tname.js" });
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./tab%09name.js",
|
|
sourcePath: "app/entry.mjs",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolved_path: "app/tab\tname.js" });
|
|
});
|
|
});
|
|
|
|
describe("loader-specific module suffix lookup", () => {
|
|
it("keeps relative ESM imports exact", () => {
|
|
const files = fileMap([
|
|
file("app/entry.mjs", "root"),
|
|
file("app/feature.js", "root"),
|
|
file("app/directory/index.js", "root"),
|
|
file("app/feature.js.js", "root"),
|
|
]);
|
|
for (const declaredPath of ["./feature", "./directory"]) {
|
|
expect(
|
|
resolve({
|
|
declaredPath,
|
|
sourcePath: "app/entry.mjs",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
|
|
}
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./feature.js",
|
|
sourcePath: "app/entry.mjs",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolved_path: "app/feature.js" });
|
|
});
|
|
|
|
it("does not use .cjs as an extensionless CommonJS probe", () => {
|
|
const files = fileMap([
|
|
file("app/entry.cjs", "root"),
|
|
file("app/feature.cjs", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./feature",
|
|
sourcePath: "app/entry.cjs",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./feature.cjs",
|
|
sourcePath: "app/entry.cjs",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolved_path: "app/feature.cjs" });
|
|
});
|
|
|
|
it("keeps present opaque inventory files distinct from missing module targets", () => {
|
|
const opaque = new Map([
|
|
[
|
|
"app/core.jsc",
|
|
{
|
|
path: "app/core.jsc",
|
|
container_sha256: file("app/entry.cjs", "root").container_sha256,
|
|
sha256: "a".repeat(64),
|
|
bytes: 7,
|
|
inventory_artifact_id: "opaque-core",
|
|
},
|
|
],
|
|
]);
|
|
const files = fileMap([file("app/entry.cjs", "root")]);
|
|
const unanalyzedFiles = {
|
|
get: (path: string) => opaque.get(path),
|
|
hasDirectory: () => false,
|
|
};
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./core.jsc",
|
|
sourcePath: "app/entry.cjs",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
unanalyzedFiles,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "unavailable",
|
|
resolved_path: null,
|
|
candidate_path: "app/core.jsc",
|
|
limitations: [
|
|
expect.stringContaining(`sha256 ${"a".repeat(64)}, 7 bytes`),
|
|
],
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./missing.jsc",
|
|
sourcePath: "app/entry.cjs",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
unanalyzedFiles,
|
|
}),
|
|
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./core.jsc",
|
|
sourcePath: "app/entry.cjs",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
unanalyzedFiles: {
|
|
...unanalyzedFiles,
|
|
get: () => ({
|
|
...opaque.get("app/core.jsc")!,
|
|
container_sha256: "b".repeat(64),
|
|
}),
|
|
},
|
|
}),
|
|
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
|
|
});
|
|
});
|
|
|
|
describe("unavailable inventoried native add-ons", () => {
|
|
it("retains an inventoried native add-on whose bytes are unavailable", () => {
|
|
const unavailable = {
|
|
path: "app/addon.node",
|
|
container_sha256: file("app/entry.cjs", "root").container_sha256,
|
|
sha256: null,
|
|
bytes: 8192,
|
|
inventory_artifact_id: null,
|
|
inventory_occurrence_id: "missing-addon-occurrence",
|
|
kind: "native-addon" as const,
|
|
hash_status: "unavailable" as const,
|
|
limitations: ["unpacked companion is missing"],
|
|
};
|
|
const result = resolve({
|
|
declaredPath: "./addon.node",
|
|
sourcePath: "app/entry.cjs",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files: fileMap([file("app/entry.cjs", "root")]),
|
|
unanalyzedFiles: {
|
|
get: (path) => (path === unavailable.path ? unavailable : undefined),
|
|
hasDirectory: () => false,
|
|
},
|
|
});
|
|
expect(result).toMatchObject({
|
|
declared_path: "./addon.node",
|
|
resolution_status: "unavailable",
|
|
candidate_path: "app/addon.node",
|
|
limitations: [
|
|
"unpacked companion is missing",
|
|
expect.stringContaining("inventoried native-addon file"),
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("bare package root file lookup", () => {
|
|
it("uses loader-specific bare package roots when only a sibling .js file exists", () => {
|
|
const absentDirectory = fileMap([
|
|
file("app/entry.mjs", "root"),
|
|
file("app/node_modules/fixture.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/entry.mjs",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files: absentDirectory,
|
|
}),
|
|
).toMatchObject({ resolution_status: "external", resolved_path: null });
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/entry.mjs",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files: absentDirectory,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/node_modules/fixture.js",
|
|
});
|
|
|
|
const packageDirectory = fileMap([
|
|
file("app/entry.mjs", "root"),
|
|
file("app/node_modules/fixture.js", "root"),
|
|
file("app/node_modules/fixture/package.json", "root", "{}"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/entry.mjs",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files: packageDirectory,
|
|
}),
|
|
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/entry.mjs",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files: packageDirectory,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/node_modules/fixture.js",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("legacy package main lookup", () => {
|
|
it("treats an empty package main as absent and resolves the root index", () => {
|
|
const files = fileMap([
|
|
file("app/src/entry.cjs", "root"),
|
|
file("app/node_modules/fixture/package.json", "root", '{"main":""}'),
|
|
file("app/node_modules/fixture/index.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/src/entry.cjs",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/node_modules/fixture/index.js",
|
|
});
|
|
});
|
|
|
|
it.each(["import", "require"] as const)(
|
|
"limits %s package-main probes to Node's file extensions",
|
|
(moduleKind) => {
|
|
const files = fileMap([
|
|
file("app/src/entry.js", "root"),
|
|
file(
|
|
"app/node_modules/fixture/package.json",
|
|
"root",
|
|
'{"main":"main"}',
|
|
),
|
|
file("app/node_modules/fixture/main.cjs", "root"),
|
|
file("app/node_modules/fixture/index.mjs", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/src/entry.js",
|
|
context: "module-specifier",
|
|
moduleKind,
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
|
|
},
|
|
);
|
|
|
|
it("retains inventoried directories during legacy ESM main finalization", () => {
|
|
const files = fileMap([
|
|
file("app/src/entry.mjs", "root"),
|
|
file(
|
|
"app/node_modules/fixture/package.json",
|
|
"root",
|
|
'{"main":"./lib/"}',
|
|
),
|
|
file("app/node_modules/fixture/lib.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/src/entry.mjs",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
directoryPaths: new Set(["app/node_modules/fixture/lib/.js"]),
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "rejected",
|
|
resolved_path: null,
|
|
candidate_path: "app/node_modules/fixture/lib/.js",
|
|
});
|
|
});
|
|
|
|
it.each(["import", "require"] as const)(
|
|
"does not recursively resolve a nested package from a %s package main",
|
|
(moduleKind) => {
|
|
const files = fileMap([
|
|
file("app/src/entry.js", "root"),
|
|
file("app/node_modules/fixture/package.json", "root", '{"main":"sub"}'),
|
|
file(
|
|
"app/node_modules/fixture/sub/package.json",
|
|
"root",
|
|
'{"main":"other"}',
|
|
),
|
|
file("app/node_modules/fixture/sub/other.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/src/entry.js",
|
|
context: "module-specifier",
|
|
moduleKind,
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("legacy package main metadata validation and fallback", () => {
|
|
it("still rejects non-string package main metadata", () => {
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./package",
|
|
sourcePath: "app/entry.js",
|
|
context: "filesystem-expression",
|
|
files: fileMap([
|
|
file("app/entry.js", "root"),
|
|
file("app/package/package.json", "root", '{"main":false}'),
|
|
file("app/package/index.js", "root"),
|
|
]),
|
|
}),
|
|
).toMatchObject({ resolution_status: "unavailable", resolved_path: null });
|
|
});
|
|
|
|
it.each(["import", "require"] as const)(
|
|
"uses Node's index fallback for a non-string %s package main",
|
|
(moduleKind) => {
|
|
const files = fileMap([
|
|
file("app/src/entry.js", "root"),
|
|
file("app/node_modules/fixture/package.json", "root", '{"main":42}'),
|
|
file("app/node_modules/fixture/index.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/src/entry.js",
|
|
context: "module-specifier",
|
|
moduleKind,
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/node_modules/fixture/index.js",
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each(["import", "require"] as const)(
|
|
"does not treat a root-pointing %s package main as a dotfile module",
|
|
(moduleKind) => {
|
|
const files = fileMap([
|
|
file("src/entry.js", "root"),
|
|
file("node_modules/fixture/package.json", "root", '{"main":"../.."}'),
|
|
file("index.js", "root"),
|
|
file(".js", "root"),
|
|
file(".json", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "src/entry.js",
|
|
context: "module-specifier",
|
|
moduleKind,
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "index.js",
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("CommonJS bare package ancestor lookup", () => {
|
|
it.each([
|
|
["absent metadata", undefined, true],
|
|
["empty metadata", "{}", true],
|
|
["empty main", '{"main":""}', true],
|
|
["truthy missing main", '{"main":"missing"}', false],
|
|
] as const)(
|
|
"handles %s before an ancestor package for both loaders",
|
|
(_name, packageJson, commonJsUsesAncestor) => {
|
|
const files = fileMap([
|
|
file("app/src/entry.js", "root"),
|
|
file("app/src/node_modules/fixture/placeholder.js", "root"),
|
|
...(packageJson === undefined
|
|
? []
|
|
: [
|
|
file(
|
|
"app/src/node_modules/fixture/package.json",
|
|
"root",
|
|
packageJson,
|
|
),
|
|
]),
|
|
file("app/node_modules/fixture/index.js", "root"),
|
|
]);
|
|
for (const moduleKind of ["import", "require"] as const) {
|
|
const resolution = resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/src/entry.js",
|
|
context: "module-specifier",
|
|
moduleKind,
|
|
files,
|
|
});
|
|
if (moduleKind === "require" && commonJsUsesAncestor) {
|
|
expect(resolution).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/node_modules/fixture/index.js",
|
|
});
|
|
} else {
|
|
expect(resolution).toMatchObject({
|
|
resolution_status: "not-found",
|
|
resolved_path: null,
|
|
});
|
|
}
|
|
}
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("CommonJS bare package subpath ancestor lookup", () => {
|
|
it.each([
|
|
["absent subpath", undefined, true],
|
|
["empty subpath metadata", "{}", true],
|
|
["truthy missing subpath main", '{"main":"missing"}', false],
|
|
] as const)(
|
|
"handles %s before an ancestor package for both loaders",
|
|
(_name, packageJson, commonJsUsesAncestor) => {
|
|
const files = fileMap([
|
|
file("app/src/entry.js", "root"),
|
|
file("app/src/node_modules/fixture/placeholder.js", "root"),
|
|
...(packageJson === undefined
|
|
? []
|
|
: [
|
|
file(
|
|
"app/src/node_modules/fixture/parser/package.json",
|
|
"root",
|
|
packageJson,
|
|
),
|
|
file(
|
|
"app/src/node_modules/fixture/parser/placeholder.js",
|
|
"root",
|
|
),
|
|
]),
|
|
file("app/node_modules/fixture/parser.js", "root"),
|
|
]);
|
|
const commonJsResolution = resolve({
|
|
declaredPath: "fixture/parser",
|
|
sourcePath: "app/src/entry.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
});
|
|
expect(commonJsResolution).toMatchObject({
|
|
resolved_path: commonJsUsesAncestor
|
|
? "app/node_modules/fixture/parser.js"
|
|
: null,
|
|
});
|
|
const esmResolution = resolve({
|
|
declaredPath: "fixture/parser",
|
|
sourcePath: "app/src/entry.js",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
});
|
|
expect(esmResolution.resolved_path).toBeNull();
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("CommonJS bare package exhausted lookup", () => {
|
|
it.each([
|
|
["empty main", "fixture", '{"main":""}', "app/src/node_modules/fixture"],
|
|
[
|
|
"missing subpath",
|
|
"fixture/parser",
|
|
"{}",
|
|
"app/src/node_modules/fixture/parser",
|
|
],
|
|
] as const)(
|
|
"preserves not-found for an exhausted %s lookup",
|
|
(_name, declaredPath, packageJson, packageDirectory) => {
|
|
const files = fileMap([
|
|
file("app/src/entry.js", "root"),
|
|
file("app/src/node_modules/fixture/package.json", "root", packageJson),
|
|
file(`${packageDirectory}/placeholder.js`, "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath,
|
|
sourcePath: "app/src/entry.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("logical path identity", () => {
|
|
it("resolves an NFD relative import to the NFC inventory path and rejects a root escape", () => {
|
|
const nfc = "caf\u00e9".normalize("NFC");
|
|
const nfd = nfc.normalize("NFD");
|
|
const files = fileMap([
|
|
file("dir/index.js", "root"),
|
|
file(`dir/${nfc}.js`, "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: `./${nfd}.js`,
|
|
sourcePath: "dir/index.js",
|
|
context: "module-specifier",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: `dir/${nfc}.js`,
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "../escape.js",
|
|
sourcePath: "index.js",
|
|
context: "module-specifier",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "rejected", resolved_path: null });
|
|
});
|
|
});
|
|
|
|
describe("contextual JavaScript artifact path resolution", () => {
|
|
it.each([
|
|
["main.js", "app/main.js"],
|
|
["./main.js", "app/main.js"],
|
|
["src/main", "app/src/main.cjs"],
|
|
["entry", "app/entry/start.mjs"],
|
|
["fallback", "app/fallback/index.ts"],
|
|
])("resolves package entrypoint %s", (declaredPath, expected) => {
|
|
const files = fileMap([
|
|
file("app/package.json", "root"),
|
|
file("app/main.js", "root"),
|
|
file("app/src/main.cjs", "root"),
|
|
file("app/entry/package.json", "root", '{"main":"start"}'),
|
|
file("app/entry/start.mjs", "root"),
|
|
file("app/fallback/index.ts", "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath,
|
|
sourcePath: "app/package.json",
|
|
context: "package-entrypoint",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_context: "package-entrypoint",
|
|
resolution_status: "resolved",
|
|
resolved_path: expected,
|
|
limitations: [],
|
|
});
|
|
});
|
|
|
|
it("resolves dirname-derived bare paths but keeps bare module specifiers external", () => {
|
|
const files = fileMap([
|
|
file("app/main.js", "root"),
|
|
file("app/preload.js", "root"),
|
|
]);
|
|
const base = {
|
|
declaredPath: "preload.js",
|
|
sourcePath: "app/main.js",
|
|
files,
|
|
} as const;
|
|
|
|
expect(
|
|
resolve({ ...base, context: "filesystem-expression" }),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/preload.js",
|
|
});
|
|
expect(resolve({ ...base, context: "module-specifier" })).toMatchObject({
|
|
resolution_status: "external",
|
|
resolved_path: null,
|
|
});
|
|
});
|
|
|
|
it("applies document and base-href semantics to HTML references", () => {
|
|
const files = fileMap([
|
|
file("renderer/index.html", "root"),
|
|
file("assets/app.js", "root"),
|
|
file("renderer/local.js", "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "app.js?v=1",
|
|
sourcePath: "renderer/index.html",
|
|
context: "html-reference",
|
|
htmlBaseHref: "/assets/",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "assets/app.js",
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./local.js",
|
|
sourcePath: "renderer/index.html",
|
|
context: "html-reference",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "renderer/local.js",
|
|
});
|
|
});
|
|
|
|
it.each(["node:fs", "electron", "../../escape.js", "%2e%2e/escape.js"])(
|
|
"fails closed for unsafe or external module declaration %s",
|
|
(declaredPath) => {
|
|
const result = resolve({
|
|
declaredPath,
|
|
sourcePath: "app/main.js",
|
|
context: "module-specifier",
|
|
files: fileMap([file("app/main.js", "root")]),
|
|
});
|
|
expect(result.resolved_path).toBeNull();
|
|
expect(["external", "rejected"]).toContain(result.resolution_status);
|
|
expect(result.limitations.length).toBeGreaterThan(0);
|
|
},
|
|
);
|
|
|
|
it("does not cross nested ASAR container identities", () => {
|
|
const files = fileMap([
|
|
file("app.asar/main.js", "asar-a"),
|
|
file("app.asar/preload.js", "asar-b"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "preload.js",
|
|
sourcePath: "app.asar/main.js",
|
|
context: "filesystem-expression",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
|
|
});
|
|
|
|
it("resolves inventoried paths longer than the former character ceiling", () => {
|
|
const declaredPath = `./${"a".repeat(5_000)}.js`;
|
|
const expectedPath = `app/${declaredPath.slice(2)}`;
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file(expectedPath, "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath,
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: expectedPath,
|
|
limitations: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("ESM module URL path decoding", () => {
|
|
it.each([
|
|
["./a%2eb.mjs", "app/esm/a.b.mjs"],
|
|
["./sub/%2e%2e/x.mjs", "app/esm/x.mjs"],
|
|
])(
|
|
"decodes confined URL path %s before artifact lookup",
|
|
(declaredPath, target) => {
|
|
expect(
|
|
resolve({
|
|
declaredPath,
|
|
sourcePath: "app/esm/entry.mjs",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files: fileMap([
|
|
file("app/esm/entry.mjs", "root"),
|
|
file("app/esm/a.b.mjs", "root"),
|
|
file("app/esm/x.mjs", "root"),
|
|
]),
|
|
}),
|
|
).toMatchObject({
|
|
resolved_path: target,
|
|
resolution_status: "resolved",
|
|
});
|
|
},
|
|
);
|
|
|
|
it("rejects encoded separators and paths escaping the artifact root", () => {
|
|
const files = fileMap([
|
|
file("app/esm/entry.mjs", "root"),
|
|
file("app/esm/x.mjs", "root"),
|
|
]);
|
|
for (const declaredPath of ["./sub%2fx.mjs", "../../../escape.mjs"]) {
|
|
expect(
|
|
resolve({
|
|
declaredPath,
|
|
sourcePath: "app/esm/entry.mjs",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolved_path: null,
|
|
resolution_status: "rejected",
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("JavaScript file URL resolution", () => {
|
|
it("resolves file URL paths without interpreting query bytes as path syntax", () => {
|
|
const files = fileMap([file("assets/app.js", "root")]);
|
|
for (const declaredPath of [
|
|
"file:///assets/app.js#entry",
|
|
"FILE:///assets/app.js?token=%2F#entry",
|
|
]) {
|
|
expect(
|
|
resolve({
|
|
declaredPath,
|
|
sourcePath: "main.js",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
declared_path: declaredPath,
|
|
resolution_status: "resolved",
|
|
resolved_path: "assets/app.js",
|
|
});
|
|
}
|
|
|
|
for (const declaredPath of [
|
|
"file:///assets%2Fapp.js",
|
|
"file:///assets%2\nFapp.js",
|
|
]) {
|
|
expect(
|
|
resolve({
|
|
declaredPath,
|
|
sourcePath: "main.js",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "rejected", resolved_path: null });
|
|
}
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "file:/assets/app.js",
|
|
sourcePath: "main.js",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "external", resolved_path: null });
|
|
});
|
|
});
|
|
|
|
describe("contextual JavaScript package path resolution", () => {
|
|
it("follows package entrypoints beyond the former recursion ceiling", () => {
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file(
|
|
"app/node_modules/pkg-1/package.json",
|
|
"root",
|
|
'{"main":"../pkg-2"}',
|
|
),
|
|
file(
|
|
"app/node_modules/pkg-2/package.json",
|
|
"root",
|
|
'{"main":"../pkg-3"}',
|
|
),
|
|
file(
|
|
"app/node_modules/pkg-3/package.json",
|
|
"root",
|
|
'{"main":"../pkg-4"}',
|
|
),
|
|
file(
|
|
"app/node_modules/pkg-4/package.json",
|
|
"root",
|
|
'{"main":"../pkg-5"}',
|
|
),
|
|
file(
|
|
"app/node_modules/pkg-5/package.json",
|
|
"root",
|
|
'{"main":"../pkg-6"}',
|
|
),
|
|
file(
|
|
"app/node_modules/pkg-6/package.json",
|
|
"root",
|
|
'{"main":"entry.js"}',
|
|
),
|
|
file("app/node_modules/pkg-6/entry.js", "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "pkg-1",
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/node_modules/pkg-6/entry.js",
|
|
limitations: [],
|
|
});
|
|
});
|
|
|
|
it("keeps an inventoried but unreadable directory package unavailable", () => {
|
|
const files = fileMap([
|
|
file("app/package.json", "root"),
|
|
file("app/entry/package.json", "root", null),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "entry",
|
|
sourcePath: "app/package.json",
|
|
context: "package-entrypoint",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "unavailable",
|
|
resolved_path: null,
|
|
limitations: [expect.stringContaining("text is unavailable")],
|
|
});
|
|
});
|
|
|
|
it("resolves nearest inventoried bare packages with import/require conditions", () => {
|
|
const files = fileMap([
|
|
file("app/src/consumer.js", "root"),
|
|
file(
|
|
"app/node_modules/fixture/package.json",
|
|
"root",
|
|
JSON.stringify({
|
|
exports: {
|
|
".": {
|
|
import: "./esm.mjs",
|
|
require: "./cjs.cjs",
|
|
default: "./fallback.js",
|
|
},
|
|
},
|
|
}),
|
|
),
|
|
file("app/node_modules/fixture/esm.mjs", "root"),
|
|
file("app/node_modules/fixture/cjs.cjs", "root"),
|
|
file("app/node_modules/fixture/fallback.js", "root"),
|
|
file("app/node_modules/fixture/hidden.js", "root"),
|
|
file("app/node_modules/fs/index.js", "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/node_modules/fixture/esm.mjs",
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/node_modules/fixture/cjs.cjs",
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fs",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "external", resolved_path: null });
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture/hidden.js",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "external",
|
|
resolved_path: null,
|
|
limitations: [
|
|
expect.stringContaining("declares no exports target for subpath"),
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("package exports subpath resolution", () => {
|
|
it("resolves exact subpath keys through import and require conditions", () => {
|
|
const files = fileMap([
|
|
file("app/src/consumer.js", "root"),
|
|
file(
|
|
"app/node_modules/fixture/package.json",
|
|
"root",
|
|
JSON.stringify({
|
|
exports: {
|
|
".": "./index.cjs",
|
|
"./parser": {
|
|
import: "./parser.mjs",
|
|
require: "./parser.cjs",
|
|
default: "./fallback.js",
|
|
},
|
|
},
|
|
}),
|
|
),
|
|
file("app/node_modules/fixture/index.cjs", "root"),
|
|
file("app/node_modules/fixture/parser.mjs", "root"),
|
|
file("app/node_modules/fixture/parser.cjs", "root"),
|
|
file("app/node_modules/fixture/fallback.js", "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture/parser",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/node_modules/fixture/parser.mjs",
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture/parser",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/node_modules/fixture/parser.cjs",
|
|
});
|
|
});
|
|
|
|
it("resolves array subpath targets at their first string entry", () => {
|
|
const files = fileMap([
|
|
file("app/src/consumer.js", "root"),
|
|
file(
|
|
"app/node_modules/fixture/package.json",
|
|
"root",
|
|
JSON.stringify({
|
|
exports: {
|
|
"./parser": [{ import: "./parser.mjs" }, "./parser.cjs"],
|
|
},
|
|
}),
|
|
),
|
|
file("app/node_modules/fixture/parser.cjs", "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture/parser",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/node_modules/fixture/parser.cjs",
|
|
});
|
|
});
|
|
|
|
it("resolves subpaths of scoped packages", () => {
|
|
const files = fileMap([
|
|
file("app/src/consumer.js", "root"),
|
|
file(
|
|
"app/node_modules/@scope/fixture/package.json",
|
|
"root",
|
|
JSON.stringify({
|
|
exports: { "./parser": "./parser.cjs" },
|
|
}),
|
|
),
|
|
file("app/node_modules/@scope/fixture/parser.cjs", "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "@scope/fixture/parser",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/node_modules/@scope/fixture/parser.cjs",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("package exports subpath encapsulation", () => {
|
|
it("keeps subpaths encapsulated when only a root target is exported", () => {
|
|
const files = fileMap([
|
|
file("app/src/consumer.js", "root"),
|
|
file(
|
|
"app/node_modules/fixture/package.json",
|
|
"root",
|
|
JSON.stringify({ exports: "./index.cjs" }),
|
|
),
|
|
file("app/node_modules/fixture/index.cjs", "root"),
|
|
file("app/node_modules/fixture/parser.cjs", "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture/parser",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "external",
|
|
resolved_path: null,
|
|
limitations: [expect.stringContaining("a root target only")],
|
|
});
|
|
});
|
|
|
|
it("blocks subpaths with an explicit null target", () => {
|
|
const files = fileMap([
|
|
file("app/src/consumer.js", "root"),
|
|
file(
|
|
"app/node_modules/fixture/package.json",
|
|
"root",
|
|
JSON.stringify({
|
|
exports: { ".": "./index.cjs", "./parser": null },
|
|
}),
|
|
),
|
|
file("app/node_modules/fixture/index.cjs", "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture/parser",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "external",
|
|
resolved_path: null,
|
|
limitations: [expect.stringContaining("explicit null exports target")],
|
|
});
|
|
});
|
|
|
|
it("reports a missing selected subpath target without index fallback", () => {
|
|
const files = fileMap([
|
|
file("app/src/consumer.js", "root"),
|
|
file(
|
|
"app/node_modules/fixture/package.json",
|
|
"root",
|
|
JSON.stringify({
|
|
exports: { "./parser": "./parser.cjs" },
|
|
}),
|
|
),
|
|
file("app/node_modules/fixture/parser/index.js", "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture/parser",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "not-found",
|
|
resolved_path: null,
|
|
limitations: [expect.stringContaining("exports targets are exact files")],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("package subpath directory ownership", () => {
|
|
it.each(["root", "other"])(
|
|
"selects metadata-less package directories only in the %s container",
|
|
(container) => {
|
|
const files = fileMap([
|
|
file("app/src/consumer.js", "root"),
|
|
file("app/src/node_modules/fixture/parser.js", container),
|
|
file("app/node_modules/fixture/package.json", "root", "{}"),
|
|
file("app/node_modules/fixture/parser.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture/parser",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
declared_path: "fixture/parser",
|
|
resolution_context: "module-specifier",
|
|
resolution_status: "resolved",
|
|
resolved_path:
|
|
container === "root"
|
|
? "app/src/node_modules/fixture/parser.js"
|
|
: "app/node_modules/fixture/parser.js",
|
|
});
|
|
},
|
|
);
|
|
|
|
it("does not mistake a package-name file or sibling prefix for a nearer directory", () => {
|
|
const files = fileMap([
|
|
file("app/src/consumer.js", "root"),
|
|
file("app/src/node_modules/fixture.js", "root"),
|
|
file("app/src/node_modules/fixture-extra/parser.js", "root"),
|
|
file("app/node_modules/fixture/parser.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture/parser",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolved_path: "app/node_modules/fixture/parser.js",
|
|
resolution_status: "resolved",
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
["malformed", "{", "not valid package JSON"],
|
|
["unavailable", null, "text is unavailable"],
|
|
])("preserves %s nested directory failures", (_name, text, reason) => {
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file("app/node_modules/fixture/package.json", "root", "{}"),
|
|
file("app/node_modules/fixture/parser/package.json", "root", text),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture/parser",
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
declared_path: "fixture/parser",
|
|
resolution_context: "module-specifier",
|
|
resolved_path: null,
|
|
resolution_status: "unavailable",
|
|
limitations: [expect.stringContaining(reason)],
|
|
});
|
|
});
|
|
|
|
it("does not recurse through a cyclic nested main during CommonJS lookup", () => {
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file("app/node_modules/fixture/package.json", "root", "{}"),
|
|
file(
|
|
"app/node_modules/fixture/parser/package.json",
|
|
"root",
|
|
'{"main":"."}',
|
|
),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture/parser",
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "not-found",
|
|
resolved_path: null,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("package subpath legacy lookup and metadata failures", () => {
|
|
it("resolves CommonJS legacy subpath files and indexes without exports", () => {
|
|
const files = fileMap([
|
|
file("app/src/consumer.js", "root"),
|
|
file(
|
|
"app/node_modules/fixture/package.json",
|
|
"root",
|
|
'{"main":"./index.cjs"}',
|
|
),
|
|
file("app/node_modules/fixture/direct.js", "root"),
|
|
file("app/node_modules/fixture/explicit.cjs", "root"),
|
|
file("app/node_modules/fixture/folder/index.js", "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture/direct",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/node_modules/fixture/direct.js",
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture/explicit.cjs",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/node_modules/fixture/explicit.cjs",
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture/folder",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/node_modules/fixture/folder/index.js",
|
|
});
|
|
});
|
|
|
|
it("rejects subpath metadata that mixes subpath and condition keys", () => {
|
|
const files = fileMap([
|
|
file("app/src/consumer.js", "root"),
|
|
file(
|
|
"app/node_modules/fixture/package.json",
|
|
"root",
|
|
JSON.stringify({
|
|
exports: { "./parser": "./parser.cjs", node: "./index.cjs" },
|
|
}),
|
|
),
|
|
file("app/node_modules/fixture/parser.cjs", "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture/parser",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "rejected",
|
|
resolved_path: null,
|
|
limitations: [expect.stringContaining("mixes subpath keys")],
|
|
});
|
|
});
|
|
|
|
it("reports malformed subpath package metadata as unavailable", () => {
|
|
const files = fileMap([
|
|
file("app/src/consumer.js", "root"),
|
|
file("app/node_modules/fixture/package.json", "root", "{"),
|
|
file("app/node_modules/fixture/parser.cjs", "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture/parser",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "unavailable",
|
|
resolved_path: null,
|
|
limitations: [expect.stringContaining("not valid package JSON")],
|
|
});
|
|
});
|
|
|
|
it("reports unavailable metadata text for subpath resolution", () => {
|
|
const files = fileMap([
|
|
file("app/src/consumer.js", "root"),
|
|
file("app/node_modules/fixture/package.json", "root", null),
|
|
file("app/node_modules/fixture/parser.cjs", "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture/parser",
|
|
sourcePath: "app/src/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "unavailable",
|
|
resolved_path: null,
|
|
limitations: [expect.stringContaining("text is unavailable")],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("generic package entry heuristics", () => {
|
|
it.each([
|
|
['{"main":"actual.cjs","module":"bundler.mjs"}', "actual.cjs"],
|
|
['{"module":"bundler.mjs"}', "bundler.mjs"],
|
|
])(
|
|
"preserves generic entry heuristics without a module kind for %s",
|
|
(metadata, entry) => {
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file("app/node_modules/fixture/package.json", "root", metadata),
|
|
file("app/node_modules/fixture/actual.cjs", "root"),
|
|
file("app/node_modules/fixture/bundler.mjs", "root"),
|
|
file("app/node_modules/fixture/index.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: `app/node_modules/fixture/${entry}`,
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("package exports exact targets", () => {
|
|
it.each([
|
|
["./file", "app/node_modules/fixture/file.js"],
|
|
["./folder", "app/node_modules/fixture/folder/index.js"],
|
|
])(
|
|
"resolves exports target %s only to that exact file",
|
|
(target, nearMiss) => {
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file(
|
|
"app/node_modules/fixture/package.json",
|
|
"root",
|
|
JSON.stringify({ exports: target }),
|
|
),
|
|
file(nearMiss, "root"),
|
|
]);
|
|
for (const moduleKind of ["import", "require"] as const) {
|
|
const resolution = resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind,
|
|
files,
|
|
});
|
|
expect(resolution).toMatchObject({
|
|
resolution_status: "not-found",
|
|
resolved_path: null,
|
|
});
|
|
expect(resolution.limitations.join(" ")).toContain(
|
|
"exports targets are exact files",
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
it("keeps extension and index fallbacks for a legacy main entry", () => {
|
|
for (const [main, target] of [
|
|
["./file", "app/node_modules/fixture/file.js"],
|
|
["./folder", "app/node_modules/fixture/folder/index.js"],
|
|
] as const) {
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file(
|
|
"app/node_modules/fixture/package.json",
|
|
"root",
|
|
JSON.stringify({ main }),
|
|
),
|
|
file(target, "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "resolved", resolved_path: target });
|
|
}
|
|
});
|
|
});
|
|
|
|
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("keeps package import aliases available for runtime entrypoints", () => {
|
|
const files = aliasFiles({
|
|
"package.json": JSON.stringify({
|
|
imports: { "#worker": "./worker.js" },
|
|
}),
|
|
"main.cjs": "",
|
|
"worker.js": "",
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "#worker",
|
|
sourcePath: "main.cjs",
|
|
context: "module-specifier",
|
|
files,
|
|
runtimePathKind: "exact-file",
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "worker.js",
|
|
declared_path: "#worker",
|
|
});
|
|
});
|
|
|
|
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"],
|
|
["require", '{"exports":{"require":"./actual.cjs"}}', "actual.cjs"],
|
|
["import", '{"exports":{"import":"./actual.mjs"}}', "actual.mjs"],
|
|
] as const)(
|
|
"prefers declared %s entrypoints over index files",
|
|
(moduleKind, metadata, entry) => {
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file("app/node_modules/fixture/package.json", "root", metadata),
|
|
file(`app/node_modules/fixture/${entry}`, "root"),
|
|
file("app/node_modules/fixture/index.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind,
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: `app/node_modules/fixture/${entry}`,
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each(["{}", '{"main":"."}'])(
|
|
"preserves index fallback for package metadata %s",
|
|
(metadata) => {
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file("app/entry/package.json", "root", metadata),
|
|
file("app/entry/index.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./entry",
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/entry/index.js",
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each([null, "{"])(
|
|
"keeps unreadable or invalid metadata explicit even when an index exists",
|
|
(metadata) => {
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file("app/entry/package.json", "root", metadata),
|
|
file("app/entry/index.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./entry",
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "unavailable",
|
|
resolved_path: null,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("does not replace a missing exports target with an index", () => {
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file(
|
|
"app/node_modules/fixture/package.json",
|
|
"root",
|
|
'{"exports":"./missing.js"}',
|
|
),
|
|
file("app/node_modules/fixture/index.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "not-found",
|
|
resolved_path: null,
|
|
});
|
|
});
|
|
|
|
it("keeps direct files ahead of a relative directory package", () => {
|
|
const files = fileMap([
|
|
file("app/consumer.js", "root"),
|
|
file("app/entry.js", "root"),
|
|
file("app/entry/package.json", "root", '{"main":"actual.js"}'),
|
|
file("app/entry/actual.js", "root"),
|
|
file("app/entry/index.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./entry",
|
|
sourcePath: "app/consumer.js",
|
|
context: "module-specifier",
|
|
moduleKind: "require",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/entry.js",
|
|
});
|
|
});
|
|
|
|
it("retains cycle diagnostics for generic package-entrypoint inference", () => {
|
|
const files = fileMap([
|
|
file("app/package.json", "root"),
|
|
file("app/a/package.json", "root", '{"main":"../b"}'),
|
|
file("app/b/package.json", "root", '{"main":"../a"}'),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./a",
|
|
sourcePath: "app/package.json",
|
|
context: "package-entrypoint",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "unavailable",
|
|
resolved_path: null,
|
|
limitations: [expect.stringContaining("cycle")],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("contextual JavaScript module identity", () => {
|
|
it("retains CommonJS and ESM file-base identity during inert extraction", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(
|
|
`
|
|
import { BrowserWindow } from "electron";
|
|
new BrowserWindow({ webPreferences: {
|
|
preload: path.resolve("ignored", __dirname, "preload.cjs")
|
|
}});
|
|
new BrowserWindow({ webPreferences: {
|
|
preload: fileURLToPath(new URL("./preload.mjs", import.meta.url))
|
|
}});
|
|
new BrowserWindow({ webPreferences: {
|
|
preload: path.join(path.dirname(__filename), "legacy-preload.js")
|
|
}});
|
|
new BrowserWindow({ webPreferences: {
|
|
preload: path.posix.resolve("/ignored", dirname(fileURLToPath(import.meta.url)), "esm-preload.js")
|
|
}});
|
|
new BrowserWindow({ webPreferences: {
|
|
preload: path.posix.resolve(__dirname, "/shared", "absolute-preload.js/")
|
|
}});
|
|
new BrowserWindow({ webPreferences: {
|
|
preload: path.posix.resolve(\`/templates\`, "template-preload.js")
|
|
}});
|
|
new BrowserWindow({ webPreferences: {
|
|
preload: new URL("./remote-base.js", location.href)
|
|
}});
|
|
window.loadURL("file:///renderer/index.html");
|
|
`,
|
|
);
|
|
|
|
expect(analysis.role_paths).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
path: "preload.cjs",
|
|
resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
path: "./preload.mjs",
|
|
resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
path: "legacy-preload.js",
|
|
resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
path: "esm-preload.js",
|
|
resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
path: "/shared/absolute-preload.js",
|
|
resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
path: "/templates/template-preload.js",
|
|
resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
path: "file:///renderer/index.html",
|
|
resolution_context: "module-specifier",
|
|
}),
|
|
]),
|
|
);
|
|
expect(analysis.electron.browser_windows).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
preload_path: "preload.cjs",
|
|
preload_resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
preload_path: "./preload.mjs",
|
|
preload_resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
preload_path: "esm-preload.js",
|
|
preload_resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
preload_path: "/shared/absolute-preload.js",
|
|
preload_resolution_context: "filesystem-expression",
|
|
}),
|
|
expect.objectContaining({
|
|
preload_path: "/templates/template-preload.js",
|
|
preload_resolution_context: "filesystem-expression",
|
|
}),
|
|
]),
|
|
);
|
|
expect(analysis.role_paths).not.toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ path: "./remote-base.js" }),
|
|
]),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("TypeScript NodeNext emitted-extension resolution", () => {
|
|
const container = "container";
|
|
|
|
it.each([
|
|
["./dep.js", "src/dep.ts"],
|
|
["./ui/Button.js", "src/ui/Button.tsx"],
|
|
["./cfg.mjs", "src/cfg.mts"],
|
|
["./legacy.cjs", "src/legacy.cts"],
|
|
] as const)(
|
|
"resolves %s to its TypeScript source counterpart",
|
|
(specifier, target) => {
|
|
const files = fileMap([
|
|
file("src/main.ts", container),
|
|
file(target, container, "export const value = 1;"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: specifier,
|
|
sourcePath: "src/main.ts",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "resolved", resolved_path: target });
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("TypeScript declaration fallback ownership", () => {
|
|
const container = "container";
|
|
|
|
it.each([
|
|
["./dep.js", "src/dep.d.ts"],
|
|
["./cfg.mjs", "src/cfg.d.mts"],
|
|
["./legacy.cjs", "src/legacy.d.cts"],
|
|
] as const)(
|
|
"retains %s as compile-time evidence without inventing a runtime target",
|
|
(specifier, target) => {
|
|
const files = fileMap([
|
|
file("src/main.ts", container),
|
|
file(target, container, "export const value = 1;"),
|
|
]);
|
|
for (const moduleKind of ["import", "require"] as const) {
|
|
expect(
|
|
resolve({
|
|
declaredPath: specifier,
|
|
sourcePath: "src/main.ts",
|
|
context: "module-specifier",
|
|
moduleKind,
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "unavailable",
|
|
resolved_path: null,
|
|
candidate_path: target,
|
|
});
|
|
}
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./" + target.slice(4),
|
|
sourcePath: "src/main.ts",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "resolved", resolved_path: target });
|
|
},
|
|
);
|
|
|
|
it("prefers a present runtime file and keeps the fallback off for JS callers", () => {
|
|
const files = fileMap([
|
|
file("src/main.ts", container),
|
|
file("src/dep.js", container),
|
|
file("src/dep.ts", container),
|
|
file("src/js-main.js", container),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./dep.js",
|
|
sourcePath: "src/main.ts",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "src/dep.js",
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./dep.js",
|
|
sourcePath: "src/js-main.js",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files: fileMap([
|
|
file("src/js-main.js", container),
|
|
file("src/dep.ts", container),
|
|
]),
|
|
}),
|
|
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
|
|
});
|
|
|
|
it("does not apply the mapping to URL resources or package export targets", () => {
|
|
const urlFiles = fileMap([
|
|
file("src/main.ts", container),
|
|
file("src/asset.ts", container),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "asset.js",
|
|
sourcePath: "src/main.ts",
|
|
context: "url-reference",
|
|
files: urlFiles,
|
|
}),
|
|
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
|
|
|
|
const packageFiles = fileMap([
|
|
file("src/main.ts", container),
|
|
file(
|
|
"src/node_modules/fixture/package.json",
|
|
container,
|
|
'{"exports":"./dep.js"}',
|
|
),
|
|
file("src/node_modules/fixture/dep.ts", container),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "fixture",
|
|
sourcePath: "src/main.ts",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files: packageFiles,
|
|
}),
|
|
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
|
|
});
|
|
|
|
it("does not substitute source counterparts for directory or index resolution", () => {
|
|
const files = fileMap([
|
|
file("src/main.ts", container),
|
|
file("src/lib/index.ts", container),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./lib.js",
|
|
sourcePath: "src/main.ts",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
|
|
});
|
|
|
|
it("resolves an emitted dotfile name within its own directory", () => {
|
|
const files = fileMap([
|
|
file("src/main.ts", container),
|
|
file("src/.ts", container),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./.js",
|
|
sourcePath: "src/main.ts",
|
|
context: "module-specifier",
|
|
moduleKind: "import",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "src/.ts",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("url-reference directory and empty-segment semantics", () => {
|
|
it.each([
|
|
"./worker.js/.",
|
|
"./worker.js/child/..",
|
|
"./worker.js/",
|
|
"/dist/worker.js/.",
|
|
"./worker.js/.?cache=1#entry",
|
|
])("keeps directory-form URLs distinct from files: %s", (declaredPath) => {
|
|
expect(
|
|
resolve({
|
|
declaredPath,
|
|
sourcePath: "dist/app.mjs",
|
|
context: "url-reference",
|
|
files: fileMap([file("dist/worker.js", "root")]),
|
|
}),
|
|
).toMatchObject({
|
|
declared_path: declaredPath,
|
|
resolution_status: "not-found",
|
|
resolved_path: null,
|
|
});
|
|
});
|
|
|
|
it.each(["./nested//worker.js", "./nested//../worker.js"])(
|
|
"does not collapse empty URL path segments: %s",
|
|
(declaredPath) => {
|
|
expect(
|
|
resolve({
|
|
declaredPath,
|
|
sourcePath: "dist/app.mjs",
|
|
context: "url-reference",
|
|
files: fileMap([
|
|
file("dist/worker.js", "root"),
|
|
file("dist/nested/worker.js", "root"),
|
|
]),
|
|
}),
|
|
).toMatchObject({ resolved_path: null });
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
["./nested/.././worker.js?cache=1#entry", "dist/worker.js"],
|
|
["./.worker.js", "dist/.worker.js"],
|
|
])("resolves exact file URLs with dots: %s", (declaredPath, resolvedPath) => {
|
|
expect(
|
|
resolve({
|
|
declaredPath,
|
|
sourcePath: "dist/app.mjs",
|
|
context: "url-reference",
|
|
files: fileMap([file(resolvedPath, "root")]),
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: resolvedPath,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("suffix-only non-runtime URL references", () => {
|
|
it.each(["", "?cache=1", "#worker"])(
|
|
"rejects URL reference %s without a path or known base document",
|
|
(declaredPath) => {
|
|
expect(
|
|
resolve({
|
|
declaredPath,
|
|
sourcePath: "dist/app.mjs",
|
|
context: "url-reference",
|
|
files: fileMap([file("dist/app.mjs", "root")]),
|
|
}),
|
|
).toMatchObject({
|
|
declared_path: declaredPath,
|
|
resolution_status: "rejected",
|
|
resolved_path: null,
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("url-reference JavaScript artifact path resolution", () => {
|
|
it.each([
|
|
["C|?cache=1#entry", "dist/C|"],
|
|
["/c:/worker.js", "c:/worker.js"],
|
|
["/./c|/worker.js", "c|/worker.js"],
|
|
["../c|/worker.js", "c|/worker.js"],
|
|
["/nested/../c|/worker.js", "c|/worker.js"],
|
|
["c|/../worker.js", "dist/worker.js"],
|
|
])("leaves drive-letter URL paths unresolved: %s", (declaredPath, decoy) => {
|
|
const resolution = resolve({
|
|
declaredPath,
|
|
sourcePath: "dist/app.mjs",
|
|
context: "url-reference",
|
|
files: fileMap([file(decoy, "root")]),
|
|
});
|
|
expect(resolution).toMatchObject({
|
|
declared_path: declaredPath,
|
|
resolution_status: "unavailable",
|
|
resolved_path: null,
|
|
});
|
|
expect(resolution.limitations.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it.each([
|
|
["./c|/worker.js", "dist/c|/worker.js"],
|
|
["c|name/worker.js", "dist/c|name/worker.js"],
|
|
["/%63|/worker.js", "c|/worker.js"],
|
|
["/c%3A/worker.js", "c:/worker.js"],
|
|
])(
|
|
"retains non-drive URL path identities: %s",
|
|
(declaredPath, resolvedPath) => {
|
|
expect(
|
|
resolve({
|
|
declaredPath,
|
|
sourcePath: "dist/app.mjs",
|
|
context: "url-reference",
|
|
files: fileMap([file(resolvedPath, "root")]),
|
|
}),
|
|
).toMatchObject({
|
|
declared_path: declaredPath,
|
|
resolution_status: "resolved",
|
|
resolved_path: resolvedPath,
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each(["?value=", "#value="])(
|
|
"admits the pathname independently of its %s suffix",
|
|
(suffix) => {
|
|
for (const value of ["%2F", "%2E", "%5C", "%00", "%FF", "\\", "\0"]) {
|
|
const declaredPath = `./worker.js${suffix}${value}`;
|
|
expect(
|
|
resolve({
|
|
declaredPath,
|
|
sourcePath: "dist/app.js",
|
|
context: "url-reference",
|
|
files: fileMap([file("dist/worker.js", "root")]),
|
|
}),
|
|
declaredPath,
|
|
).toMatchObject({
|
|
declared_path: declaredPath,
|
|
resolution_status: "resolved",
|
|
resolved_path: "dist/worker.js",
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
"app\0.js.map?valid=1",
|
|
"nested\\worker.js#valid",
|
|
"app%00.js.map",
|
|
"app%FF.js.map",
|
|
"app%C0%AF.js.map",
|
|
"app%2ejs.map",
|
|
"nested%2fworker.js",
|
|
"nested%5cworker.js",
|
|
"h\nttp://[",
|
|
"../../outside.js.map",
|
|
"../../outside/.",
|
|
])("rejects an inadmissible URL path: %s", (declaredPath) => {
|
|
const resolution = resolve({
|
|
declaredPath,
|
|
sourcePath: "dist/app.js",
|
|
context: "url-reference",
|
|
files: fileMap([
|
|
file("dist/app.js.map", "root"),
|
|
file("dist/nested/worker.js", "root"),
|
|
]),
|
|
});
|
|
expect(resolution).toMatchObject({
|
|
declared_path: declaredPath,
|
|
resolution_context: "url-reference",
|
|
resolution_status: "rejected",
|
|
resolved_path: null,
|
|
});
|
|
expect(resolution.limitations.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("resolves sibling paths and rejects external URL forms", () => {
|
|
const files = fileMap([
|
|
file("dist/app.js", "root"),
|
|
file("dist/app.js.map", "root"),
|
|
file("dist/worker.js", "root"),
|
|
]);
|
|
|
|
expect(
|
|
resolve({
|
|
declaredPath: "app.js.map",
|
|
sourcePath: "dist/app.js",
|
|
context: "url-reference",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "dist/app.js.map",
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "app.js.map?v=1#section",
|
|
sourcePath: "dist/app.js",
|
|
context: "url-reference",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "dist/app.js.map",
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "//cdn.example.com/app.js.map",
|
|
sourcePath: "dist/app.js",
|
|
context: "url-reference",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "external",
|
|
resolved_path: null,
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "https://cdn.example.com/app.js.map",
|
|
sourcePath: "dist/app.js",
|
|
context: "url-reference",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "external",
|
|
resolved_path: null,
|
|
});
|
|
});
|
|
});
|
|
|
|
const resolve = (input: ResolveArtifactPathInput) =>
|
|
resolveArtifactPathByContext(input);
|
|
|
|
const fileMap = (files: readonly JavaScriptArtifactFile[]) =>
|
|
new Map(files.map((value) => [value.path, value]));
|
|
|
|
const file = (
|
|
path: string,
|
|
container: string,
|
|
text: string | null = "",
|
|
): JavaScriptArtifactFile => ({
|
|
path,
|
|
container_sha256: container.padEnd(64, "0").slice(0, 64),
|
|
sha256: path.padEnd(64, "0").slice(0, 64),
|
|
bytes: text?.length ?? 0,
|
|
inventory_artifact_id: `artifact-${path}`,
|
|
kind: path.endsWith("package.json") ? "package-json" : "javascript",
|
|
unpacked: false,
|
|
text:
|
|
text === null
|
|
? { included: false, reason: "invalid-utf8" }
|
|
: { 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"]],
|
|
["directory index", ["app/index.js"]],
|
|
["directory package", ["app/package.json", "app/entry.js"]],
|
|
] as const)("does not infer a file from %s fallback", (_name, paths) => {
|
|
const files = fileMap([
|
|
file("index.html", "root"),
|
|
...paths.map((path) =>
|
|
file(
|
|
path,
|
|
"root",
|
|
path.endsWith("package.json") ? '{"main":"entry.js"}' : "",
|
|
),
|
|
),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./app",
|
|
sourcePath: "index.html",
|
|
context: "html-reference",
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
|
|
});
|
|
|
|
it("prefers an exact extensionless resource and retains URL suffix handling", () => {
|
|
const files = fileMap([
|
|
file("renderer/index.html", "root"),
|
|
file("assets/app", "root"),
|
|
file("assets/app.js", "root"),
|
|
file("assets/app/index.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./app?cache=1#v2",
|
|
sourcePath: "renderer/index.html",
|
|
context: "html-reference",
|
|
htmlBaseHref: "/assets/",
|
|
files,
|
|
}),
|
|
).toMatchObject({
|
|
declared_path: "./app?cache=1#v2",
|
|
resolution_status: "resolved",
|
|
resolved_path: "assets/app",
|
|
});
|
|
});
|
|
});
|
|
|
|
it.each(["require", "import"] as const)(
|
|
"rejects a literal trailing backslash package main before %s probing",
|
|
(moduleKind) => {
|
|
const files = fileMap([
|
|
file("entry.js", "root"),
|
|
file(
|
|
"node_modules/p/package.json",
|
|
"root",
|
|
JSON.stringify({ main: "lib\\" }),
|
|
),
|
|
file("node_modules/p/lib", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "p",
|
|
sourcePath: "entry.js",
|
|
context: "module-specifier",
|
|
moduleKind,
|
|
files,
|
|
}),
|
|
).toMatchObject({ resolution_status: "rejected", resolved_path: null });
|
|
},
|
|
);
|
|
|
|
describe("exact Electron runtime entrypoint resolution", () => {
|
|
it("requires a protocol for Electron URL resources", () => {
|
|
const files = fileMap([
|
|
file("app/main.cjs", "root"),
|
|
file("app/renderer", "root"),
|
|
file("app/renderer.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./renderer",
|
|
sourcePath: "app/main.cjs",
|
|
context: "module-specifier",
|
|
files,
|
|
runtimePathKind: "url-resource",
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "unavailable",
|
|
resolved_path: null,
|
|
limitations: [expect.stringContaining("requires a URL protocol")],
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "file:///app/renderer",
|
|
sourcePath: "app/main.cjs",
|
|
context: "module-specifier",
|
|
files,
|
|
runtimePathKind: "url-resource",
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/renderer",
|
|
});
|
|
});
|
|
|
|
it("checks local paths exactly while preserving bare package lookup", () => {
|
|
const files = fileMap([
|
|
file("app/main.cjs", "root"),
|
|
file("app/worker.js", "root"),
|
|
file("app/worker", "root"),
|
|
file("app/worker#v1.js", "root"),
|
|
file(
|
|
"node_modules/exported/package.json",
|
|
"root",
|
|
'{"exports":{"./parser":"./parser.cjs"}}',
|
|
),
|
|
file("node_modules/exported/parser.cjs", "root"),
|
|
file("app/fallback/index.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./worker.js",
|
|
sourcePath: "app/main.cjs",
|
|
context: "module-specifier",
|
|
files,
|
|
runtimePathKind: "exact-file",
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/worker.js",
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "exported/parser",
|
|
sourcePath: "app/main.cjs",
|
|
context: "module-specifier",
|
|
files,
|
|
runtimePathKind: "exact-file",
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "node_modules/exported/parser.cjs",
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./worker#v1.js",
|
|
sourcePath: "app/main.cjs",
|
|
context: "module-specifier",
|
|
files,
|
|
runtimePathKind: "exact-file",
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/worker#v1.js",
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./fallback",
|
|
sourcePath: "app/main.cjs",
|
|
context: "module-specifier",
|
|
files,
|
|
runtimePathKind: "exact-file",
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "not-found",
|
|
resolved_path: null,
|
|
});
|
|
});
|
|
|
|
it("accepts an exact extensionless file but not a directory index", () => {
|
|
const files = fileMap([
|
|
file("app/main.cjs", "root"),
|
|
file("app/preload", "root"),
|
|
file("app/preload.js", "root"),
|
|
file("app/fallback/index.js", "root"),
|
|
]);
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./preload",
|
|
sourcePath: "app/main.cjs",
|
|
context: "module-specifier",
|
|
files,
|
|
runtimePathKind: "exact-file",
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "resolved",
|
|
resolved_path: "app/preload",
|
|
});
|
|
expect(
|
|
resolve({
|
|
declaredPath: "./fallback",
|
|
sourcePath: "app/main.cjs",
|
|
context: "module-specifier",
|
|
files,
|
|
runtimePathKind: "exact-file",
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: "not-found",
|
|
resolved_path: null,
|
|
});
|
|
});
|
|
});
|
|
|
|
it.each(["#home", "?theme=dark", ""])(
|
|
"does not invent a document base for runtime URL %s",
|
|
(declaredPath) => {
|
|
expect(
|
|
resolve({
|
|
declaredPath,
|
|
sourcePath: "main.cjs",
|
|
context: "url-reference",
|
|
runtimePathKind: "url-resource",
|
|
files: fileMap([file("main.cjs", "root")]),
|
|
}),
|
|
).toMatchObject({
|
|
declared_path: declaredPath,
|
|
resolution_status: "unavailable",
|
|
resolved_path: null,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("resolves Node runtime entrypoints without substituting emitted TypeScript sources", () => {
|
|
const files = fileMap([
|
|
file("main.ts", "root"),
|
|
file("worker.ts", "root"),
|
|
file("worker.js", "other-container"),
|
|
file("runtime.js", "root"),
|
|
file("directory/index.js", "root"),
|
|
]);
|
|
for (const [declaredPath, status, path, candidatePath] of [
|
|
["./worker.js", "not-found", null, "worker.js"],
|
|
["./runtime", "resolved", "runtime.js", undefined],
|
|
["./directory", "resolved", "directory/index.js", undefined],
|
|
] as const)
|
|
expect(
|
|
resolve({
|
|
declaredPath,
|
|
sourcePath: "main.ts",
|
|
context: "module-specifier",
|
|
files,
|
|
runtimePathKind: "node-entrypoint",
|
|
}),
|
|
).toMatchObject({
|
|
resolution_status: status,
|
|
resolved_path: path,
|
|
...(candidatePath === undefined ? {} : { candidate_path: candidatePath }),
|
|
});
|
|
});
|