Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 138 additions & 4 deletions src/content/guides/ecma-script-modules.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: ECMAScript Modules
sort: 19
contributors:
- sokra
- ryzrr
related:
- title: ECMAScript Modules in Node.js
url: https://nodejs.org/api/esm.html
Expand Down Expand Up @@ -84,12 +85,145 @@ In DataURIs using the `text/javascript` or `application/javascript` mime type wi

In addition to the module format, flagging modules as ESM also affect the resolving logic, interop logic and the available symbols in modules.

Imports in ESM are resolved more strictly. Relative requests must include a filename and file extension (e.g. `*.js` or `*.mjs`) unless you have the behaviour disabled with [`fullySpecified=false`](/configuration/module/#resolvefullyspecified).
## 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`](/guides/dependency-management/#importmetawebpackcontext) |

**Example - using `import.meta.url` for assets:**

```js
// 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:**

```js
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).

W> Avoid top-level await in your entry point when targeting the **browser**. It delays the entire module graph evaluation. Prefer `import()` for deferred loading. For Node.js, Electron, or Web Worker targets this restriction does not apply.

```js
// user.js (async ESM module)
const response = await fetch("/api/user");

export const user = await response.json();
```

```js
// 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:

```js
// will fail - missing extension
import { helper as missingExt } from "./utils";

// correct in ESM
import { helper } from "./utils.js";
```

T> Requests to packages e.g. `import "lodash"` are still supported.

Only the "default" export can be imported from non-ESM. Named exports are not available.
To disable this check (useful when migrating a large CJS codebase), you can use [`fullySpecified=false`](/configuration/module/#resolvefullyspecified):

```js
// 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):

```js
// 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 object
```

This 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.

T> To get named exports from CommonJS modules, consider migrating to ESM
T> or using [`@babel/plugin-transform-modules-commonjs`](https://babeljs.io/docs/babel-plugin-transform-modules-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)**

CommonJs Syntax is not available: `require`, `module`, `exports`, `__filename`, `__dirname`.
In ESM, relative imports must include the file extension. Webpack follows the Node.js
ESM convention here.

T> HMR can be used with [`import.meta.webpackHot`](/api/module-variables/#importmetawebpackhot) instead of [`module.hot`](/api/module-variables/#modulehot-webpack-specific).
_Fix_: Change `import { helper } from './utils'` to `import { helper } from './utils.js'`,
or set [`fullySpecified: false`](/configuration/module/#resolvefullyspecified) in your
webpack config to disable the check while migrating.
Loading