refactor(browser): separate bundle state from AST inspection

This commit is contained in:
morluto
2026-10-22 09:33:58 +08:00
parent c465ff5a81
commit 00819258a7
13 changed files with 108 additions and 71 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ import {
type WebBundleAnalysis,
} from "../domain/webBundleAnalysis.js";
import { buildCapturedWebBundleAnalysis } from "../domain/webBundleAnalyzerResult.js";
import { isIncludedScript } from "../domain/webBundleAnalyzerInspection.js";
import { isIncludedScript } from "../domain/webBundleAnalyzerState.js";
import type { WebBundleScriptAnalysis } from "../domain/webBundleScriptAnalysis.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import {
+1 -1
View File
@@ -3,7 +3,7 @@ import { access } from "node:fs/promises";
import { expect, it } from "vitest";
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
import type { IncludedScript } from "../domain/webBundleAnalyzerInspection.js";
import type { IncludedScript } from "../domain/webBundleAnalyzerState.js";
import {
AnalysisCancelledError,
AnalysisOutputError,
+1 -1
View File
@@ -31,7 +31,7 @@ import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { jsonParts, bufferedJsonParts } from "../domain/jsonSerialization.js";
import { javaScriptRuntime } from "../domain/runtimeVersion.js";
import { err, ok, type Result } from "../domain/result.js";
import type { IncludedScript } from "../domain/webBundleAnalyzerInspection.js";
import type { IncludedScript } from "../domain/webBundleAnalyzerState.js";
import {
webBundleScriptAnalysisSchema,
type WebBundleScriptAnalysis,
@@ -18,7 +18,7 @@ vi.mock("node:v8", async (importOriginal) => {
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
import type { IncludedScript } from "../domain/webBundleAnalyzerInspection.js";
import type { IncludedScript } from "../domain/webBundleAnalyzerState.js";
import { WebBundleAnalysisWorker } from "./WebBundleAnalysisWorker.js";
it("refuses before spawning or reading source when no host result capacity remains", async () => {
@@ -47,7 +47,7 @@ vi.mock("node:fs/promises", async (importOriginal) => {
});
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
import type { IncludedScript } from "../domain/webBundleAnalyzerInspection.js";
import type { IncludedScript } from "../domain/webBundleAnalyzerState.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { cleanupOwnedProcessGroup } from "../process/ProcessOwnership.js";
import {
@@ -8,7 +8,7 @@ import { createInterface } from "node:readline";
import { expect, it } from "vitest";
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
import type { IncludedScript } from "../domain/webBundleAnalyzerInspection.js";
import type { IncludedScript } from "../domain/webBundleAnalyzerState.js";
import {
webBundleWorkerMessageSchema,
type WebBundleWorkerMessage,
+1 -1
View File
@@ -7,7 +7,7 @@ import { withArtifactFileHandle } from "../artifacts/ArtifactFileOperation.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { jsonParts } from "../domain/jsonSerialization.js";
import type { IncludedScript } from "../domain/webBundleAnalyzerInspection.js";
import type { IncludedScript } from "../domain/webBundleAnalyzerState.js";
import {
webBundleScriptAnalysisSchema,
type WebBundleScriptAnalysis,
+1 -1
View File
@@ -8,7 +8,7 @@ import {
} from "./webBundleAnalyzer.js";
import { buildCapturedWebBundleAnalysis } from "./webBundleAnalyzerResult.js";
import { webBundleAnalysisSchema } from "./webBundleAnalysis.js";
import { isIncludedScript } from "./webBundleAnalyzerInspection.js";
import { isIncludedScript } from "./webBundleAnalyzerState.js";
import { webBundleScriptAnalysisSchema } from "./webBundleScriptAnalysis.js";
import { createWebTextArtifact } from "./webContentArtifact.js";
+4 -4
View File
@@ -1,10 +1,10 @@
import type { WebPageInspection } from "./browserObservationSchemas.js";
import { analyzeScript } from "./webBundleAnalyzerInspection.js";
import {
analyzeScript,
emptyAccumulator,
createWebBundleAnalysisAccumulator,
isIncludedScript,
type IncludedScript,
} from "./webBundleAnalyzerInspection.js";
} from "./webBundleAnalyzerState.js";
import { buildCapturedWebBundleAnalysis } from "./webBundleAnalyzerResult.js";
import type { WebBundleAnalysis } from "./webBundleAnalysis.js";
import {
@@ -16,7 +16,7 @@ import {
export const analyzeCapturedWebScript = (
script: IncludedScript,
): WebBundleScriptAnalysis => {
const accumulator = emptyAccumulator();
const accumulator = createWebBundleAnalysisAccumulator();
analyzeScript(script, accumulator);
const failed = accumulator.failedScripts.length > 0;
const recovered = accumulator.recoveredScripts.length > 0;
+4 -39
View File
@@ -1,8 +1,11 @@
import type {
AnalysisAccumulator,
IncludedScript,
} from "./webBundleAnalyzerState.js";
import * as t from "@babel/types";
import { sanitizeEndpointCandidate } from "./browserObservation.js";
import { compositeKey } from "./unicodeCodePointOrder.js";
import type { WebPageInspection } from "./browserObservationSchemas.js";
import type { WebBundleAnalysis } from "./webBundleAnalysis.js";
import { traverseJavaScriptAst } from "./javascript/javascriptSemanticTraversal.js";
import { indexJavaScriptBindingsWithReceiversSteps } from "./javascript/javascriptSemanticAnalysis.js";
@@ -37,26 +40,6 @@ type ChunkEdge = BundleObservations["chunks"]["edges"][number];
type Finding = BundleObservations["routes"][number];
type WebMcpDeclaration = BundleObservations["webmcp_declarations"][number];
type Inference = WebBundleAnalysis["inferences"][number];
type BrowserScript = WebPageInspection["scripts"]["items"][number];
type IncludedSource = Extract<BrowserScript["source"], { included: true }>;
export type IncludedScript = Omit<BrowserScript, "source"> & {
readonly source: IncludedSource;
};
export interface AnalysisAccumulator {
readonly edges: ChunkEdge[];
readonly routes: Finding[];
readonly endpoints: Finding[];
readonly webMcp: WebMcpDeclaration[];
readonly inferences: Inference[];
readonly seen: Set<string>;
visitedNodes: number;
parsedScripts: number;
readonly failedScripts: string[];
readonly recoveredScripts: string[];
readonly widenedScripts: string[];
}
export const analyzeScript = (
script: IncludedScript,
accumulator: AnalysisAccumulator,
@@ -501,24 +484,6 @@ const basis = (script: IncludedScript, detector: string) => ({
detector,
});
export const emptyAccumulator = (): AnalysisAccumulator => ({
edges: [],
routes: [],
endpoints: [],
webMcp: [],
inferences: [],
seen: new Set(),
visitedNodes: 0,
parsedScripts: 0,
failedScripts: [],
recoveredScripts: [],
widenedScripts: [],
});
export const isIncludedScript = (
script: BrowserScript,
): script is IncludedScript => script.source.included;
const routeCallNames = [
"route",
"addRoute",
+8 -20
View File
@@ -4,10 +4,12 @@ import {
webBundleAnalysisSchema,
type WebBundleAnalysis,
} from "./webBundleAnalysis.js";
import type {
AnalysisAccumulator,
IncludedScript,
} from "./webBundleAnalyzerInspection.js";
import {
createWebBundleAnalysisAccumulator,
isIncludedScript,
type AnalysisAccumulator,
type IncludedScript,
} from "./webBundleAnalyzerState.js";
import {
webBundleScriptAnalysisSchema,
type WebBundleScriptAnalysis,
@@ -29,9 +31,7 @@ export const buildCapturedWebBundleAnalysis = (
);
byKey.set(result.script_key, result);
}
const sourceScripts = inspection.scripts.items.filter(
(script): script is IncludedScript => script.source.included,
);
const sourceScripts = inspection.scripts.items.filter(isIncludedScript);
const expectedKeys = new Set(
sourceScripts.map(({ script_key }) => script_key),
);
@@ -145,19 +145,7 @@ const mergeScriptResults = (
{ readonly status: "analyzed" }
>[],
): AnalysisAccumulator => {
const accumulator: AnalysisAccumulator = {
edges: [],
routes: [],
endpoints: [],
webMcp: [],
inferences: [],
seen: new Set(),
visitedNodes: 0,
parsedScripts: 0,
failedScripts: [],
recoveredScripts: [],
widenedScripts: [],
};
const accumulator = createWebBundleAnalysisAccumulator();
for (const result of results) {
accumulator.visitedNodes += result.visited_ast_nodes;
if (result.parse_status === "failed") {
+44
View File
@@ -0,0 +1,44 @@
import type { WebPageInspection } from "./browserObservationSchemas.js";
import type { WebBundleAnalysis } from "./webBundleAnalysis.js";
type BundleObservations = WebBundleAnalysis["observations"];
type BrowserScript = WebPageInspection["scripts"]["items"][number];
type IncludedSource = Extract<BrowserScript["source"], { included: true }>;
/** Captured script whose source bytes are available for inert analysis. */
export type IncludedScript = Omit<BrowserScript, "source"> & {
readonly source: IncludedSource;
};
/** Mutable facts shared by per-script inspection and bundle result assembly. */
export interface AnalysisAccumulator {
readonly edges: BundleObservations["chunks"]["edges"];
readonly routes: BundleObservations["routes"];
readonly endpoints: BundleObservations["endpoints"];
readonly webMcp: BundleObservations["webmcp_declarations"];
readonly inferences: WebBundleAnalysis["inferences"];
readonly seen: Set<string>;
visitedNodes: number;
parsedScripts: number;
readonly failedScripts: string[];
readonly recoveredScripts: string[];
readonly widenedScripts: string[];
}
export const createWebBundleAnalysisAccumulator = (): AnalysisAccumulator => ({
edges: [],
routes: [],
endpoints: [],
webMcp: [],
inferences: [],
seen: new Set(),
visitedNodes: 0,
parsedScripts: 0,
failedScripts: [],
recoveredScripts: [],
widenedScripts: [],
});
export const isIncludedScript = (
script: BrowserScript,
): script is IncludedScript => script.source.included;
@@ -0,0 +1,40 @@
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { expect, it } from "vitest";
it("loads the isolated coordinator without loading its worker's JavaScript parser", async () => {
const { stdout } = await promisify(execFile)(
process.execPath,
[
"--input-type=module",
"-e",
`
import { registerHooks } from "node:module";
const parser = import.meta.resolve("@babel/parser");
let parserLoaded = false;
const hook = registerHooks({
load(url, context, nextLoad) {
if (url === parser) parserLoaded = true;
return nextLoad(url, context);
},
});
const { IsolatedWebBundleAnalyzer } = await import(process.argv[1]);
hook.deregister();
process.stdout.write(JSON.stringify({
coordinatorLoaded: typeof IsolatedWebBundleAnalyzer === "function",
parserLoaded,
}));
`,
new URL(
"../../../dist/browser/IsolatedWebBundleAnalyzer.js",
import.meta.url,
).href,
],
{ timeout: 10_000 },
);
expect(JSON.parse(stdout)).toEqual({
coordinatorLoaded: true,
parserLoaded: false,
});
});