Browse Source

docs: esm tutorial (#39722)

* docs: esm tutorial

* Update esm.md

* Update docs/tutorial/esm.md

Co-authored-by: Michaela Laurencin <[email protected]>

* table adjustment

* fix lint

* Update docs/tutorial/esm.md

Co-authored-by: David Sanders <[email protected]>

* Update docs/tutorial/esm.md

Co-authored-by: David Sanders <[email protected]>

* Update docs/tutorial/esm.md

Co-authored-by: David Sanders <[email protected]>

* Update docs/tutorial/esm.md

Co-authored-by: David Sanders <[email protected]>

* Update docs/tutorial/esm.md

Co-authored-by: David Sanders <[email protected]>

* Update esm.md

---------

Co-authored-by: Michaela Laurencin <[email protected]>
Co-authored-by: David Sanders <[email protected]>
Erick Zhao 1 year ago
parent
commit
2085aae915
2 changed files with 172 additions and 38 deletions
  1. 0 38
      docs/tutorial/esm-limitations.md
  2. 172 0
      docs/tutorial/esm.md

+ 0 - 38
docs/tutorial/esm-limitations.md

@@ -1,38 +0,0 @@
-# ESM Limitations
-
-This document serves to outline the limitations / differences between ESM in Electron and ESM in Node.js and Chromium.
-
-## ESM Support Matrix
-
-This table gives a general overview of where ESM is supported and most importantly which ESM loader is used.
-
-| | Supported | Loader | Supported in Preload | Loader in Preload | Applicable Requirements |
-|-|-|-|-|-|-|
-| Main Process | Yes | Node.js | N/A | N/A | <ul><li> [You must `await` generously in the main process to avoid race conditions](#you-must-use-await-generously-in-the-main-process-to-avoid-race-conditions) </li></ul> |
-| Sandboxed Renderer | Yes | Chromium | No | | <ul><li> [Sandboxed preload scripts can't use ESM imports](#sandboxed-preload-scripts-cant-use-esm-imports) </li></ul> |
-| Node.js Renderer + Context Isolation | Yes | Chromium | Yes | Node.js | <ul><li> [Node.js ESM Preload Scripts will run after page load on pages with no content](#nodejs-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> |
-| Node.js Renderer + No Context Isolation | Yes | Chromium | Yes | Node.js | <ul><li> [Non-context-isolated renderers can't use dynamic Node.js ESM imports](#non-context-isolated-renderers-cant-use-dynamic-nodejs-esm-imports) </li> <li>[ESM Preload Scripts must have the `.mjs` extension](#esm-preload-scripts-must-have-the-mjs-extension)</li></ul> |
-
-## Requirements
-
-### You must use `await` generously in the main process to avoid race conditions
-
-Certain APIs in Electron (`app.setPath` for instance) are documented as needing to be called **before** the `app.on('ready')` event is emitted.  When using ESM in the main process it is only guaranteed that the `ready` event hasn't been emitted while executing the side-effects of the primary import.  i.e. 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.  To avoid this you should `await import('./set-up-paths.mjs')` at the top level of `index.mjs`.  It's not just import calls you should await, if you are reading files asynchronously or performing other asynchronous actions you must await those at the top-level as well to ensure the app does not resume initialization and become ready too early.
-
-### Sandboxed preload scripts can't use ESM imports
-
-Sandboxed preload scripts are run as plain javascript without an ESM context.  It is recommended that preload scripts are bundled via something like `webpack` or `vite` for performance reasons regardless, so your preload script should just be a single file that doesn't need to use ESM imports.  Loading the `electron` API is still done via `require('electron')`.
-
-### Node.js ESM Preload Scripts will run after page load on pages with no content
-
-If the response body for the page is **completely** empty, i.e. `Content-Length: 0`, the 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, for example 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 have the `.mjs` extension
-
-In order to load an ESM preload script it must have a `.mjs` file extension.  Using `type: module` in a nearby package.json is not sufficient.  Please also note the limitation above around not blocking page load if the page is empty.
-
-### Non-context-isolated renderers can't use dynamic Node.js ESM imports
-
-If your renderer process does not have `contextIsolation` enabled you can not `import()` ESM files via the Node.js module loader.  This means that you can't `import('fs')` or `import('./foo')`.  If you want to be able to do so you must enable context isolation.  This is because in the renderer Chromium's `import()` function takes precedence and without context isolation there is no way for Electron to know which loader to route the request to.
-
-If you enable context isolation `import()` from the isolated preload context will use the Node.js loader and `import()` from the main context will continue using Chromium's loader.

+ 172 - 0
docs/tutorial/esm.md

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