mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
refactor(javascript): centralize package and JSON document facts
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import type { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
|
||||
import type { ElectronIpcFinding } from "../../domain/javascript/electronStaticAnalysisTypes.js";
|
||||
import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js";
|
||||
@@ -164,6 +165,7 @@ export const summarizeElectronBoundaries = (
|
||||
specifier: finding.module_path,
|
||||
context: finding.module_resolution_context,
|
||||
files,
|
||||
jsonDocuments: analysis.jsonDocuments,
|
||||
directoryPaths,
|
||||
runtimePathKind: "node-entrypoint",
|
||||
}),
|
||||
@@ -175,6 +177,7 @@ export const summarizeElectronBoundaries = (
|
||||
specifier: finding.specifier,
|
||||
context: "module-specifier",
|
||||
files,
|
||||
jsonDocuments: analysis.jsonDocuments,
|
||||
directoryPaths,
|
||||
kind: "native-addon",
|
||||
moduleKind: finding.module_kind,
|
||||
@@ -209,6 +212,7 @@ interface ResolveToFileInput {
|
||||
readonly specifier: string;
|
||||
readonly context: "module-specifier" | "filesystem-expression";
|
||||
readonly files: ReadonlyMap<string, JavaScriptArtifactFile>;
|
||||
readonly jsonDocuments: JavaScriptJsonDocuments;
|
||||
readonly directoryPaths?: ReadonlySet<string> | undefined;
|
||||
readonly kind?: JavaScriptArtifactFile["kind"];
|
||||
readonly moduleKind?: "import" | "require";
|
||||
@@ -221,6 +225,7 @@ const resolvesToFile = (input: ResolveToFileInput): boolean => {
|
||||
sourcePath: input.file.path,
|
||||
context: input.context,
|
||||
files: input.files,
|
||||
jsonDocuments: input.jsonDocuments,
|
||||
...(input.directoryPaths === undefined
|
||||
? {}
|
||||
: { directoryPaths: input.directoryPaths }),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js";
|
||||
import { javascriptSourceRangeKey } from "../../domain/javascript/javascriptSourceCoordinates.js";
|
||||
import type { ElectronNativeAddonBindingFinding } from "../../domain/javascript/electronStaticAnalysisTypes.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import {
|
||||
javascriptAnalysisCoverage,
|
||||
resolveArtifactPath,
|
||||
type JavaScriptArtifactGraphContext,
|
||||
type JavaScriptArtifactGraphCoverage,
|
||||
} from "./JavaScriptArtifactGraphContext.js";
|
||||
@@ -45,13 +45,17 @@ const addNativeBinding = (input: NativeBindingInput): void => {
|
||||
value.module_runtime,
|
||||
);
|
||||
if (source === undefined) return;
|
||||
const resolved = resolveArtifactPath(
|
||||
value.specifier,
|
||||
file.path,
|
||||
context.filesByPath,
|
||||
value.module_kind,
|
||||
context.directoryPaths,
|
||||
);
|
||||
const resolved = resolveArtifactPathByContext({
|
||||
declaredPath: value.specifier,
|
||||
sourcePath: file.path,
|
||||
context: "module-specifier",
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
...(value.module_kind === undefined
|
||||
? {}
|
||||
: { moduleKind: value.module_kind }),
|
||||
}).resolved_path;
|
||||
const addon = resolved === null ? undefined : context.fileNodes.get(resolved);
|
||||
const binding = context.accumulator.addNode({
|
||||
kind: "native-export",
|
||||
|
||||
@@ -155,6 +155,7 @@ const addWindowPreload = (
|
||||
sourcePath: input.file.path,
|
||||
context: resolutionContext,
|
||||
files: input.context.filesByPath,
|
||||
jsonDocuments: input.context.analysis.jsonDocuments,
|
||||
directoryPaths: input.context.directoryPaths,
|
||||
unanalyzedFiles: input.context.unanalyzedFiles,
|
||||
runtimePathKind: "exact-file",
|
||||
@@ -257,6 +258,7 @@ const addUtilityProcess = (
|
||||
sourcePath: file.path,
|
||||
context: value.module_resolution_context,
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
unanalyzedFiles: context.unanalyzedFiles,
|
||||
runtimePathKind: "node-entrypoint",
|
||||
|
||||
@@ -34,6 +34,50 @@ it.each([null, false, 123, {}, []].map((main) => ({ main })))(
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps valid JSON numeric overflow invalid as a package main", () => {
|
||||
expect(packageObservation('{"main":1e400}')).toMatchObject({
|
||||
status: "included",
|
||||
main: null,
|
||||
main_status: "invalid",
|
||||
main_entrypoint: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("treats a valid numeric JSON root as a package metadata shape failure", () => {
|
||||
expect(packageObservation("1e400")).toMatchObject({
|
||||
status: "invalid",
|
||||
main: null,
|
||||
main_status: "unavailable",
|
||||
main_entrypoint: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("retains valid numeric overflow as an ordinary JSON module", () => {
|
||||
const text = "1e400";
|
||||
const analysis = analyzeJavaScriptArtifactFiles({
|
||||
files: [
|
||||
{
|
||||
path: "settings.json",
|
||||
container_sha256: "a".repeat(64),
|
||||
sha256: "b".repeat(64),
|
||||
bytes: Buffer.byteLength(text),
|
||||
inventory_artifact_id: "numeric-json-fixture",
|
||||
kind: "json",
|
||||
unpacked: false,
|
||||
text: { included: true, value: text },
|
||||
},
|
||||
],
|
||||
unavailable_relevant_files: [],
|
||||
containers: [],
|
||||
text_bytes_read: Buffer.byteLength(text),
|
||||
invalid_utf8_files: 0,
|
||||
});
|
||||
|
||||
expect(analysis.json_modules).toMatchObject([
|
||||
{ status: "included", top_level_keys: [] },
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["{}", "default", ""],
|
||||
['{"main":""}', "default", ""],
|
||||
|
||||
@@ -40,6 +40,10 @@ import type {
|
||||
} from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import { javascriptArtifactAnalysisScopes } from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import { analyzeJavaScriptJsonModule } from "./JavaScriptJsonModules.js";
|
||||
import {
|
||||
JavaScriptJsonDocuments,
|
||||
type JavaScriptJsonDocumentFacts,
|
||||
} from "./JavaScriptJsonDocuments.js";
|
||||
import { completeJavaScriptAnalysisSteps } from "./JavaScriptAnalysisControl.js";
|
||||
import { htmlArtifactReferences } from "../../domain/javascript/htmlArtifactReferences.js";
|
||||
import type { JavaScriptSourceAnalysisPort } from "../../domain/javascript/javascriptSourceAnalysis.js";
|
||||
@@ -57,6 +61,7 @@ import {
|
||||
interface MutableArtifactAnalysis<
|
||||
SemanticIr extends JavaScriptModuleSemanticIr,
|
||||
> {
|
||||
readonly jsonDocuments: JavaScriptJsonDocuments;
|
||||
readonly files: AnalyzedJavaScriptArtifactFile<SemanticIr>[];
|
||||
readonly packages: JavaScriptPackageObservation[];
|
||||
readonly jsonModules: JavaScriptJsonModuleObservation[];
|
||||
@@ -359,6 +364,7 @@ const finalizeArtifactAnalysis = <
|
||||
state: MutableArtifactAnalysis<SemanticIr>,
|
||||
): JavaScriptArtifactAnalysis<SemanticIr> => {
|
||||
return {
|
||||
jsonDocuments: state.jsonDocuments,
|
||||
files: state.files,
|
||||
packages: state.packages,
|
||||
json_modules: state.jsonModules,
|
||||
@@ -482,6 +488,7 @@ const addSourceMap = (
|
||||
const emptyArtifactAnalysis = <
|
||||
SemanticIr extends JavaScriptModuleSemanticIr,
|
||||
>(): MutableArtifactAnalysis<SemanticIr> => ({
|
||||
jsonDocuments: new JavaScriptJsonDocuments(),
|
||||
files: [],
|
||||
packages: [],
|
||||
jsonModules: [],
|
||||
@@ -498,27 +505,25 @@ const addStructuredObservations = (
|
||||
file: JavaScriptArtifactFile,
|
||||
state: MutableArtifactAnalysis<JavaScriptModuleSemanticIr>,
|
||||
): void => {
|
||||
if (file.kind === "package-json") state.packages.push(parsePackage(file));
|
||||
if (file.kind === "package-json")
|
||||
state.packages.push(parsePackage(file, state.jsonDocuments.read(file)));
|
||||
if (file.kind !== "json") return;
|
||||
const json = analyzeJavaScriptJsonModule(file);
|
||||
const json = analyzeJavaScriptJsonModule(file, state.jsonDocuments);
|
||||
state.jsonModules.push(json);
|
||||
if (json.status === "invalid") state.parseFailures += 1;
|
||||
};
|
||||
|
||||
const parsePackage = (
|
||||
file: JavaScriptArtifactFile,
|
||||
document: JavaScriptJsonDocumentFacts,
|
||||
): JavaScriptPackageObservation => {
|
||||
if (!file.text.included)
|
||||
if (document.status === "unavailable")
|
||||
return unavailablePackage(file, "Package metadata text was unavailable.");
|
||||
let value: unknown;
|
||||
try {
|
||||
value = JSON.parse(file.text.value);
|
||||
} catch (cause: unknown) {
|
||||
void cause;
|
||||
if (document.status === "invalid")
|
||||
return invalidPackage(file, "package.json is not valid JSON.");
|
||||
}
|
||||
if (!isRecord(value))
|
||||
if (document.packageFields === null)
|
||||
return invalidPackage(file, "package.json root is not an object.");
|
||||
const value = document.packageFields;
|
||||
return {
|
||||
path: file.path,
|
||||
sha256: file.sha256,
|
||||
@@ -780,6 +785,3 @@ const optionalString = (value: unknown): string | null =>
|
||||
|
||||
const sha256 = (value: string): string =>
|
||||
createHash("sha256").update(value).digest("hex");
|
||||
|
||||
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
JavaScriptArtifactFileKind,
|
||||
} from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import type { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
|
||||
interface JavaScriptStructuredObservation {
|
||||
readonly path: string;
|
||||
@@ -200,6 +201,8 @@ export const isSkippedJavaScriptAnalysis = (
|
||||
export interface JavaScriptArtifactAnalysis<
|
||||
SemanticIr extends JavaScriptModuleSemanticIr = JavaScriptSemanticIr,
|
||||
> {
|
||||
/** Internal source-facts owner shared with graph path resolution. */
|
||||
readonly jsonDocuments: JavaScriptJsonDocuments;
|
||||
readonly files: readonly AnalyzedJavaScriptArtifactFile<SemanticIr>[];
|
||||
readonly packages: readonly JavaScriptPackageObservation[];
|
||||
readonly json_modules: readonly JavaScriptJsonModuleObservation[];
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
} from "./JavaScriptArtifactGraphEvidence.js";
|
||||
import {
|
||||
artifactPathResolutionProperties,
|
||||
resolveArtifactPathByContext,
|
||||
type ArtifactPathResolution,
|
||||
} from "./JavaScriptArtifactPathResolution.js";
|
||||
|
||||
@@ -513,23 +512,6 @@ export const chunkLookupKey = (
|
||||
chunkKey: string,
|
||||
): string => compositeKey([path, runtime, chunkKey]);
|
||||
|
||||
/** Resolve a relative static specifier without escaping the artifact root. */
|
||||
export const resolveArtifactPath = (
|
||||
specifier: string,
|
||||
sourcePath: string,
|
||||
files: ReadonlyMap<string, JavaScriptArtifactFile>,
|
||||
moduleKind?: "import" | "require" | "compiler-dependent",
|
||||
directoryPaths?: ReadonlySet<string>,
|
||||
): string | null =>
|
||||
resolveArtifactPathByContext({
|
||||
declaredPath: specifier,
|
||||
sourcePath,
|
||||
context: "module-specifier",
|
||||
files,
|
||||
...(directoryPaths === undefined ? {} : { directoryPaths }),
|
||||
...(moduleKind === undefined ? {} : { moduleKind }),
|
||||
}).resolved_path;
|
||||
|
||||
/** Locate the recovered module that owns a static finding. */
|
||||
export const sourceNodeFor = (
|
||||
context: JavaScriptArtifactGraphContext,
|
||||
|
||||
@@ -58,6 +58,7 @@ export const addJavaScriptHtmlRoles = (
|
||||
sourcePath: html.path,
|
||||
context: "html-reference",
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
unanalyzedFiles: context.unanalyzedFiles,
|
||||
htmlBaseHref: script.base_href,
|
||||
|
||||
@@ -488,6 +488,7 @@ const addDiscoveredRole = (
|
||||
sourcePath: input.file.path,
|
||||
context: input.value.resolution_context,
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
unanalyzedFiles: context.unanalyzedFiles,
|
||||
...(input.value.role === "preload"
|
||||
@@ -556,6 +557,7 @@ const addSourceMapEdge = (
|
||||
sourcePath: input.file.path,
|
||||
context: "url-reference",
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
unanalyzedFiles: context.unanalyzedFiles,
|
||||
});
|
||||
@@ -660,6 +662,7 @@ const resolveReference = (
|
||||
sourcePath,
|
||||
context: "url-reference",
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
unanalyzedFiles: context.unanalyzedFiles,
|
||||
})
|
||||
@@ -668,6 +671,7 @@ const resolveReference = (
|
||||
sourcePath,
|
||||
context: "module-specifier",
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
unanalyzedFiles: context.unanalyzedFiles,
|
||||
...("require" === reference.kind ||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "./JavaScriptArtifactGraphStructure.js";
|
||||
import { addJavaScriptStaticFindingsSteps } from "./JavaScriptArtifactGraphFindings.js";
|
||||
import { applicationContainerIdentity } from "./JavaScriptArtifactGraphContext.js";
|
||||
import { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
|
||||
const SHA256 = "a".repeat(64);
|
||||
const REASON =
|
||||
@@ -51,6 +52,7 @@ const skippedEntry = (
|
||||
const analysisFor = (
|
||||
entries: readonly AnalyzedJavaScriptArtifactFile[],
|
||||
): JavaScriptModuleArtifactAnalysis => ({
|
||||
jsonDocuments: new JavaScriptJsonDocuments(),
|
||||
files: entries,
|
||||
packages: [],
|
||||
json_modules: [],
|
||||
|
||||
@@ -562,6 +562,7 @@ export const addJavaScriptPackageNodes = (
|
||||
context: "package-entrypoint",
|
||||
runtimePathKind: "node-entrypoint",
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
unanalyzedFiles: context.unanalyzedFiles,
|
||||
}),
|
||||
@@ -689,6 +690,7 @@ const addPackageRole = (
|
||||
? { runtimePathKind: "node-entrypoint" as const }
|
||||
: {}),
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
unanalyzedFiles: context.unanalyzedFiles,
|
||||
});
|
||||
|
||||
+4
@@ -1,6 +1,7 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js";
|
||||
import { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
|
||||
const files = new Map(
|
||||
@@ -20,6 +21,7 @@ const files = new Map(
|
||||
],
|
||||
),
|
||||
);
|
||||
const jsonDocuments = new JavaScriptJsonDocuments();
|
||||
|
||||
it.each([
|
||||
"../../rea-artifact/shared/window.cjs",
|
||||
@@ -38,6 +40,7 @@ it.each([
|
||||
context: "module-specifier",
|
||||
moduleKind: "import",
|
||||
files,
|
||||
jsonDocuments,
|
||||
}),
|
||||
).toMatchObject({ resolution_status: "rejected", resolved_path: null });
|
||||
},
|
||||
@@ -61,6 +64,7 @@ it.each([
|
||||
context: "module-specifier",
|
||||
moduleKind: "import",
|
||||
files,
|
||||
jsonDocuments,
|
||||
}),
|
||||
).toMatchObject({ resolution_status: "resolved", resolved_path: target });
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
resolveArtifactPathByContext,
|
||||
type ResolveArtifactPathInput,
|
||||
} from "./JavaScriptArtifactPathResolution.js";
|
||||
import { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import { artifactLocalIdentity } from "./JavaScriptArtifactGraphContext.js";
|
||||
import { applicationNodeIdentitySchema } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
|
||||
@@ -3565,8 +3566,9 @@ describe("module-backed package main host paths", () => {
|
||||
);
|
||||
});
|
||||
|
||||
const resolve = (input: ResolveArtifactPathInput) =>
|
||||
resolveArtifactPathByContext(input);
|
||||
const jsonDocuments = new JavaScriptJsonDocuments();
|
||||
const resolve = (input: Omit<ResolveArtifactPathInput, "jsonDocuments">) =>
|
||||
resolveArtifactPathByContext({ ...input, jsonDocuments });
|
||||
|
||||
const fileMap = (files: readonly JavaScriptArtifactFile[]) =>
|
||||
new Map(files.map((value) => [value.path, value]));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
import { isBuiltin } from "node:module";
|
||||
import { posix, win32 } from "node:path";
|
||||
import { ELECTRON_MODULE } from "../../domain/javascript/javascriptElectronMemberWrites.js";
|
||||
@@ -79,6 +80,7 @@ export type ArtifactPathResolution = ArtifactPathResolutionBase &
|
||||
|
||||
/** Values needed to resolve a declaration without filesystem access. */
|
||||
export interface ResolveArtifactPathInput {
|
||||
readonly jsonDocuments: JavaScriptJsonDocuments;
|
||||
readonly declaredPath: string;
|
||||
readonly sourcePath: string;
|
||||
readonly context: ArtifactPathResolutionContext;
|
||||
@@ -1449,25 +1451,17 @@ const readPackageMetadata = (
|
||||
kind: "unavailable",
|
||||
limitation: `Directory package metadata ${path} was inventoried but its text is unavailable: ${file.text.reason}.`,
|
||||
};
|
||||
let value: unknown;
|
||||
try {
|
||||
value = JSON.parse(file.text.value);
|
||||
} catch (cause: unknown) {
|
||||
void cause;
|
||||
const document = input.jsonDocuments.read(file);
|
||||
if (document.status !== "included" || document.packageFields === null)
|
||||
return {
|
||||
kind: "unavailable",
|
||||
limitation: `Directory package metadata ${path} is not valid package JSON.`,
|
||||
};
|
||||
}
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value))
|
||||
return {
|
||||
kind: "unavailable",
|
||||
limitation: `Directory package metadata ${path} is not valid package JSON.`,
|
||||
};
|
||||
const name = Reflect.get(value, "name");
|
||||
const exports = Reflect.get(value, "exports");
|
||||
const imports = Reflect.get(value, "imports");
|
||||
const main = Reflect.get(value, "main");
|
||||
const value = document.packageFields;
|
||||
const name = value.name;
|
||||
const exports = value.exports;
|
||||
const imports = value.imports;
|
||||
const main = value.main;
|
||||
return {
|
||||
kind: "available",
|
||||
path,
|
||||
@@ -1488,7 +1482,7 @@ const readPackageMetadata = (
|
||||
input.moduleKind !== undefined && typeof main !== "string"
|
||||
? undefined
|
||||
: main,
|
||||
module: Reflect.get(value, "module"),
|
||||
module: value.module,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1565,6 +1559,7 @@ const resolveDirectory = (
|
||||
sourcePath: metadata.path,
|
||||
context: "package-entrypoint",
|
||||
files: input.files,
|
||||
jsonDocuments: input.jsonDocuments,
|
||||
...(input.directoryPaths === undefined
|
||||
? {}
|
||||
: { directoryPaths: input.directoryPaths }),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js";
|
||||
import { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
const paths = [
|
||||
"renderer/index.html",
|
||||
@@ -24,6 +25,7 @@ const files = new Map<string, JavaScriptArtifactFile>(
|
||||
},
|
||||
]),
|
||||
);
|
||||
const jsonDocuments = new JavaScriptJsonDocuments();
|
||||
const resolve = (declaredPath: string, htmlBaseHref: string) =>
|
||||
resolveArtifactPathByContext({
|
||||
declaredPath,
|
||||
@@ -31,6 +33,7 @@ const resolve = (declaredPath: string, htmlBaseHref: string) =>
|
||||
sourcePath: "renderer/index.html",
|
||||
context: "html-reference",
|
||||
files,
|
||||
jsonDocuments,
|
||||
});
|
||||
const documentUrl = "https://artifact.test/renderer/index.html";
|
||||
describe("HTML base href URL components", () => {
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
|
||||
type MutableArtifactFile = {
|
||||
-readonly [Key in keyof JavaScriptArtifactFile]: JavaScriptArtifactFile[Key];
|
||||
};
|
||||
|
||||
const mutableFile = (): MutableArtifactFile => ({
|
||||
path: "package.json",
|
||||
container_sha256: "container-a",
|
||||
sha256: "file-a",
|
||||
bytes: 13,
|
||||
inventory_artifact_id: "inventory-a",
|
||||
kind: "package-json",
|
||||
unpacked: false,
|
||||
text: { included: true, value: '{"main":"a.cjs"}' },
|
||||
});
|
||||
|
||||
it("reparses changed text and availability on a reused file object", () => {
|
||||
const documents = new JavaScriptJsonDocuments();
|
||||
const file = mutableFile();
|
||||
const original = documents.read(file);
|
||||
expect(original).toMatchObject({
|
||||
status: "included",
|
||||
packageFields: { main: "a.cjs" },
|
||||
});
|
||||
|
||||
file.text = { included: true, value: "{" };
|
||||
const invalid = documents.read(file);
|
||||
expect(invalid.status).toBe("invalid");
|
||||
|
||||
file.text = { included: false, reason: "invalid-utf8" };
|
||||
const unavailable = documents.read(file);
|
||||
expect(unavailable).toMatchObject({
|
||||
status: "unavailable",
|
||||
reason: "invalid-utf8",
|
||||
});
|
||||
|
||||
file.text = { included: true, value: '{"main":"b.cjs"}' };
|
||||
const updated = documents.read(file);
|
||||
expect(updated).toMatchObject({
|
||||
status: "included",
|
||||
packageFields: { main: "b.cjs" },
|
||||
});
|
||||
expect(original).toMatchObject({
|
||||
status: "included",
|
||||
source: { path: "package.json" },
|
||||
packageFields: { main: "a.cjs" },
|
||||
});
|
||||
expect(invalid.status).toBe("invalid");
|
||||
expect(unavailable.status).toBe("unavailable");
|
||||
});
|
||||
|
||||
it("rebuilds source provenance when path and content digests change", () => {
|
||||
const documents = new JavaScriptJsonDocuments();
|
||||
const file = mutableFile();
|
||||
const original = documents.read(file);
|
||||
|
||||
file.path = "nested/package.json";
|
||||
const moved = documents.read(file);
|
||||
expect(moved.source.path).toBe("nested/package.json");
|
||||
|
||||
file.container_sha256 = "container-b";
|
||||
const recontained = documents.read(file);
|
||||
expect(recontained.source).toMatchObject({
|
||||
path: "nested/package.json",
|
||||
container_sha256: "container-b",
|
||||
sha256: "file-a",
|
||||
});
|
||||
|
||||
file.sha256 = "file-b";
|
||||
const rehashed = documents.read(file);
|
||||
expect(rehashed.source).toMatchObject({
|
||||
path: "nested/package.json",
|
||||
container_sha256: "container-b",
|
||||
sha256: "file-b",
|
||||
});
|
||||
expect(original.source).toEqual({
|
||||
path: "package.json",
|
||||
container_sha256: "container-a",
|
||||
sha256: "file-a",
|
||||
});
|
||||
expect(original).toMatchObject({
|
||||
status: "included",
|
||||
packageFields: { main: "a.cjs" },
|
||||
});
|
||||
});
|
||||
|
||||
it("retains only syntax and keys for ordinary JSON, with package fields scoped to its format", () => {
|
||||
const documents = new JavaScriptJsonDocuments();
|
||||
const file = mutableFile();
|
||||
file.kind = "json";
|
||||
file.text = {
|
||||
included: true,
|
||||
value: '{"exports":{"nested":["large config"]},"main":"entry.cjs"}',
|
||||
};
|
||||
const ordinary = documents.read(file);
|
||||
expect(ordinary).toMatchObject({
|
||||
status: "included",
|
||||
topLevelKeys: ["exports", "main"],
|
||||
packageFields: null,
|
||||
});
|
||||
file.kind = "package-json";
|
||||
expect(documents.read(file)).toMatchObject({
|
||||
status: "included",
|
||||
packageFields: { main: "entry.cjs", exports: { nested: ["large config"] } },
|
||||
});
|
||||
expect(ordinary.packageFields).toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
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),
|
||||
);
|
||||
const packageFields =
|
||||
snapshot.kind === "package-json"
|
||||
? Object.freeze(
|
||||
Object.fromEntries(
|
||||
PACKAGE_PROPERTIES.flatMap((key) =>
|
||||
Object.hasOwn(record, key)
|
||||
? [[key, Reflect.get(record, key)]]
|
||||
: [],
|
||||
),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
return Object.freeze({
|
||||
source: snapshot.source,
|
||||
status: "included",
|
||||
packageFields,
|
||||
topLevelKeys,
|
||||
});
|
||||
};
|
||||
@@ -1,25 +1,23 @@
|
||||
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
|
||||
import type { JavaScriptJsonModuleObservation } from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import type { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
|
||||
/** Parse one approved JSON module without evaluating JavaScript or resolving imports. */
|
||||
export const analyzeJavaScriptJsonModule = (
|
||||
file: JavaScriptArtifactFile,
|
||||
documents: JavaScriptJsonDocuments,
|
||||
): JavaScriptJsonModuleObservation => {
|
||||
if (!file.text.included)
|
||||
const document = documents.read(file);
|
||||
if (document.status === "unavailable")
|
||||
return {
|
||||
path: file.path,
|
||||
sha256: file.sha256,
|
||||
status: "unavailable",
|
||||
top_level_keys: [],
|
||||
omitted_top_level_keys: null,
|
||||
limitation: `JSON module text was unavailable: ${file.text.reason}.`,
|
||||
limitation: `JSON module text was unavailable: ${document.reason}.`,
|
||||
};
|
||||
let value: unknown;
|
||||
try {
|
||||
value = JSON.parse(file.text.value);
|
||||
} catch (cause: unknown) {
|
||||
void cause;
|
||||
if (document.status === "invalid")
|
||||
return {
|
||||
path: file.path,
|
||||
sha256: file.sha256,
|
||||
@@ -28,17 +26,11 @@ export const analyzeJavaScriptJsonModule = (
|
||||
omitted_top_level_keys: 0,
|
||||
limitation: "JSON module is not valid JSON.",
|
||||
};
|
||||
}
|
||||
const keys =
|
||||
typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
? Object.keys(value).sort(compareUnicodeCodePoints)
|
||||
: [];
|
||||
const retained = keys;
|
||||
return {
|
||||
path: file.path,
|
||||
sha256: file.sha256,
|
||||
status: "included",
|
||||
top_level_keys: retained,
|
||||
top_level_keys: document.topLevelKeys,
|
||||
omitted_top_level_keys: 0,
|
||||
limitation: null,
|
||||
};
|
||||
|
||||
@@ -55,6 +55,7 @@ interface RelationshipInput {
|
||||
type ResolvedModuleTarget = {
|
||||
readonly node: ApplicationNode;
|
||||
readonly limitations: readonly string[];
|
||||
readonly jsonStatus: "included" | "invalid" | "unavailable" | null;
|
||||
readonly resolution?: ArtifactPathResolution;
|
||||
} & (
|
||||
| {
|
||||
@@ -289,6 +290,7 @@ const resolveModuleTarget = (
|
||||
sourcePath: input.file.path,
|
||||
context: "module-specifier",
|
||||
files: input.context.filesByPath,
|
||||
jsonDocuments: input.context.analysis.jsonDocuments,
|
||||
directoryPaths: input.context.directoryPaths,
|
||||
unanalyzedFiles: input.context.unanalyzedFiles,
|
||||
moduleKind: moduleResolutionModeForJavaScriptReference(
|
||||
@@ -304,6 +306,7 @@ const resolveModuleTarget = (
|
||||
node: unresolvedModuleNode(input, specifier, resolution),
|
||||
path: null,
|
||||
file: null,
|
||||
jsonStatus: null,
|
||||
status: resolution.resolution_status,
|
||||
limitations: resolution.limitations,
|
||||
resolution,
|
||||
@@ -321,6 +324,10 @@ const resolveModuleTarget = (
|
||||
node,
|
||||
path,
|
||||
file,
|
||||
jsonStatus:
|
||||
file.kind === "json" || file.kind === "package-json"
|
||||
? input.context.analysis.jsonDocuments.read(file).status
|
||||
: null,
|
||||
status: "resolved",
|
||||
limitations: resolution.limitations,
|
||||
resolution,
|
||||
@@ -404,10 +411,7 @@ const relationshipProperties = (
|
||||
resolution_status: target?.status ?? null,
|
||||
...artifactPathResolutionProperties(target?.resolution),
|
||||
target_file_kind: target?.file?.kind ?? null,
|
||||
target_json_status:
|
||||
target?.file?.kind === "json" || target?.file?.kind === "package-json"
|
||||
? jsonStatus(target.file)
|
||||
: null,
|
||||
target_json_status: target?.jsonStatus ?? null,
|
||||
});
|
||||
|
||||
const relationshipEvidence = (
|
||||
@@ -467,20 +471,6 @@ const moduleFormat = (
|
||||
: "unknown";
|
||||
};
|
||||
|
||||
const jsonStatus = (
|
||||
file: JavaScriptArtifactFile,
|
||||
): "included" | "invalid" | "unavailable" => {
|
||||
if (!file.text.included) return "unavailable";
|
||||
try {
|
||||
JSON.parse(file.text.value);
|
||||
return "included";
|
||||
} catch (cause: unknown) {
|
||||
// Invalid JSON is classified without further detail.
|
||||
void cause;
|
||||
return "invalid";
|
||||
}
|
||||
};
|
||||
|
||||
const isExportLink = (link: JavaScriptSemanticModuleLink): boolean =>
|
||||
link.kind === "export" ||
|
||||
link.kind === "re-export" ||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { buildJavaScriptSemanticGraph } from "./JavaScriptSemanticGraphBuilder.j
|
||||
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js";
|
||||
import { resolveJavaScriptSemanticEvidence } from "../../domain/javascript/javascriptSemanticGraph.js";
|
||||
import { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
|
||||
const SHA256 = "a".repeat(64);
|
||||
const GRAPH_ID = `jag_${"b".repeat(64)}`;
|
||||
@@ -347,6 +348,7 @@ it("uses unavailable evidence when no source produced semantic IR", () => {
|
||||
});
|
||||
|
||||
const emptyAnalysis = (): JavaScriptArtifactAnalysis => ({
|
||||
jsonDocuments: new JavaScriptJsonDocuments(),
|
||||
files: [],
|
||||
packages: [],
|
||||
json_modules: [],
|
||||
|
||||
+167
@@ -20,6 +20,8 @@ import { createJavaScriptArtifactReader } from "../../../../src/artifacts/javasc
|
||||
import { createJavaScriptSourceAnalysis } from "../../../../src/artifacts/javascript/JavaScriptSourceAnalysis.js";
|
||||
import { analyzeAndAdoptJavaScriptArtifactFiles } from "../../../../src/application/javascript/JavaScriptArtifactAnalysis.js";
|
||||
import { createJavaScriptSemanticGraphProjection } from "../../../../src/application/javascript/JavaScriptSemanticGraphBuilder.js";
|
||||
import { buildJavaScriptArtifactGraph } from "../../../../src/application/javascript/JavaScriptArtifactGraphBuilder.js";
|
||||
import { writeFixtureFiles } from "../../../support/javascriptApplicationFixture.js";
|
||||
|
||||
describe("JavaScript source selection cancellation boundary", () => {
|
||||
it("keeps unselected files outside scope when the real worker cancels after the last selected file", async () => {
|
||||
@@ -86,6 +88,171 @@ describe("JavaScript source selection cancellation boundary", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves lazy package and JSON facts without completing their skipped observations", async () => {
|
||||
const root = await createTestTempDirectory("rea-js-lazy-json-facts-");
|
||||
await writeFixtureFiles(root, {
|
||||
"app/first.cjs": 'require("late-pkg"); require("./config.json");\n',
|
||||
"app/second.cjs": "module.exports = 2;\n",
|
||||
"app/config.json": "true",
|
||||
"app/node_modules/late-pkg/package.json": JSON.stringify({
|
||||
main: "index.cjs",
|
||||
}),
|
||||
"app/node_modules/late-pkg/index.cjs": "module.exports = 1;\n",
|
||||
"app/vendor/package.json": JSON.stringify({ main: "vendor.cjs" }),
|
||||
});
|
||||
const snapshot = await scanCanonicalArtifactInventory(root, {});
|
||||
const reader = createJavaScriptArtifactReader(root, "directory");
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "stop before structured files" };
|
||||
const sourcePaths = [
|
||||
"app/first.cjs",
|
||||
"app/second.cjs",
|
||||
"app/node_modules/late-pkg/package.json",
|
||||
"app/node_modules/late-pkg/index.cjs",
|
||||
"app/config.json",
|
||||
];
|
||||
try {
|
||||
const files = await readJavaScriptArtifactFiles(
|
||||
reader,
|
||||
snapshot,
|
||||
controller.signal,
|
||||
sourcePaths,
|
||||
);
|
||||
const worker = await createJavaScriptSourceAnalysis(
|
||||
{ heapMb: 512, timeoutMs: 30_000 },
|
||||
1,
|
||||
);
|
||||
let outcome: Awaited<
|
||||
ReturnType<typeof analyzeAndAdoptJavaScriptArtifactFiles>
|
||||
>;
|
||||
let completedWorkers = 0;
|
||||
const projection = createJavaScriptSemanticGraphProjection();
|
||||
try {
|
||||
const realWorker = worker.analyze.bind(worker);
|
||||
outcome = await analyzeAndAdoptJavaScriptArtifactFiles(
|
||||
files,
|
||||
{
|
||||
analyze: async (...args) => {
|
||||
const result = await realWorker(...args);
|
||||
completedWorkers += 1;
|
||||
if (completedWorkers === 2) controller.abort(reason);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
projection,
|
||||
async () => {},
|
||||
controller.signal,
|
||||
);
|
||||
} finally {
|
||||
await worker.close();
|
||||
}
|
||||
|
||||
expect(completedWorkers).toBe(2);
|
||||
expect(outcome.interruption).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(outcome.interruption?.cause).toBe(reason);
|
||||
const analysisByPath = new Map(
|
||||
outcome.analysis.files.map((entry) => [entry.file.path, entry]),
|
||||
);
|
||||
for (const path of sourcePaths.slice(0, 2))
|
||||
expect(analysisByPath.get(path)?.javascript).toMatchObject({
|
||||
parse_status: "complete",
|
||||
});
|
||||
for (const path of [
|
||||
"app/node_modules/late-pkg/package.json",
|
||||
"app/config.json",
|
||||
])
|
||||
expect(analysisByPath.get(path)?.analysis_failure?.status).toBe(
|
||||
"not-started",
|
||||
);
|
||||
expect(
|
||||
outcome.analysis.packages.some(
|
||||
({ path }) => path === "app/node_modules/late-pkg/package.json",
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
outcome.analysis.json_modules.some(
|
||||
({ path }) => path === "app/config.json",
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
const graph = buildJavaScriptArtifactGraph(
|
||||
snapshot,
|
||||
files,
|
||||
outcome.analysis,
|
||||
);
|
||||
expect(graph.coverage.status).toBe("partial");
|
||||
expect(
|
||||
graph.edges.find(
|
||||
({ relation, properties }) =>
|
||||
relation === "imports" && properties.specifier === "late-pkg",
|
||||
)?.properties,
|
||||
).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "app/node_modules/late-pkg/index.cjs",
|
||||
});
|
||||
expect(
|
||||
graph.edges.find(
|
||||
({ relation, properties }) =>
|
||||
relation === "imports" && properties.specifier === "./config.json",
|
||||
)?.properties,
|
||||
).toMatchObject({
|
||||
target_file_kind: "json",
|
||||
target_json_status: "included",
|
||||
});
|
||||
for (const [path, operation] of [
|
||||
["app/node_modules/late-pkg/package.json", "parse-package-json"],
|
||||
["app/config.json", "parse-json-module"],
|
||||
] as const)
|
||||
expectSkippedStructuredSource(graph, path, operation);
|
||||
const excluded = analysisByPath.get("app/vendor/package.json");
|
||||
expect(excluded?.file.text).toMatchObject({
|
||||
included: false,
|
||||
reason: "outside-analysis-scope",
|
||||
});
|
||||
expect(excluded?.analysis_failure).toBeUndefined();
|
||||
expect(
|
||||
unknownScopeObservation(graph, "app/vendor/package.json"),
|
||||
).toMatchObject({
|
||||
properties: { operation: "select-javascript-sources" },
|
||||
evidence: { confidence: "unknown", state: "unavailable" },
|
||||
});
|
||||
} finally {
|
||||
await reader.close();
|
||||
}
|
||||
});
|
||||
|
||||
const unknownScopeObservation = (
|
||||
graph: ReturnType<typeof buildJavaScriptArtifactGraph>,
|
||||
path: string,
|
||||
) =>
|
||||
graph.nodes
|
||||
.filter(({ kind }) => kind === "unknown")
|
||||
.flatMap(({ observations }) => observations)
|
||||
.find(({ properties }) => properties.path === path);
|
||||
|
||||
const expectSkippedStructuredSource = (
|
||||
graph: ReturnType<typeof buildJavaScriptArtifactGraph>,
|
||||
path: string,
|
||||
operation: string,
|
||||
): void => {
|
||||
expect(
|
||||
graph.nodes.some(
|
||||
({ kind, observations }) =>
|
||||
kind === "package" &&
|
||||
observations.some(({ properties }) => properties.path === path),
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
graph.nodes
|
||||
.filter(({ kind }) => kind === "artifact")
|
||||
.flatMap(({ observations }) => observations)
|
||||
.find(({ properties }) => properties.path === path),
|
||||
).toMatchObject({ properties: { json_parse_status: null } });
|
||||
expect(unknownScopeObservation(graph, path)).toMatchObject({
|
||||
properties: { operation, analysis_status: "not-started" },
|
||||
});
|
||||
};
|
||||
|
||||
describe("Selected JavaScript source failure diagnostics", () => {
|
||||
it("analyzes selected files in first-request order and reports only selected progress", async () => {
|
||||
const root = await createTestTempDirectory("rea-js-selected-priority-");
|
||||
|
||||
+9
-9
@@ -6,7 +6,7 @@ import { promisify } from "node:util";
|
||||
|
||||
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
|
||||
import type { JavaScriptArtifactFile } from "../../../../src/domain/javascript/javascriptArtifactFiles.js";
|
||||
import { resolveArtifactPathByContext } from "../../../../src/application/javascript/JavaScriptArtifactPathResolution.js";
|
||||
import { resolveStandaloneArtifactPath } from "../../../support/javascriptArtifactPathResolution.js";
|
||||
import { analyzeJavaScriptApplication } from "../../../support/javascriptApplicationScope.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
||||
|
||||
@@ -99,7 +99,7 @@ describe("module specifier punctuation", () => {
|
||||
(punctuation) => {
|
||||
const literal = `app/dep.cjs${punctuation}literal.cjs`;
|
||||
const stripped = "app/dep.cjs";
|
||||
const commonJs = resolveArtifactPathByContext({
|
||||
const commonJs = resolveStandaloneArtifactPath({
|
||||
declaredPath: `./dep.cjs${punctuation}literal.cjs`,
|
||||
sourcePath: "app/main.js",
|
||||
context: "module-specifier",
|
||||
@@ -111,7 +111,7 @@ describe("module specifier punctuation", () => {
|
||||
resolved_path: literal,
|
||||
});
|
||||
|
||||
const esm = resolveArtifactPathByContext({
|
||||
const esm = resolveStandaloneArtifactPath({
|
||||
declaredPath: `./dep.cjs${punctuation}literal.cjs`,
|
||||
sourcePath: "app/main.js",
|
||||
context: "module-specifier",
|
||||
@@ -123,7 +123,7 @@ describe("module specifier punctuation", () => {
|
||||
resolved_path: stripped,
|
||||
});
|
||||
|
||||
const noStrippedTarget = resolveArtifactPathByContext({
|
||||
const noStrippedTarget = resolveStandaloneArtifactPath({
|
||||
declaredPath: `./dep.cjs${punctuation}literal.cjs`,
|
||||
sourcePath: "app/main.js",
|
||||
context: "module-specifier",
|
||||
@@ -141,7 +141,7 @@ describe("module specifier punctuation", () => {
|
||||
"keeps HTML URL suffix handling for %s",
|
||||
(punctuation) => {
|
||||
expect(
|
||||
resolveArtifactPathByContext({
|
||||
resolveStandaloneArtifactPath({
|
||||
declaredPath: `./dep.cjs${punctuation}literal.cjs`,
|
||||
sourcePath: "app/main.js",
|
||||
context: "html-reference",
|
||||
@@ -216,7 +216,7 @@ describe("ESM module URL decoding", () => {
|
||||
|
||||
it("preserves CommonJS literal percent names", () => {
|
||||
expect(
|
||||
resolveArtifactPathByContext({
|
||||
resolveStandaloneArtifactPath({
|
||||
declaredPath: "./space%20name.cjs",
|
||||
sourcePath: "app/main.js",
|
||||
context: "module-specifier",
|
||||
@@ -233,7 +233,7 @@ describe("ESM module URL decoding", () => {
|
||||
"rejects malformed or NUL URL %s",
|
||||
(declaredPath) => {
|
||||
expect(
|
||||
resolveArtifactPathByContext({
|
||||
resolveStandaloneArtifactPath({
|
||||
declaredPath,
|
||||
sourcePath: "app/main.js",
|
||||
context: "module-specifier",
|
||||
@@ -365,7 +365,7 @@ describe("package exports URL path rejections", () => {
|
||||
},
|
||||
});
|
||||
expect(
|
||||
resolveArtifactPathByContext({
|
||||
resolveStandaloneArtifactPath({
|
||||
declaredPath: "fixture",
|
||||
sourcePath: "app/main.js",
|
||||
context: "module-specifier",
|
||||
@@ -399,7 +399,7 @@ describe("package exports URL path rejections", () => {
|
||||
container_sha256: "c".repeat(64),
|
||||
});
|
||||
const resolveTarget = () =>
|
||||
resolveArtifactPathByContext({
|
||||
resolveStandaloneArtifactPath({
|
||||
declaredPath: "fixture",
|
||||
sourcePath: "app/main.js",
|
||||
context: "module-specifier",
|
||||
|
||||
+1
@@ -117,6 +117,7 @@ it.each(
|
||||
context: "module-specifier",
|
||||
moduleKind: "require",
|
||||
files: new Map(files.files.map((file) => [file.path, file])),
|
||||
jsonDocuments: analysis.jsonDocuments,
|
||||
}),
|
||||
).toMatchObject({
|
||||
resolution_status: valid ? "resolved" : "unavailable",
|
||||
|
||||
@@ -4,12 +4,10 @@ import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
resolveArtifactPathByContext,
|
||||
type ArtifactPathResolution,
|
||||
} from "../../../src/application/javascript/JavaScriptArtifactPathResolution.js";
|
||||
import type { ArtifactPathResolution } from "../../../src/application/javascript/JavaScriptArtifactPathResolution.js";
|
||||
import type { JavaScriptArtifactFile } from "../../../src/domain/javascript/javascriptArtifactFiles.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { resolveStandaloneArtifactPath } from "../../support/javascriptArtifactPathResolution.js";
|
||||
const execute = promisify(execFile);
|
||||
const TARGETS = [
|
||||
"default.cjs",
|
||||
@@ -138,7 +136,7 @@ const compareWithNode = async (
|
||||
node: node.target,
|
||||
node_error: node.error_code,
|
||||
files,
|
||||
outcome: resolveArtifactPathByContext({
|
||||
outcome: resolveStandaloneArtifactPath({
|
||||
declaredPath: specifier,
|
||||
sourcePath,
|
||||
context: "module-specifier",
|
||||
@@ -338,7 +336,7 @@ describe("package exports before sibling files", () => {
|
||||
});
|
||||
// The Node control has one runtime directory. A different inventoried
|
||||
// container without its loader mapping cannot prove the metadata absent.
|
||||
const outcome = resolveArtifactPathByContext({
|
||||
const outcome = resolveStandaloneArtifactPath({
|
||||
declaredPath: "example",
|
||||
sourcePath: "main.js",
|
||||
context: "module-specifier",
|
||||
@@ -618,7 +616,7 @@ describe("package self-reference scope boundaries", () => {
|
||||
resolved_path: "node_modules/example/specific.cjs",
|
||||
resolution_status: "resolved",
|
||||
});
|
||||
const outcome = resolveArtifactPathByContext({
|
||||
const outcome = resolveStandaloneArtifactPath({
|
||||
declaredPath: "example",
|
||||
sourcePath: "node_modules/example/main.js",
|
||||
context: "module-specifier",
|
||||
|
||||
@@ -7,13 +7,11 @@ import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
resolveArtifactPathByContext,
|
||||
type ArtifactPathResolution,
|
||||
} from "../../../src/application/javascript/JavaScriptArtifactPathResolution.js";
|
||||
import type { ArtifactPathResolution } from "../../../src/application/javascript/JavaScriptArtifactPathResolution.js";
|
||||
import type { JavaScriptArtifactFile } from "../../../src/domain/javascript/javascriptArtifactFiles.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { writeFixtureFiles } from "../../support/workspace/writeFixtureFiles.js";
|
||||
import { resolveStandaloneArtifactPath } from "../../support/javascriptArtifactPathResolution.js";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
const nodeOutcomeSchema = z.strictObject({
|
||||
@@ -101,7 +99,7 @@ const resolveAlias = (
|
||||
sourcePath: string,
|
||||
moduleKind: "import" | "require",
|
||||
): ArtifactPathResolution =>
|
||||
resolveArtifactPathByContext({
|
||||
resolveStandaloneArtifactPath({
|
||||
declaredPath,
|
||||
sourcePath,
|
||||
context: "module-specifier",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
|
||||
import { join } from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { resolveArtifactPathByContext } from "../../../src/application/javascript/JavaScriptArtifactPathResolution.js";
|
||||
import { resolveStandaloneArtifactPath } from "../../support/javascriptArtifactPathResolution.js";
|
||||
import type { JavaScriptArtifactFile } from "../../../src/domain/javascript/javascriptArtifactFiles.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { writeFixtureFiles } from "../../support/workspace/writeFixtureFiles.js";
|
||||
@@ -55,7 +55,7 @@ it.each(["absolute-host", "relative-package"] as const)(
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
resolveArtifactPathByContext({
|
||||
resolveStandaloneArtifactPath({
|
||||
declaredPath: "fixture",
|
||||
sourcePath: "app/main.cjs",
|
||||
context: "module-specifier",
|
||||
|
||||
+2
@@ -1,3 +1,4 @@
|
||||
import { JavaScriptJsonDocuments } from "../../src/application/javascript/JavaScriptJsonDocuments.js";
|
||||
import { buildJavaScriptSemanticGraph } from "../../src/application/javascript/JavaScriptSemanticGraphBuilder.js";
|
||||
import type { JavaScriptArtifactAnalysis } from "../../src/application/javascript/JavaScriptArtifactAnalysisTypes.js";
|
||||
import type { JavaScriptArtifactFile } from "../../src/domain/javascript/javascriptArtifactFiles.js";
|
||||
@@ -22,6 +23,7 @@ export const graphForJavaScript = (
|
||||
text: { included: true, value: source },
|
||||
};
|
||||
const analysis: JavaScriptArtifactAnalysis = {
|
||||
jsonDocuments: new JavaScriptJsonDocuments(),
|
||||
files: [{ file, javascript: null, semantic: { ir } }],
|
||||
packages: [],
|
||||
json_modules: [],
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { expect, it, onTestFinished, vi } from "vitest";
|
||||
|
||||
import { createJavaScriptArtifactReader } from "../../src/artifacts/javascript/JavaScriptArtifactReader.js";
|
||||
import { analyzeJavaScriptArtifactFiles } from "../../src/application/javascript/JavaScriptArtifactAnalysis.js";
|
||||
import { buildJavaScriptArtifactGraph } from "../../src/application/javascript/JavaScriptArtifactGraphBuilder.js";
|
||||
import type {
|
||||
JavaScriptArtifactFile,
|
||||
JavaScriptArtifactFileSet,
|
||||
} from "../../src/domain/javascript/javascriptArtifactFiles.js";
|
||||
import { scanCanonicalArtifactInventory } from "../fixtures/artifactInventory.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
import { writeFixtureFiles } from "../support/javascriptApplicationFixture.js";
|
||||
import { readJavaScriptArtifactFiles } from "../support/javascriptApplicationScope.js";
|
||||
|
||||
const packageJsonText = '{"name":"fault-package"}';
|
||||
const jsonText = '{"kind":"fault-json"}';
|
||||
|
||||
const artifactFile = (
|
||||
path: string,
|
||||
kind: "package-json" | "json",
|
||||
text: string,
|
||||
): JavaScriptArtifactFile => ({
|
||||
path,
|
||||
container_sha256: "container-digest",
|
||||
sha256: `digest-${path}`,
|
||||
bytes: Buffer.byteLength(text),
|
||||
inventory_artifact_id: `artifact-${path}`,
|
||||
kind,
|
||||
unpacked: false,
|
||||
text: { included: true, value: text },
|
||||
});
|
||||
|
||||
const fileSet = (): JavaScriptArtifactFileSet => ({
|
||||
files: [
|
||||
artifactFile("package.json", "package-json", packageJsonText),
|
||||
artifactFile("settings.json", "json", jsonText),
|
||||
],
|
||||
unavailable_relevant_files: [],
|
||||
containers: [],
|
||||
text_bytes_read:
|
||||
Buffer.byteLength(packageJsonText) + Buffer.byteLength(jsonText),
|
||||
invalid_utf8_files: 0,
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
document: "package.json",
|
||||
text: packageJsonText,
|
||||
create: () => new TypeError("parser fault"),
|
||||
},
|
||||
{
|
||||
document: "package.json",
|
||||
text: packageJsonText,
|
||||
create: () => new Error("parser fault"),
|
||||
},
|
||||
{
|
||||
document: "ordinary JSON",
|
||||
text: jsonText,
|
||||
create: () => new TypeError("parser fault"),
|
||||
},
|
||||
{
|
||||
document: "ordinary JSON",
|
||||
text: jsonText,
|
||||
create: () => new Error("parser fault"),
|
||||
},
|
||||
])(
|
||||
"propagates unexpected JSON.parse faults from $document",
|
||||
({ text, create }) => {
|
||||
const files = fileSet();
|
||||
const originalParse = JSON.parse;
|
||||
const thrown = create();
|
||||
vi.spyOn(JSON, "parse").mockImplementation((input) => {
|
||||
if (input === text) throw thrown;
|
||||
return originalParse(input);
|
||||
});
|
||||
onTestFinished(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
analyzeJavaScriptArtifactFiles(files);
|
||||
} catch (cause: unknown) {
|
||||
caught = cause;
|
||||
}
|
||||
expect(caught).toBe(thrown);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps malformed JSON as an invalid observation", () => {
|
||||
const malformedPackage = artifactFile("package.json", "package-json", "{");
|
||||
const malformedJson = artifactFile("settings.json", "json", "{");
|
||||
const analysis = analyzeJavaScriptArtifactFiles({
|
||||
...fileSet(),
|
||||
files: [malformedPackage, malformedJson],
|
||||
});
|
||||
|
||||
expect(analysis.packages).toMatchObject([{ status: "invalid" }]);
|
||||
expect(analysis.json_modules).toMatchObject([{ status: "invalid" }]);
|
||||
});
|
||||
|
||||
it("reuses parsed package and JSON facts while building the public graph", async () => {
|
||||
const root = await createTestTempDirectory("rea-json-document-graph-");
|
||||
await writeFixtureFiles(root, {
|
||||
"main.cjs": 'require("./dep"); require("./config.json");',
|
||||
"dep/package.json": '{"main":"index.cjs"}',
|
||||
"dep/index.cjs": "module.exports = 1;",
|
||||
"config.json": '{"enabled":true}',
|
||||
});
|
||||
const snapshot = await scanCanonicalArtifactInventory(root, {});
|
||||
const reader = createJavaScriptArtifactReader(root, "directory");
|
||||
try {
|
||||
const files = await readJavaScriptArtifactFiles(reader, snapshot);
|
||||
const analysis = analyzeJavaScriptArtifactFiles(files);
|
||||
const depPackageText = '{"main":"index.cjs"}';
|
||||
const configText = '{"enabled":true}';
|
||||
const originalParse = JSON.parse;
|
||||
vi.spyOn(JSON, "parse").mockImplementation((text) => {
|
||||
if (text === depPackageText || text === configText)
|
||||
throw new TypeError("unexpected JSON reparse");
|
||||
return originalParse(text);
|
||||
});
|
||||
|
||||
const graph = buildJavaScriptArtifactGraph(snapshot, files, analysis);
|
||||
expect(
|
||||
graph.edges.find(
|
||||
({ relation, properties }) =>
|
||||
relation === "imports" && properties.specifier === "./dep",
|
||||
)?.properties,
|
||||
).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "dep/index.cjs",
|
||||
});
|
||||
expect(
|
||||
graph.edges.find(
|
||||
({ relation, properties }) =>
|
||||
relation === "imports" &&
|
||||
properties.specifier === "./config.json" &&
|
||||
Object.hasOwn(properties, "target_json_status"),
|
||||
)?.properties,
|
||||
).toMatchObject({
|
||||
target_file_kind: "json",
|
||||
target_json_status: "included",
|
||||
});
|
||||
} finally {
|
||||
vi.restoreAllMocks();
|
||||
await reader.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { JavaScriptJsonDocuments } from "../../src/application/javascript/JavaScriptJsonDocuments.js";
|
||||
import {
|
||||
resolveArtifactPathByContext,
|
||||
type ResolveArtifactPathInput,
|
||||
} from "../../src/application/javascript/JavaScriptArtifactPathResolution.js";
|
||||
|
||||
/** Resolve one standalone test request with its own JSON facts owner. */
|
||||
export const resolveStandaloneArtifactPath = (
|
||||
input: Omit<ResolveArtifactPathInput, "jsonDocuments">,
|
||||
) =>
|
||||
resolveArtifactPathByContext({
|
||||
...input,
|
||||
jsonDocuments: new JavaScriptJsonDocuments(),
|
||||
});
|
||||
Reference in New Issue
Block a user