Skip to content
This repository was archived by the owner on Apr 16, 2020. It is now read-only.

Commit d3bd8e0

Browse files
committed
doc: notes from call
1 parent 4ed5323 commit d3bd8e0

File tree

1 file changed

+174
-138
lines changed

1 file changed

+174
-138
lines changed

doc/api/esm.md

Lines changed: 174 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,17 @@ specifier resolution, and default behavior.
1717

1818
<!-- type=misc -->
1919

20-
The `--experimental-modules` flag can be used to enable features for loading
21-
ECMAScript modules.
20+
The `--experimental-modules` flag can be used to enable support for
21+
ECMAScript modules (ES modules).
2222

23-
Once this has been set, there are a few different ways to run a file as an ES
24-
module:
23+
## Running Node.js with an ECMAScript Module
2524

26-
### <code>.mjs</code> extension
25+
There are a few ways to start Node.js with an ES module as its input.
2726

28-
Files ending with `.mjs` will be loaded as ES modules.
27+
### Initial entry point with an <code>.mjs</code> extension
28+
29+
A file ending with `.mjs` passed to Node.js as an initial entry point will be
30+
loaded as an ES module.
2931

3032
```sh
3133
node --experimental-modules my-app.mjs
@@ -55,7 +57,7 @@ to Node.js via `STDIN`.
5557
node --experimental-modules --type=module --eval \
5658
"import { sep } from 'path'; console.log(sep);"
5759

58-
coffee --print file-containing-import-statements.coffee | \
60+
echo "import { sep } from 'path'; console.log(sep);" | \
5961
node --experimental-modules --type=module
6062
```
6163

@@ -111,11 +113,11 @@ import './startup/init.js';
111113
// Loaded as ES module since ./startup contains no package.json file,
112114
// and therefore inherits the ES module package scope from one level up
113115

114-
import './node_modules/commonjs-package/index.js';
116+
import 'commonjs-package';
115117
// Loaded as CommonJS since ./node_modules/commonjs-package/package.json
116118
// lacks a "type" field or contains "type": "commonjs"
117119

118-
import 'commonjs-package';
120+
import './node_modules/commonjs-package/index.js';
119121
// Loaded as CommonJS since ./node_modules/commonjs-package/package.json
120122
// lacks a "type" field or contains "type": "commonjs"
121123
```
@@ -172,23 +174,52 @@ An attempt to `require` the above `es-module-package` would attempt to load
172174
an error as Node.js would not be able to parse the `export` statement in
173175
CommonJS.
174176

175-
Even if the `package.json` `"main"` points to a file ending in `.mjs`, the
176-
`"type": "module"` is required.
177-
178177
As with `import` statements, for ES module usage the value of `"main"` must be
179178
a full path including extension: `"./index.mjs"`, not `"./index"`.
180179

181-
> Currently a package can define _either_ a CommonJS entry point or an ES module
182-
> entry point; there is no way to specify separate entry points for CommonJS and
183-
> ES module usage. This means that a package entry point can be included via
184-
> `require` or via `import` but not both.
180+
> Currently a package can define _either_ a CommonJS entry point **or** an ES
181+
> module entry point; there is no way to specify separate entry points for
182+
> CommonJS and ES module usage. This means that a package entry point can be
183+
> included via `require` or via `import` but not both.
185184
>
186185
> Such a limitation makes it difficult for packages to support both new versions
187186
> of Node.js that understand ES modules and older versions of Node.js that
188187
> understand only CommonJS. There is work ongoing to remove this limitation, and
189188
> it will very likely entail changes to the behavior of `"main"` as defined
190189
> here.
191190
191+
## <code>import</code> Specifiers
192+
193+
### Terminology
194+
195+
The _specifier_ of an `import` statement is the string after the `from` keyword,
196+
e.g. `'path'` in `import { sep } from 'path'`. Specifiers are also used in
197+
`export from` statements, and as the argument to an `import()` expression.
198+
199+
There are four types of specifiers:
200+
201+
- _Bare specifiers_ like `'some-package'`. They refer to an entry point of a
202+
package by the package name.
203+
204+
- _Deep import specifiers_ like `'some-package/lib/shuffle.mjs'`. They refer to
205+
a path within a package prefixed by the package name.
206+
207+
- _Relative specifiers_ like `'./startup.js'` or `'../config.mjs'`. They refer
208+
to a path relative to the location of the importing file.
209+
210+
- _Absolute specifiers_ like `'file:///opt/nodejs/config.js'`. They refer
211+
directly and explicitly to a full path.
212+
213+
Bare specifiers, and the bare specifier portion of deep import specifiers, are
214+
strings; but everything else in a specifier is a URL.
215+
216+
Only `file://` URLs are supported. A specifier like
217+
`'https://example.com/app.js'` may be supported by browsers but it is not
218+
supported in Node.js.
219+
220+
Specifiers may not begin with `/` or `//`. These are reserved for potential
221+
future use. The root of the current volume may be referenced via `file:///`.
222+
192223
## import.meta
193224

194225
* {Object}
@@ -282,9 +313,9 @@ import { sin, cos } from 'geometry/trigonometry-functions.mjs';
282313
>
283314
> <!-- eslint-disable no-duplicate-imports -->
284315
> ```js
285-
> import _ from 'underscore'; // Works
316+
> import packageMain from 'commonjs-package'; // Works
286317
>
287-
> import { shuffle } from 'underscore'; // Errors
318+
> import { method } from 'commonjs-package'; // Errors
288319
> ```
289320
>
290321
> There are ongoing efforts to make the latter code possible.
@@ -349,6 +380,127 @@ fs.readFileSync = () => Buffer.from('Hello, ESM');
349380
fs.readFileSync === readFileSync;
350381
```
351382
383+
## Experimental Loader hooks
384+
385+
**Note: This API is currently being redesigned and will still change.**.
386+
387+
<!-- type=misc -->
388+
389+
To customize the default module resolution, loader hooks can optionally be
390+
provided via a `--loader ./loader-name.mjs` argument to Node.js.
391+
392+
When hooks are used they only apply to ES module loading and not to any
393+
CommonJS modules loaded.
394+
395+
### Resolve hook
396+
397+
The resolve hook returns the resolved file URL and module format for a
398+
given module specifier and parent file URL:
399+
400+
```js
401+
const baseURL = new URL('file://');
402+
baseURL.pathname = `${process.cwd()}/`;
403+
404+
export async function resolve(specifier,
405+
parentModuleURL = baseURL,
406+
defaultResolver) {
407+
return {
408+
url: new URL(specifier, parentModuleURL).href,
409+
format: 'esm'
410+
};
411+
}
412+
```
413+
414+
The `parentModuleURL` is provided as `undefined` when performing main Node.js
415+
load itself.
416+
417+
The default Node.js ES module resolution function is provided as a third
418+
argument to the resolver for easy compatibility workflows.
419+
420+
In addition to returning the resolved file URL value, the resolve hook also
421+
returns a `format` property specifying the module format of the resolved
422+
module. This can be one of the following:
423+
424+
| `format` | Description |
425+
| --- | --- |
426+
| `'module'` | Load a standard JavaScript module |
427+
| `'commonjs'` | Load a Node.js CommonJS module |
428+
| `'builtin'` | Load a Node.js builtin module |
429+
| `'dynamic'` | Use a [dynamic instantiate hook][] |
430+
431+
For example, a dummy loader to load JavaScript restricted to browser resolution
432+
rules with only JS file extension and Node.js builtin modules support could
433+
be written:
434+
435+
```js
436+
import path from 'path';
437+
import process from 'process';
438+
import Module from 'module';
439+
440+
const builtins = Module.builtinModules;
441+
const JS_EXTENSIONS = new Set(['.js', '.mjs']);
442+
443+
const baseURL = new URL('file://');
444+
baseURL.pathname = `${process.cwd()}/`;
445+
446+
export function resolve(specifier, parentModuleURL = baseURL, defaultResolve) {
447+
if (builtins.includes(specifier)) {
448+
return {
449+
url: specifier,
450+
format: 'builtin'
451+
};
452+
}
453+
if (/^\.{0,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) {
454+
// For node_modules support:
455+
// return defaultResolve(specifier, parentModuleURL);
456+
throw new Error(
457+
`imports must begin with '/', './', or '../'; '${specifier}' does not`);
458+
}
459+
const resolved = new URL(specifier, parentModuleURL);
460+
const ext = path.extname(resolved.pathname);
461+
if (!JS_EXTENSIONS.has(ext)) {
462+
throw new Error(
463+
`Cannot load file with non-JavaScript file extension ${ext}.`);
464+
}
465+
return {
466+
url: resolved.href,
467+
format: 'esm'
468+
};
469+
}
470+
```
471+
472+
With this loader, running:
473+
474+
```console
475+
NODE_OPTIONS='--experimental-modules --loader ./custom-loader.mjs' node x.js
476+
```
477+
478+
would load the module `x.js` as an ES module with relative resolution support
479+
(with `node_modules` loading skipped in this example).
480+
481+
### Dynamic instantiate hook
482+
483+
To create a custom dynamic module that doesn't correspond to one of the
484+
existing `format` interpretations, the `dynamicInstantiate` hook can be used.
485+
This hook is called only for modules that return `format: 'dynamic'` from
486+
the `resolve` hook.
487+
488+
```js
489+
export async function dynamicInstantiate(url) {
490+
return {
491+
exports: ['customExportName'],
492+
execute: (exports) => {
493+
// Get and set functions provided for pre-allocated export names
494+
exports.customExportName.set('value');
495+
}
496+
};
497+
}
498+
```
499+
500+
With the list of module exports provided upfront, the `execute` function will
501+
then be called at the exact point of module evaluation order for that module
502+
in the import tree.
503+
352504
## Resolution Algorithm
353505
354506
### Features
@@ -385,6 +537,9 @@ entirely for the CommonJS loader.
385537
If the top-level `--type` is _"module"_, then the ESM resolver is used
386538
as described here, with the conditional `--type` check in **ESM_FORMAT**.
387539
540+
<details>
541+
<summary>Resolver algorithm psuedocode</summary>
542+
388543
**ESM_RESOLVE(_specifier_, _parentURL_, _isMain_)**
389544
> 1. Let _resolvedURL_ be **undefined**.
390545
> 1. If _specifier_ is a valid URL, then
@@ -500,126 +655,7 @@ READ_PACKAGE_JSON(_packageURL_)
500655
> 1. Throw an _Invalid Package Configuration_ error.
501656
> 1. Return the parsed JSON source of the file at _pjsonURL_.
502657
503-
## Experimental Loader hooks
504-
505-
**Note: This API is currently being redesigned and will still change.**.
506-
507-
<!-- type=misc -->
508-
509-
To customize the default module resolution, loader hooks can optionally be
510-
provided via a `--loader ./loader-name.mjs` argument to Node.js.
511-
512-
When hooks are used they only apply to ES module loading and not to any
513-
CommonJS modules loaded.
514-
515-
### Resolve hook
516-
517-
The resolve hook returns the resolved file URL and module format for a
518-
given module specifier and parent file URL:
519-
520-
```js
521-
const baseURL = new URL('file://');
522-
baseURL.pathname = `${process.cwd()}/`;
523-
524-
export async function resolve(specifier,
525-
parentModuleURL = baseURL,
526-
defaultResolver) {
527-
return {
528-
url: new URL(specifier, parentModuleURL).href,
529-
format: 'esm'
530-
};
531-
}
532-
```
533-
534-
The `parentModuleURL` is provided as `undefined` when performing main Node.js
535-
load itself.
536-
537-
The default Node.js ES module resolution function is provided as a third
538-
argument to the resolver for easy compatibility workflows.
539-
540-
In addition to returning the resolved file URL value, the resolve hook also
541-
returns a `format` property specifying the module format of the resolved
542-
module. This can be one of the following:
543-
544-
| `format` | Description |
545-
| --- | --- |
546-
| `'module'` | Load a standard JavaScript module |
547-
| `'commonjs'` | Load a Node.js CommonJS module |
548-
| `'builtin'` | Load a Node.js builtin module |
549-
| `'dynamic'` | Use a [dynamic instantiate hook][] |
550-
551-
For example, a dummy loader to load JavaScript restricted to browser resolution
552-
rules with only JS file extension and Node.js builtin modules support could
553-
be written:
554-
555-
```js
556-
import path from 'path';
557-
import process from 'process';
558-
import Module from 'module';
559-
560-
const builtins = Module.builtinModules;
561-
const JS_EXTENSIONS = new Set(['.js', '.mjs']);
562-
563-
const baseURL = new URL('file://');
564-
baseURL.pathname = `${process.cwd()}/`;
565-
566-
export function resolve(specifier, parentModuleURL = baseURL, defaultResolve) {
567-
if (builtins.includes(specifier)) {
568-
return {
569-
url: specifier,
570-
format: 'builtin'
571-
};
572-
}
573-
if (/^\.{0,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) {
574-
// For node_modules support:
575-
// return defaultResolve(specifier, parentModuleURL);
576-
throw new Error(
577-
`imports must begin with '/', './', or '../'; '${specifier}' does not`);
578-
}
579-
const resolved = new URL(specifier, parentModuleURL);
580-
const ext = path.extname(resolved.pathname);
581-
if (!JS_EXTENSIONS.has(ext)) {
582-
throw new Error(
583-
`Cannot load file with non-JavaScript file extension ${ext}.`);
584-
}
585-
return {
586-
url: resolved.href,
587-
format: 'esm'
588-
};
589-
}
590-
```
591-
592-
With this loader, running:
593-
594-
```console
595-
NODE_OPTIONS='--experimental-modules --loader ./custom-loader.mjs' node x.js
596-
```
597-
598-
would load the module `x.js` as an ES module with relative resolution support
599-
(with `node_modules` loading skipped in this example).
600-
601-
### Dynamic instantiate hook
602-
603-
To create a custom dynamic module that doesn't correspond to one of the
604-
existing `format` interpretations, the `dynamicInstantiate` hook can be used.
605-
This hook is called only for modules that return `format: 'dynamic'` from
606-
the `resolve` hook.
607-
608-
```js
609-
export async function dynamicInstantiate(url) {
610-
return {
611-
exports: ['customExportName'],
612-
execute: (exports) => {
613-
// Get and set functions provided for pre-allocated export names
614-
exports.customExportName.set('value');
615-
}
616-
};
617-
}
618-
```
619-
620-
With the list of module exports provided upfront, the `execute` function will
621-
then be called at the exact point of module evaluation order for that module
622-
in the import tree.
658+
</details>
623659
624660
[Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md
625661
[dynamic instantiate hook]: #esm_dynamic_instantiate_hook

0 commit comments

Comments
 (0)