diff --git a/.eslintrc.js b/.eslintrc.js index 0b4c170813..5f40d3a7a1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -36,7 +36,6 @@ module.exports = { files: [ 'doc/api/esm.md', '*.mjs', - 'test/es-module/test-esm-example-loader.js', ], parserOptions: { sourceType: 'module' }, }, diff --git a/doc/api/cli.md b/doc/api/cli.md index 66ee946c32..b67df45c6e 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -97,13 +97,6 @@ added: v10.0.0 Enable experimental top-level `await` keyword support in REPL. -### `--experimental-vm-modules` - - -Enable experimental ES Module support in the `vm` module. - ### `--experimental-worker` - -Specify the `file` of the custom [experimental ECMAScript Module][] loader. - ### `--napi-modules` Node.js contains support for ES Modules based upon the -[Node.js EP for ES Modules][]. +[Node.js EP for ES Modules][] and the [ESM Minimal Kernel][]. -Not all features of the EP are complete and will be landing as both VM support -and implementation is ready. Error messages are still being polished. +The minimal feature set is designed to be compatible with all potential +future implementations. Expect major changes in the implementation including +interoperability support, specifier resolution, and default behavior. ## Enabling @@ -54,6 +55,14 @@ property: ## Notable differences between `import` and `require` +### Only Support for .mjs + +ESM must have the `.mjs` extension. + +### Mandatory file extensions + +You must provide a file extension when using the `import` keyword. + ### No NODE_PATH `NODE_PATH` is not part of resolving `import` specifiers. Please use symlinks @@ -78,31 +87,32 @@ Modules will be loaded multiple times if the `import` specifier used to resolve them have a different query or fragment. ```js -import './foo?query=1'; // loads ./foo with query of "?query=1" -import './foo?query=2'; // loads ./foo with query of "?query=2" +import './foo.mjs?query=1'; // loads ./foo.mjs with query of "?query=1" +import './foo.mjs?query=2'; // loads ./foo.mjs with query of "?query=2" ``` For now, only modules using the `file:` protocol can be loaded. -## Interop with existing modules +## CommonJS, JSON, and Native Modules -All CommonJS, JSON, and C++ modules can be used with `import`. +CommonJS, JSON, and Native modules can be used with [`module.createRequireFromPath()`][]. -Modules loaded this way will only be loaded once, even if their query -or fragment string differs between `import` statements. +```js +// cjs.js +module.exports = 'cjs'; -When loaded via `import` these modules will provide a single `default` export -representing the value of `module.exports` at the time they finished evaluating. +// esm.mjs +import { createRequireFromPath as createRequire } from 'module'; +import { fileURLToPath as fromPath } from 'url'; -```js -// foo.js -module.exports = { one: 1 }; +const require = createRequire(fromPath(import.meta.url)); -// bar.js -import foo from './foo.js'; -foo.one === 1; // true +const cjs = require('./cjs'); +cjs === 'cjs'; // true ``` +## Builtin modules + Builtin modules will provide named exports of their public API, as well as a default export which can be used for, among other things, modifying the named exports. Named exports of builtin modules are updated when the corresponding @@ -132,127 +142,52 @@ fs.readFileSync = () => Buffer.from('Hello, ESM'); fs.readFileSync === readFileSync; ``` -## Loader hooks - - - -To customize the default module resolution, loader hooks can optionally be -provided via a `--loader ./loader-name.mjs` argument to Node.js. - -When hooks are used they only apply to ES module loading and not to any -CommonJS modules loaded. - -### Resolve hook - -The resolve hook returns the resolved file URL and module format for a -given module specifier and parent file URL: - -```js -const baseURL = new URL('file://'); -baseURL.pathname = `${process.cwd()}/`; - -export async function resolve(specifier, - parentModuleURL = baseURL, - defaultResolver) { - return { - url: new URL(specifier, parentModuleURL).href, - format: 'esm' - }; -} -``` - -The `parentModuleURL` is provided as `undefined` when performing main Node.js -load itself. - -The default Node.js ES module resolution function is provided as a third -argument to the resolver for easy compatibility workflows. - -In addition to returning the resolved file URL value, the resolve hook also -returns a `format` property specifying the module format of the resolved -module. This can be one of the following: - -| `format` | Description | -| --- | --- | -| `'esm'` | Load a standard JavaScript module | -| `'cjs'` | Load a node-style CommonJS module | -| `'builtin'` | Load a node builtin CommonJS module | -| `'json'` | Load a JSON file | -| `'addon'` | Load a [C++ Addon][addons] | -| `'dynamic'` | Use a [dynamic instantiate hook][] | - -For example, a dummy loader to load JavaScript restricted to browser resolution -rules with only JS file extension and Node.js builtin modules support could -be written: - -```js -import path from 'path'; -import process from 'process'; -import Module from 'module'; - -const builtins = Module.builtinModules; -const JS_EXTENSIONS = new Set(['.js', '.mjs']); - -const baseURL = new URL('file://'); -baseURL.pathname = `${process.cwd()}/`; - -export function resolve(specifier, parentModuleURL = baseURL, defaultResolve) { - if (builtins.includes(specifier)) { - return { - url: specifier, - format: 'builtin' - }; - } - if (/^\.{0,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) { - // For node_modules support: - // return defaultResolve(specifier, parentModuleURL); - throw new Error( - `imports must begin with '/', './', or '../'; '${specifier}' does not`); - } - const resolved = new URL(specifier, parentModuleURL); - const ext = path.extname(resolved.pathname); - if (!JS_EXTENSIONS.has(ext)) { - throw new Error( - `Cannot load file with non-JavaScript file extension ${ext}.`); - } - return { - url: resolved.href, - format: 'esm' - }; -} -``` - -With this loader, running: - -```console -NODE_OPTIONS='--experimental-modules --loader ./custom-loader.mjs' node x.js -``` - -would load the module `x.js` as an ES module with relative resolution support -(with `node_modules` loading skipped in this example). - -### Dynamic instantiate hook - -To create a custom dynamic module that doesn't correspond to one of the -existing `format` interpretations, the `dynamicInstantiate` hook can be used. -This hook is called only for modules that return `format: 'dynamic'` from -the `resolve` hook. - -```js -export async function dynamicInstantiate(url) { - return { - exports: ['customExportName'], - execute: (exports) => { - // get and set functions provided for pre-allocated export names - exports.customExportName.set('value'); - } - }; -} -``` - -With the list of module exports provided upfront, the `execute` function will -then be called at the exact point of module evaluation order for that module -in the import tree. +## Resolution Algorithm + +### Features + +The resolver has the following properties: + +* FileURL-based resolution as is used by ES modules +* Support for builtin module loading +* Relative and absolute URL resolution +* No default extensions +* No folder mains +* Bare specifier package resolution lookup through node_modules + +### Resolver Algorithm + +The algorithm to resolve an ES module specifier is provided through +_ESM_RESOLVE_: + +**ESM_RESOLVE**(_specifier_, _parentURL_) +> 1. Let _resolvedURL_ be _undefined_. +> 1. If _specifier_ is a valid URL then, +> 1. Set _resolvedURL_ to the result of parsing and reserializing +> _specifier_ as a URL. +> 1. Otherwise, if _specifier_ starts with _"/"_, _"./"_ or _"../"_ then, +> 1. Set _resolvedURL_ to the URL resolution of _specifier_ relative to +> _parentURL_. +> 1. Otherwise, +> 1. Note: _specifier_ is now a bare specifier. +> 1. Set _resolvedURL_ the result of +> **PACKAGE_RESOLVE**(_specifier_, _parentURL_). +> 1. If the file at _resolvedURL_ does not exist then, +> 1. Throw a _Module Not Found_ error. +> 1. Return _resolvedURL_. + +**PACKAGE_RESOLVE**(_packageSpecifier_, _parentURL_) +> 1. Assert: _packageSpecifier_ is a bare specifier. +> 1. If _packageSpecifier_ is a Node.js builtin module then, +> 1. Return the string _"node:"_ concatenated with _packageSpecifier_. +> 1. While _parentURL_ contains a non-empty _pathname_, +> 1. Set _parentURL_ to the parent folder URL of _parentURL_. +> 1. Let _packageURL_ be the URL resolution of the string concatenation of +> _parentURL_, _"/node_modules/"_ and _"packageSpecifier"_. +> 1. If the file at _packageURL_ exists then, +> 1. Return _packageURL_. +> 1. Throw a _Module Not Found_ error. [Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md -[addons]: addons.html -[dynamic instantiate hook]: #esm_dynamic_instantiate_hook +[`module.createRequireFromPath()`]: modules.html#modules_module_createrequirefrompath_filename +[ESM Minimal Kernel]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md diff --git a/doc/api/vm.md b/doc/api/vm.md index f5b43df61a..291ad20071 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -43,367 +43,6 @@ console.log(sandbox.y); // 17 console.log(x); // 1; y is not defined. ``` -## Class: vm.SourceTextModule - - -> Stability: 1 - Experimental - -*This feature is only available with the `--experimental-vm-modules` command -flag enabled.* - -The `vm.SourceTextModule` class provides a low-level interface for using -ECMAScript modules in VM contexts. It is the counterpart of the `vm.Script` -class that closely mirrors [Source Text Module Record][]s as defined in the -ECMAScript specification. - -Unlike `vm.Script` however, every `vm.SourceTextModule` object is bound to a -context from its creation. Operations on `vm.SourceTextModule` objects are -intrinsically asynchronous, in contrast with the synchronous nature of -`vm.Script` objects. With the help of async functions, however, manipulating -`vm.SourceTextModule` objects is fairly straightforward. - -Using a `vm.SourceTextModule` object requires four distinct steps: -creation/parsing, linking, instantiation, and evaluation. These four steps are -illustrated in the following example. - -This implementation lies at a lower level than the [ECMAScript Module -loader][]. There is also currently no way to interact with the Loader, though -support is planned. - -```js -const vm = require('vm'); - -const contextifiedSandbox = vm.createContext({ secret: 42 }); - -(async () => { - // Step 1 - // - // Create a Module by constructing a new `vm.SourceTextModule` object. This - // parses the provided source text, throwing a `SyntaxError` if anything goes - // wrong. By default, a Module is created in the top context. But here, we - // specify `contextifiedSandbox` as the context this Module belongs to. - // - // Here, we attempt to obtain the default export from the module "foo", and - // put it into local binding "secret". - - const bar = new vm.SourceTextModule(` - import s from 'foo'; - s; - `, { context: contextifiedSandbox }); - - // Step 2 - // - // "Link" the imported dependencies of this Module to it. - // - // The provided linking callback (the "linker") accepts two arguments: the - // parent module (`bar` in this case) and the string that is the specifier of - // the imported module. The callback is expected to return a Module that - // corresponds to the provided specifier, with certain requirements documented - // in `module.link()`. - // - // If linking has not started for the returned Module, the same linker - // callback will be called on the returned Module. - // - // Even top-level Modules without dependencies must be explicitly linked. The - // callback provided would never be called, however. - // - // The link() method returns a Promise that will be resolved when all the - // Promises returned by the linker resolve. - // - // Note: This is a contrived example in that the linker function creates a new - // "foo" module every time it is called. In a full-fledged module system, a - // cache would probably be used to avoid duplicated modules. - - async function linker(specifier, referencingModule) { - if (specifier === 'foo') { - return new vm.SourceTextModule(` - // The "secret" variable refers to the global variable we added to - // "contextifiedSandbox" when creating the context. - export default secret; - `, { context: referencingModule.context }); - - // Using `contextifiedSandbox` instead of `referencingModule.context` - // here would work as well. - } - throw new Error(`Unable to resolve dependency: ${specifier}`); - } - await bar.link(linker); - - // Step 3 - // - // Instantiate the top-level Module. - // - // Only the top-level Module needs to be explicitly instantiated; its - // dependencies will be recursively instantiated by instantiate(). - - bar.instantiate(); - - // Step 4 - // - // Evaluate the Module. The evaluate() method returns a Promise with a single - // property "result" that contains the result of the very last statement - // executed in the Module. In the case of `bar`, it is `s;`, which refers to - // the default export of the `foo` module, the `secret` we set in the - // beginning to 42. - - const { result } = await bar.evaluate(); - - console.log(result); - // Prints 42. -})(); -``` - -### Constructor: new vm.SourceTextModule(code[, options]) - -* `code` {string} JavaScript Module code to parse -* `options` - * `url` {string} URL used in module resolution and stack traces. **Default:** - `'vm:module(i)'` where `i` is a context-specific ascending index. - * `context` {Object} The [contextified][] object as returned by the - `vm.createContext()` method, to compile and evaluate this `Module` in. - * `lineOffset` {integer} Specifies the line number offset that is displayed - in stack traces produced by this `Module`. - * `columnOffset` {integer} Specifies the column number offset that is - displayed in stack traces produced by this `Module`. - * `initializeImportMeta` {Function} Called during evaluation of this `Module` - to initialize the `import.meta`. This function has the signature `(meta, - module)`, where `meta` is the `import.meta` object in the `Module`, and - `module` is this `vm.SourceTextModule` object. - * `importModuleDynamically` {Function} Called during evaluation of this - module when `import()` is called. This function has the signature - `(specifier, module)` where `specifier` is the specifier passed to - `import()` and `module` is this `vm.SourceTextModule`. If this option is - not specified, calls to `import()` will reject with - [`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][]. This method can return a - [Module Namespace Object][], but returning a `vm.SourceTextModule` is - recommended in order to take advantage of error tracking, and to avoid - issues with namespaces that contain `then` function exports. - -Creates a new ES `Module` object. - -Properties assigned to the `import.meta` object that are objects may -allow the `Module` to access information outside the specified `context`, if the -object is created in the top level context. Use `vm.runInContext()` to create -objects in a specific context. - -```js -const vm = require('vm'); - -const contextifiedSandbox = vm.createContext({ secret: 42 }); - -(async () => { - const module = new vm.SourceTextModule( - 'Object.getPrototypeOf(import.meta.prop).secret = secret;', - { - initializeImportMeta(meta) { - // Note: this object is created in the top context. As such, - // Object.getPrototypeOf(import.meta.prop) points to the - // Object.prototype in the top context rather than that in - // the sandbox. - meta.prop = {}; - } - }); - // Since module has no dependencies, the linker function will never be called. - await module.link(() => {}); - module.instantiate(); - await module.evaluate(); - - // Now, Object.prototype.secret will be equal to 42. - // - // To fix this problem, replace - // meta.prop = {}; - // above with - // meta.prop = vm.runInContext('{}', contextifiedSandbox); -})(); -``` - -### module.dependencySpecifiers - -* {string[]} - -The specifiers of all dependencies of this module. The returned array is frozen -to disallow any changes to it. - -Corresponds to the `[[RequestedModules]]` field of -[Source Text Module Record][]s in the ECMAScript specification. - -### module.error - -* {any} - -If the `module.status` is `'errored'`, this property contains the exception -thrown by the module during evaluation. If the status is anything else, -accessing this property will result in a thrown exception. - -The value `undefined` cannot be used for cases where there is not a thrown -exception due to possible ambiguity with `throw undefined;`. - -Corresponds to the `[[EvaluationError]]` field of [Source Text Module Record][]s -in the ECMAScript specification. - -### module.evaluate([options]) - -* `options` {Object} - * `timeout` {integer} Specifies the number of milliseconds to evaluate - before terminating execution. If execution is interrupted, an [`Error`][] - will be thrown. This value must be a strictly positive integer. - * `breakOnSigint` {boolean} If `true`, the execution will be terminated when - `SIGINT` (Ctrl+C) is received. Existing handlers for the event that have - been attached via `process.on('SIGINT')` will be disabled during script - execution, but will continue to work after that. If execution is - interrupted, an [`Error`][] will be thrown. -* Returns: {Promise} - -Evaluate the module. - -This must be called after the module has been instantiated; otherwise it will -throw an error. It could be called also when the module has already been -evaluated, in which case it will do one of the following two things: - -- return `undefined` if the initial evaluation ended in success (`module.status` - is `'evaluated'`) -- rethrow the same exception the initial evaluation threw if the initial - evaluation ended in an error (`module.status` is `'errored'`) - -This method cannot be called while the module is being evaluated -(`module.status` is `'evaluating'`) to prevent infinite recursion. - -Corresponds to the [Evaluate() concrete method][] field of [Source Text Module -Record][]s in the ECMAScript specification. - -### module.instantiate() - -Instantiate the module. This must be called after linking has completed -(`linkingStatus` is `'linked'`); otherwise it will throw an error. It may also -throw an exception if one of the dependencies does not provide an export the -parent module requires. - -However, if this function succeeded, further calls to this function after the -initial instantiation will be no-ops, to be consistent with the ECMAScript -specification. - -Unlike other methods operating on `Module`, this function completes -synchronously and returns nothing. - -Corresponds to the [Instantiate() concrete method][] field of [Source Text -Module Record][]s in the ECMAScript specification. - -### module.link(linker) - -* `linker` {Function} -* Returns: {Promise} - -Link module dependencies. This method must be called before instantiation, and -can only be called once per module. - -Two parameters will be passed to the `linker` function: - -- `specifier` The specifier of the requested module: - - ```js - import foo from 'foo'; - // ^^^^^ the module specifier - ``` -- `referencingModule` The `Module` object `link()` is called on. - -The function is expected to return a `Module` object or a `Promise` that -eventually resolves to a `Module` object. The returned `Module` must satisfy the -following two invariants: - -- It must belong to the same context as the parent `Module`. -- Its `linkingStatus` must not be `'errored'`. - -If the returned `Module`'s `linkingStatus` is `'unlinked'`, this method will be -recursively called on the returned `Module` with the same provided `linker` -function. - -`link()` returns a `Promise` that will either get resolved when all linking -instances resolve to a valid `Module`, or rejected if the linker function either -throws an exception or returns an invalid `Module`. - -The linker function roughly corresponds to the implementation-defined -[HostResolveImportedModule][] abstract operation in the ECMAScript -specification, with a few key differences: - -- The linker function is allowed to be asynchronous while - [HostResolveImportedModule][] is synchronous. -- The linker function is executed during linking, a Node.js-specific stage - before instantiation, while [HostResolveImportedModule][] is called during - instantiation. - -The actual [HostResolveImportedModule][] implementation used during module -instantiation is one that returns the modules linked during linking. Since at -that point all modules would have been fully linked already, the -[HostResolveImportedModule][] implementation is fully synchronous per -specification. - -### module.linkingStatus - -* {string} - -The current linking status of `module`. It will be one of the following values: - -- `'unlinked'`: `module.link()` has not yet been called. -- `'linking'`: `module.link()` has been called, but not all Promises returned by - the linker function have been resolved yet. -- `'linked'`: `module.link()` has been called, and all its dependencies have - been successfully linked. -- `'errored'`: `module.link()` has been called, but at least one of its - dependencies failed to link, either because the callback returned a `Promise` - that is rejected, or because the `Module` the callback returned is invalid. - -### module.namespace - -* {Object} - -The namespace object of the module. This is only available after instantiation -(`module.instantiate()`) has completed. - -Corresponds to the [GetModuleNamespace][] abstract operation in the ECMAScript -specification. - -### module.status - -* {string} - -The current status of the module. Will be one of: - -- `'uninstantiated'`: The module is not instantiated. It may because of any of - the following reasons: - - - The module was just created. - - `module.instantiate()` has been called on this module, but it failed for - some reason. - - This status does not convey any information regarding if `module.link()` has - been called. See `module.linkingStatus` for that. - -- `'instantiating'`: The module is currently being instantiated through a - `module.instantiate()` call on itself or a parent module. - -- `'instantiated'`: The module has been instantiated successfully, but - `module.evaluate()` has not yet been called. - -- `'evaluating'`: The module is being evaluated through a `module.evaluate()` on - itself or a parent module. - -- `'evaluated'`: The module has been successfully evaluated. - -- `'errored'`: The module has been evaluated, but an exception was thrown. - -Other than `'errored'`, this status string corresponds to the specification's -[Source Text Module Record][]'s `[[Status]]` field. `'errored'` corresponds to -`'evaluated'` in the specification, but with `[[EvaluationError]]` set to a -value that is not `undefined`. - -### module.url - -* {string} - -The URL of the current module, as set in the constructor. - ## Class: vm.Script