docs: update formatting for mdx3 compat Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Erick Zhao <erick@hotmail.ca>
		
			
				
	
	
		
			173 lines
		
	
	
	
		
			7.5 KiB
			
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			173 lines
		
	
	
	
		
			7.5 KiB
			
		
	
	
	
		
			Markdown
		
	
	
	
	
	
---
 | 
						|
title: "ES Modules (ESM) in Electron"
 | 
						|
description: "The ES module (ESM) format is the standard way of loading JavaScript packages."
 | 
						|
slug: esm
 | 
						|
hide_title: false
 | 
						|
---
 | 
						|
 | 
						|
# ES Modules (ESM) in Electron
 | 
						|
 | 
						|
## Introduction
 | 
						|
 | 
						|
The ECMAScript module (ESM) format is [the standard way of loading JavaScript packages](https://tc39.es/ecma262/#sec-modules).
 | 
						|
 | 
						|
Chromium and Node.js have their own implementations of the ESM specification, and Electron
 | 
						|
chooses which module loader to use depending on the context.
 | 
						|
 | 
						|
This document serves to outline the limitations of ESM in Electron and the differences between
 | 
						|
ESM in Electron and ESM in Node.js and Chromium.
 | 
						|
 | 
						|
:::info
 | 
						|
 | 
						|
This feature was added in `electron@28.0.0`.
 | 
						|
 | 
						|
:::
 | 
						|
 | 
						|
## Summary: ESM support matrix
 | 
						|
 | 
						|
This table gives a general overview of where ESM is supported and which ESM loader is used.
 | 
						|
 | 
						|
| Process              | ESM Loader | ESM Loader in Preload | Applicable Requirements |
 | 
						|
|----------------------|------------|-----------------------|-------------------------|
 | 
						|
| Main                 | Node.js    | N/A                   | <ul><li> [You must use `await` generously before the app's `ready` event](#you-must-use-await-generously-before-the-apps-ready-event) </li></ul> |
 | 
						|
| Renderer (Sandboxed) | Chromium   | Unsupported           | <ul><li> [Sandboxed preload scripts can't use ESM imports](#sandboxed-preload-scripts-cant-use-esm-imports) </li></ul> |
 | 
						|
| Renderer (Unsandboxed & Context Isolated) | Chromium | Node.js | <ul><li> [Unsandboxed ESM preload scripts will run after page load on pages with no content](#unsandboxed-esm-preload-scripts-will-run-after-page-load-on-pages-with-no-content) </li> <li>[ESM Preload Scripts must have the `.mjs` extension](#esm-preload-scripts-must-have-the-mjs-extension)</li></ul> |
 | 
						|
| Renderer (Unsandboxed & Non Context Isolated) | Chromium | Node.js | <ul><li>[Unsandboxed ESM preload scripts will run after page load on pages with no content](#unsandboxed-esm-preload-scripts-will-run-after-page-load-on-pages-with-no-content)</li><li>[ESM Preload Scripts must have the `.mjs` extension](#esm-preload-scripts-must-have-the-mjs-extension)</li><li>[ESM preload scripts must be context isolated to use dynamic Node.js ESM imports](#esm-preload-scripts-must-be-context-isolated-to-use-dynamic-nodejs-esm-imports)</li></ul> |
 | 
						|
 | 
						|
## Main process
 | 
						|
 | 
						|
Electron's main process runs in a Node.js context and uses its ESM loader. Usage should follow
 | 
						|
[Node's ESM documentation](https://nodejs.org/api/esm.html). To enable ESM in a file in the
 | 
						|
main process, one of the following conditions must be met:
 | 
						|
 | 
						|
- The file ends with the `.mjs` extension
 | 
						|
- The nearest parent package.json has `"type": "module"` set
 | 
						|
 | 
						|
See Node's [Determining Module System](https://nodejs.org/api/packages.html#determining-module-system)
 | 
						|
doc for more details.
 | 
						|
 | 
						|
### Caveats
 | 
						|
 | 
						|
#### You must use `await` generously before the app's `ready` event
 | 
						|
 | 
						|
ES Modules are loaded **asynchronously**. This means that only side effects
 | 
						|
from the main process entry point's imports will execute before the `ready` event.
 | 
						|
 | 
						|
This is important because certain Electron APIs (e.g. [`app.setPath`](../api/app.md#appsetpathname-path))
 | 
						|
need to be called **before** the app's `ready` event is emitted.
 | 
						|
 | 
						|
With top-level `await` available in Node.js ESM, make sure to `await` every Promise that you need to
 | 
						|
execute before the `ready` event. Otherwise, your app may be `ready` before your code executes.
 | 
						|
 | 
						|
This is particularly important to keep in mind for dynamic ESM import statements (static imports are unaffected).
 | 
						|
For example, if `index.mjs` calls `import('./set-up-paths.mjs')` at the top level, the app will
 | 
						|
likely already be `ready` by the time that dynamic import resolves.
 | 
						|
 | 
						|
```js @ts-expect-error=[2] title='index.mjs (Main Process)'
 | 
						|
// add an await call here to guarantee that path setup will finish before `ready`
 | 
						|
import('./set-up-paths.mjs')
 | 
						|
 | 
						|
app.whenReady().then(() => {
 | 
						|
  console.log('This code may execute before the above import')
 | 
						|
})
 | 
						|
```
 | 
						|
 | 
						|
:::caution Transpiler translations
 | 
						|
 | 
						|
JavaScript transpilers (e.g. Babel, TypeScript) have historically supported ES Module
 | 
						|
syntax before Node.js supported ESM imports by turning these calls to CommonJS
 | 
						|
`require` calls.
 | 
						|
 | 
						|
<details>
 | 
						|
<summary>Example: @babel/plugin-transform-modules-commonjs</summary>
 | 
						|
 | 
						|
The `@babel/plugin-transform-modules-commonjs` plugin will transform
 | 
						|
ESM imports down to `require` calls. The exact syntax will depend on the
 | 
						|
[`importInterop` setting](https://babeljs.io/docs/babel-plugin-transform-modules-commonjs#importinterop).
 | 
						|
 | 
						|
```js @nolint @ts-nocheck title='@babel/plugin-transform-modules-commonjs'
 | 
						|
import foo from "foo";
 | 
						|
import { bar } from "bar";
 | 
						|
foo;
 | 
						|
bar;
 | 
						|
 | 
						|
// with "importInterop: node", compiles to ...
 | 
						|
 | 
						|
"use strict";
 | 
						|
 | 
						|
var _foo = require("foo");
 | 
						|
var _bar = require("bar");
 | 
						|
 | 
						|
_foo;
 | 
						|
_bar.bar;
 | 
						|
```
 | 
						|
 | 
						|
</details>
 | 
						|
 | 
						|
These CommonJS calls load module code synchronously. If you are migrating transpiled CJS code
 | 
						|
to native ESM, be careful about the timing differences between CJS and ESM.
 | 
						|
 | 
						|
:::
 | 
						|
 | 
						|
## Renderer process
 | 
						|
 | 
						|
Electron's renderer processes run in a Chromium context and will use Chromium's ESM loader.
 | 
						|
In practice, this means that `import` statements:
 | 
						|
 | 
						|
- will not have access to Node.js built-in modules
 | 
						|
- will not be able to load npm packages from `node_modules`
 | 
						|
 | 
						|
```html
 | 
						|
<script type="module">
 | 
						|
    import { exists } from 'node:fs' // ❌ will not work!
 | 
						|
</script>
 | 
						|
```
 | 
						|
 | 
						|
If you wish to load JavaScript packages via npm directly into the renderer process, we recommend
 | 
						|
using a bundler such as webpack or Vite to compile your code for client-side consumption.
 | 
						|
 | 
						|
## Preload scripts
 | 
						|
 | 
						|
A renderer's preload script will use the Node.js ESM loader _when available_.
 | 
						|
ESM availability will depend on the values of its renderer's `sandbox` and `contextIsolation`
 | 
						|
preferences, and comes with a few other caveats due to the asynchronous nature of ESM loading.
 | 
						|
 | 
						|
### Caveats
 | 
						|
 | 
						|
#### ESM preload scripts must have the `.mjs` extension
 | 
						|
 | 
						|
Preload scripts will ignore `"type": "module"` fields, so you _must_ use the `.mjs` file
 | 
						|
extension in your ESM preload scripts.
 | 
						|
 | 
						|
#### Sandboxed preload scripts can't use ESM imports
 | 
						|
 | 
						|
Sandboxed preload scripts are run as plain JavaScript without an ESM context. If you need to
 | 
						|
use external modules, we recommend using a bundler for your preload code. Loading the
 | 
						|
`electron` API is still done via `require('electron')`.
 | 
						|
 | 
						|
For more information on sandboxing, see the [Process Sandboxing](./sandbox.md) docs.
 | 
						|
 | 
						|
#### Unsandboxed ESM preload scripts will run after page load on pages with no content
 | 
						|
 | 
						|
If the response body for a renderer's loaded page is _completely_ empty (i.e. `Content-Length: 0`),
 | 
						|
its preload script will not block the page load, which may result in race conditions.
 | 
						|
 | 
						|
If this impacts you, change your response body to have _something_ in it
 | 
						|
(e.g. an empty `html` tag (`<html></html>`)) or swap back to using a CommonJS preload script
 | 
						|
(`.js` or `.cjs`), which will block the page load.
 | 
						|
 | 
						|
### ESM preload scripts must be context isolated to use dynamic Node.js ESM imports
 | 
						|
 | 
						|
If your unsandboxed renderer process does not have the `contextIsolation` flag enabled,
 | 
						|
you cannot dynamically `import()` files via Node's ESM loader.
 | 
						|
 | 
						|
```js @ts-nocheck title='preload.mjs'
 | 
						|
// ❌ these won't work without context isolation
 | 
						|
const fs = await import('node:fs')
 | 
						|
await import('./foo')
 | 
						|
```
 | 
						|
 | 
						|
This is because Chromium's dynamic ESM `import()` function usually takes precedence in the
 | 
						|
renderer process and without context isolation, there is no way of knowing if Node.js is available
 | 
						|
in a dynamic import statement. If you enable context isolation, `import()` statements
 | 
						|
from the renderer's isolated preload context can be routed to the Node.js module loader.
 |