From 0cf224e76214abbcf47274aa936d1b5cad28db2f Mon Sep 17 00:00:00 2001 From: LouisChen <65096591@qq.com> Date: Tue, 13 Oct 2026 17:42:41 +0800 Subject: [PATCH] fix(javascript): retain distinct chunk registration callback metadata (#2737) Co-authored-by: LouisChen <3361008+chqcjq@users.noreply.github.com> --- ...ascriptBundlerRegistrationIdentity.test.ts | 78 +++++++++++++++++++ .../javascriptStaticAnalysisHelpers.ts | 8 ++ .../bundlerCallbackMetadataCli.test.ts | 61 +++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 src/domain/javascript/javascriptBundlerRegistrationIdentity.test.ts create mode 100644 tests/acceptance/applications/bundlerCallbackMetadataCli.test.ts diff --git a/src/domain/javascript/javascriptBundlerRegistrationIdentity.test.ts b/src/domain/javascript/javascriptBundlerRegistrationIdentity.test.ts new file mode 100644 index 0000000000..1b251e3132 --- /dev/null +++ b/src/domain/javascript/javascriptBundlerRegistrationIdentity.test.ts @@ -0,0 +1,78 @@ +import { expect, it } from "vitest"; +import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; + +const registration = (runtime: string, callback: string, chunk = 1) => + "(globalThis." + + runtime + + " = globalThis." + + runtime + + " || []).push([[" + + chunk + + "], {a: function() {}}, " + + callback + + "]);"; + +it.each(["webpackChunkFixture", "rspackChunkFixture"])( + "keeps differing callback facts for %s independent of source order", + (runtime) => { + const first = registration( + runtime, + 'function(r) { r(7); r.e("chunk-a"); }', + ); + const second = registration( + runtime, + 'function(r) { r(8); r.e("chunk-b"); }', + ); + for (const source of [first + "\n" + second, second + "\n" + first]) { + const result = analyzeJavaScriptStaticSource(source); + expect(result.parse_status).toBe("complete"); + expect(result.bundler_registrations).toHaveLength(2); + expect( + result.bundler_registrations.flatMap((r) => r.entry_module_keys).sort(), + ).toEqual(["7", "8"]); + expect( + result.bundler_registrations.flatMap((r) => r.async_chunk_keys).sort(), + ).toEqual(["chunk-a", "chunk-b"]); + } + expect( + analyzeJavaScriptStaticSource(first + "\n" + first).bundler_registrations, + ).toHaveLength(1); + expect( + analyzeJavaScriptStaticSource(first).bundler_registrations[0], + ).toMatchObject({ + entry_module_keys: ["7"], + async_chunk_keys: ["chunk-a"], + }); + expect( + analyzeJavaScriptStaticSource(second).bundler_registrations[0], + ).toMatchObject({ + entry_module_keys: ["8"], + async_chunk_keys: ["chunk-b"], + }); + }, +); + +it.each([ + ["function(r) { r(7); }", "function(r) { r(8); }"], + ['function(r) { r.e("a"); }', 'function(r) { r.e("b"); }'], + ["function(r) {}", "function(s) {}"], + ["function(r) {}", "function(r) { r(dynamic); }"], + ["function(r) {}", "function(r) { r.e(dynamic); }"], +])("retains callback metadata differences: %s / %s", (left, right) => { + const result = analyzeJavaScriptStaticSource( + registration("webpackChunkFixture", left) + + "\n" + + registration("webpackChunkFixture", right), + ); + expect(result.bundler_registrations).toHaveLength(2); +}); + +it("retains distinct chunk identities with identical callbacks", () => { + const callback = "function(r) { r(7); }"; + const result = analyzeJavaScriptStaticSource( + registration("webpackChunkFixture", callback, 1) + + "\n" + + registration("webpackChunkFixture", callback, 2), + ); + expect(result.bundler_registrations).toHaveLength(2); +}); diff --git a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts index a6a6ebc445..71562e6ca2 100644 --- a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts +++ b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts @@ -691,6 +691,14 @@ export const registrationKey = ( registration.runtime, registration.chunk_keys, String(registration.unknown_chunk_keys), + // Equal factory tables can be registered with different runtime callbacks. + // Both early collection and finalization must retain those recovered facts. + registration.runtime_require_name, + registration.runtime_module_cache_status, + registration.entry_module_keys, + String(registration.unknown_entry_module_keys), + registration.async_chunk_keys, + String(registration.unknown_async_chunk_keys), registration.modules.map(({ module_key: key, source_sha256: digest }) => [ key, digest, diff --git a/tests/acceptance/applications/bundlerCallbackMetadataCli.test.ts b/tests/acceptance/applications/bundlerCallbackMetadataCli.test.ts new file mode 100644 index 0000000000..1f3987237d --- /dev/null +++ b/tests/acceptance/applications/bundlerCallbackMetadataCli.test.ts @@ -0,0 +1,61 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; +import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js"; + +cliTest.for(["CLI", "stdio MCP"] as const)( + "retains both repeated chunk callback observations through %s", + async (surface, { cli }) => { + const root = await createTestTempDirectory("rea-bundler-callbacks-"); + const source = [ + '(globalThis.webpackChunkFixture = globalThis.webpackChunkFixture || []).push([[1], {a: function() {}}, function(r) { r(7); r.e("chunk-a"); }]);', + '(globalThis.webpackChunkFixture = globalThis.webpackChunkFixture || []).push([[1], {a: function() {}}, function(r) { r(8); r.e("chunk-b"); }]);', + ].join("\n"); + await writeFile(join(root, "bundle.js"), source); + const { graph } = await analyzeJavaScriptDirectory(surface, cli, root); + const chunk = graph.nodes.find(({ kind }) => kind === "javascript-chunk"); + for (const [index, entry, asyncChunk] of [ + [0, "7", "chunk-a"], + [1, "8", "chunk-b"], + ] as const) { + expect(chunk?.observations).toContainEqual( + expect.objectContaining({ + properties: expect.objectContaining({ + entry_module_keys: [entry], + async_chunk_keys: [asyncChunk], + }), + evidence: expect.objectContaining({ + location: expect.objectContaining({ + value: expect.objectContaining({ + source: "bundle.js", + start: { line: index + 1, column: 0 }, + }), + }), + }), + }), + ); + expect(graph.edges).toContainEqual( + expect.objectContaining({ + source_node_id: chunk?.node_id, + properties: expect.objectContaining({ + kind: "bundler-entry-module", + module_key: entry, + resolution_status: "not-found", + }), + }), + ); + expect(graph.edges).toContainEqual( + expect.objectContaining({ + source_node_id: chunk?.node_id, + properties: expect.objectContaining({ + kind: "bundler-async-chunk", + chunk_key: asyncChunk, + resolution_status: "not-found", + }), + }), + ); + } + }, +);