fix(javascript): isolate package facts from host prototypes
Build frozen package field snapshots with a null prototype and restrict reference-source main lookup to own properties. Preserve declared null and empty values, index fallback, source identity and unavailable manifest evidence. Compare inherited main and exports lookup with the native Node resolver, retain package observation coverage and verify CLI/MCP package workflows.
This commit is contained in:
1 parent
2bcd7e6d13
commit
610966b836
5 files changed
+141
-33
No files matched your search
@@ -155,9 +155,12 @@ it("retains Kotlin source identity and language across the inventory projection"
|
||||
expect(parsed.relationships).toEqual([]);
|
||||
});
|
||||
|
||||
it("resolves runtime require package mains without an index and before typed indexes", () => {
|
||||
it("resolves declared package mains and index fallback from source-owned fields", () => {
|
||||
const files = [
|
||||
["main.cjs", 'require("./pkg");\nrequire("./dep");\n'],
|
||||
[
|
||||
"main.cjs",
|
||||
'require("./pkg");\nrequire("./dep");\nrequire("./fallback");\n',
|
||||
],
|
||||
[
|
||||
"main.ts",
|
||||
'import dep = require("./typed");\n' +
|
||||
@@ -167,6 +170,9 @@ it("resolves runtime require package mains without an index and before typed ind
|
||||
],
|
||||
["pkg/package.json", '{"main":"actual.js"}\n'],
|
||||
["pkg/actual.js", 'module.exports = "package main";\n'],
|
||||
["fallback/package.json", "{}\n"],
|
||||
["fallback/index.js", 'module.exports = "index";\n'],
|
||||
["fallback/phantom.js", 'module.exports = "host decoy";\n'],
|
||||
["dep.js", 'module.exports = "file";\n'],
|
||||
["dep/index.ts", 'export const value = "index";\n'],
|
||||
["typed.js", 'module.exports = "runtime file";\n'],
|
||||
@@ -192,12 +198,32 @@ it("resolves runtime require package mains without an index and before typed ind
|
||||
limitations: [],
|
||||
};
|
||||
|
||||
const parsed = parseReferenceSourceEntries(
|
||||
read,
|
||||
new Set(files.map(([path]) => path)),
|
||||
[],
|
||||
);
|
||||
const previous = Object.getOwnPropertyDescriptor(Object.prototype, "main");
|
||||
let parsed: ReturnType<typeof parseReferenceSourceEntries>;
|
||||
try {
|
||||
Object.defineProperty(Object.prototype, "main", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: "phantom.js",
|
||||
});
|
||||
parsed = parseReferenceSourceEntries(
|
||||
read,
|
||||
new Set(files.map(([path]) => path)),
|
||||
[],
|
||||
);
|
||||
} finally {
|
||||
if (previous === undefined)
|
||||
Reflect.deleteProperty(Object.prototype, "main");
|
||||
else Object.defineProperty(Object.prototype, "main", previous);
|
||||
}
|
||||
|
||||
expect(parsed.relationships).toContainEqual({
|
||||
from_path: "main.cjs",
|
||||
to: "fallback/index.js",
|
||||
kind: "requires",
|
||||
resolution: "internal",
|
||||
parse_state: "parsed",
|
||||
});
|
||||
expect(parsed.relationships).toContainEqual({
|
||||
from_path: "main.cjs",
|
||||
to: "pkg/actual.js",
|
||||
|
||||
@@ -136,7 +136,9 @@ const readPackageMains = (
|
||||
new TextDecoder("utf-8", { fatal: true }).decode(entry.bytes),
|
||||
);
|
||||
const main =
|
||||
typeof manifest === "object" && manifest !== null
|
||||
typeof manifest === "object" &&
|
||||
manifest !== null &&
|
||||
Object.hasOwn(manifest, "main")
|
||||
? Reflect.get(manifest, "main")
|
||||
: undefined;
|
||||
mains.set(
|
||||
|
||||
@@ -22,6 +22,62 @@ const packageObservation = (text: string) =>
|
||||
invalid_utf8_files: 0,
|
||||
}).packages[0];
|
||||
|
||||
it("retains only declared package metadata despite inherited host fields", () => {
|
||||
const inherited = {
|
||||
name: "host-name",
|
||||
version: "host-version",
|
||||
main: "host-entry.cjs",
|
||||
renderer: "host-renderer.cjs",
|
||||
browser: "host-browser.cjs",
|
||||
module: "host-module.mjs",
|
||||
};
|
||||
const previous = Object.fromEntries(
|
||||
Object.keys(inherited).map((key) => [
|
||||
key,
|
||||
Object.getOwnPropertyDescriptor(Object.prototype, key),
|
||||
]),
|
||||
);
|
||||
let absent: ReturnType<typeof packageObservation>;
|
||||
let declared: ReturnType<typeof packageObservation>;
|
||||
try {
|
||||
for (const [key, value] of Object.entries(inherited))
|
||||
Object.defineProperty(Object.prototype, key, {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value,
|
||||
});
|
||||
absent = packageObservation("{}");
|
||||
declared = packageObservation(
|
||||
'{"name":"source-name","version":"1.0.0","main":null,"browser":"source-browser.cjs"}',
|
||||
);
|
||||
} finally {
|
||||
for (const key of Object.keys(inherited)) {
|
||||
const descriptor = previous[key];
|
||||
if (descriptor === undefined)
|
||||
Reflect.deleteProperty(Object.prototype, key);
|
||||
else Object.defineProperty(Object.prototype, key, descriptor);
|
||||
}
|
||||
}
|
||||
expect(absent).toMatchObject({
|
||||
status: "included",
|
||||
name: null,
|
||||
version: null,
|
||||
main: null,
|
||||
main_status: "default",
|
||||
main_entrypoint: "",
|
||||
renderer: null,
|
||||
});
|
||||
expect(declared).toMatchObject({
|
||||
status: "included",
|
||||
name: "source-name",
|
||||
version: "1.0.0",
|
||||
main: null,
|
||||
main_status: "invalid",
|
||||
main_entrypoint: "",
|
||||
renderer: "source-browser.cjs",
|
||||
});
|
||||
});
|
||||
|
||||
it.each([null, false, 123, {}, []].map((main) => ({ main })))(
|
||||
"retains non-string main $main evidence separately from Node index fallback",
|
||||
({ main }) => {
|
||||
|
||||
@@ -135,18 +135,16 @@ const parseDocument = (
|
||||
const topLevelKeys = Object.freeze(
|
||||
Object.keys(record).sort(compareUnicodeCodePoints),
|
||||
);
|
||||
const packageFields =
|
||||
snapshot.kind === "package-json"
|
||||
? Object.freeze(
|
||||
Object.fromEntries(
|
||||
PACKAGE_PROPERTIES.flatMap((key) =>
|
||||
Object.hasOwn(record, key)
|
||||
? [[key, Reflect.get(record, key)]]
|
||||
: [],
|
||||
),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
// Absent source fields must stay absent when consumers read named properties.
|
||||
const packageFields: Partial<
|
||||
Record<(typeof PACKAGE_PROPERTIES)[number], unknown>
|
||||
> | null = snapshot.kind === "package-json" ? Object.create(null) : null;
|
||||
if (packageFields !== null) {
|
||||
for (const key of PACKAGE_PROPERTIES)
|
||||
if (Object.hasOwn(record, key))
|
||||
packageFields[key] = Reflect.get(record, key);
|
||||
Object.freeze(packageFields);
|
||||
}
|
||||
return Object.freeze({
|
||||
source: snapshot.source,
|
||||
status: "included",
|
||||
|
||||
@@ -7,16 +7,22 @@ import type { JavaScriptArtifactFile } from "../../../src/domain/javascript/java
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { writeFixtureFiles } from "../../support/workspace/writeFixtureFiles.js";
|
||||
|
||||
it.each(["absolute-host", "relative-package"] as const)(
|
||||
"compares %s package main with the native Node resolver",
|
||||
it.each([
|
||||
"absolute-host",
|
||||
"relative-package",
|
||||
"inherited-main",
|
||||
"inherited-exports",
|
||||
] as const)(
|
||||
"compares %s package selection with the native Node resolver",
|
||||
async (kind) => {
|
||||
const root = await createTestTempDirectory("rea-package-main-host-");
|
||||
const artifact = join(root, "artifact");
|
||||
const host = join(root, "host/window.cjs");
|
||||
const main = kind === "absolute-host" ? host : "entry.cjs";
|
||||
const metadata = kind.startsWith("inherited-") ? {} : { main };
|
||||
const contents = {
|
||||
"app/main.cjs": "throw new Error('Analyzed input must never execute');",
|
||||
"app/node_modules/fixture/package.json": JSON.stringify({ main }),
|
||||
"app/node_modules/fixture/package.json": JSON.stringify(metadata),
|
||||
"app/node_modules/fixture/entry.cjs":
|
||||
"throw new Error('Package input must never execute');",
|
||||
"app/node_modules/fixture/index.js":
|
||||
@@ -34,11 +40,6 @@ it.each(["absolute-host", "relative-package"] as const)(
|
||||
),
|
||||
});
|
||||
const loader = createRequire(join(artifact, "app/main.cjs"));
|
||||
expect(loader.resolve("fixture")).toBe(
|
||||
kind === "absolute-host"
|
||||
? host
|
||||
: join(artifact, "app/node_modules/fixture/entry.cjs"),
|
||||
);
|
||||
const files = new Map<string, JavaScriptArtifactFile>(
|
||||
Object.entries(contents).map(([path, value]) => [
|
||||
path,
|
||||
@@ -54,18 +55,43 @@ it.each(["absolute-host", "relative-package"] as const)(
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
resolveStandaloneArtifactPath({
|
||||
const property = kind === "inherited-exports" ? "exports" : "main";
|
||||
const previous = Object.getOwnPropertyDescriptor(
|
||||
Object.prototype,
|
||||
property,
|
||||
);
|
||||
let nativePath: string;
|
||||
let resolution: ReturnType<typeof resolveStandaloneArtifactPath>;
|
||||
try {
|
||||
if (kind.startsWith("inherited-"))
|
||||
Object.defineProperty(Object.prototype, property, {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: property === "main" ? host : "./entry.cjs",
|
||||
});
|
||||
nativePath = loader.resolve("fixture");
|
||||
resolution = resolveStandaloneArtifactPath({
|
||||
declaredPath: "fixture",
|
||||
sourcePath: "app/main.cjs",
|
||||
context: "module-specifier",
|
||||
moduleKind: "require",
|
||||
files,
|
||||
}),
|
||||
).toMatchObject({
|
||||
});
|
||||
} finally {
|
||||
if (previous === undefined)
|
||||
Reflect.deleteProperty(Object.prototype, property);
|
||||
else Object.defineProperty(Object.prototype, property, previous);
|
||||
}
|
||||
const target = kind.startsWith("inherited-") ? "index.js" : "entry.cjs";
|
||||
expect(nativePath).toBe(
|
||||
kind === "absolute-host"
|
||||
? host
|
||||
: join(artifact, "app/node_modules/fixture", target),
|
||||
);
|
||||
expect(resolution).toMatchObject({
|
||||
resolution_status: kind === "absolute-host" ? "unavailable" : "resolved",
|
||||
resolved_path:
|
||||
kind === "absolute-host" ? null : "app/node_modules/fixture/entry.cjs",
|
||||
kind === "absolute-host" ? null : `app/node_modules/fixture/${target}`,
|
||||
});
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user