From 4b26f38d6f09423df25aa0ab4ebacabf268a459d Mon Sep 17 00:00:00 2001 From: Kaoru0822 Date: Fri, 13 Nov 2026 06:17:43 -0800 Subject: [PATCH] fix(javascript): preserve wide async chunk collection (#10362) * fix(javascript): collect async chunks without call spreads * test(javascript): bound the public checkpoint control --- .../javascriptStaticAnalysisBundler.ts | 2 +- .../javascriptAsyncChunkBudget.test.ts | 70 +++++++++++++++++++ .../javascript/asyncChunkCapacity.test.ts | 41 +++++++++++ .../fixtures/javascriptAsyncChunkCapacity.ts | 13 ++++ 4 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 tests/acceptance/javascriptAsyncChunkBudget.test.ts create mode 100644 tests/boundary/javascript/asyncChunkCapacity.test.ts create mode 100644 tests/fixtures/javascriptAsyncChunkCapacity.ts diff --git a/src/domain/javascript/javascriptStaticAnalysisBundler.ts b/src/domain/javascript/javascriptStaticAnalysisBundler.ts index 429654af56..8c207d5e24 100644 --- a/src/domain/javascript/javascriptStaticAnalysisBundler.ts +++ b/src/domain/javascript/javascriptStaticAnalysisBundler.ts @@ -230,7 +230,7 @@ const recoverBundlerModules = ( ); if (recovered === null) continue; modules.push(recovered.module); - asyncChunkKeys.push(...recovered.asyncChunkKeys); + for (const key of recovered.asyncChunkKeys) asyncChunkKeys.push(key); unknownAsyncChunkKeys += recovered.unknownAsyncChunkKeys; } return { diff --git a/tests/acceptance/javascriptAsyncChunkBudget.test.ts b/tests/acceptance/javascriptAsyncChunkBudget.test.ts new file mode 100644 index 0000000000..4939f7bf9d --- /dev/null +++ b/tests/acceptance/javascriptAsyncChunkBudget.test.ts @@ -0,0 +1,70 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; +import { parseEvidence } from "../../src/domain/evidence.js"; +import { javascriptApplicationAnalysisResultSchema } from "../../src/domain/javascript/javascriptApplicationAnalysis.js"; +import { JAVASCRIPT_STATIC_FACT_BYTES } from "../../src/domain/javascript/javascriptAnalysisResourceControls.js"; +import { javascriptAsyncChunkCapacity } from "../fixtures/javascriptAsyncChunkCapacity.js"; +import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; +import { cliTest } from "../support/cli/cliFixture.js"; + +cliTest( + "reports the complete-fact ceiling for wide async chunk metadata", + { timeout: 60_000 }, + async ({ cli }) => { + // This bounded public control exceeds the complete-fact allowance. The + // forked boundary regression separately owns call-argument capacity. + const { source } = javascriptAsyncChunkCapacity(50_000); + const root = await createTestTempDirectory("rea-async-chunk-budget-"); + await writeFile(join(root, "entry.cjs"), source); + const response = await cli.run({ + arguments: [ + "analyze-javascript-application", + root, + "--max-semantic-nodes", + "1", + "--max-semantic-nodes-per-file", + "1", + "--max-heap-mb", + "2048", + "--analysis-timeout-ms", + "30000", + "--json", + ], + timeoutMs: 55_000, + }); + // Analysis retains the interrupted file rather than failing the command. + expect(response.exitCode, `${response.stdout}\n${response.stderr}`).toBe(0); + const result = javascriptApplicationAnalysisResultSchema.parse( + parseEvidence(response.json).normalized_result, + ); + const failures = result.graph.nodes.flatMap(({ observations }) => + observations.flatMap(({ properties }) => + properties.analysis_failure === undefined + ? [] + : [properties.analysis_failure], + ), + ); + expect(failures).toEqual([ + expect.objectContaining({ + code: "resource_constraint", + retryable: false, + message: expect.stringContaining("static checkpoint"), + remediation: { + action: expect.stringContaining("no runtime override"), + }, + details: expect.objectContaining({ + resource: "transport", + reported_limits: expect.objectContaining({ + checkpoint_bytes: expect.any(Number), + checkpoint_limit_bytes: JAVASCRIPT_STATIC_FACT_BYTES, + }), + }), + }), + ]); + expect(JSON.stringify(failures)).not.toContain("call stack"); + expect(JSON.stringify(failures)).toContain( + "Raising max_heap_mb does not raise it", + ); + }, +); diff --git a/tests/boundary/javascript/asyncChunkCapacity.test.ts b/tests/boundary/javascript/asyncChunkCapacity.test.ts new file mode 100644 index 0000000000..f9bb088112 --- /dev/null +++ b/tests/boundary/javascript/asyncChunkCapacity.test.ts @@ -0,0 +1,41 @@ +import { createHash } from "node:crypto"; +import { expect, it } from "vitest"; +import { javascriptAsyncChunkCapacity } from "../../fixtures/javascriptAsyncChunkCapacity.js"; +import { analyzeJavaScriptStaticSource } from "../../fixtures/javascriptStaticAnalysis.js"; + +it("retains a wide factory's async keys before normalizing the registration", () => { + // The forked boundary project preserves the default main-thread stack. + const count = 130_000; + const { source, factory } = javascriptAsyncChunkCapacity(count); + const result = analyzeJavaScriptStaticSource(source); + expect(result.parse_status).toBe("partial"); + expect(result.analysis_coverage?.syntax).toEqual({ + status: "complete", + error_count: 0, + }); + expect(result.bundler_registrations).toHaveLength(1); + const registration = result.bundler_registrations[0]; + if (registration === undefined) throw new Error("Missing registration"); + const keys = new Set(registration.async_chunk_keys); + expect(registration.async_chunk_keys).toHaveLength(count + 1); + expect(keys.size).toBe(count + 1); + expect( + Array.from({ length: count }, (_, index) => String(index)).every((key) => + keys.has(key), + ), + ).toBe(true); + expect(registration.async_chunk_keys[0]).toBe("0"); + expect(registration.async_chunk_keys.at(-1)).toBe("later"); + expect(registration.unknown_async_chunk_keys).toBe(1); + expect(registration.modules).toHaveLength(2); + const start = source.indexOf(factory); + expect(registration.modules[0]).toMatchObject({ + module_key: "1", + factory_require_name: "r", + source_sha256: createHash("sha256").update(factory).digest("hex"), + location: { + start: { line: 1, column: start }, + end: { line: 1, column: start + factory.length }, + }, + }); +}, 30_000); diff --git a/tests/fixtures/javascriptAsyncChunkCapacity.ts b/tests/fixtures/javascriptAsyncChunkCapacity.ts new file mode 100644 index 0000000000..e4a8406125 --- /dev/null +++ b/tests/fixtures/javascriptAsyncChunkCapacity.ts @@ -0,0 +1,13 @@ +/** Inert generated Webpack source; no fixture code is executed. */ +export const javascriptAsyncChunkCapacity = (count: number) => { + const factory = `(m,e,r)=>{${Array.from( + { length: count }, + (_, index) => `r.e(${String(index)});`, + ).join("")}}`; + return { + factory, + source: + `self.webpackChunkapp.push([[1],{1:${factory},` + + "2:(m,e,r)=>{r.e(0);r.e('later');r.e(dynamic);}}]);", + }; +};