fix(javascript): handle deep static member paths (#939)
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user