mirror of https://github.com/webpack/webpack.git
feat: static analyze member expression for dynamic import (#19818)
This commit is contained in:
parent
e2ded49ee6
commit
a0f19f7c50
|
@ -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()) {
|
||||
|
|
|
@ -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"
|
||||
`;
|
||||
|
||||
|
|
|
@ -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/
|
||||
];
|
||||
|
|
|
@ -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/
|
||||
];
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
export const a = 1;
|
||||
export default 3;
|
||||
export const usedExports = __webpack_exports_info__.usedExports;
|
|
@ -0,0 +1,2 @@
|
|||
exports.a = 1;
|
||||
exports.b = 2;
|
|
@ -0,0 +1 @@
|
|||
["a"]
|
|
@ -0,0 +1 @@
|
|||
{"a": 1}
|
|
@ -0,0 +1 @@
|
|||
"a"
|
|
@ -0,0 +1,2 @@
|
|||
exports.a = 1;
|
||||
exports.b = 2;
|
|
@ -0,0 +1 @@
|
|||
["a"]
|
|
@ -0,0 +1 @@
|
|||
{"a": 1}
|
|
@ -0,0 +1 @@
|
|||
"a"
|
|
@ -0,0 +1,5 @@
|
|||
export const a = 1;
|
||||
export function f() {
|
||||
return this.a;
|
||||
}
|
||||
export const usedExports = __webpack_exports_info__.usedExports;
|
|
@ -0,0 +1,2 @@
|
|||
export const a = 1;
|
||||
export const usedExports = __webpack_exports_info__.usedExports;
|
|
@ -0,0 +1,5 @@
|
|||
export function f() {
|
||||
return 1;
|
||||
}
|
||||
export const b = 2;
|
||||
export const usedExports = __webpack_exports_info__.usedExports;
|
|
@ -0,0 +1,4 @@
|
|||
export * as a from "./a";
|
||||
export * as b from "./b";
|
||||
|
||||
export const usedExports = __webpack_exports_info__.usedExports;
|
|
@ -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"]);
|
||||
})
|
|
@ -0,0 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = function filter(config) {
|
||||
// This test can't run in development mode
|
||||
return config.mode !== "development";
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = [
|
||||
/You don't need `webpackExports` if the usage of dynamic import is statically analyse-able/
|
||||
];
|
|
@ -0,0 +1,5 @@
|
|||
export const a = 1;
|
||||
export function f() {
|
||||
return this.a;
|
||||
}
|
||||
export const usedExports = __webpack_exports_info__.usedExports;
|
|
@ -0,0 +1,2 @@
|
|||
export const a = 1;
|
||||
export const usedExports = __webpack_exports_info__.usedExports;
|
|
@ -0,0 +1,5 @@
|
|||
export function f() {
|
||||
return 1;
|
||||
}
|
||||
export const b = 2;
|
||||
export const usedExports = __webpack_exports_info__.usedExports;
|
|
@ -0,0 +1,4 @@
|
|||
export * as a from "./a";
|
||||
export * as b from "./b";
|
||||
|
||||
export const usedExports = __webpack_exports_info__.usedExports;
|
|
@ -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"]);
|
||||
})
|
|
@ -0,0 +1,8 @@
|
|||
"use strict";
|
||||
|
||||
/** @type {import("../../../../").Configuration} */
|
||||
module.exports = {
|
||||
module: {
|
||||
strictThisContextOnImports: true
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue