Repository navigation
Description
Have you used AI?
Yes
Which part of webpack is affected?
optimization (tree-shaking, splitChunks, concatenation)
Bug Description
With module concatenation enabled (the default in `mode: "production"`), an unused CommonJS
re-export is emitted as a bare parenthesised module reference. When the source file is written
without semicolons, consecutive statements merge into a call chain and the bundle throws while
it is still loading.
`[email protected]` is written in semicolon-free style and re-exports like this:
```js
module.exports.Type = require('./lib/type')
module.exports.Schema = require('./lib/schema')
module.exports.FAILSAFE_SCHEMA = require('./lib/schema/failsafe')
module.exports.JSON_SCHEMA = require('./lib/schema/json')
module.exports.CORE_SCHEMA = require('./lib/schema/core')
module.exports.DEFAULT_SCHEMA = require('./lib/schema/default')
module.exports.load = loader.load
electron-updater only reads load, so the six re-exports above are unused and their
assignments are dropped. 5.110.3 emits what is left as:
Ge()(Fe())(Be())(Ve())(je())(Ue()),e.exports.load=r.load, …Ge() returns js-yaml's Type constructor, so it ends up invoked as a plain function with
Fe() as its argument. The module is in strict mode, so this is undefined and the first
assignment inside Type throws.
The same input builds and runs correctly on 5.110.2.
### Link to Minimal Reproduction and steps to reproduce
Steps:
- npm install
- npm run build
- npm start
Expected: prints loaded object
Actual: throws TypeError while loading the bundle
To confirm the regression:
- npm i -D [email protected] && npm run build && npm start → prints
loaded object - npm i -D [email protected] && npm run build && npm start → throws
Search out/bundle.js for is removed in js-yaml 4 to see the emitted code.
Notes:
- An ES module entry is required: concatenation starts at the ESM root and pulls the CommonJS
dependencies in. With a CommonJS entry the output is fine. - [email protected] is unaffected — it is written with semicolons, so dropping the assignments
still leaves separate statements. electron-stub.jsin the repro only exists so the bundle can run under plain Node;
Electron is not needed to reproduce the bad output.
### Expected Behavior
Dropping an unused re-export must not change how the remaining expression binds to the
following statement. Each re-export should stay a separate statement, as it does in 5.110.2.
### Actual Behavior
The re-exports are emitted as a single chained call expression, and loading the bundle throws:
TypeError: Cannot set properties of undefined (setting 'options')
at e.exports (out/bundle.js:2:185867)
at Object. (out/bundle.js:2:152584)
at out/bundle.js:2:55215
at Object. (out/bundle.js:2:77397)
### Environment
```shell
System:
OS: Linux 6.8 Ubuntu 22.04.5 LTS 22.04.5 LTS (Jammy Jellyfish)
CPU: (12) x64 11th Gen Intel(R) Core(TM) i5-11400H @ 2.70GHz
Memory: 18.07 GB / 30.99 GB
Binaries:
Node: 18.19.0
npm: 10.2.3
Packages:
webpack: 5.110.3 => 5.110.3
webpack-cli: 4.10.0 => 4.10.0
Is this a regression?
Yes (please specify version below)
Last Working Version
v5.110.2
Additional Context
Likely cause.
`ConcatenatedModule` gained this branch in 5.110.3, as part of "Fix `new` on a default import
of a wrapped CommonJS module" (#21876):
```js
} else if (accessorCall && !asCall) {
// without parentheses `new X()` would construct the accessor rather
// than what it returns
return asiSafe === false ? `;(${reference})` : `(${reference})`;
}
The reference is now wrapped in parentheses, and the guarding semicolon is only emitted when
the caller passes asiSafe === false.
CommonJsExportRequireDependency passes asiSafe: true, which holds while the statement keeps
its module.exports.X = left-hand side. For an unused re-export that assignment is removed:
source.replace(
dep.range[0],
dep.range[1] - 1,
used
? `${base}${propertyAccess(used)} = ${requireExpr}`
: `/* unused reexport */ ${requireExpr}`
);What is left is a bare expression, so the parenthesised reference on the next line attaches to
it and ASI never happens.
For comparison, CommonJsExportsDependency handles its unused case by keeping an assignment
(__webpack_unused_export__ = …), which is immune to this. Making the re-export path do the
same produced correct output against the reproduction above, with both concatenateModules
and usedExports left enabled.
Workarounds that avoid the broken output: optimization.concatenateModules: { commonjs: false }
or optimization.usedExports: false.
Possibly related: a hand-written semicolon-free package that hits the binding.info.wrapped
branch produces the same shape of output on 5.110.2 as well, so that path may share the root
cause and predate this regression.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Effort
Activity
blankse commentedon Sep 4, 2026
I hit the same regression independently and filed #21956 before seeing this one — closing mine as a duplicate. Adding what my investigation covers that this report does not, in case it helps narrow the fix.
The trigger does not need a dropped re-export
Here the bare parenthesized reference appears because unused
module.exportsassignments are eliminated. It also happens with nothing eliminated at all: two adjacentrequire()expression statements, where the first has no trailing semicolon, are enough.Four files, webpack and webpack-cli only, no third-party dependency:
src/index.js— theimportis what makes this module ESM and therefore the concatenation root:src/x.js:src/a.jsandsrc/b.js:webpack.config.js:node -e "global.window={}; require('./dist/bundle.js')"throwsTypeError: o(...) is not a functionon 5.110.3 and runs on 5.110.2.Unminified, the difference is only the parentheses:
So the fix needs to hold for a plain
CommonJsRequireDependencytoo, not only for theCommonJsExportRequireDependencypath — no export elimination is involved above.It is the
accessorCall && !asCallbranch, not thebinding.info.wrappedoneConcatenatedModule.jshas the same parenthesizing expression twice. Neutralizing them one at a time in an installed 5.110.3 isolates it:else if (binding.info.wrapped)else if (accessorCall && !asCall)That is the branch #21876 added and the one quoted above, so both reports land on the same line.
Two more data points from patching line 989 in place:
`(${reference})`→`${reference}``;(${reference})`Bisect, and terser is not involved
Same reproduction, only the version changed:
The collapse is already present in the unminified output. Crossing the versions confirms terser is not a factor:
For completeness: #21907 landed in the same release and removed the
ConcatenationScope.registerReplacedRequire/isInsideReplacedRequirecontainment guard. It looks related in the source diff but is not the cause — reverting only the parenthesization is sufficient.Impact note
In our case this took down a production site after a routine dependency bump. The two semicolon-free
require()lines had been in the source for over two years and were harmless until this release, and there is no build error or warning.Reproduction, bisect and branch attribution were produced with Claude Code; every number above comes from an actual build in an isolated directory.
fix(concatenate-modules): guard a module reference against ASI
fix(concatenate-modules): guard a module reference against ASI
fix(concatenate-modules): guard a module reference against ASI (webpa…