Have you used AI?
Yes
Which part of webpack is affected?
optimization (tree-shaking, splitChunks, concatenation)
Bug Description
Since 5.110.0, new X(...) is mis-compiled when X is the default import of a
CommonJS module and the importing module is strict ESM (.mjs, or .js under
"type": "module").
The default import binds directly to the inner module's lazy accessor, and the
emitted code is
new thing_namespaceFn()({ a: 1 })
which JavaScript parses as (new thing_namespaceFn())({ a: 1 }). The new is
applied to the accessor itself rather than to its return value. Since
__webpack_require__.cw returns an arrow function, and arrow functions are not
constructors, this throws at runtime.
The required output is new (thing_namespaceFn())({ a: 1 }).
Link to Minimal Reproduction and steps to reproduce
https://github.com/rproserpio/webpack-cjs-concat-new-repro
Two source files and a config, no dependencies beyond webpack.
src/thing.cjs
"use strict";
class Thing {
constructor(options) {
this.options = options;
}
}
module.exports = Thing;
src/entry.mjs
import Thing from "./thing.cjs";
export function make() {
return new Thing({ a: 1 });
}
webpack.config.mjs
export default {
mode: "production",
target: "node",
entry: { index: "./src/entry.mjs" },
optimization: { minimize: false },
experiments: { outputModule: true },
output: {
filename: "[name].js",
library: { type: "module", export: "make" },
module: true,
},
};
Steps:
npm install
npx webpack
node -e "import('./dist/index.js').then(m => m.make())"
Expected Behavior
make() returns a Thing instance. This is what 5.109.2 and earlier do.
dist/index.js, webpack 5.109.2:
function make() {
return new thing_namespaceObject({ a: 1 });
}
Actual Behavior
TypeError: thing_namespaceFn is not a constructor
dist/index.js, webpack 5.110.0 and later:
var thing_namespaceFn = /*#__PURE__*/__webpack_require__.cw(function(module, exports) {
class Thing {
constructor(options) {
this.options = options;
}
}
module.exports = Thing;
});
;// ./src/thing.cjs
thing_namespaceFn();
;// ./src/entry.mjs
function make() {
return new thing_namespaceFn()({ a: 1 });
}
Environment
Binaries:
Node: 24.20.0
npm: 11.19.0
pnpm: 10.34.5
Packages:
webpack: 5.110.2
webpack-cli: 6.0.1
Is this a regression?
Yes (please specify version below)
Last Working Version
v5.109.2
Additional Context
Version bisect
Same repro, only the webpack version changed. Each row is an actual make()
call, not just a reading of the emitted text:
| webpack |
emitted |
make() |
| 5.108.4 |
new _thing_cjs__WEBPACK_IMPORTED_MODULE_0__({ a: 1 }) |
Thing { options: { a: 1 } } |
| 5.109.0 |
new thing_namespaceObject({ a: 1 }) |
Thing { options: { a: 1 } } |
| 5.109.1 |
new thing_namespaceObject({ a: 1 }) |
Thing { options: { a: 1 } } |
| 5.109.2 |
new thing_namespaceObject({ a: 1 }) |
Thing { options: { a: 1 } } |
| 5.110.0 |
new thing_namespaceFn()({ a: 1 }) |
TypeError |
| 5.110.1 |
new thing_namespaceFn()({ a: 1 }) |
TypeError |
| 5.110.2 |
new thing_namespaceFn()({ a: 1 }) |
TypeError |
The three 5.109.x bundles are byte-identical to each other, as are the three
5.110.x ones, so the boundary is exactly 5.109.2 -> 5.110.0. The repro repo keeps
one emitted bundle per version under builds/.
Diffing the two emitted bundles across that boundary shows the whole mechanism:
-var thing_namespaceObject = /*#__PURE__*/__webpack_require__.cjs(function(module, exports) {
+var thing_namespaceFn = /*#__PURE__*/__webpack_require__.cw(function(module, exports) {
class Thing {
constructor(options) {
this.options = options;
}
}
module.exports = Thing;
});
+;// ./src/thing.cjs
+thing_namespaceFn();
+
;// ./src/entry.mjs
function make() {
- return new thing_namespaceObject({ a: 1 });
+ return new thing_namespaceFn()({ a: 1 });
}
5.109 already had CommonJS concatenation, but under __webpack_require__.cjs,
which produced an eagerly evaluated namespaceObject identifier.
new <identifier>(...) needs no parentheses, so it was correct.
5.110.0 replaced that with __webpack_require__.cw, a lazy memoized accessor
(#21519, listed in the 5.110.0 release notes). Module evaluation is now forced by
a separate thing_namespaceFn(); statement, and the reference itself became a
call expression — but the substitution site still emits it bare, so the new
binds to the accessor instead of to its return value.
The javascript/auto path is already correct
Renaming entry.mjs to entry.js (so the importer is javascript/auto rather
than strict ESM) makes webpack route through the __webpack_require__.n()
interop, and there the parentheses are emitted correctly:
return new (thing_default()())({ a: 1 });
So the escaping rule exists — it is just not reached on the direct binding path
that strict-ESM importers take, where the default import resolves straight to
module.exports.
Suspected cause
getFinalName in lib/optimize/ConcatenatedModule.js (unchanged on main as of
2026-08-31) only parenthesises inside the isPropertyAccess branch:
if (isPropertyAccess) {
if (asCall && callContext === false) {
return asiSafe
? `(0,${reference})`
: asiSafe === false
? `;(0,${reference})`
: `/*#__PURE__*/Object(${reference})`;
} else if (binding.info.wrapped) {
return asiSafe === false ? `;(${reference})` : `(${reference})`;
}
}
return reference;
isPropertyAccess is ids.length > 0 for a raw-name binding. A strict-ESM
default import of a wrapped CommonJS module has ids.length === 0, so the
binding.info.wrapped check never runs and the bare thing_namespaceFn() is
returned. binding.info.wrapped already encodes "this reference is a call
expression and needs wrapping"; it just is not consulted when there is no
property access.
Impact
Not limited to experiments.outputModule — output.library.type: "commonjs2"
produces the same broken new thing_namespaceFn()(...).
This is silent: the build succeeds with no warning, and the failure only appears
when the affected line executes. We hit it in production as
ajv_namespaceFn is not a constructor, from new Ajv(...) in
@modelcontextprotocol/sdk's dist/esm/validation/ajv-provider.js (that package
is "type": "module", so every file in it takes the strict-ESM path). The same
build also mis-compiled new HttpsProxyAgent(...) inside axios, which would
only have thrown once a proxy env var was set.
Workaround
optimization: {
concatenateModules: { commonjs: false },
},
ESM concatenation and tree shaking are unaffected, and in our case the bundle
came out marginally smaller.
Have you used AI?
Yes
Which part of webpack is affected?
optimization (tree-shaking, splitChunks, concatenation)
Bug Description
Since 5.110.0,
new X(...)is mis-compiled whenXis the default import of aCommonJS module and the importing module is strict ESM (
.mjs, or.jsunder"type": "module").The default import binds directly to the inner module's lazy accessor, and the
emitted code is
which JavaScript parses as
(new thing_namespaceFn())({ a: 1 }). Thenewisapplied to the accessor itself rather than to its return value. Since
__webpack_require__.cwreturns an arrow function, and arrow functions are notconstructors, this throws at runtime.
The required output is
new (thing_namespaceFn())({ a: 1 }).Link to Minimal Reproduction and steps to reproduce
https://github.com/rproserpio/webpack-cjs-concat-new-repro
Two source files and a config, no dependencies beyond webpack.
src/thing.cjssrc/entry.mjswebpack.config.mjsSteps:
npm installnpx webpacknode -e "import('./dist/index.js').then(m => m.make())"Expected Behavior
make()returns aThinginstance. This is what 5.109.2 and earlier do.dist/index.js, webpack 5.109.2:Actual Behavior
dist/index.js, webpack 5.110.0 and later:Environment
Binaries: Node: 24.20.0 npm: 11.19.0 pnpm: 10.34.5 Packages: webpack: 5.110.2 webpack-cli: 6.0.1Is this a regression?
Yes (please specify version below)
Last Working Version
v5.109.2
Additional Context
Version bisect
Same repro, only the webpack version changed. Each row is an actual
make()call, not just a reading of the emitted text:
make()new _thing_cjs__WEBPACK_IMPORTED_MODULE_0__({ a: 1 })Thing { options: { a: 1 } }new thing_namespaceObject({ a: 1 })Thing { options: { a: 1 } }new thing_namespaceObject({ a: 1 })Thing { options: { a: 1 } }new thing_namespaceObject({ a: 1 })Thing { options: { a: 1 } }new thing_namespaceFn()({ a: 1 })TypeErrornew thing_namespaceFn()({ a: 1 })TypeErrornew thing_namespaceFn()({ a: 1 })TypeErrorThe three 5.109.x bundles are byte-identical to each other, as are the three
5.110.x ones, so the boundary is exactly 5.109.2 -> 5.110.0. The repro repo keeps
one emitted bundle per version under
builds/.Diffing the two emitted bundles across that boundary shows the whole mechanism:
5.109 already had CommonJS concatenation, but under
__webpack_require__.cjs,which produced an eagerly evaluated
namespaceObjectidentifier.new <identifier>(...)needs no parentheses, so it was correct.5.110.0 replaced that with
__webpack_require__.cw, a lazy memoized accessor(#21519, listed in the 5.110.0 release notes). Module evaluation is now forced by
a separate
thing_namespaceFn();statement, and the reference itself became acall expression — but the substitution site still emits it bare, so the
newbinds to the accessor instead of to its return value.
The
javascript/autopath is already correctRenaming
entry.mjstoentry.js(so the importer isjavascript/autoratherthan strict ESM) makes webpack route through the
__webpack_require__.n()interop, and there the parentheses are emitted correctly:
So the escaping rule exists — it is just not reached on the direct binding path
that strict-ESM importers take, where the default import resolves straight to
module.exports.Suspected cause
getFinalNameinlib/optimize/ConcatenatedModule.js(unchanged onmainas of2026-08-31) only parenthesises inside the
isPropertyAccessbranch:isPropertyAccessisids.length > 0for a raw-name binding. A strict-ESMdefault import of a wrapped CommonJS module has
ids.length === 0, so thebinding.info.wrappedcheck never runs and the barething_namespaceFn()isreturned.
binding.info.wrappedalready encodes "this reference is a callexpression and needs wrapping"; it just is not consulted when there is no
property access.
Impact
Not limited to
experiments.outputModule—output.library.type: "commonjs2"produces the same broken
new thing_namespaceFn()(...).This is silent: the build succeeds with no warning, and the failure only appears
when the affected line executes. We hit it in production as
ajv_namespaceFn is not a constructor, fromnew Ajv(...)in@modelcontextprotocol/sdk'sdist/esm/validation/ajv-provider.js(that packageis
"type": "module", so every file in it takes the strict-ESM path). The samebuild also mis-compiled
new HttpsProxyAgent(...)insideaxios, which wouldonly have thrown once a proxy env var was set.
Workaround
ESM concatenation and tree shaking are unaffected, and in our case the bundle
came out marginally smaller.