fix(javascript): handle deep static member paths (#939)

This commit is contained in:
morluto
2026-10-07 19:51:43 +08:00
committed by GitHub
parent 9e39795895
commit 348c65c7ec
3 changed files with 119 additions and 21 deletions
@@ -119,14 +119,30 @@ const addExport = (output: Set<string>, name: string): void => {
};
const memberPath = (node: t.Node | null | undefined): string => {
if (node === undefined || node === null) return "";
if (t.isIdentifier(node) || t.isPrivateName(node))
return t.isIdentifier(node) ? node.name : "";
if (t.isThisExpression(node)) return "this";
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
const object = t.isNode(node.object) ? memberPath(node.object) : "";
const property = propertyName(node.property);
return object === "" ? property : `${object}.${property}`;
const properties: string[] = [];
let current = node;
while (
current !== undefined &&
current !== null &&
(t.isMemberExpression(current) || t.isOptionalMemberExpression(current))
) {
properties.push(propertyName(current.property));
current = t.isNode(current.object) ? current.object : undefined;
}
return "";
const root =
current !== undefined && t.isIdentifier(current)
? current.name
: current !== undefined && t.isThisExpression(current)
? "this"
: "";
if (properties.length === 0) return root;
properties.reverse();
const path = root === "" ? [] : [root];
for (const property of properties) {
if (path.length === 0 && property === "") continue;
path.push(property);
}
return path.join(".");
};
@@ -0,0 +1,63 @@
import { expect, it } from "vitest";
import { collectJavaScriptExports } from "./javascriptAstFingerprint.js";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
const memberChain = ".next".repeat(12_000);
it("collects a 12,000-member CommonJS export path without recursion", () => {
const file = parseJavaScriptSource(`exports${memberChain}.last = 1;`);
if (file === null) throw new Error("Expected valid JavaScript");
expect(collectJavaScriptExports(file).values).toEqual([
`${"next.".repeat(12_000)}last`,
]);
});
it("preserves empty, dynamic, this, and private member path behavior", () => {
const file = parseJavaScriptSource(`
exports[""].value = 1;
exports[""] = 0;
exports[key].value = 2;
this.exports.ignored = 3;
class Example { #private = 0; read() { this.#private = 1; } }
`);
if (file === null) throw new Error("Expected valid JavaScript");
expect(collectJavaScriptExports(file).values).toEqual([
"",
".value",
"key.value",
]);
});
it("keeps left-first and outer-member-first bundler runtime matches", () => {
const source = `
(globalThis.webpackChunkLeft || globalThis.webpackChunkRight).push([[1], { 1: function() {} }]);
globalThis.webpackChunkOuter.webpackChunkInner.push([[2], { 2: function() {} }]);
`;
const analysis = analyzeJavaScriptStaticSource(source);
expect(
analysis.bundler_registrations.find(({ chunk_keys }) =>
chunk_keys.includes("1"),
)?.runtime,
).toBe("webpackChunkLeft");
expect(
analysis.bundler_registrations.find(({ chunk_keys }) =>
chunk_keys.includes("2"),
)?.runtime,
).toBe("webpackChunkInner");
});
it("analyzes a 12,000-member bundler runtime chain without recursion", () => {
const source = `globalThis.webpackChunkApp${memberChain}.push([[1], { 1: function(module) { module.exports.ok = 42; } }]);`;
const analysis = analyzeJavaScriptStaticSource(source);
expect(analysis.parse_status).toBe("complete");
expect(analysis.parse_error_count).toBe(0);
expect(analysis.bundler_registrations).toHaveLength(1);
expect(analysis.bundler_registrations[0]).toMatchObject({
bundler: "webpack",
runtime: "webpackChunkApp",
chunk_keys: ["1"],
modules: [expect.objectContaining({ exports: ["ok"] })],
});
});
@@ -73,19 +73,38 @@ export const chunkRuntime = (call: t.CallExpression): string | undefined => {
};
const findChunkRuntime = (node: t.Node): string | undefined => {
if (t.isIdentifier(node) && /(?:webpack|rspack)Chunk/iu.test(node.name))
return node.name;
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
const property = semanticStaticPropertyName(node.property, node.computed);
if (/(?:webpack|rspack)Chunk/iu.test(property)) return property;
return t.isNode(node.object) ? findChunkRuntime(node.object) : undefined;
const pending: t.Node[] = [node];
while (pending.length > 0) {
const current = pending.pop();
if (current === undefined) continue;
if (
t.isIdentifier(current) &&
/(?:webpack|rspack)Chunk/iu.test(current.name)
)
return current.name;
if (
t.isMemberExpression(current) ||
t.isOptionalMemberExpression(current)
) {
const property = semanticStaticPropertyName(
current.property,
current.computed,
);
if (/(?:webpack|rspack)Chunk/iu.test(property)) return property;
if (t.isNode(current.object)) pending.push(current.object);
continue;
}
if (t.isAssignmentExpression(current)) {
pending.push(current.right, current.left);
continue;
}
if (t.isLogicalExpression(current) || t.isBinaryExpression(current)) {
pending.push(current.right, current.left);
continue;
}
if (t.isParenthesizedExpression(current) || t.isTSAsExpression(current))
pending.push(current.expression);
}
if (t.isAssignmentExpression(node))
return findChunkRuntime(node.left) ?? findChunkRuntime(node.right);
if (t.isLogicalExpression(node) || t.isBinaryExpression(node))
return findChunkRuntime(node.left) ?? findChunkRuntime(node.right);
if (t.isParenthesizedExpression(node) || t.isTSAsExpression(node))
return findChunkRuntime(node.expression);
return undefined;
};