From 00819258a7bc1dedafdec0736f8c99fba9952136 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:33:58 +0800 Subject: [PATCH] refactor(browser): separate bundle state from AST inspection --- src/browser/IsolatedWebBundleAnalyzer.ts | 2 +- src/browser/WebBundleAnalysisWorker.test.ts | 2 +- src/browser/WebBundleAnalysisWorker.ts | 2 +- .../WebBundleAnalysisWorkerCapacity.test.ts | 2 +- ...BundleAnalysisWorkerReadCompletion.test.ts | 2 +- .../WebBundleAnalysisWorkerServer.test.ts | 2 +- src/browser/WebBundleAnalysisWorkerServer.ts | 2 +- src/domain/webBundleAnalyzer.test.ts | 2 +- src/domain/webBundleAnalyzer.ts | 8 ++-- src/domain/webBundleAnalyzerInspection.ts | 43 ++---------------- src/domain/webBundleAnalyzerResult.ts | 28 ++++-------- src/domain/webBundleAnalyzerState.ts | 44 +++++++++++++++++++ .../webBundleCoordinatorImports.test.ts | 40 +++++++++++++++++ 13 files changed, 108 insertions(+), 71 deletions(-) create mode 100644 src/domain/webBundleAnalyzerState.ts create mode 100644 tests/boundary/browser/webBundleCoordinatorImports.test.ts diff --git a/src/browser/IsolatedWebBundleAnalyzer.ts b/src/browser/IsolatedWebBundleAnalyzer.ts index 70fd54c3c8..86fcae1c21 100644 --- a/src/browser/IsolatedWebBundleAnalyzer.ts +++ b/src/browser/IsolatedWebBundleAnalyzer.ts @@ -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 { diff --git a/src/browser/WebBundleAnalysisWorker.test.ts b/src/browser/WebBundleAnalysisWorker.test.ts index 406a7690e7..118eccec86 100644 --- a/src/browser/WebBundleAnalysisWorker.test.ts +++ b/src/browser/WebBundleAnalysisWorker.test.ts @@ -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, diff --git a/src/browser/WebBundleAnalysisWorker.ts b/src/browser/WebBundleAnalysisWorker.ts index 83c3a1a644..3a08651dfb 100644 --- a/src/browser/WebBundleAnalysisWorker.ts +++ b/src/browser/WebBundleAnalysisWorker.ts @@ -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, diff --git a/src/browser/WebBundleAnalysisWorkerCapacity.test.ts b/src/browser/WebBundleAnalysisWorkerCapacity.test.ts index 522587f29e..805fca7a61 100644 --- a/src/browser/WebBundleAnalysisWorkerCapacity.test.ts +++ b/src/browser/WebBundleAnalysisWorkerCapacity.test.ts @@ -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 () => { diff --git a/src/browser/WebBundleAnalysisWorkerReadCompletion.test.ts b/src/browser/WebBundleAnalysisWorkerReadCompletion.test.ts index e7135d723a..66b6531c6b 100644 --- a/src/browser/WebBundleAnalysisWorkerReadCompletion.test.ts +++ b/src/browser/WebBundleAnalysisWorkerReadCompletion.test.ts @@ -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 { diff --git a/src/browser/WebBundleAnalysisWorkerServer.test.ts b/src/browser/WebBundleAnalysisWorkerServer.test.ts index fdd9159182..726c38699e 100644 --- a/src/browser/WebBundleAnalysisWorkerServer.test.ts +++ b/src/browser/WebBundleAnalysisWorkerServer.test.ts @@ -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, diff --git a/src/browser/WebBundleAnalysisWorkerServer.ts b/src/browser/WebBundleAnalysisWorkerServer.ts index 4c3e701c8a..1a55205096 100644 --- a/src/browser/WebBundleAnalysisWorkerServer.ts +++ b/src/browser/WebBundleAnalysisWorkerServer.ts @@ -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, diff --git a/src/domain/webBundleAnalyzer.test.ts b/src/domain/webBundleAnalyzer.test.ts index c8b5f820fa..756c783906 100644 --- a/src/domain/webBundleAnalyzer.test.ts +++ b/src/domain/webBundleAnalyzer.test.ts @@ -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"; diff --git a/src/domain/webBundleAnalyzer.ts b/src/domain/webBundleAnalyzer.ts index 2fa6cc4f20..e51ca3c400 100644 --- a/src/domain/webBundleAnalyzer.ts +++ b/src/domain/webBundleAnalyzer.ts @@ -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; diff --git a/src/domain/webBundleAnalyzerInspection.ts b/src/domain/webBundleAnalyzerInspection.ts index a9a36b78c0..875e3ce01a 100644 --- a/src/domain/webBundleAnalyzerInspection.ts +++ b/src/domain/webBundleAnalyzerInspection.ts @@ -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; -export type IncludedScript = Omit & { - readonly source: IncludedSource; -}; - -export interface AnalysisAccumulator { - readonly edges: ChunkEdge[]; - readonly routes: Finding[]; - readonly endpoints: Finding[]; - readonly webMcp: WebMcpDeclaration[]; - readonly inferences: Inference[]; - readonly seen: Set; - 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", diff --git a/src/domain/webBundleAnalyzerResult.ts b/src/domain/webBundleAnalyzerResult.ts index 94bf162ee9..46af3bea8d 100644 --- a/src/domain/webBundleAnalyzerResult.ts +++ b/src/domain/webBundleAnalyzerResult.ts @@ -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") { diff --git a/src/domain/webBundleAnalyzerState.ts b/src/domain/webBundleAnalyzerState.ts new file mode 100644 index 0000000000..f24c813680 --- /dev/null +++ b/src/domain/webBundleAnalyzerState.ts @@ -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; + +/** Captured script whose source bytes are available for inert analysis. */ +export type IncludedScript = Omit & { + 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; + 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; diff --git a/tests/boundary/browser/webBundleCoordinatorImports.test.ts b/tests/boundary/browser/webBundleCoordinatorImports.test.ts new file mode 100644 index 0000000000..bb455d96d2 --- /dev/null +++ b/tests/boundary/browser/webBundleCoordinatorImports.test.ts @@ -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, + }); +});