diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 455440cb7..ff1edb359 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -173,7 +173,8 @@ jobs: - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: - node-version: ${{ matrix.node-version }} + # TODO: Remove version override when https://github.com/nodejs/node/issues/59480 is fixed + node-version: ${{ matrix.node-version == '24.x' && '24.5.0' || matrix.node-version }} architecture: ${{ steps.calculate_architecture.outputs.result }} cache: "yarn" # Install old `jest` version and deps for legacy node versions diff --git a/examples/persistent-caching/README.md b/examples/persistent-caching/README.md index 4c471076a..9fcfa8042 100644 --- a/examples/persistent-caching/README.md +++ b/examples/persistent-caching/README.md @@ -59,28 +59,28 @@ module.exports = (env = "development") => ({ ``` asset output.js 3.61 MiB [emitted] (name: main) -chunk (runtime: main) output.js (main) 2.24 MiB (javascript) 1.29 KiB (runtime) [entry] +chunk (runtime: main) output.js (main) 2.25 MiB (javascript) 1.29 KiB (runtime) [entry] > ./example.js main - cached modules 2.24 MiB (javascript) 1.29 KiB (runtime) [cached] 1516 modules + cached modules 2.25 MiB (javascript) 1.29 KiB (runtime) [cached] 1523 modules webpack X.X.X compiled successfully ``` ## Production mode ``` -asset output.js 548 KiB [emitted] [minimized] [big] (name: main) 1 related asset +asset output.js 549 KiB [emitted] [minimized] [big] (name: main) 1 related asset chunk (runtime: main) output.js (main) 2.19 MiB (javascript) 1.29 KiB (runtime) [entry] > ./example.js main - cached modules 2.19 MiB (javascript) 1.29 KiB (runtime) [cached] 893 modules + cached modules 2.19 MiB (javascript) 1.29 KiB (runtime) [cached] 900 modules WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB). This can impact web performance. Assets: - output.js (548 KiB) + output.js (549 KiB) WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance. Entrypoints: - main (548 KiB) + main (549 KiB) output.js WARNING in webpack performance recommendations: diff --git a/examples/virtual-modules/README.md b/examples/virtual-modules/README.md index 9413ee0b4..9afd3f6b7 100644 --- a/examples/virtual-modules/README.md +++ b/examples/virtual-modules/README.md @@ -539,7 +539,10 @@ const msg = "from virtual module with custom scheme"; /******/ // "1" is the signal for "already loaded" /******/ if(!installedChunks[chunkId]) { /******/ if(true) { // all chunks have JS -/******/ installChunk(require("./" + __webpack_require__.u(chunkId))); +/******/ var installedChunk = require("./" + __webpack_require__.u(chunkId)); +/******/ if (!installedChunks[chunkId]) { +/******/ installChunk(installedChunk); +/******/ } /******/ } else installedChunks[chunkId] = 1; /******/ } /******/ }; @@ -572,13 +575,13 @@ const msg = "from virtual module with custom scheme"; ## Unoptimized ``` -asset output.js 16.3 KiB [emitted] (name: main) +asset output.js 16.4 KiB [emitted] (name: main) asset 2.output.js 815 bytes [emitted] asset 1.output.js 814 bytes [emitted] -chunk (runtime: main) output.js (main) 1.46 KiB (javascript) 4.13 KiB (runtime) [entry] [rendered] +chunk (runtime: main) output.js (main) 1.46 KiB (javascript) 4.21 KiB (runtime) [entry] [rendered] > ./example.js main dependent modules 514 bytes [dependent] 8 modules - runtime modules 4.13 KiB 7 modules + runtime modules 4.21 KiB 7 modules ./example.js 977 bytes [built] [code generated] [no exports] [used exports unknown] @@ -601,7 +604,7 @@ webpack X.X.X compiled successfully ## Production mode ``` -asset output.js 2.5 KiB [emitted] [minimized] (name: main) +asset output.js 2.52 KiB [emitted] [minimized] (name: main) asset 263.output.js 121 bytes [emitted] [minimized] asset 722.output.js 121 bytes [emitted] [minimized] chunk (runtime: main) 263.output.js 20 bytes [rendered] @@ -614,10 +617,10 @@ chunk (runtime: main) 722.output.js 20 bytes [rendered] ./routes/b.js 20 bytes [built] [code generated] [exports: default] import() ./routes/b.js virtual:routes 2:9-32 -chunk (runtime: main) output.js (main) 1.46 KiB (javascript) 4.13 KiB (runtime) [entry] [rendered] +chunk (runtime: main) output.js (main) 1.46 KiB (javascript) 4.21 KiB (runtime) [entry] [rendered] > ./example.js main dependent modules 514 bytes [dependent] 8 modules - runtime modules 4.13 KiB 7 modules + runtime modules 4.21 KiB 7 modules ./example.js 977 bytes [built] [code generated] [no exports] [no exports used] diff --git a/lib/ConstPlugin.js b/lib/ConstPlugin.js index 82ac18cac..5efb75a45 100644 --- a/lib/ConstPlugin.js +++ b/lib/ConstPlugin.js @@ -180,7 +180,7 @@ class ConstPlugin { ? statement.alternate : statement.consequent; if (branchToRemove) { - this.eliminateUnusedStatement(parser, branchToRemove); + this.eliminateUnusedStatement(parser, branchToRemove, true); } return bool; } @@ -193,7 +193,7 @@ class ConstPlugin { ) { return; } - this.eliminateUnusedStatement(parser, statement); + this.eliminateUnusedStatement(parser, statement, false); return true; }); parser.hooks.expressionConditionalOperator.tap( @@ -509,9 +509,10 @@ class ConstPlugin { * Eliminate an unused statement. * @param {JavascriptParser} parser the parser * @param {Statement} statement the statement to remove + * @param {boolean} alwaysInBlock whether to always generate curly brackets * @returns {void} */ - eliminateUnusedStatement(parser, statement) { + eliminateUnusedStatement(parser, statement, alwaysInBlock) { // Before removing the unused branch, the hoisted declarations // must be collected. // @@ -545,8 +546,14 @@ class ConstPlugin { const declarations = parser.scope.isStrict ? getHoistedDeclarations(statement, false) : getHoistedDeclarations(statement, true); - const replacement = - declarations.length > 0 ? `{ var ${declarations.join(", ")}; }` : "{}"; + + const inBlock = alwaysInBlock || statement.type === "BlockStatement"; + + let replacement = inBlock ? "{" : ""; + replacement += + declarations.length > 0 ? ` var ${declarations.join(", ")}; ` : ""; + replacement += inBlock ? "}" : ""; + const dep = new ConstDependency( `// removed by dead control flow\n${replacement}`, /** @type {Range} */ (statement.range) diff --git a/lib/DefinePlugin.js b/lib/DefinePlugin.js index e3d75973f..b3270f75d 100644 --- a/lib/DefinePlugin.js +++ b/lib/DefinePlugin.js @@ -489,6 +489,13 @@ class DefinePlugin { if (nested && !hooked.has(nested)) { // only detect the same nested key once hooked.add(nested); + parser.hooks.collectDestructuringAssignmentProperties.tap( + PLUGIN_NAME, + (expr) => { + const nameInfo = parser.getNameForExpression(expr); + if (nameInfo && nameInfo.name === nested) return true; + } + ); parser.hooks.expression.for(nested).tap( { name: PLUGIN_NAME, @@ -687,6 +694,13 @@ class DefinePlugin { PLUGIN_NAME, withValueDependency(key, evaluateToString("object")) ); + parser.hooks.collectDestructuringAssignmentProperties.tap( + PLUGIN_NAME, + (expr) => { + const nameInfo = parser.getNameForExpression(expr); + if (nameInfo && nameInfo.name === key) return true; + } + ); parser.hooks.expression.for(key).tap(PLUGIN_NAME, (expr) => { addValueDependency(key); let strCode = stringifyObj( diff --git a/lib/async-modules/AwaitDependenciesInitFragment.js b/lib/async-modules/AwaitDependenciesInitFragment.js index 0928d5927..a0a25a8e8 100644 --- a/lib/async-modules/AwaitDependenciesInitFragment.js +++ b/lib/async-modules/AwaitDependenciesInitFragment.js @@ -62,9 +62,12 @@ class AwaitDependenciesInitFragment extends InitFragment { this.dependencies.size === 1 || !runtimeTemplate.supportsDestructuring() ) { + templateInput.push( + "var __webpack_async_dependencies_result__ = (__webpack_async_dependencies__.then ? (await __webpack_async_dependencies__)() : __webpack_async_dependencies__);" + ); for (const [index, importVar] of importVars.entries()) { templateInput.push( - `${importVar} = (__webpack_async_dependencies__.then ? (await __webpack_async_dependencies__)() : __webpack_async_dependencies__)[${index}];` + `${importVar} = __webpack_async_dependencies_result__[${index}];` ); } } else { diff --git a/lib/dependencies/HarmonyImportDependencyParserPlugin.js b/lib/dependencies/HarmonyImportDependencyParserPlugin.js index a54a4542c..7eaba5ed7 100644 --- a/lib/dependencies/HarmonyImportDependencyParserPlugin.js +++ b/lib/dependencies/HarmonyImportDependencyParserPlugin.js @@ -8,7 +8,10 @@ const CommentCompilationWarning = require("../CommentCompilationWarning"); const HotModuleReplacementPlugin = require("../HotModuleReplacementPlugin"); const WebpackError = require("../WebpackError"); -const { getImportAttributes } = require("../javascript/JavascriptParser"); +const { + VariableInfo, + getImportAttributes +} = require("../javascript/JavascriptParser"); const InnerGraph = require("../optimize/InnerGraph"); const ConstDependency = require("./ConstDependency"); const HarmonyAcceptDependency = require("./HarmonyAcceptDependency"); @@ -211,6 +214,20 @@ module.exports = class HarmonyImportDependencyParserPlugin { InnerGraph.onUsage(parser.state, (e) => (dep.usedByExports = e)); return true; }); + parser.hooks.collectDestructuringAssignmentProperties.tap( + PLUGIN_NAME, + (expr) => { + const nameInfo = parser.getNameForExpression(expr); + if ( + nameInfo && + nameInfo.rootInfo instanceof VariableInfo && + nameInfo.rootInfo.name && + parser.getTagData(nameInfo.rootInfo.name, harmonySpecifierTag) + ) { + return true; + } + } + ); parser.hooks.expression .for(harmonySpecifierTag) .tap(PLUGIN_NAME, (expr) => { diff --git a/lib/dependencies/ImportMetaPlugin.js b/lib/dependencies/ImportMetaPlugin.js index 17817f7de..e904a526c 100644 --- a/lib/dependencies/ImportMetaPlugin.js +++ b/lib/dependencies/ImportMetaPlugin.js @@ -96,6 +96,12 @@ class ImportMetaPlugin { PLUGIN_NAME, toConstantDependency(parser, JSON.stringify("object")) ); + parser.hooks.collectDestructuringAssignmentProperties.tap( + PLUGIN_NAME, + (expr) => { + if (expr.type === "MetaProperty") return true; + } + ); parser.hooks.expression .for("import.meta") .tap(PLUGIN_NAME, (metaProperty) => { diff --git a/lib/dependencies/ImportParserPlugin.js b/lib/dependencies/ImportParserPlugin.js index 8ad5da34b..7b9904a4e 100644 --- a/lib/dependencies/ImportParserPlugin.js +++ b/lib/dependencies/ImportParserPlugin.js @@ -46,6 +46,12 @@ class ImportParserPlugin { */ const exportsFromEnumerable = (enumerable) => Array.from(enumerable, (e) => [e]); + parser.hooks.collectDestructuringAssignmentProperties.tap( + PLUGIN_NAME, + (expr) => { + if (expr.type === "ImportExpression") return true; + } + ); parser.hooks.importCall.tap(PLUGIN_NAME, (expr) => { const param = parser.evaluateExpression(expr.source); diff --git a/lib/javascript/JavascriptParser.js b/lib/javascript/JavascriptParser.js index f6741b03b..995a45bee 100644 --- a/lib/javascript/JavascriptParser.js +++ b/lib/javascript/JavascriptParser.js @@ -522,6 +522,10 @@ class JavascriptParser extends Parser { varDeclarationVar: new HookMap(() => new SyncBailHook(["declaration"])), /** @type {HookMap>} */ pattern: new HookMap(() => new SyncBailHook(["pattern"])), + /** @type {SyncBailHook<[Expression], boolean | void>} */ + collectDestructuringAssignmentProperties: new SyncBailHook([ + "expression" + ]), /** @type {HookMap>} */ canRename: new HookMap(() => new SyncBailHook(["initExpression"])), /** @type {HookMap>} */ @@ -2607,34 +2611,48 @@ class JavascriptParser extends Parser { * @param {AssignmentExpression} expression assignment expression */ preWalkAssignmentExpression(expression) { + this.enterDestructuringAssignment(expression.left, expression.right); + } + + /** + * @param {Pattern} pattern pattern + * @param {Expression} expression assignment expression + * @returns {Expression | undefined} destructuring expression + */ + enterDestructuringAssignment(pattern, expression) { if ( - expression.left.type !== "ObjectPattern" || + pattern.type !== "ObjectPattern" || !this.destructuringAssignmentProperties ) { return; } - const keys = this._preWalkObjectPattern(expression.left); - if (!keys) return; - // check multiple assignments - if (this.destructuringAssignmentProperties.has(expression)) { - const set = - /** @type {Set} */ - (this.destructuringAssignmentProperties.get(expression)); - this.destructuringAssignmentProperties.delete(expression); - for (const id of set) keys.add(id); + const expr = + expression.type === "AwaitExpression" ? expression.argument : expression; + + const destructuring = + expr.type === "AssignmentExpression" + ? this.enterDestructuringAssignment(expr.left, expr.right) + : this.hooks.collectDestructuringAssignmentProperties.call(expr) + ? expr + : undefined; + + if (destructuring) { + const keys = this._preWalkObjectPattern(pattern); + if (!keys) return; + + // check multiple assignments + if (this.destructuringAssignmentProperties.has(destructuring)) { + const set = + /** @type {Set} */ + (this.destructuringAssignmentProperties.get(destructuring)); + for (const id of keys) set.add(id); + } else { + this.destructuringAssignmentProperties.set(destructuring, keys); + } } - this.destructuringAssignmentProperties.set( - expression.right.type === "AwaitExpression" - ? expression.right.argument - : expression.right, - keys - ); - - if (expression.right.type === "AssignmentExpression") { - this.preWalkAssignmentExpression(expression.right); - } + return destructuring; } /** @@ -2995,25 +3013,8 @@ class JavascriptParser extends Parser { * @param {VariableDeclarator} declarator variable declarator */ preWalkVariableDeclarator(declarator) { - if ( - !declarator.init || - declarator.id.type !== "ObjectPattern" || - !this.destructuringAssignmentProperties - ) { - return; - } - const keys = this._preWalkObjectPattern(declarator.id); - - if (!keys) return; - this.destructuringAssignmentProperties.set( - declarator.init.type === "AwaitExpression" - ? declarator.init.argument - : declarator.init, - keys - ); - - if (declarator.init.type === "AssignmentExpression") { - this.preWalkAssignmentExpression(declarator.init); + if (declarator.init) { + this.enterDestructuringAssignment(declarator.id, declarator.init); } } @@ -5179,7 +5180,7 @@ class JavascriptParser extends Parser { } /** - * @param {MemberExpression} expression an expression + * @param {Expression} expression an expression * @returns {{ name: string, rootInfo: ExportedVariableInfo, getMembers: () => string[]} | undefined} name info */ getNameForExpression(expression) { diff --git a/test/cases/esm/import-meta/index.js b/test/cases/esm/import-meta/index.js index 97fea14af..0cbd61d00 100644 --- a/test/cases/esm/import-meta/index.js +++ b/test/cases/esm/import-meta/index.js @@ -48,10 +48,16 @@ it("should add warning on direct import.meta usage", () => { expect(Object.keys(import.meta)).toHaveLength(0); }); -it("should support destructuring assignment", () => { +it("should support destructuring assignment", async () => { let version, url2, c; ({ webpack: version } = { url: url2 } = { c } = import.meta); expect(version).toBeTypeOf("number"); expect(url2).toBe(url); expect(c).toBe(undefined); + + let version2, url3, d; + ({ webpack: version2 } = await ({ url: url3 } = ({ d } = await import.meta))); + expect(version2).toBeTypeOf("number"); + expect(url3).toBe(url); + expect(d).toBe(undefined); }); diff --git a/test/configCases/async-module/issue-19803/a.js b/test/configCases/async-module/issue-19803/a.js new file mode 100644 index 000000000..9cfe94a72 --- /dev/null +++ b/test/configCases/async-module/issue-19803/a.js @@ -0,0 +1,2 @@ +await 1; +export const a = "a" \ No newline at end of file diff --git a/test/configCases/async-module/issue-19803/b.js b/test/configCases/async-module/issue-19803/b.js new file mode 100644 index 000000000..100314f82 --- /dev/null +++ b/test/configCases/async-module/issue-19803/b.js @@ -0,0 +1,2 @@ +await 1; +export const b = "b" \ No newline at end of file diff --git a/test/configCases/async-module/issue-19803/c.js b/test/configCases/async-module/issue-19803/c.js new file mode 100644 index 000000000..db512b575 --- /dev/null +++ b/test/configCases/async-module/issue-19803/c.js @@ -0,0 +1,4 @@ +import {a} from "./a"; +import {b} from "./b"; + +export const c = a + b \ No newline at end of file diff --git a/test/configCases/async-module/issue-19803/d.js b/test/configCases/async-module/issue-19803/d.js new file mode 100644 index 000000000..860f9255c --- /dev/null +++ b/test/configCases/async-module/issue-19803/d.js @@ -0,0 +1,3 @@ +import {c} from "./c"; + +export const d = c \ No newline at end of file diff --git a/test/configCases/async-module/issue-19803/index.js b/test/configCases/async-module/issue-19803/index.js new file mode 100644 index 000000000..c921c6254 --- /dev/null +++ b/test/configCases/async-module/issue-19803/index.js @@ -0,0 +1,5 @@ +it("should work", () => { + return import("./d").then(d => { + expect(d.d).toBe("ab"); + }); +}); \ No newline at end of file diff --git a/test/configCases/async-module/issue-19803/webpack.config.js b/test/configCases/async-module/issue-19803/webpack.config.js new file mode 100644 index 000000000..ae2274e3b --- /dev/null +++ b/test/configCases/async-module/issue-19803/webpack.config.js @@ -0,0 +1,10 @@ +"use strict"; + +/** @type {import("../../../../").Configuration} */ +module.exports = { + output: { + environment: { + destructuring: false + } + } +}; diff --git a/test/runner/index.js b/test/runner/index.js index b9f4e3bc6..0a8ef5c84 100644 --- a/test/runner/index.js +++ b/test/runner/index.js @@ -402,7 +402,7 @@ class TestRunner { ); const esmCache = new Map(); const { category, name, round } = this.testMeta; - const esmIdentifier = `${category.name}-${name}-${round || 0}`; + const esmIdentifier = `${category}-${name}-${round || 0}`; let esmContext = null; return (moduleInfo, context) => { const asModule = require("../helpers/asModule"); diff --git a/types.d.ts b/types.d.ts index b92f44951..a13716678 100644 --- a/types.d.ts +++ b/types.d.ts @@ -663,12 +663,12 @@ declare abstract class BasicEvaluatedExpression { | MethodDefinition | PropertyDefinition | VariableDeclarator - | SwitchCase - | CatchClause | ObjectPattern | ArrayPattern | RestElement | AssignmentPattern + | SwitchCase + | CatchClause | Property | AssignmentProperty | ClassBody @@ -894,12 +894,12 @@ declare abstract class BasicEvaluatedExpression { | MethodDefinition | PropertyDefinition | VariableDeclarator - | SwitchCase - | CatchClause | ObjectPattern | ArrayPattern | RestElement | AssignmentPattern + | SwitchCase + | CatchClause | Property | AssignmentProperty | ClassBody @@ -6855,6 +6855,10 @@ declare class JavascriptParser extends ParserClass { varDeclarationUsing: HookMap>; varDeclarationVar: HookMap>; pattern: HookMap>; + collectDestructuringAssignmentProperties: SyncBailHook< + [Expression], + boolean | void + >; canRename: HookMap>; rename: HookMap>; assign: HookMap>; @@ -7329,6 +7333,38 @@ declare class JavascriptParser extends ParserClass { ): void; blockPreWalkExpressionStatement(statement: ExpressionStatement): void; preWalkAssignmentExpression(expression: AssignmentExpression): void; + enterDestructuringAssignment( + pattern: Pattern, + expression: Expression + ): + | undefined + | ImportExpressionImport + | UnaryExpression + | ArrayExpression + | ArrowFunctionExpression + | AssignmentExpression + | AwaitExpression + | BinaryExpression + | SimpleCallExpression + | NewExpression + | ChainExpression + | ClassExpression + | ConditionalExpression + | FunctionExpression + | Identifier + | SimpleLiteral + | RegExpLiteral + | BigIntLiteral + | LogicalExpression + | MemberExpression + | MetaProperty + | ObjectExpression + | SequenceExpression + | TaggedTemplateExpression + | TemplateLiteral + | ThisExpression + | UpdateExpression + | YieldExpression; modulePreWalkImportDeclaration( statement: ImportDeclarationJavascriptParser ): void; @@ -7874,7 +7910,7 @@ declare class JavascriptParser extends ParserClass { allowedTypes: number ): undefined | CallExpressionInfo | ExpressionExpressionInfo; getNameForExpression( - expression: MemberExpression + expression: Expression ): | undefined | {