mirror of
https://github.com/morluto/rea.git
synced 2026-10-28 10:25:42 +00:00
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.
155 lines
4.4 KiB
TypeScript
155 lines
4.4 KiB
TypeScript
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
|
|
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
|
|
|
interface JavaScriptJsonDocumentSource {
|
|
readonly path: string;
|
|
readonly container_sha256: string;
|
|
readonly sha256: string;
|
|
}
|
|
|
|
interface JavaScriptJsonDocumentFactBase {
|
|
readonly source: JavaScriptJsonDocumentSource;
|
|
readonly topLevelKeys: readonly string[];
|
|
}
|
|
|
|
export type JavaScriptJsonDocumentFacts =
|
|
| (JavaScriptJsonDocumentFactBase & {
|
|
readonly status: "included";
|
|
readonly packageFields: Readonly<
|
|
Partial<Record<(typeof PACKAGE_PROPERTIES)[number], unknown>>
|
|
> | null;
|
|
})
|
|
| (JavaScriptJsonDocumentFactBase & {
|
|
readonly status: "invalid";
|
|
readonly packageFields: null;
|
|
readonly syntaxError: SyntaxError;
|
|
})
|
|
| (JavaScriptJsonDocumentFactBase & {
|
|
readonly status: "unavailable";
|
|
readonly packageFields: null;
|
|
readonly reason:
|
|
| "not-applicable"
|
|
| "invalid-utf8"
|
|
| "outside-analysis-scope";
|
|
});
|
|
|
|
interface SourceSnapshot {
|
|
readonly source: JavaScriptJsonDocumentSource;
|
|
readonly kind: JavaScriptArtifactFile["kind"];
|
|
readonly text: JavaScriptArtifactFile["text"];
|
|
}
|
|
|
|
interface CachedFacts {
|
|
readonly snapshot: SourceSnapshot;
|
|
readonly facts: JavaScriptJsonDocumentFacts;
|
|
}
|
|
|
|
const PACKAGE_PROPERTIES = [
|
|
"name",
|
|
"version",
|
|
"main",
|
|
"module",
|
|
"renderer",
|
|
"browser",
|
|
"exports",
|
|
"imports",
|
|
] as const;
|
|
|
|
/**
|
|
* Lazy JSON facts shared by one analysis and its graph projection. Entries follow
|
|
* the admitted source files' lifetime; changed identity, format or text replaces them.
|
|
*/
|
|
export class JavaScriptJsonDocuments {
|
|
readonly #byFile = new Map<JavaScriptArtifactFile, CachedFacts>();
|
|
|
|
read(file: JavaScriptArtifactFile): JavaScriptJsonDocumentFacts {
|
|
const cached = this.#byFile.get(file);
|
|
if (cached !== undefined && sameSnapshot(cached.snapshot, file))
|
|
return cached.facts;
|
|
|
|
const snapshot = sourceSnapshot(file);
|
|
const facts = parseDocument(snapshot);
|
|
this.#byFile.set(file, { snapshot, facts });
|
|
return facts;
|
|
}
|
|
}
|
|
|
|
const sourceSnapshot = (file: JavaScriptArtifactFile): SourceSnapshot => ({
|
|
source: Object.freeze({
|
|
path: file.path,
|
|
container_sha256: file.container_sha256,
|
|
sha256: file.sha256,
|
|
}),
|
|
kind: file.kind,
|
|
text: { ...file.text },
|
|
});
|
|
|
|
const sameSnapshot = (
|
|
left: SourceSnapshot,
|
|
right: JavaScriptArtifactFile,
|
|
): boolean =>
|
|
left.source.path === right.path &&
|
|
left.source.container_sha256 === right.container_sha256 &&
|
|
left.source.sha256 === right.sha256 &&
|
|
left.kind === right.kind &&
|
|
left.text.included === right.text.included &&
|
|
(left.text.included
|
|
? right.text.included && left.text.value === right.text.value
|
|
: !right.text.included && left.text.reason === right.text.reason);
|
|
|
|
const parseDocument = (
|
|
snapshot: SourceSnapshot,
|
|
): JavaScriptJsonDocumentFacts => {
|
|
if (!snapshot.text.included)
|
|
return Object.freeze({
|
|
source: snapshot.source,
|
|
status: "unavailable",
|
|
packageFields: null,
|
|
topLevelKeys: Object.freeze([]),
|
|
reason: snapshot.text.reason,
|
|
});
|
|
|
|
let root: unknown;
|
|
try {
|
|
root = JSON.parse(snapshot.text.value);
|
|
} catch (cause: unknown) {
|
|
if (!(cause instanceof SyntaxError)) throw cause;
|
|
return Object.freeze({
|
|
source: snapshot.source,
|
|
status: "invalid",
|
|
packageFields: null,
|
|
topLevelKeys: Object.freeze([]),
|
|
syntaxError: cause,
|
|
});
|
|
}
|
|
|
|
if (typeof root !== "object" || root === null || Array.isArray(root))
|
|
return Object.freeze({
|
|
source: snapshot.source,
|
|
status: "included",
|
|
packageFields: null,
|
|
topLevelKeys: Object.freeze([]),
|
|
});
|
|
|
|
const record = root as Record<string, unknown>;
|
|
const topLevelKeys = Object.freeze(
|
|
Object.keys(record).sort(compareUnicodeCodePoints),
|
|
);
|
|
// 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",
|
|
packageFields,
|
|
topLevelKeys,
|
|
});
|
|
};
|