|
@@ -0,0 +1,172 @@
|
|
|
+---
|
|
|
+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 `[email protected]`.
|
|
|
+
|
|
|
+:::
|
|
|
+
|
|
|
+## 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 statmements (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.
|