feat: static analyze destructuring assignment dynamic import variable for tree shaking (#19925)

This commit is contained in:
Gengkun 2025-09-24 01:52:59 +08:00 committed by GitHub
parent ae52500e95
commit 73cecf5e78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 80 additions and 56 deletions

View File

@ -8,7 +8,10 @@
const AsyncDependenciesBlock = require("../AsyncDependenciesBlock"); const AsyncDependenciesBlock = require("../AsyncDependenciesBlock");
const CommentCompilationWarning = require("../CommentCompilationWarning"); const CommentCompilationWarning = require("../CommentCompilationWarning");
const UnsupportedFeatureWarning = require("../UnsupportedFeatureWarning"); const UnsupportedFeatureWarning = require("../UnsupportedFeatureWarning");
const { getImportAttributes } = require("../javascript/JavascriptParser"); const {
VariableInfo,
getImportAttributes
} = require("../javascript/JavascriptParser");
const ContextDependencyHelpers = require("./ContextDependencyHelpers"); const ContextDependencyHelpers = require("./ContextDependencyHelpers");
const ImportContextDependency = require("./ImportContextDependency"); const ImportContextDependency = require("./ImportContextDependency");
const ImportDependency = require("./ImportDependency"); const ImportDependency = require("./ImportDependency");
@ -184,6 +187,15 @@ class ImportParserPlugin {
PLUGIN_NAME, PLUGIN_NAME,
(expr) => { (expr) => {
if (expr.type === "ImportExpression") return true; if (expr.type === "ImportExpression") return true;
const nameInfo = parser.getNameForExpression(expr);
if (
nameInfo &&
nameInfo.rootInfo instanceof VariableInfo &&
nameInfo.rootInfo.name &&
parser.getTagData(nameInfo.rootInfo.name, dynamicImportTag)
) {
return true;
}
} }
); );
parser.hooks.preDeclarator.tap(PLUGIN_NAME, (decl) => { parser.hooks.preDeclarator.tap(PLUGIN_NAME, (decl) => {
@ -196,9 +208,19 @@ class ImportParserPlugin {
tagDynamicImportReferenced(parser, decl.init.argument, decl.id.name); tagDynamicImportReferenced(parser, decl.init.argument, decl.id.name);
} }
}); });
parser.hooks.expression.for(dynamicImportTag).tap(PLUGIN_NAME, () => { parser.hooks.expression.for(dynamicImportTag).tap(PLUGIN_NAME, (expr) => {
const settings = /** @type {ImportSettings} */ (parser.currentTagData); const settings = /** @type {ImportSettings} */ (parser.currentTagData);
const referencedPropertiesInDestructuring =
parser.destructuringAssignmentPropertiesFor(expr);
if (referencedPropertiesInDestructuring) {
for (const ids of exportsFromEnumerable(
[...referencedPropertiesInDestructuring].map(({ id }) => id)
)) {
settings.references.push(ids);
}
} else {
settings.references.push([]); settings.references.push([]);
}
return true; return true;
}); });
parser.hooks.expressionMemberChain parser.hooks.expressionMemberChain

View File

@ -1,3 +0,0 @@
export const a = 1;
export default 3;
export const usedExports = __webpack_exports_info__.usedExports;

View File

@ -1,2 +0,0 @@
exports.a = 1;
exports.b = 2;

View File

@ -1,2 +0,0 @@
exports.a = 1;
exports.b = 2;

View File

@ -1,36 +0,0 @@
it("should load only used exports", async (done) => {
const { default: def, usedExports } = await import("./dir1/a");
expect(def).toBe(3);
expect(usedExports).toEqual(["default", "usedExports"]);
done();
});
it("should get warning on using 'webpackExports' with destructuring assignment", async (done) => {
const { default: def } = await import(/* webpackExports: ["a"] */"./dir1/a?2");
expect(def).toBe(3);
done();
});
it("should not tree-shake default export for exportsType=default module", async () => {
const { default: object } = await import("./dir2/json/object.json");
const { default: array } = await import("./dir2/json/array.json");
const { default: primitive } = await import("./dir2/json/primitive.json");
expect(object).toEqual({ a: 1 });
expect(array).toEqual(["a"]);
expect(primitive).toBe("a");
const { default: a } = await import("./dir2/a");
expect(a).toEqual({ a: 1, b: 2 });
});
it("should not tree-shake default export for exportsType=default context module", async () => {
const dir = "json";
const { default: object } = await import(`./dir3/${dir}/object.json`);
const { default: array } = await import(`./dir3/${dir}/array.json`);
const { default: primitive } = await import(`./dir3/${dir}/primitive.json`);
expect(object).toEqual({ a: 1 });
expect(array).toEqual(["a"]);
expect(primitive).toBe("a");
const file = "a";
const { default: a } = await import(`./dir3/${file}`);
expect(a).toEqual({ a: 1, b: 2 });
});

View File

@ -0,0 +1,43 @@
it("should load only used exports", async (done) => {
const { default: def, usedExports } = await import("../statical-dynamic-import/dir1/a");
expect(def).toBe(3);
expect(usedExports).toEqual(["default", "usedExports"]);
done();
});
it("should get warning on using 'webpackExports' with destructuring assignment", async (done) => {
const { default: def } = await import(/* webpackExports: ["a"] */"../statical-dynamic-import/dir1/a?2");
expect(def).toBe(3);
done();
});
it("should not tree-shake default export for exportsType=default module", async () => {
const { default: object } = await import("../statical-dynamic-import/dir2/json/object.json");
const { default: array } = await import("../statical-dynamic-import/dir2/json/array.json");
const { default: primitive } = await import("../statical-dynamic-import/dir2/json/primitive.json");
expect(object).toEqual({ a: 1 });
expect(array).toEqual(["a"]);
expect(primitive).toBe("a");
const { default: a } = await import("../statical-dynamic-import/dir2/a");
expect(a).toEqual({ a: 1, b: 2 });
});
it("should not tree-shake default export for exportsType=default context module", async () => {
const dir = "json";
const { default: object } = await import(`../statical-dynamic-import/dir3/${dir}/object.json`);
const { default: array } = await import(`../statical-dynamic-import/dir3/${dir}/array.json`);
const { default: primitive } = await import(`../statical-dynamic-import/dir3/${dir}/primitive.json`);
expect(object).toEqual({ a: 1 });
expect(array).toEqual(["a"]);
expect(primitive).toBe("a");
const file = "a";
const { default: a } = await import(`../statical-dynamic-import/dir3/${file}`);
expect(a).toEqual({ a: 1, b: 2 });
});
it("should static analyze dynamic import variable destructuring assignment", async () => {
const m = await import("../statical-dynamic-import/dir1/a?3");
const { default: def, usedExports } = m;
expect(def).toBe(3);
expect(usedExports).toEqual(["default", "usedExports"]);
});

View File

@ -53,12 +53,20 @@ it("should walk with correct order", async () => {
}); });
it("should analyze arguments in call member chain", async () => { it("should analyze arguments in call member chain", async () => {
import("../statical-dynamic-import/dir4/lib?2").then(({ b }) => { await import("../statical-dynamic-import/dir4/lib?2").then(({ b }) => {
b.f((async () => { b.f((async () => {
import("../statical-dynamic-import/dir4/a?2").then(({ a, usedExports }) => { await import("../statical-dynamic-import/dir4/a?2").then(({ a, usedExports }) => {
expect(a).toBe(1); expect(a).toBe(1);
expect(usedExports).toEqual(["a", "usedExports"]); expect(usedExports).toEqual(["a", "usedExports"]);
}); });
})()); })());
}); });
}); });
it("should static analyze dynamic import variable destructuring assignment", async () => {
await import("../statical-dynamic-import/dir1/a?3").then(m => {
const { default: def, usedExports } = m;
expect(def).toBe(3);
expect(usedExports).toEqual(["default", "usedExports"]);
});
});

View File

@ -73,9 +73,9 @@ it("should walk with correct order", async () => {
}); });
it("should analyze arguments in call member chain", async () => { it("should analyze arguments in call member chain", async () => {
import("../statical-dynamic-import/dir4/lib?2").then(m => { await import("../statical-dynamic-import/dir4/lib?2").then(m => {
m.b.f((async () => { m.b.f((async () => {
import("../statical-dynamic-import/dir4/a?2").then(m2 => { await import("../statical-dynamic-import/dir4/a?2").then(m2 => {
expect(m2.a).toBe(1); expect(m2.a).toBe(1);
expect(m2.usedExports).toEqual(["a", "usedExports"]); expect(m2.usedExports).toEqual(["a", "usedExports"]);
}); });