fix(javascript): retain package entrypoint provenance

This commit is contained in:
morluto committed 2026-10-27 06:28:15 +08:00
1 parent c27560e434
commit 7c8eb63fcc
8 files changed
+228 -26

No files matched your search

@@ -2,23 +2,26 @@ import { expect, it } from "vitest";
import { analyzeArtifactFiles } from "../../../tests/fixtures/javascriptArtifactAnalysis.js";
const packageObservation = async (text: string) => {
const packageObservation = async (text: string | null) => {
const { analysis } = await analyzeArtifactFiles({
files: [
{
path: "app/package.json",
container_sha256: "a".repeat(64),
sha256: "b".repeat(64),
bytes: Buffer.byteLength(text),
bytes: Buffer.byteLength(text ?? ""),
inventory_artifact_id: "package-main-fixture",
kind: "package-json",
unpacked: false,
text: { included: true, value: text },
text:
text === null
? { included: false, reason: "invalid-utf8" }
: { included: true, value: text },
},
],
unavailable_relevant_files: [],
containers: [],
text_bytes_read: Buffer.byteLength(text),
text_bytes_read: Buffer.byteLength(text ?? ""),
invalid_utf8_files: 0,
});
return analysis.packages[0];
@@ -36,7 +39,7 @@ it("retains declared package metadata and normal defaults", async () => {
main: null,
main_status: "default",
main_entrypoint: "",
renderer: null,
renderer_declarations: {},
});
expect(declared).toMatchObject({
status: "included",
@@ -45,10 +48,60 @@ it("retains declared package metadata and normal defaults", async () => {
main: null,
main_status: "invalid",
main_entrypoint: "",
renderer: "source-browser.cjs",
renderer_declarations: { browser: "source-browser.cjs" },
renderer_entrypoint: { field: "browser", path: "source-browser.cjs" },
});
});
it.each([
{
fields: {
renderer: "renderer.js",
browser: "browser.js",
module: "module.js",
},
field: "renderer",
path: "renderer.js",
},
{
fields: { renderer: false, browser: "browser.js", module: "module.js" },
field: "browser",
path: "browser.js",
},
{
fields: { renderer: "", browser: {}, module: "module.js" },
field: "module",
path: "module.js",
},
])(
"keeps simultaneous renderer declarations separate from selected $field provenance",
async ({ fields, field, path }) => {
const observation = await packageObservation(JSON.stringify(fields));
expect(observation).toMatchObject({ renderer_entrypoint: { field, path } });
for (const [key, value] of Object.entries(fields))
expect(observation).toMatchObject({
renderer_declarations: {
[key]: typeof value === "string" ? value : { status: "non-string" },
},
});
},
);
it.each([
["{}", {}],
["{", null],
["null", null],
[null, null],
] as const)(
"keeps absent/invalid/unavailable renderer origins distinct for %s",
async (text, declarations) => {
expect(await packageObservation(text)).toMatchObject({
renderer_declarations: declarations,
renderer_entrypoint: null,
});
},
);
it.each([null, false, 123, {}, []].map((main) => ({ main })))(
"retains non-string main $main evidence separately from Node index fallback",
async ({ main }) => {
@@ -379,6 +379,23 @@ const parsePackage = (
if (document.packageFields === null)
return invalidPackage(file, "package.json root is not an object.");
const value = document.packageFields;
const rendererDeclarations: NonNullable<
JavaScriptPackageObservation["renderer_declarations"]
> = {};
let rendererEntrypoint: JavaScriptPackageObservation["renderer_entrypoint"] =
null;
for (const field of ["renderer", "browser", "module"] as const) {
if (!Object.hasOwn(value, field)) continue;
const declaration = value[field];
rendererDeclarations[field] =
typeof declaration === "string" ? declaration : { status: "non-string" };
if (
rendererEntrypoint === null &&
typeof declaration === "string" &&
declaration !== ""
)
rendererEntrypoint = { field, path: declaration };
}
return {
path: file.path,
sha256: file.sha256,
@@ -393,10 +410,8 @@ const parsePackage = (
? "declared"
: "invalid",
main_entrypoint: typeof value.main === "string" ? value.main : "",
renderer:
optionalString(value.renderer) ??
optionalString(value.browser) ??
optionalString(value.module),
renderer_declarations: rendererDeclarations,
renderer_entrypoint: rendererEntrypoint,
limitation: null,
};
};
@@ -421,7 +436,8 @@ const unavailablePackage = (
main: null,
main_status: "unavailable",
main_entrypoint: null,
renderer: null,
renderer_declarations: null,
renderer_entrypoint: null,
limitation,
});
@@ -20,6 +20,14 @@ interface JavaScriptStructuredObservation {
readonly sha256: string;
}
/** Present source declarations only; non-strings stay bounded and retain source ownership. */
export type JavaScriptPackageRendererDeclarations = Partial<
Record<
"renderer" | "browser" | "module",
string | { readonly status: "non-string" }
>
>;
interface JavaScriptPackageValues {
readonly name: string | null;
readonly version: string | null;
@@ -27,7 +35,12 @@ interface JavaScriptPackageValues {
readonly main_status: "default" | "declared" | "invalid";
/** Node launch declaration; an empty string requests the package index fallback. */
readonly main_entrypoint: string;
readonly renderer: string | null;
readonly renderer_declarations: JavaScriptPackageRendererDeclarations;
/** Selected static renderer candidate, retaining the declaration that supplied it. */
readonly renderer_entrypoint: {
readonly field: "renderer" | "browser" | "module";
readonly path: string;
} | null;
}
/** Parsed package metadata used only for static entrypoint discovery. */
@@ -44,7 +57,8 @@ export type JavaScriptPackageObservation = JavaScriptStructuredObservation &
readonly main: null;
readonly main_status: "unavailable";
readonly main_entrypoint: null;
readonly renderer: null;
readonly renderer_declarations: null;
readonly renderer_entrypoint: null;
readonly limitation: string;
}
);
@@ -53,6 +53,7 @@ interface PackageRoleInput {
readonly packageFile: JavaScriptArtifactFile;
readonly kind: "electron-main" | "electron-renderer";
readonly declaredPath: string | null;
readonly field: "main" | "renderer" | "browser" | "module";
}
/** Create the exact root artifact node from the inventory manifest. */
@@ -552,7 +553,9 @@ export const addJavaScriptPackageNodes = (
version: packageValue.version,
main: packageValue.main,
main_status: packageValue.main_status,
renderer: packageValue.renderer,
// Legacy derived candidate; source fields live in renderer_declarations.
renderer: packageValue.renderer_entrypoint?.path ?? null,
renderer_declarations: packageValue.renderer_declarations,
parse_status: packageValue.status,
},
evidence: artifactObservationEvidence({
@@ -612,13 +615,16 @@ export const addJavaScriptPackageNodes = (
packageFile: file,
kind: "electron-main",
declaredPath: packageValue.main,
field: "main",
});
addPackageRole(context, {
packageNode: node,
packageFile: file,
kind: "electron-renderer",
declaredPath: packageValue.renderer,
});
if (packageValue.renderer_entrypoint !== null)
addPackageRole(context, {
packageNode: node,
packageFile: file,
kind: "electron-renderer",
declaredPath: packageValue.renderer_entrypoint.path,
field: packageValue.renderer_entrypoint.field,
});
}
return roots;
};
@@ -755,16 +761,14 @@ const addPackageRole = (
kind: input.kind,
anchor: input.packageFile,
resolution,
mechanism:
input.kind === "electron-main"
? "package.json:main"
: "package.json:renderer",
mechanism: `package.json:${input.field}`,
});
context.accumulator.addEdge({
source_node_id: input.packageNode.node_id,
target_node_id: role.node_id,
relation: "loads",
properties: {
source_field: input.field,
declared_path: input.declaredPath,
resolution_context: resolution.resolution_context,
resolved_path: resolution.resolved_path,