mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
fix(javascript): retain package entrypoint provenance
This commit is contained in:
@@ -72,6 +72,15 @@ code-point order. Dependency manifests remain package observations but do not
|
||||
declare Electron main or renderer entries. If only dependency manifests are
|
||||
present, the inventory artifact remains the graph root.
|
||||
|
||||
Package observations keep the legacy `renderer` property as a derived entrypoint
|
||||
candidate: the first nonempty string from `renderer`, `browser`, then `module`.
|
||||
It does not assert that the source declares a literal `renderer` field.
|
||||
`renderer_declarations` records the actual present fields: strings, including
|
||||
empty strings, remain exact; non-string values use `{ "status": "non-string" }`
|
||||
without copying nested browser mappings. An empty record means no such fields
|
||||
were present; `null` means package metadata was invalid or unavailable. Renderer
|
||||
role mechanisms and package load edges name the field that supplied the candidate.
|
||||
|
||||
## Select sources before analysis
|
||||
|
||||
For a question about known entry sources, supply exact artifact-relative file
|
||||
|
||||
@@ -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,
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ it.each(["browser", "module", "renderer", "main"] as const)(
|
||||
kind === "electron-renderer" &&
|
||||
observations.some(
|
||||
({ properties }) =>
|
||||
properties.mechanism === "package.json:renderer" &&
|
||||
properties.mechanism === `package.json:${field}` &&
|
||||
properties.resolved_path === "lib/window.cjs",
|
||||
),
|
||||
),
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
|
||||
import { analyzeJavaScriptApplication } from "../../../support/javascriptApplicationScope.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
||||
|
||||
const packageGraph = async (metadata: Record<string, unknown>) => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-package-renderer-provenance-",
|
||||
);
|
||||
await Promise.all([
|
||||
writeFile(join(root, "package.json"), JSON.stringify(metadata)),
|
||||
writeFile(join(root, "entry.js"), "export const value = 42;"),
|
||||
]);
|
||||
const result = await analyzeJavaScriptApplication({
|
||||
input_path: root,
|
||||
format: "directory",
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
return javascriptApplicationAnalysisResultSchema.parse(
|
||||
result.value.normalized_result,
|
||||
).graph;
|
||||
};
|
||||
|
||||
it.each(["renderer", "browser", "module"] as const)(
|
||||
"attributes the package renderer candidate to its actual %s field",
|
||||
async (field) => {
|
||||
const graph = await packageGraph({ [field]: "entry.js" });
|
||||
const role = graph.nodes.find(({ kind }) => kind === "electron-renderer");
|
||||
expect(role?.observations).toContainEqual(
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
mechanism: `package.json:${field}`,
|
||||
resolved_path: "entry.js",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const metadata = graph.nodes.find(({ kind }) => kind === "package");
|
||||
expect(metadata?.observations).toContainEqual(
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
renderer: "entry.js",
|
||||
renderer_declarations: { [field]: "entry.js" },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(graph.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
source_node_id: metadata?.node_id,
|
||||
target_node_id: role?.node_id,
|
||||
relation: "loads",
|
||||
properties: expect.objectContaining({
|
||||
source_field: field,
|
||||
declared_path: "entry.js",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(metadata?.observations[0]?.properties.renderer_declarations).toEqual(
|
||||
{ [field]: "entry.js" },
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps present browser mappings distinct from absent renderer metadata when module supplies the candidate", async () => {
|
||||
const graph = await packageGraph({
|
||||
browser: { "./server.js": "./client.js" },
|
||||
module: "entry.js",
|
||||
});
|
||||
const metadata = graph.nodes.find(({ kind }) => kind === "package");
|
||||
expect(metadata?.observations[0]?.properties).toMatchObject({
|
||||
renderer: "entry.js",
|
||||
renderer_declarations: {
|
||||
browser: { status: "non-string" },
|
||||
module: "entry.js",
|
||||
},
|
||||
});
|
||||
expect(
|
||||
graph.nodes.find(({ kind }) => kind === "electron-renderer")
|
||||
?.observations[0]?.properties.mechanism,
|
||||
).toBe("package.json:module");
|
||||
});
|
||||
|
||||
it("retains simultaneous string declarations while keeping renderer priority", async () => {
|
||||
const graph = await packageGraph({
|
||||
renderer: "entry.js",
|
||||
browser: "browser.js",
|
||||
module: "module.js",
|
||||
});
|
||||
const metadata = graph.nodes.find(({ kind }) => kind === "package");
|
||||
expect(metadata?.observations[0]?.properties).toMatchObject({
|
||||
renderer: "entry.js",
|
||||
renderer_declarations: {
|
||||
renderer: "entry.js",
|
||||
browser: "browser.js",
|
||||
module: "module.js",
|
||||
},
|
||||
});
|
||||
expect(
|
||||
graph.nodes.find(({ kind }) => kind === "electron-renderer")
|
||||
?.observations[0]?.properties.mechanism,
|
||||
).toBe("package.json:renderer");
|
||||
});
|
||||
@@ -67,7 +67,8 @@ it("uses the first structured observation when paths repeat", async () => {
|
||||
name: null,
|
||||
version: null,
|
||||
main: null,
|
||||
renderer: null,
|
||||
renderer_declarations: null,
|
||||
renderer_entrypoint: null,
|
||||
limitation: "later duplicate",
|
||||
} satisfies JavaScriptPackageObservation,
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user