webpack 4: import() and CommonJs
One of the breaking changes in webpack 4 is the behavior of import()
when importing non-ESM (i. e. CommonJS modules).
Actually there are a lot of cases to consider when using import()
.
But let’s start with few naming hints:
Source: the module containing the import()
expression
Target: the module referenced by the request in the import()
expression
non-ESM: a CommonJs or AMD module not setting __esModule: true
transpiled-ESM: a CommonJS module setting __esModule: true
because it was transpiled from ESM
ESM: a normal EcmaScript module
strict-ESM: a more strict EcmaScript module i. e. from a .mjs
file
JSON: a json file
These cases need to be considered:
- (A) Source: non-ESM, transpiled-ESM or ESM
- (B) Source: strict-ESM (mjs)
- (1) Target: non-ESM
- (2) Target: transpiled-ESM (
__esModule
) - (3) Target: ESM or strict-ESM (mjs)
- (4) Target: JSON
Here are some examples to make it easier to understand:
// (A) source.js
import("./target").then(result => console.log(result));// (B) source.mjs
import("./target").then(result => console.log(result));// (1) target.js
exports.name = "name";
exports.default = "default";// (2) target.js
exports.__esModule = true;
exports.name = "name";
exports.default = "default";// (3) target.js or target.mjs
export const name = "name";
export default "default";// (4) target.json
{ name: "name", default: "default" }
Let’s start with the easy ones:
A3 and B3: import(ESM)
These cases are actually covered by the ESM spec. They are the only ones spec’ed.
import()
will resolve to the namespace object of the target module. For compatibility we also add a __esModule
flag to the namespace object to be usable by transpiled imports.
{ __esModule: true, name: "name", default: "default" }
A1: import(CJS)
We import a CommonJs module. webpack 3 just resolved to the value of module.exports
. webpack 4 will now create a artificial namespace object for the CommonJs module, to let import()
consistently resolve to namespace objects.
The default export of a CommonJs module is always the value of module.exports
. webpack also allows to pick properties from a CommonJs module via import import { property } from "cjs"
, so we allow this for import()
.
Note: In this case the property default
is hidden by the default default
.
// webpack 3
{ name: "name", default: "default" }// webpack 4
{ name: "name", default: { name: "name", default: "default" } }
B1: import(CJS).mjs
In strict-ESM we don’t allow picking properties via import
and only allow the default export for non-ESM.
{ default: { name: "name", default: "default" } }
A2: import(transpiled-ESM)
webpack supports the __esModule
flag, with upgrades a CJS module to a ESM.
{ __esModule: true, name: "name", default: "default" }
B2: import(transpiled-ESM).mjs
In strict-ESM the __esModule
flag is not supported.
You could call this broken, but at least it’s consistent with node.js.
{ default: { __esModule: true, name: "name", default: "default" } }
A4 and B4: import(json)
Property picking is also supported when importing JSON, even in strict-ESM.
JSON also exposes the complete object as default export.
{ name: "name", default: { name: "name", default: "default" } }
So in summary only a single case has changed. It’s not that problematic when exporting an object. But you’ll get into trouble when using module.exports
with non-objects.
Example:
module.exports = 42;
You would need to use the default
property.
// webpack 3
42// webpack 4
{ default: 42 }