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 }