Bug report
What is the current behavior?
Since webpack 5.107.1, CommonJS tree-shaking (introduced in #21003, "tree-shake CommonJS exports through const NAME = require(LITERAL) bindings") removes exports that are only referenced lexically before the const X = require(...) declaration — e.g. inside functions defined above a deferred require at the bottom of the file.
This is the exact code shape that webidl2js generates for every wrapper class in whatwg-url (the require is placed at the end of the file to handle circular dependencies):
// whatwg-url/lib/URL.js (generated by webidl2js)
exports.setup = (wrapper, globalObject, constructorArgs, privateData) => {
// ...
new Impl.implementation(constructorArgs, globalObject, privateData)
// ...
}
// ~700 lines later, last line of the file:
const Impl = require("./URL-impl.js");
// whatwg-url/lib/URL-impl.js
exports.implementation = class URLImpl { /* ... */ };
With webpack >= 5.107.1 (production mode, target: 'node'), exports.implementation is removed from the bundle entirely, and the first new URL(...) call throws at runtime:
TypeError: Impl.implementation is not a constructor
at exports.setup (bundle.js)
at new URL (bundle.js)
Real-world impact: any server bundle containing whatwg-url breaks at runtime — including mongodb-connection-string-url → the mongodb driver → mongoose (MongoDB connections fail), and jsdom. We hit this when our Lambda bundle could no longer connect to MongoDB after a routine webpack bump.
Minimal reproduction
Three files, no dependencies:
// src/impl.js
exports.implementation = class URLImpl {
constructor(args) {
this.href = args[0]
}
}
// src/wrapper.js
exports.setup = (obj, constructorArgs) => {
obj._impl = new Impl.implementation(constructorArgs) // ref BEFORE the declaration below
return obj
}
class URL {
constructor(url) {
return exports.setup(Object.create(new.target.prototype), [url])
}
}
exports.interface = URL
// deferred require (handles circular dependencies), as generated by webidl2js
const Impl = require("./impl.js")
// src/entry.js
const { interface: URL } = require("./wrapper.js")
const u = new URL("https://example.com/")
console.log("OK", u._impl.href)
// webpack.config.js
module.exports = {
entry: './src/entry.js',
mode: 'production',
optimization: { minimize: false }, // minimization not required to reproduce
output: { filename: 'bundle.js' },
target: 'node',
}
$ npx webpack && node dist/bundle.js
TypeError: Impl.implementation is not a constructor
at exports.setup (dist/bundle.js:21:15)
at new URL (dist/bundle.js:27:20)
The emitted bundle contains no assignment to implementation at all — the export is tree-shaken even though it is referenced through the Impl binding.
Moving const Impl = require("./impl.js") to the top of wrapper.js makes the bundle work, which suggests the new analysis only collects property accesses that appear after the binding's declaration, missing references hoisted into earlier function bodies (valid code — the functions run after module evaluation completes).
Alternatively, reproducible with the real packages:
// entry.js
const ConnectionString = require('mongodb-connection-string-url').default
new ConnectionString('mongodb://localhost/test') // throws with webpack >= 5.107.1
Bisection
| webpack |
result |
| 5.106.2 |
✅ works |
| 5.107.0 |
✅ works |
| 5.107.1 |
❌ Impl.implementation is not a constructor |
| 5.107.2 |
❌ same |
Introduced by #21003.
What is the expected behavior?
Property reads on the required-module binding should count as export usages regardless of where they appear lexically relative to the const X = require(...) declaration (function bodies above the declaration execute after it), or the optimization should bail out in that case.
Environment
- webpack: 5.107.1 / 5.107.2 (5.107.0 and earlier OK)
- Node.js: 22.x
- OS: reproduced on macOS and Linux (Docker)
- mode:
production; also reproduces with mode: 'none' + optimization: { providedExports: true, sideEffects: true, usedExports: true } and optimization.minimize: false, so it is the usedExports analysis, not minification
Bug report
What is the current behavior?
Since webpack 5.107.1, CommonJS tree-shaking (introduced in #21003, "tree-shake CommonJS exports through
const NAME = require(LITERAL)bindings") removes exports that are only referenced lexically before theconst X = require(...)declaration — e.g. inside functions defined above a deferred require at the bottom of the file.This is the exact code shape that webidl2js generates for every wrapper class in whatwg-url (the require is placed at the end of the file to handle circular dependencies):
With webpack >= 5.107.1 (production mode,
target: 'node'),exports.implementationis removed from the bundle entirely, and the firstnew URL(...)call throws at runtime:Real-world impact: any server bundle containing
whatwg-urlbreaks at runtime — includingmongodb-connection-string-url→ themongodbdriver →mongoose(MongoDB connections fail), andjsdom. We hit this when our Lambda bundle could no longer connect to MongoDB after a routine webpack bump.Minimal reproduction
Three files, no dependencies:
The emitted bundle contains no assignment to
implementationat all — the export is tree-shaken even though it is referenced through theImplbinding.Moving
const Impl = require("./impl.js")to the top ofwrapper.jsmakes the bundle work, which suggests the new analysis only collects property accesses that appear after the binding's declaration, missing references hoisted into earlier function bodies (valid code — the functions run after module evaluation completes).Alternatively, reproducible with the real packages:
Bisection
Impl.implementation is not a constructorIntroduced by #21003.
What is the expected behavior?
Property reads on the required-module binding should count as export usages regardless of where they appear lexically relative to the
const X = require(...)declaration (function bodies above the declaration execute after it), or the optimization should bail out in that case.Environment
production; also reproduces withmode: 'none'+optimization: { providedExports: true, sideEffects: true, usedExports: true }andoptimization.minimize: false, so it is theusedExportsanalysis, not minification