ECMAScript Modules
ECMAScript Modules (ESM) is a specification for using Modules in the Web. It's supported by all modern browsers and the recommended way of writing modular code for the Web.
Webpack supports processing ECMAScript Modules to optimize them.
Exporting
The export keyword allows to expose things from an ESM to other modules:
export const CONSTANT = 42;
export let variable = 42;
// only reading is exposed
// it's not possible to modify the variable from outside
export function fun() {
console.log("fun");
}
export class C extends Super {
method() {
console.log("method");
}
}
let a, b, other;
export { a, b, other as c };
export default 1 + 2 + 3 + more();Importing
The import keyword allows to get references to things from other modules into an ESM:
// import "bindings" to exports from another module
// these bindings are live. The values are not copied,
// instead accessing "variable" will get the current value
// in the imported module
import { CONSTANT, variable } from "./module.js";
// shortcut to import the "default" export
import theDefaultValue from "./module.js";
// import the "namespace object" which contains all exports
import * as module from "./module.js";
module.fun();When importing a namespace object from an ECMAScript Module, webpack follows the ESM convention of setting Symbol.toStringTag to "Module" on the namespace object.
Flagging modules as ESM
By default webpack will automatically detect whether a file is an ESM or a different module system.
Node.js established a way of explicitly setting the module type of files by using a property in the package.json.
Setting "type": "module" in a package.json does force all files below this package.json to be ECMAScript Modules.
Setting "type": "commonjs" will instead force them to be CommonJS Modules.
{
"type": "module"
}In addition to that, files can set the module type by using .mjs or .cjs extension. .mjs will force them to be ESM, .cjs force them to be CommonJs.
In DataURIs using the text/javascript or application/javascript mime type will also force module type to ESM.
In addition to the module format, flagging modules as ESM also affect the resolving logic, interop logic and the available symbols in modules.
import.meta in ESM
Webpack exposes several import.meta properties for use in ESM:
| Property | Description |
|---|---|
import.meta.url | The URL of the current module file - use it for new Worker() or new URL() |
import.meta.webpack | The webpack major version number (e.g. 5) |
import.meta.webpackHot | Equivalent of module.hot - use for HMR in ESM |
import.meta.webpackContext | ESM equivalent of require.context |
Example - using import.meta.url for assets:
// Resolve a sibling file relative to the current module
const iconUrl = new URL("./icon.png", import.meta.url);
const img = document.createElement("img");
img.src = iconUrl.href;Example - HMR in ESM:
if (import.meta.webpackHot) {
import.meta.webpackHot.accept("./module.js", () => {
// handle update
});
}Top-Level Await
In ESM, you can use await at the top level of a module. Webpack treats the module
as an async module automatically. Enabled by default since 5.83.0; the experiments.topLevelAwait option itself was removed in 5.102.0 (it just works).
// user.js (async ESM module)
const response = await fetch("/api/user");
export const user = await response.json();// index.js - importing an async module works as expected
import { user } from "./user.js";
console.log(user.name);Fully Specified Imports
Imports in ESM are resolved more strictly. Relative requests must include a file extension (e.g. *.js or *.mjs) following the Node.js convention when the file is flagged as ESM:
// will fail - missing extension
import { helper as missingExt } from "./utils";
// correct in ESM
import { helper } from "./utils.js";To disable this check (useful when migrating a large CJS codebase), you can use fullySpecified=false:
// webpack.config.js
export default {
module: {
rules: [
{
test: /\.m?js/,
resolve: {
fullySpecified: false,
},
},
],
},
};CommonJS Interop
CommonJS syntax is not available in ESM: require, module, exports, __filename, __dirname.
When importing from a CommonJS module inside ESM, only the default export
is available (the entire module.exports object):
// esm-consumer.js (ESM)
import cjs from "./cjs-module.js";
// named imports from CJS don't work
import { foo } from "./cjs-module.js"; // undefined
// cjs-module.js (CommonJS)
module.exports = { foo: 1, bar: 2 };
console.log(cjs.foo); // works - cjs is the whole exports objectThis strict behavior applies when webpack treats the imported module as CommonJS.
If that module itself uses ESM export syntax, webpack will auto-detect it as ESM
and named imports will work normally. This commonly affects projects that mix .js files
files in a project that has "type": "module" set - webpack may treat some files as
ESM while third-party packages in node_modules remain CommonJS.
Common Migration Errors
ReferenceError: require is not defined
When a file is treated as ESM, CommonJS globals (require, module, exports,
__filename, __dirname) are unavailable.
Fix: Replace require() with import statements. For conditional or dynamic
loading, use import().
Must use import to load ES Module (Node.js) / SyntaxError: Cannot use import statement in a module (browser)
This happens when a file using ESM import/export syntax is not flagged as ESM -
either "type": "module" is missing from package.json, or the file uses a .js
extension instead of .mjs.
Fix: Add "type": "module" to your package.json, or rename the file to .mjs.
Module not found: Error: Can't resolve './utils' (missing extension)
In ESM, relative imports must include the file extension. Webpack follows the Node.js ESM convention here.
Fix: Change import { helper } from './utils' to import { helper } from './utils.js',
or set fullySpecified: false in your
webpack config to disable the check while migrating.



