diff --git a/lib/dependencies/ImportParserPlugin.js b/lib/dependencies/ImportParserPlugin.js index 7b9904a4e..449bc4cab 100644 --- a/lib/dependencies/ImportParserPlugin.js +++ b/lib/dependencies/ImportParserPlugin.js @@ -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} State */ + +/** @type {WeakMap} */ +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()) { diff --git a/test/__snapshots__/StatsTestCases.basictest.js.snap b/test/__snapshots__/StatsTestCases.basictest.js.snap index c69ea7438..609c312b2 100644 --- a/test/__snapshots__/StatsTestCases.basictest.js.snap +++ b/test/__snapshots__/StatsTestCases.basictest.js.snap @@ -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" `; diff --git a/test/cases/chunks/destructuring-assignment/warnings.js b/test/cases/chunks/destructuring-assignment/warnings.js index 08df36bcb..fc07af60c 100644 --- a/test/cases/chunks/destructuring-assignment/warnings.js +++ b/test/cases/chunks/destructuring-assignment/warnings.js @@ -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/ ]; diff --git a/test/cases/chunks/inline-options/warnings.js b/test/cases/chunks/inline-options/warnings.js new file mode 100644 index 000000000..4f6036eb6 --- /dev/null +++ b/test/cases/chunks/inline-options/warnings.js @@ -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/ + ]; +}; diff --git a/test/cases/chunks/statical-dynamic-import/dir1/a.js b/test/cases/chunks/statical-dynamic-import/dir1/a.js new file mode 100644 index 000000000..ce622ee65 --- /dev/null +++ b/test/cases/chunks/statical-dynamic-import/dir1/a.js @@ -0,0 +1,3 @@ +export const a = 1; +export default 3; +export const usedExports = __webpack_exports_info__.usedExports; diff --git a/test/cases/chunks/statical-dynamic-import/dir2/a.js b/test/cases/chunks/statical-dynamic-import/dir2/a.js new file mode 100644 index 000000000..59aa6ffd1 --- /dev/null +++ b/test/cases/chunks/statical-dynamic-import/dir2/a.js @@ -0,0 +1,2 @@ +exports.a = 1; +exports.b = 2; diff --git a/test/cases/chunks/statical-dynamic-import/dir2/json/array.json b/test/cases/chunks/statical-dynamic-import/dir2/json/array.json new file mode 100644 index 000000000..eac5f7b46 --- /dev/null +++ b/test/cases/chunks/statical-dynamic-import/dir2/json/array.json @@ -0,0 +1 @@ +["a"] \ No newline at end of file diff --git a/test/cases/chunks/statical-dynamic-import/dir2/json/object.json b/test/cases/chunks/statical-dynamic-import/dir2/json/object.json new file mode 100644 index 000000000..cb5b2f69b --- /dev/null +++ b/test/cases/chunks/statical-dynamic-import/dir2/json/object.json @@ -0,0 +1 @@ +{"a": 1} diff --git a/test/cases/chunks/statical-dynamic-import/dir2/json/primitive.json b/test/cases/chunks/statical-dynamic-import/dir2/json/primitive.json new file mode 100644 index 000000000..231f150c5 --- /dev/null +++ b/test/cases/chunks/statical-dynamic-import/dir2/json/primitive.json @@ -0,0 +1 @@ +"a" diff --git a/test/cases/chunks/statical-dynamic-import/dir3/a.js b/test/cases/chunks/statical-dynamic-import/dir3/a.js new file mode 100644 index 000000000..59aa6ffd1 --- /dev/null +++ b/test/cases/chunks/statical-dynamic-import/dir3/a.js @@ -0,0 +1,2 @@ +exports.a = 1; +exports.b = 2; diff --git a/test/cases/chunks/statical-dynamic-import/dir3/json/array.json b/test/cases/chunks/statical-dynamic-import/dir3/json/array.json new file mode 100644 index 000000000..eac5f7b46 --- /dev/null +++ b/test/cases/chunks/statical-dynamic-import/dir3/json/array.json @@ -0,0 +1 @@ +["a"] \ No newline at end of file diff --git a/test/cases/chunks/statical-dynamic-import/dir3/json/object.json b/test/cases/chunks/statical-dynamic-import/dir3/json/object.json new file mode 100644 index 000000000..cb5b2f69b --- /dev/null +++ b/test/cases/chunks/statical-dynamic-import/dir3/json/object.json @@ -0,0 +1 @@ +{"a": 1} diff --git a/test/cases/chunks/statical-dynamic-import/dir3/json/primitive.json b/test/cases/chunks/statical-dynamic-import/dir3/json/primitive.json new file mode 100644 index 000000000..231f150c5 --- /dev/null +++ b/test/cases/chunks/statical-dynamic-import/dir3/json/primitive.json @@ -0,0 +1 @@ +"a" diff --git a/test/cases/chunks/statical-dynamic-import/dir4/a.js b/test/cases/chunks/statical-dynamic-import/dir4/a.js new file mode 100644 index 000000000..593fac19a --- /dev/null +++ b/test/cases/chunks/statical-dynamic-import/dir4/a.js @@ -0,0 +1,5 @@ +export const a = 1; +export function f() { + return this.a; +} +export const usedExports = __webpack_exports_info__.usedExports; diff --git a/test/cases/chunks/statical-dynamic-import/dir4/lib/a.js b/test/cases/chunks/statical-dynamic-import/dir4/lib/a.js new file mode 100644 index 000000000..7761ba968 --- /dev/null +++ b/test/cases/chunks/statical-dynamic-import/dir4/lib/a.js @@ -0,0 +1,2 @@ +export const a = 1; +export const usedExports = __webpack_exports_info__.usedExports; diff --git a/test/cases/chunks/statical-dynamic-import/dir4/lib/b.js b/test/cases/chunks/statical-dynamic-import/dir4/lib/b.js new file mode 100644 index 000000000..a298658c6 --- /dev/null +++ b/test/cases/chunks/statical-dynamic-import/dir4/lib/b.js @@ -0,0 +1,5 @@ +export function f() { + return 1; +} +export const b = 2; +export const usedExports = __webpack_exports_info__.usedExports; diff --git a/test/cases/chunks/statical-dynamic-import/dir4/lib/index.js b/test/cases/chunks/statical-dynamic-import/dir4/lib/index.js new file mode 100644 index 000000000..2d57f3a8e --- /dev/null +++ b/test/cases/chunks/statical-dynamic-import/dir4/lib/index.js @@ -0,0 +1,4 @@ +export * as a from "./a"; +export * as b from "./b"; + +export const usedExports = __webpack_exports_info__.usedExports; diff --git a/test/cases/chunks/statical-dynamic-import/index.js b/test/cases/chunks/statical-dynamic-import/index.js new file mode 100644 index 000000000..1a81f0eea --- /dev/null +++ b/test/cases/chunks/statical-dynamic-import/index.js @@ -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"]); +}) diff --git a/test/cases/chunks/statical-dynamic-import/test.filter.js b/test/cases/chunks/statical-dynamic-import/test.filter.js new file mode 100644 index 000000000..a8a402b15 --- /dev/null +++ b/test/cases/chunks/statical-dynamic-import/test.filter.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = function filter(config) { + // This test can't run in development mode + return config.mode !== "development"; +}; diff --git a/test/cases/chunks/statical-dynamic-import/warnings.js b/test/cases/chunks/statical-dynamic-import/warnings.js new file mode 100644 index 000000000..fc07af60c --- /dev/null +++ b/test/cases/chunks/statical-dynamic-import/warnings.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = [ + /You don't need `webpackExports` if the usage of dynamic import is statically analyse-able/ +]; diff --git a/test/configCases/parsing/statical-dynamic-import-this/dir4/a.js b/test/configCases/parsing/statical-dynamic-import-this/dir4/a.js new file mode 100644 index 000000000..593fac19a --- /dev/null +++ b/test/configCases/parsing/statical-dynamic-import-this/dir4/a.js @@ -0,0 +1,5 @@ +export const a = 1; +export function f() { + return this.a; +} +export const usedExports = __webpack_exports_info__.usedExports; diff --git a/test/configCases/parsing/statical-dynamic-import-this/dir4/lib/a.js b/test/configCases/parsing/statical-dynamic-import-this/dir4/lib/a.js new file mode 100644 index 000000000..7761ba968 --- /dev/null +++ b/test/configCases/parsing/statical-dynamic-import-this/dir4/lib/a.js @@ -0,0 +1,2 @@ +export const a = 1; +export const usedExports = __webpack_exports_info__.usedExports; diff --git a/test/configCases/parsing/statical-dynamic-import-this/dir4/lib/b.js b/test/configCases/parsing/statical-dynamic-import-this/dir4/lib/b.js new file mode 100644 index 000000000..a298658c6 --- /dev/null +++ b/test/configCases/parsing/statical-dynamic-import-this/dir4/lib/b.js @@ -0,0 +1,5 @@ +export function f() { + return 1; +} +export const b = 2; +export const usedExports = __webpack_exports_info__.usedExports; diff --git a/test/configCases/parsing/statical-dynamic-import-this/dir4/lib/index.js b/test/configCases/parsing/statical-dynamic-import-this/dir4/lib/index.js new file mode 100644 index 000000000..2d57f3a8e --- /dev/null +++ b/test/configCases/parsing/statical-dynamic-import-this/dir4/lib/index.js @@ -0,0 +1,4 @@ +export * as a from "./a"; +export * as b from "./b"; + +export const usedExports = __webpack_exports_info__.usedExports; diff --git a/test/configCases/parsing/statical-dynamic-import-this/index.js b/test/configCases/parsing/statical-dynamic-import-this/index.js new file mode 100644 index 000000000..e378e7dcd --- /dev/null +++ b/test/configCases/parsing/statical-dynamic-import-this/index.js @@ -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"]); +}) diff --git a/test/configCases/parsing/statical-dynamic-import-this/webpack.config.js b/test/configCases/parsing/statical-dynamic-import-this/webpack.config.js new file mode 100644 index 000000000..0310d5448 --- /dev/null +++ b/test/configCases/parsing/statical-dynamic-import-this/webpack.config.js @@ -0,0 +1,8 @@ +"use strict"; + +/** @type {import("../../../../").Configuration} */ +module.exports = { + module: { + strictThisContextOnImports: true + } +};