feat: static analyze member expression for dynamic import (#19818)

This commit is contained in:
Gengkun 2025-08-25 21:43:57 +08:00 committed by GitHub
parent e2ded49ee6
commit a0f19f7c50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 237 additions and 7 deletions

View File

@ -23,6 +23,25 @@ const ImportWeakDependency = require("./ImportWeakDependency");
/** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
/** @typedef {import("../javascript/JavascriptParser").ImportExpression} ImportExpression */
/** @typedef {import("../javascript/JavascriptParser").Range} Range */
/** @typedef {import("../javascript/JavascriptParser").ParserState} ParserState */
/** @typedef {{ references: string[][], expression: ImportExpression }} ImportSettings */
/** @typedef {WeakMap<ImportExpression, string[][]>} State */
/** @type {WeakMap<ParserState, State>} */
const parserStateMap = new WeakMap();
const dynamicImportTag = Symbol("import()");
/**
* @param {JavascriptParser} parser javascript parser
* @returns {State} import parser plugin state
*/
function getState(parser) {
if (!parserStateMap.has(parser.state)) {
parserStateMap.set(parser.state, new WeakMap());
}
return /** @type {State} */ (parserStateMap.get(parser.state));
}
const PLUGIN_NAME = "ImportParserPlugin";
@ -46,12 +65,74 @@ class ImportParserPlugin {
*/
const exportsFromEnumerable = (enumerable) =>
Array.from(enumerable, (e) => [e]);
/**
* @param {string[]} members members
* @param {boolean[]} membersOptionals members Optionals
* @returns {string[]} a non optional part
*/
function getNonOptionalPart(members, membersOptionals) {
let i = 0;
while (i < members.length && membersOptionals[i] === false) i++;
return i !== members.length ? members.slice(0, i) : members;
}
parser.hooks.collectDestructuringAssignmentProperties.tap(
PLUGIN_NAME,
(expr) => {
if (expr.type === "ImportExpression") return true;
}
);
parser.hooks.preDeclarator.tap(PLUGIN_NAME, (decl) => {
if (
decl.init &&
decl.init.type === "AwaitExpression" &&
decl.init.argument.type === "ImportExpression" &&
decl.id.type === "Identifier"
) {
const importCall = decl.init.argument;
const state = getState(parser);
/** @type {string[][]} */
const references = [];
state.set(importCall, references);
parser.tagVariable(
decl.id.name,
dynamicImportTag,
/** @type {ImportSettings} */ ({
references,
expression: importCall
})
);
}
});
parser.hooks.expression.for(dynamicImportTag).tap(PLUGIN_NAME, () => {
const settings = /** @type {ImportSettings} */ (parser.currentTagData);
settings.references.push([]);
return true;
});
parser.hooks.expressionMemberChain
.for(dynamicImportTag)
.tap(PLUGIN_NAME, (_expression, members, membersOptionals) => {
const settings = /** @type {ImportSettings} */ (parser.currentTagData);
const ids = getNonOptionalPart(members, membersOptionals);
settings.references.push(ids);
return true;
});
parser.hooks.callMemberChain
.for(dynamicImportTag)
.tap(PLUGIN_NAME, (_expression, members, membersOptionals) => {
const settings = /** @type {ImportSettings} */ (parser.currentTagData);
let ids = getNonOptionalPart(members, membersOptionals);
const directImport = members.length === 0;
if (
!directImport &&
(this.options.strictThisContextOnImports || ids.length > 1)
) {
ids = ids.slice(0, -1);
}
settings.references.push(ids);
return true;
});
parser.hooks.importCall.tap(PLUGIN_NAME, (expr) => {
const param = parser.evaluateExpression(expr.source);
@ -281,19 +362,25 @@ class ImportParserPlugin {
const referencedPropertiesInDestructuring =
parser.destructuringAssignmentPropertiesFor(expr);
if (referencedPropertiesInDestructuring) {
const state = getState(parser);
const referencedPropertiesInMember = state.get(expr);
if (referencedPropertiesInDestructuring || referencedPropertiesInMember) {
if (exports) {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
"`webpackExports` could not be used with destructuring assignment.",
"You don't need `webpackExports` if the usage of dynamic import is statically analyse-able. You can safely remove the `webpackExports` magic comment.",
/** @type {DependencyLocation} */ (expr.loc)
)
);
}
exports = exportsFromEnumerable(
[...referencedPropertiesInDestructuring].map(({ id }) => id)
);
if (referencedPropertiesInDestructuring) {
exports = exportsFromEnumerable(
[...referencedPropertiesInDestructuring].map(({ id }) => id)
);
} else if (referencedPropertiesInMember) {
exports = referencedPropertiesInMember;
}
}
if (param.isString()) {

View File

@ -1392,7 +1392,7 @@ built modules X bytes [built]
./templates/baz.js X bytes [optional] [built] [code generated]
./templates/foo.js X bytes [optional] [built] [code generated]
./entry.js X bytes [built] [code generated]
./templates/ lazy ^\\\\.\\\\/.*$ include: \\\\.js$ exclude: \\\\.noimport\\\\.js$ na...(truncated) X bytes [optional] [built] [code generated]
./templates/ lazy ^\\\\.\\\\/.*$ include: \\\\.js$ exclude: \\\\.noimport\\\\.js$ referencedExp...(truncated) X bytes [optional] [built] [code generated]
webpack x.x.x compiled successfully in X ms"
`;

View File

@ -1,5 +1,5 @@
"use strict";
module.exports = [
[/`webpackExports` could not be used with destructuring assignment./]
/You don't need `webpackExports` if the usage of dynamic import is statically analyse-able/
];

View File

@ -0,0 +1,17 @@
"use strict";
module.exports = (options) => {
if (options.mode === "development") {
return [];
}
return [
/You don't need `webpackExports` if the usage of dynamic import is statically analyse-able/,
/You don't need `webpackExports` if the usage of dynamic import is statically analyse-able/,
/You don't need `webpackExports` if the usage of dynamic import is statically analyse-able/,
/You don't need `webpackExports` if the usage of dynamic import is statically analyse-able/,
/You don't need `webpackExports` if the usage of dynamic import is statically analyse-able/,
/You don't need `webpackExports` if the usage of dynamic import is statically analyse-able/,
/You don't need `webpackExports` if the usage of dynamic import is statically analyse-able/,
/You don't need `webpackExports` if the usage of dynamic import is statically analyse-able/
];
};

View File

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

View File

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

View File

@ -0,0 +1 @@
["a"]

View File

@ -0,0 +1 @@
{"a": 1}

View File

@ -0,0 +1 @@
"a"

View File

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

View File

@ -0,0 +1 @@
["a"]

View File

@ -0,0 +1 @@
{"a": 1}

View File

@ -0,0 +1 @@
"a"

View File

@ -0,0 +1,5 @@
export const a = 1;
export function f() {
return this.a;
}
export const usedExports = __webpack_exports_info__.usedExports;

View File

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

View File

@ -0,0 +1,5 @@
export function f() {
return 1;
}
export const b = 2;
export const usedExports = __webpack_exports_info__.usedExports;

View File

@ -0,0 +1,4 @@
export * as a from "./a";
export * as b from "./b";
export const usedExports = __webpack_exports_info__.usedExports;

View File

@ -0,0 +1,53 @@
it("should load only used exports", async (done) => {
const m = await import("./dir1/a");
expect(m.default).toBe(3);
expect(m.usedExports).toEqual(["default", "usedExports"]);
done();
});
it("should get warning on using 'webpackExports' with statically analyze-able dynamic import", async (done) => {
const m = await import(/* webpackExports: ["default"] */"./dir1/a?2");
expect(m.a).toBe(1);
done();
});
it("should not tree-shake default export for exportsType=default module", async () => {
const m1 = await import("./dir2/json/object.json");
const m2 = await import("./dir2/json/array.json");
const m3 = await import("./dir2/json/primitive.json");
expect(m1.default).toEqual({ a: 1 });
expect(m2.default).toEqual(["a"]);
expect(m3.default).toBe("a");
const m4 = await import("./dir2/a");
expect(m4.default).toEqual({ a: 1, b: 2 });
});
it("should not tree-shake default export for exportsType=default context module", async () => {
const dir = "json";
const m1 = await import(`./dir3/${dir}/object.json`);
const m2 = await import(`./dir3/${dir}/array.json`);
const m3 = await import(`./dir3/${dir}/primitive.json`);
expect(m1.default).toEqual({ a: 1 });
expect(m2.default).toEqual(["a"]);
expect(m3.default).toBe("a");
const file = "a";
const m4 = await import(`./dir3/${file}`);
expect(m4.default).toEqual({ a: 1, b: 2 });
});
it("should not tree-shake if reassigin", async () => {
let m = await import("./dir1/a?3");
expect(m.default).toBe(3);
expect(m.usedExports).toBe(true);
m = {};
})
it("should tree-shake if its member call and strictThisContextOnImports is false", async () => {
let m = await import("./dir4/a");
expect(m.f()).toBe(undefined);
expect(m.usedExports).toEqual(["f", "usedExports"]);
let m2 = await import("./dir4/lib");
expect(m2.b.f()).toBe(1);
expect(m2.b.usedExports).toEqual(true);
expect(m2.usedExports).toEqual(["b", "usedExports"]);
})

View File

@ -0,0 +1,6 @@
"use strict";
module.exports = function filter(config) {
// This test can't run in development mode
return config.mode !== "development";
};

View File

@ -0,0 +1,5 @@
"use strict";
module.exports = [
/You don't need `webpackExports` if the usage of dynamic import is statically analyse-able/
];

View File

@ -0,0 +1,5 @@
export const a = 1;
export function f() {
return this.a;
}
export const usedExports = __webpack_exports_info__.usedExports;

View File

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

View File

@ -0,0 +1,5 @@
export function f() {
return 1;
}
export const b = 2;
export const usedExports = __webpack_exports_info__.usedExports;

View File

@ -0,0 +1,4 @@
export * as a from "./a";
export * as b from "./b";
export const usedExports = __webpack_exports_info__.usedExports;

View File

@ -0,0 +1,9 @@
it("should respect strictThisContextOnImports for member call", async () => {
let m = await import("./dir4/a");
expect(m.f()).toBe(1);
expect(m.usedExports).toBe(true);
let m2 = await import("./dir4/lib");
expect(m2.b.f()).toBe(1);
expect(m2.b.usedExports).toBe(true);
expect(m2.usedExports).toEqual(["b", "usedExports"]);
})

View File

@ -0,0 +1,8 @@
"use strict";
/** @type {import("../../../../").Configuration} */
module.exports = {
module: {
strictThisContextOnImports: true
}
};