From f4a819989fb2536ebb2dec5849c9ac969d106a61 Mon Sep 17 00:00:00 2001 From: guybedford Date: Tue, 28 Aug 2018 17:28:46 +0200 Subject: [PATCH 1/5] esm: minimal kernel Refs: https://github.com/nodejs/modules/pull/180 PR-URL: https://github.com/nodejs/ecmascript-modules/pull/6 PR-URL: https://github.com/nodejs/ecmascript-modules/pull/12 Co-authored-by: Myles Borins Co-authored-by: John-David Dalton --- .eslintrc.js | 1 - doc/api/cli.md | 9 - doc/api/errors.md | 13 +- doc/api/esm.md | 300 ++++++++++-------- lib/internal/errors.js | 16 +- lib/internal/modules/cjs/loader.js | 6 +- lib/internal/modules/esm/default_resolve.js | 19 +- lib/internal/modules/esm/translators.js | 40 +-- lib/internal/process/esm_loader.js | 10 +- src/module_wrap.cc | 248 +++------------ src/module_wrap.h | 8 +- src/node_errors.h | 2 +- src/node_options.cc | 9 - src/node_options.h | 1 - test/addons/hello-world-esm/binding.cc | 14 - test/addons/hello-world-esm/binding.gyp | 9 - test/addons/hello-world-esm/test.js | 20 -- test/addons/hello-world-esm/test.mjs | 6 - test/common/index.mjs | 14 +- test/es-module/test-esm-basic-imports.mjs | 3 +- .../test-esm-cyclic-dynamic-import.mjs | 5 +- test/es-module/test-esm-double-encoding.mjs | 5 +- test/es-module/test-esm-dynamic-import.js | 1 + test/es-module/test-esm-encoded-path.mjs | 3 +- test/es-module/test-esm-error-cache.js | 6 +- test/es-module/test-esm-example-loader.js | 6 - test/es-module/test-esm-forbidden-globals.mjs | 3 +- test/es-module/test-esm-import-meta.mjs | 3 +- test/es-module/test-esm-json.mjs | 8 - test/es-module/test-esm-live-binding.mjs | 3 +- test/es-module/test-esm-loader-dependency.mjs | 5 - .../test-esm-loader-invalid-format.mjs | 11 - .../es-module/test-esm-loader-invalid-url.mjs | 12 - ...oader-missing-dynamic-instantiate-hook.mjs | 9 - test/es-module/test-esm-loader-search.js | 17 - test/es-module/test-esm-main-lookup.mjs | 26 +- test/es-module/test-esm-named-exports.mjs | 8 - test/es-module/test-esm-namespace.mjs | 4 +- ...-esm-preserve-symlinks-not-found-plain.mjs | 3 - .../test-esm-preserve-symlinks-not-found.mjs | 3 - test/es-module/test-esm-process.mjs | 3 +- test/es-module/test-esm-require-cache.mjs | 11 +- test/es-module/test-esm-resolve-hook.mjs | 8 - test/es-module/test-esm-shared-loader-dep.mjs | 7 - test/es-module/test-esm-shebang.mjs | 3 +- test/es-module/test-esm-snapshot.mjs | 7 - test/es-module/test-esm-symlink-main.js | 2 +- test/es-module/test-esm-symlink.js | 10 +- test/es-module/test-esm-throw-undefined.mjs | 14 - .../builtin-named-exports-loader.mjs | 24 -- .../es-module-loaders/example-loader.mjs | 34 -- test/fixtures/es-module-loaders/js-as-esm.js | 1 - test/fixtures/es-module-loaders/js-loader.mjs | 20 -- test/fixtures/es-module-loaders/loader-dep.js | 1 - .../loader-invalid-format.mjs | 8 - .../es-module-loaders/loader-invalid-url.mjs | 9 - .../es-module-loaders/loader-shared-dep.mjs | 7 - .../loader-unknown-builtin-module.mjs | 6 - .../es-module-loaders/loader-with-dep.mjs | 7 - .../missing-dynamic-instantiate-hook.mjs | 6 - .../module-named-exports.mjs | 2 - .../not-found-assert-loader.mjs | 22 -- .../es-module-loaders/syntax-error-import.mjs | 1 - .../es-module-loaders/syntax-error.mjs | 2 - .../es-module-loaders/throw-undefined.mjs | 3 - .../es-modules/esm-snapshot-mutator.js | 4 - test/fixtures/es-modules/esm-snapshot.js | 2 - test/fixtures/es-modules/json.json | 3 - test/fixtures/es-modules/loop.mjs | 2 +- test/fixtures/es-modules/pjson-main/main.js | 1 - test/fixtures/es-modules/pjson-main/main.mjs | 1 + .../es-modules/pjson-main/package.json | 2 +- ...=> test-esm-double-encoding-native%20.mjs} | 2 +- test/fixtures/syntax/bad_syntax.mjs | 1 + .../esm_display_syntax_error_import.mjs | 7 - .../esm_display_syntax_error_import.out | 6 - ...esm_display_syntax_error_import_module.mjs | 3 - ...esm_display_syntax_error_import_module.out | 6 - .../esm_display_syntax_error_module.mjs | 3 - .../esm_display_syntax_error_module.out | 6 - .../test-loaders-unknown-builtin-module.mjs | 12 - .../test-module-main-extension-lookup.js | 2 +- 82 files changed, 321 insertions(+), 849 deletions(-) delete mode 100644 test/addons/hello-world-esm/binding.cc delete mode 100644 test/addons/hello-world-esm/binding.gyp delete mode 100644 test/addons/hello-world-esm/test.js delete mode 100644 test/addons/hello-world-esm/test.mjs delete mode 100644 test/es-module/test-esm-example-loader.js delete mode 100644 test/es-module/test-esm-json.mjs delete mode 100644 test/es-module/test-esm-loader-dependency.mjs delete mode 100644 test/es-module/test-esm-loader-invalid-format.mjs delete mode 100644 test/es-module/test-esm-loader-invalid-url.mjs delete mode 100644 test/es-module/test-esm-loader-missing-dynamic-instantiate-hook.mjs delete mode 100644 test/es-module/test-esm-loader-search.js delete mode 100644 test/es-module/test-esm-named-exports.mjs delete mode 100644 test/es-module/test-esm-preserve-symlinks-not-found-plain.mjs delete mode 100644 test/es-module/test-esm-preserve-symlinks-not-found.mjs delete mode 100644 test/es-module/test-esm-resolve-hook.mjs delete mode 100644 test/es-module/test-esm-shared-loader-dep.mjs delete mode 100644 test/es-module/test-esm-snapshot.mjs delete mode 100644 test/es-module/test-esm-throw-undefined.mjs delete mode 100644 test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs delete mode 100644 test/fixtures/es-module-loaders/example-loader.mjs delete mode 100644 test/fixtures/es-module-loaders/js-as-esm.js delete mode 100644 test/fixtures/es-module-loaders/js-loader.mjs delete mode 100644 test/fixtures/es-module-loaders/loader-dep.js delete mode 100644 test/fixtures/es-module-loaders/loader-invalid-format.mjs delete mode 100644 test/fixtures/es-module-loaders/loader-invalid-url.mjs delete mode 100644 test/fixtures/es-module-loaders/loader-shared-dep.mjs delete mode 100644 test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs delete mode 100644 test/fixtures/es-module-loaders/loader-with-dep.mjs delete mode 100644 test/fixtures/es-module-loaders/missing-dynamic-instantiate-hook.mjs delete mode 100644 test/fixtures/es-module-loaders/module-named-exports.mjs delete mode 100644 test/fixtures/es-module-loaders/not-found-assert-loader.mjs delete mode 100644 test/fixtures/es-module-loaders/syntax-error-import.mjs delete mode 100644 test/fixtures/es-module-loaders/syntax-error.mjs delete mode 100644 test/fixtures/es-module-loaders/throw-undefined.mjs delete mode 100644 test/fixtures/es-modules/esm-snapshot-mutator.js delete mode 100644 test/fixtures/es-modules/esm-snapshot.js delete mode 100644 test/fixtures/es-modules/json.json delete mode 100644 test/fixtures/es-modules/pjson-main/main.js create mode 100644 test/fixtures/es-modules/pjson-main/main.mjs rename test/fixtures/es-modules/{test-esm-double-encoding-native%20.js => test-esm-double-encoding-native%20.mjs} (86%) create mode 100644 test/fixtures/syntax/bad_syntax.mjs delete mode 100644 test/message/esm_display_syntax_error_import.mjs delete mode 100644 test/message/esm_display_syntax_error_import.out delete mode 100644 test/message/esm_display_syntax_error_import_module.mjs delete mode 100644 test/message/esm_display_syntax_error_import_module.out delete mode 100644 test/message/esm_display_syntax_error_module.mjs delete mode 100644 test/message/esm_display_syntax_error_module.out delete mode 100644 test/parallel/test-loaders-unknown-builtin-module.mjs diff --git a/.eslintrc.js b/.eslintrc.js index 475c75ff96..750c230ca1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -39,7 +39,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 7e7ef3e285..f8fa53a5cc 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -268,13 +268,6 @@ default) is not firewall-protected.** See the [debugging security implications][] section for more information. -### `--loader=file` - - -Specify the `file` of the custom [experimental ECMAScript Module][] loader. - ### `--max-http-header-size=size` 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,10 @@ property: ## Notable differences between `import` and `require` +### 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 +83,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.mjs -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 +138,143 @@ 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 load an ES module specifier is given through the +**ESM_RESOLVE** method below. It returns the resolved URL for a +module specifier relative to a parentURL, in addition to the unique module +format for that resolved URL given by the **ESM_FORMAT** routine. + +The _"esm"_ format is returned for an ECMAScript Module, while the +_"legacy"_ format is used to indicate loading through the legacy +CommonJS loader. Additional formats such as _"wasm"_ or _"addon"_ can be +extended in future updates. + +In the following algorithms, all subroutine errors are propogated as errors +of these top-level routines. + +_isMain_ is **true** when resolving the Node.js application entry point. + +**ESM_RESOLVE(_specifier_, _parentURL_, _isMain_)** +> 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 _"/"_, then +> 1. Throw an _Invalid Specifier_ error. +> 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. Let _format_ be the result of **ESM_FORMAT**(_url_, _isMain_). +> 1. Load _resolvedURL_ as module format, _format_. + +PACKAGE_RESOLVE(_packageSpecifier_, _parentURL_) +> 1. Let _packageName_ be *undefined*. +> 1. Let _packageSubpath_ be *undefined*. +> 1. If _packageSpecifier_ is an empty string, then +> 1. Throw an _Invalid Specifier_ error. +> 1. If _packageSpecifier_ does not start with _"@"_, then +> 1. Set _packageName_ to the substring of _packageSpecifier_ until the +> first _"/"_ separator or the end of the string. +> 1. Otherwise, +> 1. If _packageSpecifier_ does not contain a _"/"_ separator, then +> 1. Throw an _Invalid Specifier_ error. +> 1. Set _packageName_ to the substring of _packageSpecifier_ +> until the second _"/"_ separator or the end of the string. +> 1. Let _packageSubpath_ be the substring of _packageSpecifier_ from the +> position at the length of _packageName_ plus one, if any. +> 1. Assert: _packageName_ is a valid package name or scoped package name. +> 1. Assert: _packageSubpath_ is either empty, or a path without a leading +> separator. +> 1. If _packageSubpath_ contains any _"."_ or _".."_ segments or percent +> encoded strings for _"/"_ or _"\"_ then, +> 1. Throw an _Invalid Specifier_ error. +> 1. If _packageSubpath_ is empty and _packageName_ is a Node.js builtin +> module, then +> 1. Return the string _"node:"_ concatenated with _packageSpecifier_. +> 1. While _parentURL_ is not the file system root, +> 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 folder at _packageURL_ does not exist, then +> 1. Set _parentURL_ to the parent URL path of _parentURL_. +> 1. Continue the next loop iteration. +> 1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_packageURL_). +> 1. If _packageSubpath_ is empty, then +> 1. Return the result of **PACKAGE_MAIN_RESOLVE**(_packageURL_, +> _pjson_). +> 1. Otherwise, +> 1. Return the URL resolution of _packageSubpath_ in _packageURL_. +> 1. Throw a _Module Not Found_ error. + +PACKAGE_MAIN_RESOLVE(_packageURL_, _pjson_) +> 1. If _pjson_ is **null**, then +> 1. Throw a _Module Not Found_ error. +> 1. If _pjson.main_ is a String, then +> 1. Let _resolvedMain_ be the concatenation of _packageURL_, "/", and +> _pjson.main_. +> 1. If the file at _resolvedMain_ exists, then +> 1. Return _resolvedMain_. +> 1. If _pjson.type_ is equal to _"esm"_, then +> 1. Throw a _Module Not Found_ error. +> 1. Let _legacyMainURL_ be the result applying the legacy +> **LOAD_AS_DIRECTORY** CommonJS resolver to _packageURL_, throwing a +> _Module Not Found_ error for no resolution. +> 1. If _legacyMainURL_ does not end in _".js"_ then, +> 1. Throw an _Unsupported File Extension_ error. +> 1. Return _legacyMainURL_. + +**ESM_FORMAT(_url_, _isMain_)** +> 1. Assert: _url_ corresponds to an existing file. +> 1. Let _pjson_ be the result of **READ_PACKAGE_BOUNDARY**(_url_). +> 1. If _pjson_ is **null** and _isMain_ is **true**, then +> 1. Return _"legacy"_. +> 1. If _pjson.type_ exists and is _"esm"_, then +> 1. If _url_ does not end in _".js"_ or _".mjs"_, then +> 1. Throw an _Unsupported File Extension_ error. +> 1. Return _"esm"_. +> 1. Otherwise, +> 1. If _url_ ends in _".mjs"_, then +> 1. Return _"esm"_. +> 1. Otherwise, +> 1. Return _"legacy"_. + +READ_PACKAGE_BOUNDARY(_url_) +> 1. Let _boundaryURL_ be _url_. +> 1. While _boundaryURL_ is not the file system root, +> 1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_boundaryURL_). +> 1. If _pjson_ is not **null**, then +> 1. Return _pjson_. +> 1. Set _boundaryURL_ to the parent URL of _boundaryURL_. +> 1. Return **null**. + +READ_PACKAGE_JSON(_packageURL_) +> 1. Let _pjsonURL_ be the resolution of _"package.json"_ within _packageURL_. +> 1. If the file at _pjsonURL_ does not exist, then +> 1. Return **null**. +> 1. If the file at _packageURL_ does not parse as valid JSON, then +> 1. Throw an _Invalid Package Configuration_ error. +> 1. Return the parsed JSON source of the file at _pjsonURL_. [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/lib/internal/errors.js b/lib/internal/errors.js index a76153297e..5e7f504be8 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -865,11 +865,13 @@ E('ERR_MISSING_ARGS', E('ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK', 'The ES Module loader may not return a format of \'dynamic\' when no ' + 'dynamicInstantiate function was provided', Error); -E('ERR_MISSING_MODULE', 'Cannot find module %s', Error); -E('ERR_MODULE_RESOLUTION_LEGACY', - '%s not found by import in %s.' + - ' Legacy behavior in require() would have found it at %s', - Error); +E('ERR_MODULE_NOT_FOUND', (module, base, legacyResolution) => { + let msg = `Cannot find module '${module}' imported from ${base}.`; + if (legacyResolution) + msg += ' Legacy behavior in require() would have found it at ' + + legacyResolution; + return msg; +}, Error); E('ERR_MULTIPLE_CALLBACK', 'Callback called multiple times', Error); E('ERR_NAPI_CONS_FUNCTION', 'Constructor must be a function', TypeError); E('ERR_NAPI_INVALID_DATAVIEW_ARGS', @@ -967,11 +969,13 @@ E('ERR_UNHANDLED_ERROR', if (err === undefined) return msg; return `${msg} (${err})`; }, Error); +// This should probably be a `TypeError`. E('ERR_UNKNOWN_CREDENTIAL', '%s identifier does not exist: %s', Error); E('ERR_UNKNOWN_ENCODING', 'Unknown encoding: %s', TypeError); // This should probably be a `TypeError`. -E('ERR_UNKNOWN_FILE_EXTENSION', 'Unknown file extension: %s', Error); +E('ERR_UNKNOWN_FILE_EXTENSION', 'Unknown file extension: \'%s\' imported ' + + 'from %s', Error); E('ERR_UNKNOWN_MODULE_FORMAT', 'Unknown module format: %s', RangeError); E('ERR_UNKNOWN_SIGNAL', 'Unknown signal: %s', TypeError); diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 6bd7f77535..f28dadc654 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -864,7 +864,11 @@ if (experimentalModules) { // bootstrap main module. Module.runMain = function() { // Load the main module--the command line argument. - if (experimentalModules) { + const base = path.basename(process.argv[1]); + const ext = path.extname(base); + const isESM = ext === '.mjs'; + + if (experimentalModules && isESM) { if (asyncESM === undefined) lazyLoadESM(); asyncESM.loaderPromise.then((loader) => { return loader.import(pathToFileURL(process.argv[1]).pathname); diff --git a/lib/internal/modules/esm/default_resolve.js b/lib/internal/modules/esm/default_resolve.js index 33366f0069..d1d400c6b5 100644 --- a/lib/internal/modules/esm/default_resolve.js +++ b/lib/internal/modules/esm/default_resolve.js @@ -10,8 +10,7 @@ const { getOptionValue } = require('internal/options'); const preserveSymlinks = getOptionValue('--preserve-symlinks'); const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); const { - ERR_MISSING_MODULE, - ERR_MODULE_RESOLUTION_LEGACY, + ERR_MODULE_NOT_FOUND, ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes; const { resolve: moduleWrapResolve } = internalBinding('module_wrap'); @@ -21,10 +20,6 @@ const { pathToFileURL, fileURLToPath } = require('internal/url'); const realpathCache = new Map(); function search(target, base) { - if (base === undefined) { - // We cannot search without a base. - throw new ERR_MISSING_MODULE(target); - } try { return moduleWrapResolve(target, base); } catch (e) { @@ -36,7 +31,7 @@ function search(target, base) { tmpMod.paths = CJSmodule._nodeModulePaths( new URL('./', questionedBase).pathname); const found = CJSmodule._resolveFilename(target, tmpMod); - error = new ERR_MODULE_RESOLUTION_LEGACY(target, base, found); + error = new ERR_MODULE_NOT_FOUND(target, fileURLToPath(base), found); } catch { // ignore } @@ -46,10 +41,7 @@ function search(target, base) { const extensionFormatMap = { '__proto__': null, - '.mjs': 'esm', - '.json': 'json', - '.node': 'addon', - '.js': 'cjs' + '.mjs': 'esm' }; function resolve(specifier, parentURL) { @@ -93,12 +85,11 @@ function resolve(specifier, parentURL) { if (isMain) format = 'cjs'; else - throw new ERR_UNKNOWN_FILE_EXTENSION(url.pathname); + throw new ERR_UNKNOWN_FILE_EXTENSION(url.pathname, + fileURLToPath(parentURL)); } return { url: `${url}`, format }; } module.exports = resolve; -// exported for tests -module.exports.search = search; diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 25552cff0e..46e3bdd998 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -3,29 +3,22 @@ const { NativeModule } = require('internal/bootstrap/loaders'); const { ModuleWrap, callbackMap } = internalBinding('module_wrap'); const { - stripShebang, - stripBOM + stripShebang } = require('internal/modules/cjs/helpers'); const CJSModule = require('internal/modules/cjs/loader'); const internalURLModule = require('internal/url'); const createDynamicModule = require( 'internal/modules/esm/create_dynamic_module'); const fs = require('fs'); -const { _makeLong } = require('path'); const { SafeMap, - JSON, - FunctionPrototype, - StringPrototype } = primordials; const { URL } = require('url'); const { debuglog, promisify } = require('util'); const esmLoader = require('internal/process/esm_loader'); const readFileAsync = promisify(fs.readFile); -const readFileSync = fs.readFileSync; -const StringReplace = FunctionPrototype.call.bind(StringPrototype.replace); -const JsonParse = JSON.parse; +const StringReplace = Function.call.bind(String.prototype.replace); const debug = debuglog('esm'); @@ -96,32 +89,3 @@ translators.set('builtin', async (url) => { reflect.exports.default.set(module.exports); }); }); - -// Strategy for loading a node native module -translators.set('addon', async (url) => { - debug(`Translating NativeModule ${url}`); - return createDynamicModule(['default'], url, (reflect) => { - debug(`Loading NativeModule ${url}`); - const module = { exports: {} }; - const pathname = internalURLModule.fileURLToPath(new URL(url)); - process.dlopen(module, _makeLong(pathname)); - reflect.exports.default.set(module.exports); - }); -}); - -// Strategy for loading a JSON file -translators.set('json', async (url) => { - debug(`Translating JSONModule ${url}`); - return createDynamicModule(['default'], url, (reflect) => { - debug(`Loading JSONModule ${url}`); - const pathname = internalURLModule.fileURLToPath(new URL(url)); - const content = readFileSync(pathname, 'utf8'); - try { - const exports = JsonParse(stripBOM(content)); - reflect.exports.default.set(exports); - } catch (err) { - err.message = pathname + ': ' + err.message; - throw err; - } - }); -}); diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js index 0b7f1be6ff..6ed43970cb 100644 --- a/lib/internal/process/esm_loader.js +++ b/lib/internal/process/esm_loader.js @@ -4,7 +4,6 @@ const { callbackMap, } = internalBinding('module_wrap'); -const { pathToFileURL } = require('internal/url'); const Loader = require('internal/modules/esm/loader'); const { wrapToModuleMap, @@ -41,15 +40,8 @@ exports.loaderPromise = new Promise((resolve, reject) => { exports.ESMLoader = undefined; exports.initializeLoader = function(cwd, userLoader) { - let ESMLoader = new Loader(); + const ESMLoader = new Loader(); const loaderPromise = (async () => { - if (userLoader) { - const hooks = await ESMLoader.import( - userLoader, pathToFileURL(`${cwd}/`).href); - ESMLoader = new Loader(); - ESMLoader.hook(hooks); - exports.ESMLoader = ESMLoader; - } return ESMLoader; })(); loaderResolve(loaderPromise); diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 3d55d12cc3..3880551338 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -27,7 +27,6 @@ using v8::HandleScope; using v8::Integer; using v8::IntegrityLevel; using v8::Isolate; -using v8::JSON; using v8::Just; using v8::Local; using v8::Maybe; @@ -44,7 +43,7 @@ using v8::String; using v8::Undefined; using v8::Value; -static const char* const EXTENSIONS[] = {".mjs", ".js", ".json", ".node"}; +static const char* const EXTENSIONS[] = {".mjs"}; ModuleWrap::ModuleWrap(Environment* env, Local object, @@ -469,219 +468,66 @@ std::string ReadFile(uv_file file) { return contents; } -enum CheckFileOptions { - LEAVE_OPEN_AFTER_CHECK, - CLOSE_AFTER_CHECK +enum DescriptorType { + NONE, + FILE, + DIRECTORY }; -Maybe CheckFile(const std::string& path, - CheckFileOptions opt = CLOSE_AFTER_CHECK) { +DescriptorType CheckDescriptor(const std::string& path) { uv_fs_t fs_req; - if (path.empty()) { - return Nothing(); - } - - uv_file fd = uv_fs_open(nullptr, &fs_req, path.c_str(), O_RDONLY, 0, nullptr); - uv_fs_req_cleanup(&fs_req); - - if (fd < 0) { - return Nothing(); - } - - uv_fs_fstat(nullptr, &fs_req, fd, nullptr); - uint64_t is_directory = fs_req.statbuf.st_mode & S_IFDIR; - uv_fs_req_cleanup(&fs_req); - - if (is_directory) { - CHECK_EQ(0, uv_fs_close(nullptr, &fs_req, fd, nullptr)); + int rc = uv_fs_stat(nullptr, &fs_req, path.c_str(), nullptr); + if (rc == 0) { + uint64_t is_directory = fs_req.statbuf.st_mode & S_IFDIR; uv_fs_req_cleanup(&fs_req); - return Nothing(); - } - - if (opt == CLOSE_AFTER_CHECK) { - CHECK_EQ(0, uv_fs_close(nullptr, &fs_req, fd, nullptr)); - uv_fs_req_cleanup(&fs_req); - } - - return Just(fd); -} - -using Exists = PackageConfig::Exists; -using IsValid = PackageConfig::IsValid; -using HasMain = PackageConfig::HasMain; - -const PackageConfig& GetPackageConfig(Environment* env, - const std::string& path) { - auto existing = env->package_json_cache.find(path); - if (existing != env->package_json_cache.end()) { - return existing->second; - } - Maybe check = CheckFile(path, LEAVE_OPEN_AFTER_CHECK); - if (check.IsNothing()) { - auto entry = env->package_json_cache.emplace(path, - PackageConfig { Exists::No, IsValid::Yes, HasMain::No, "" }); - return entry.first->second; + return is_directory ? DIRECTORY : FILE; } - - Isolate* isolate = env->isolate(); - v8::HandleScope handle_scope(isolate); - - std::string pkg_src = ReadFile(check.FromJust()); - uv_fs_t fs_req; - CHECK_EQ(0, uv_fs_close(nullptr, &fs_req, check.FromJust(), nullptr)); uv_fs_req_cleanup(&fs_req); - - Local src; - if (!String::NewFromUtf8(isolate, - pkg_src.c_str(), - v8::NewStringType::kNormal, - pkg_src.length()).ToLocal(&src)) { - auto entry = env->package_json_cache.emplace(path, - PackageConfig { Exists::No, IsValid::Yes, HasMain::No, "" }); - return entry.first->second; - } - - Local pkg_json_v; - Local pkg_json; - - if (!JSON::Parse(env->context(), src).ToLocal(&pkg_json_v) || - !pkg_json_v->ToObject(env->context()).ToLocal(&pkg_json)) { - auto entry = env->package_json_cache.emplace(path, - PackageConfig { Exists::Yes, IsValid::No, HasMain::No, "" }); - return entry.first->second; - } - - Local pkg_main; - HasMain has_main = HasMain::No; - std::string main_std; - if (pkg_json->Get(env->context(), env->main_string()).ToLocal(&pkg_main)) { - has_main = HasMain::Yes; - Utf8Value main_utf8(isolate, pkg_main); - main_std.assign(std::string(*main_utf8, main_utf8.length())); - } - - auto entry = env->package_json_cache.emplace(path, - PackageConfig { Exists::Yes, IsValid::Yes, has_main, main_std }); - return entry.first->second; + return NONE; } -enum ResolveExtensionsOptions { - TRY_EXACT_NAME, - ONLY_VIA_EXTENSIONS -}; - -template -Maybe ResolveExtensions(const URL& search) { - if (options == TRY_EXACT_NAME) { - std::string filePath = search.ToFilePath(); - Maybe check = CheckFile(filePath); - if (!check.IsNothing()) { - return Just(search); - } - } - - for (const char* extension : EXTENSIONS) { - URL guess(search.path() + extension, &search); - Maybe check = CheckFile(guess.ToFilePath()); - if (!check.IsNothing()) { - return Just(guess); - } - } - - return Nothing(); -} - -inline Maybe ResolveIndex(const URL& search) { - return ResolveExtensions(URL("index", search)); -} - -Maybe ResolveMain(Environment* env, const URL& search) { - URL pkg("package.json", &search); - - const PackageConfig& pjson = - GetPackageConfig(env, pkg.ToFilePath()); - // Note invalid package.json should throw in resolver - // currently we silently ignore which is incorrect - if (pjson.exists == Exists::No || - pjson.is_valid == IsValid::No || - pjson.has_main == HasMain::No) { - return Nothing(); - } - if (!ShouldBeTreatedAsRelativeOrAbsolutePath(pjson.main)) { - return Resolve(env, "./" + pjson.main, search, IgnoreMain); - } - return Resolve(env, pjson.main, search, IgnoreMain); -} - -Maybe ResolveModule(Environment* env, - const std::string& specifier, - const URL& base) { +Maybe PackageResolve(Environment* env, + const std::string& specifier, + const URL& base) { URL parent(".", base); - URL dir(""); + std::string last_path; do { - dir = parent; - Maybe check = - Resolve(env, "./node_modules/" + specifier, dir, CheckMain); - if (!check.IsNothing()) { - const size_t limit = specifier.find('/'); - const size_t spec_len = - limit == std::string::npos ? specifier.length() : - limit + 1; - std::string chroot = - dir.path() + "node_modules/" + specifier.substr(0, spec_len); - if (check.FromJust().path().substr(0, chroot.length()) != chroot) { - return Nothing(); - } - return check; - } else { - // TODO(bmeck) PREVENT FALLTHROUGH - } - parent = URL("..", &dir); - } while (parent.path() != dir.path()); + URL pkg_url("./node_modules/" + specifier, &parent); + DescriptorType check = CheckDescriptor(pkg_url.ToFilePath()); + if (check == FILE) return Just(pkg_url); + last_path = parent.path(); + parent = URL("..", &parent); + // cross-platform root check + } while (parent.path() != last_path); return Nothing(); } -Maybe ResolveDirectory(Environment* env, - const URL& search, - PackageMainCheck check_pjson_main) { - if (check_pjson_main) { - Maybe main = ResolveMain(env, search); - if (!main.IsNothing()) - return main; - } - return ResolveIndex(search); -} - } // anonymous namespace Maybe Resolve(Environment* env, const std::string& specifier, - const URL& base, - PackageMainCheck check_pjson_main) { - URL pure_url(specifier); - if (!(pure_url.flags() & URL_FLAGS_FAILED)) { - // just check existence, without altering - Maybe check = CheckFile(pure_url.ToFilePath()); - if (check.IsNothing()) { - return Nothing(); + const URL& base) { + // Order swapped from spec for minor perf gain. + // Ok since relative URLs cannot parse as URLs. + URL resolved; + if (ShouldBeTreatedAsRelativeOrAbsolutePath(specifier)) { + resolved = URL(specifier, base); + } else { + URL pure_url(specifier); + if (!(pure_url.flags() & URL_FLAGS_FAILED)) { + resolved = pure_url; + } else { + return PackageResolve(env, specifier, base); } - return Just(pure_url); } - if (specifier.length() == 0) { + DescriptorType check = CheckDescriptor(resolved.ToFilePath()); + if (check != FILE) { + std::string msg = "Cannot find module '" + resolved.ToFilePath() + + "' imported from " + base.ToFilePath(); + node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str()); return Nothing(); } - if (ShouldBeTreatedAsRelativeOrAbsolutePath(specifier)) { - URL resolved(specifier, base); - Maybe file = ResolveExtensions(resolved); - if (!file.IsNothing()) - return file; - if (specifier.back() != '/') { - resolved = URL(specifier + "/", base); - } - return ResolveDirectory(env, resolved, check_pjson_main); - } else { - return ResolveModule(env, specifier, base); - } + return Just(resolved); } void ModuleWrap::Resolve(const FunctionCallbackInfo& args) { @@ -703,10 +549,18 @@ void ModuleWrap::Resolve(const FunctionCallbackInfo& args) { env, "second argument is not a URL string"); } + TryCatchScope try_catch(env); Maybe result = node::loader::Resolve(env, specifier_std, url); - if (result.IsNothing() || (result.FromJust().flags() & URL_FLAGS_FAILED)) { - std::string msg = "Cannot find module " + specifier_std; - return node::THROW_ERR_MISSING_MODULE(env, msg.c_str()); + if (try_catch.HasCaught()) { + try_catch.ReThrow(); + return; + } else if (result.IsNothing() || + (result.FromJust().flags() & URL_FLAGS_FAILED)) { + std::string msg = "Cannot find module '" + specifier_std + + "' imported from " + url.ToFilePath(); + node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str()); + try_catch.ReThrow(); + return; } MaybeLocal obj = result.FromJust().ToObject(env); diff --git a/src/module_wrap.h b/src/module_wrap.h index dc34685fed..d3089202ba 100644 --- a/src/module_wrap.h +++ b/src/module_wrap.h @@ -12,11 +12,6 @@ namespace node { namespace loader { -enum PackageMainCheck : bool { - CheckMain = true, - IgnoreMain = false -}; - enum ScriptType : int { kScript, kModule, @@ -31,8 +26,7 @@ enum HostDefinedOptions : int { v8::Maybe Resolve(Environment* env, const std::string& specifier, - const url::URL& base, - PackageMainCheck read_pkg_json = CheckMain); + const url::URL& base); class ModuleWrap : public BaseObject { public: diff --git a/src/node_errors.h b/src/node_errors.h index 835794b178..42d34458ed 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -49,8 +49,8 @@ void FatalException(v8::Isolate* isolate, V(ERR_MEMORY_ALLOCATION_FAILED, Error) \ V(ERR_MISSING_ARGS, TypeError) \ V(ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST, TypeError) \ - V(ERR_MISSING_MODULE, Error) \ V(ERR_MISSING_PLATFORM_FOR_WORKER, Error) \ + V(ERR_MODULE_NOT_FOUND, Error) \ V(ERR_OUT_OF_RANGE, RangeError) \ V(ERR_SCRIPT_EXECUTION_INTERRUPTED, Error) \ V(ERR_SCRIPT_EXECUTION_TIMEOUT, Error) \ diff --git a/src/node_options.cc b/src/node_options.cc index 85f0d44710..74b9c00213 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -101,10 +101,6 @@ void PerIsolateOptions::CheckOptions(std::vector* errors) { } void EnvironmentOptions::CheckOptions(std::vector* errors) { - if (!userland_loader.empty() && !experimental_modules) { - errors->push_back("--loader requires --experimental-modules be enabled"); - } - if (syntax_check_only && has_eval_string) { errors->push_back("either --check or --eval can be used, not both"); } @@ -246,11 +242,6 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "(default: llhttp).", &EnvironmentOptions::http_parser, kAllowedInEnvironment); - AddOption("--loader", - "(with --experimental-modules) use the specified file as a " - "custom loader", - &EnvironmentOptions::userland_loader, - kAllowedInEnvironment); AddOption("--no-deprecation", "silence deprecation warnings", &EnvironmentOptions::no_deprecation, diff --git a/src/node_options.h b/src/node_options.h index a49425388c..7346e955db 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -104,7 +104,6 @@ class EnvironmentOptions : public Options { bool trace_deprecation = false; bool trace_sync_io = false; bool trace_warnings = false; - std::string userland_loader; bool syntax_check_only = false; bool has_eval_string = false; diff --git a/test/addons/hello-world-esm/binding.cc b/test/addons/hello-world-esm/binding.cc deleted file mode 100644 index 02eecec099..0000000000 --- a/test/addons/hello-world-esm/binding.cc +++ /dev/null @@ -1,14 +0,0 @@ -#include -#include - -void Method(const v8::FunctionCallbackInfo& args) { - v8::Isolate* isolate = args.GetIsolate(); - args.GetReturnValue().Set(v8::String::NewFromUtf8( - isolate, "world", v8::NewStringType::kNormal).ToLocalChecked()); -} - -void init(v8::Local exports) { - NODE_SET_METHOD(exports, "hello", Method); -} - -NODE_MODULE(NODE_GYP_MODULE_NAME, init) diff --git a/test/addons/hello-world-esm/binding.gyp b/test/addons/hello-world-esm/binding.gyp deleted file mode 100644 index 55fbe7050f..0000000000 --- a/test/addons/hello-world-esm/binding.gyp +++ /dev/null @@ -1,9 +0,0 @@ -{ - 'targets': [ - { - 'target_name': 'binding', - 'sources': [ 'binding.cc' ], - 'includes': ['../common.gypi'], - } - ] -} diff --git a/test/addons/hello-world-esm/test.js b/test/addons/hello-world-esm/test.js deleted file mode 100644 index d0faf65540..0000000000 --- a/test/addons/hello-world-esm/test.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; -const common = require('../../common'); - -const assert = require('assert'); -const { spawnSync } = require('child_process'); -const { copyFileSync } = require('fs'); -const { join } = require('path'); - -const buildDir = join(__dirname, 'build'); - -copyFileSync(join(buildDir, common.buildType, 'binding.node'), - join(buildDir, 'binding.node')); - -const result = spawnSync(process.execPath, - ['--experimental-modules', `${__dirname}/test.mjs`]); - -assert.ifError(result.error); -// TODO: Uncomment this once ESM is no longer experimental. -// assert.strictEqual(result.stderr.toString().trim(), ''); -assert.strictEqual(result.stdout.toString().trim(), 'binding.hello() = world'); diff --git a/test/addons/hello-world-esm/test.mjs b/test/addons/hello-world-esm/test.mjs deleted file mode 100644 index d98de5bf87..0000000000 --- a/test/addons/hello-world-esm/test.mjs +++ /dev/null @@ -1,6 +0,0 @@ -/* eslint-disable node-core/required-modules */ - -import assert from 'assert'; -import binding from './build/binding.node'; -assert.strictEqual(binding.hello(), 'world'); -console.log('binding.hello() =', binding.hello()); diff --git a/test/common/index.mjs b/test/common/index.mjs index de9119f37e..41592098eb 100644 --- a/test/common/index.mjs +++ b/test/common/index.mjs @@ -1,6 +1,15 @@ // Flags: --experimental-modules /* eslint-disable node-core/required-modules */ -import common from './index.js'; + +import { createRequireFromPath } from 'module'; +import { fileURLToPath as toPath } from 'url'; + +function createRequire(metaUrl) { + return createRequireFromPath(toPath(metaUrl)); +} + +const require = createRequire(import.meta.url); +const common = require('./index.js'); const { isMainThread, @@ -91,5 +100,6 @@ export { getBufferSources, disableCrashOnUnhandledRejection, getTTYfd, - runWithInvalidFD + runWithInvalidFD, + createRequire }; diff --git a/test/es-module/test-esm-basic-imports.mjs b/test/es-module/test-esm-basic-imports.mjs index 78a4106f94..d9bb22be0a 100644 --- a/test/es-module/test-esm-basic-imports.mjs +++ b/test/es-module/test-esm-basic-imports.mjs @@ -1,5 +1,6 @@ // Flags: --experimental-modules -import '../common'; +/* eslint-disable node-core/required-modules */ +import '../common/index.mjs'; import assert from 'assert'; import ok from '../fixtures/es-modules/test-esm-ok.mjs'; import okShebang from './test-esm-shebang.mjs'; diff --git a/test/es-module/test-esm-cyclic-dynamic-import.mjs b/test/es-module/test-esm-cyclic-dynamic-import.mjs index c8dfff919c..a207efc73e 100644 --- a/test/es-module/test-esm-cyclic-dynamic-import.mjs +++ b/test/es-module/test-esm-cyclic-dynamic-import.mjs @@ -1,3 +1,4 @@ // Flags: --experimental-modules -import '../common'; -import('./test-esm-cyclic-dynamic-import'); +/* eslint-disable node-core/required-modules */ +import '../common/index.mjs'; +import('./test-esm-cyclic-dynamic-import.mjs'); diff --git a/test/es-module/test-esm-double-encoding.mjs b/test/es-module/test-esm-double-encoding.mjs index c81d0530d3..9366d4bd6b 100644 --- a/test/es-module/test-esm-double-encoding.mjs +++ b/test/es-module/test-esm-double-encoding.mjs @@ -1,6 +1,7 @@ // Flags: --experimental-modules -import '../common'; +/* eslint-disable node-core/required-modules */ +import '../common/index.mjs'; // Assert we can import files with `%` in their pathname. -import '../fixtures/es-modules/test-esm-double-encoding-native%2520.js'; +import '../fixtures/es-modules/test-esm-double-encoding-native%2520.mjs'; diff --git a/test/es-module/test-esm-dynamic-import.js b/test/es-module/test-esm-dynamic-import.js index b271d43c80..07294d4d24 100644 --- a/test/es-module/test-esm-dynamic-import.js +++ b/test/es-module/test-esm-dynamic-import.js @@ -1,4 +1,5 @@ // Flags: --experimental-modules + 'use strict'; const common = require('../common'); const assert = require('assert'); diff --git a/test/es-module/test-esm-encoded-path.mjs b/test/es-module/test-esm-encoded-path.mjs index 365a425afa..2cabfdacff 100644 --- a/test/es-module/test-esm-encoded-path.mjs +++ b/test/es-module/test-esm-encoded-path.mjs @@ -1,5 +1,6 @@ // Flags: --experimental-modules -import '../common'; +/* eslint-disable node-core/required-modules */ +import '../common/index.mjs'; import assert from 'assert'; // ./test-esm-ok.mjs import ok from '../fixtures/es-modules/test-%65%73%6d-ok.mjs'; diff --git a/test/es-module/test-esm-error-cache.js b/test/es-module/test-esm-error-cache.js index 98244615ef..79f76357ec 100644 --- a/test/es-module/test-esm-error-cache.js +++ b/test/es-module/test-esm-error-cache.js @@ -1,11 +1,11 @@ -'use strict'; - // Flags: --experimental-modules +'use strict'; + require('../common'); const assert = require('assert'); -const file = '../fixtures/syntax/bad_syntax.js'; +const file = '../fixtures/syntax/bad_syntax.mjs'; let error; (async () => { diff --git a/test/es-module/test-esm-example-loader.js b/test/es-module/test-esm-example-loader.js deleted file mode 100644 index 0b0001acea..0000000000 --- a/test/es-module/test-esm-example-loader.js +++ /dev/null @@ -1,6 +0,0 @@ -// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/example-loader.mjs -/* eslint-disable node-core/required-modules */ -import assert from 'assert'; -import ok from '../fixtures/es-modules/test-esm-ok.mjs'; - -assert(ok); diff --git a/test/es-module/test-esm-forbidden-globals.mjs b/test/es-module/test-esm-forbidden-globals.mjs index 4e777412a3..cf110ff290 100644 --- a/test/es-module/test-esm-forbidden-globals.mjs +++ b/test/es-module/test-esm-forbidden-globals.mjs @@ -1,5 +1,6 @@ // Flags: --experimental-modules -import '../common'; +/* eslint-disable node-core/required-modules */ +import '../common/index.mjs'; // eslint-disable-next-line no-undef if (typeof arguments !== 'undefined') { diff --git a/test/es-module/test-esm-import-meta.mjs b/test/es-module/test-esm-import-meta.mjs index c17e0e20d4..4c34b337fb 100644 --- a/test/es-module/test-esm-import-meta.mjs +++ b/test/es-module/test-esm-import-meta.mjs @@ -1,6 +1,7 @@ // Flags: --experimental-modules +/* eslint-disable node-core/required-modules */ -import '../common'; +import '../common/index.mjs'; import assert from 'assert'; assert.strictEqual(Object.getPrototypeOf(import.meta), null); diff --git a/test/es-module/test-esm-json.mjs b/test/es-module/test-esm-json.mjs deleted file mode 100644 index a7146d19a9..0000000000 --- a/test/es-module/test-esm-json.mjs +++ /dev/null @@ -1,8 +0,0 @@ -// Flags: --experimental-modules -import '../common'; -import assert from 'assert'; -import ok from '../fixtures/es-modules/test-esm-ok.mjs'; -import json from '../fixtures/es-modules/json.json'; - -assert(ok); -assert.strictEqual(json.val, 42); diff --git a/test/es-module/test-esm-live-binding.mjs b/test/es-module/test-esm-live-binding.mjs index d151e004df..880a6c389b 100644 --- a/test/es-module/test-esm-live-binding.mjs +++ b/test/es-module/test-esm-live-binding.mjs @@ -1,6 +1,7 @@ // Flags: --experimental-modules +/* eslint-disable node-core/required-modules */ -import '../common'; +import '../common/index.mjs'; import assert from 'assert'; import fs, { readFile, readFileSync } from 'fs'; diff --git a/test/es-module/test-esm-loader-dependency.mjs b/test/es-module/test-esm-loader-dependency.mjs deleted file mode 100644 index 1ed8685a6f..0000000000 --- a/test/es-module/test-esm-loader-dependency.mjs +++ /dev/null @@ -1,5 +0,0 @@ -// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-with-dep.mjs -/* eslint-disable node-core/required-modules */ -import '../fixtures/es-modules/test-esm-ok.mjs'; - -// We just test that this module doesn't fail loading diff --git a/test/es-module/test-esm-loader-invalid-format.mjs b/test/es-module/test-esm-loader-invalid-format.mjs deleted file mode 100644 index f8714d4aa1..0000000000 --- a/test/es-module/test-esm-loader-invalid-format.mjs +++ /dev/null @@ -1,11 +0,0 @@ -// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-invalid-format.mjs -import { expectsError, mustCall } from '../common'; -import assert from 'assert'; - -import('../fixtures/es-modules/test-esm-ok.mjs') -.then(assert.fail, expectsError({ - code: 'ERR_INVALID_RETURN_PROPERTY_VALUE', - message: 'Expected string to be returned for the "format" from the ' + - '"loader resolve" function but got type undefined.' -})) -.then(mustCall()); diff --git a/test/es-module/test-esm-loader-invalid-url.mjs b/test/es-module/test-esm-loader-invalid-url.mjs deleted file mode 100644 index 43971a2e6e..0000000000 --- a/test/es-module/test-esm-loader-invalid-url.mjs +++ /dev/null @@ -1,12 +0,0 @@ -// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-invalid-url.mjs -import { expectsError, mustCall } from '../common'; -import assert from 'assert'; - -import('../fixtures/es-modules/test-esm-ok.mjs') -.then(assert.fail, expectsError({ - code: 'ERR_INVALID_RETURN_PROPERTY', - message: 'Expected a valid url to be returned for the "url" from the ' + - '"loader resolve" function but got ' + - '../fixtures/es-modules/test-esm-ok.mjs.' -})) -.then(mustCall()); diff --git a/test/es-module/test-esm-loader-missing-dynamic-instantiate-hook.mjs b/test/es-module/test-esm-loader-missing-dynamic-instantiate-hook.mjs deleted file mode 100644 index f2b37f7e8a..0000000000 --- a/test/es-module/test-esm-loader-missing-dynamic-instantiate-hook.mjs +++ /dev/null @@ -1,9 +0,0 @@ -// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/missing-dynamic-instantiate-hook.mjs - -import { expectsError } from '../common'; - -import('test').catch(expectsError({ - code: 'ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK', - message: 'The ES Module loader may not return a format of \'dynamic\' ' + - 'when no dynamicInstantiate function was provided' -})); diff --git a/test/es-module/test-esm-loader-search.js b/test/es-module/test-esm-loader-search.js deleted file mode 100644 index 0ca8990cb7..0000000000 --- a/test/es-module/test-esm-loader-search.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; -// Flags: --expose-internals - -// This test ensures that search throws errors appropriately - -const common = require('../common'); - -const { search } = require('internal/modules/esm/default_resolve'); - -common.expectsError( - () => search('target', undefined), - { - code: 'ERR_MISSING_MODULE', - type: Error, - message: 'Cannot find module target' - } -); diff --git a/test/es-module/test-esm-main-lookup.mjs b/test/es-module/test-esm-main-lookup.mjs index ca313a1d26..76c6263853 100644 --- a/test/es-module/test-esm-main-lookup.mjs +++ b/test/es-module/test-esm-main-lookup.mjs @@ -1,6 +1,26 @@ // Flags: --experimental-modules -import '../common'; +/* eslint-disable node-core/required-modules */ +import '../common/index.mjs'; import assert from 'assert'; -import main from '../fixtures/es-modules/pjson-main'; -assert.strictEqual(main, 'main'); +async function main() { + let mod; + try { + mod = await import('../fixtures/es-modules/pjson-main'); + } catch (e) { + assert.strictEqual(e.code, 'MODULE_NOT_FOUND'); + } + + assert.strictEqual(mod, undefined); + + try { + mod = await import('../fixtures/es-modules/pjson-main/main.mjs'); + } catch (e) { + console.log(e); + assert.fail(); + } + + assert.strictEqual(mod.main, 'main'); +} + +main(); diff --git a/test/es-module/test-esm-named-exports.mjs b/test/es-module/test-esm-named-exports.mjs deleted file mode 100644 index 3aae9230de..0000000000 --- a/test/es-module/test-esm-named-exports.mjs +++ /dev/null @@ -1,8 +0,0 @@ -// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs -import '../common'; -import { readFile } from 'fs'; -import assert from 'assert'; -import ok from '../fixtures/es-modules/test-esm-ok.mjs'; - -assert(ok); -assert(readFile); diff --git a/test/es-module/test-esm-namespace.mjs b/test/es-module/test-esm-namespace.mjs index da1286d0f4..38b7ef12d5 100644 --- a/test/es-module/test-esm-namespace.mjs +++ b/test/es-module/test-esm-namespace.mjs @@ -1,5 +1,7 @@ // Flags: --experimental-modules -import '../common'; +/* eslint-disable node-core/required-modules */ + +import '../common/index.mjs'; import * as fs from 'fs'; import assert from 'assert'; import Module from 'module'; diff --git a/test/es-module/test-esm-preserve-symlinks-not-found-plain.mjs b/test/es-module/test-esm-preserve-symlinks-not-found-plain.mjs deleted file mode 100644 index 2ca0f56581..0000000000 --- a/test/es-module/test-esm-preserve-symlinks-not-found-plain.mjs +++ /dev/null @@ -1,3 +0,0 @@ -// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/not-found-assert-loader.mjs -/* eslint-disable node-core/required-modules */ -import './not-found.js'; diff --git a/test/es-module/test-esm-preserve-symlinks-not-found.mjs b/test/es-module/test-esm-preserve-symlinks-not-found.mjs deleted file mode 100644 index 5119957bae..0000000000 --- a/test/es-module/test-esm-preserve-symlinks-not-found.mjs +++ /dev/null @@ -1,3 +0,0 @@ -// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/not-found-assert-loader.mjs -/* eslint-disable node-core/required-modules */ -import './not-found'; diff --git a/test/es-module/test-esm-process.mjs b/test/es-module/test-esm-process.mjs index 24cf489a98..5274d75164 100644 --- a/test/es-module/test-esm-process.mjs +++ b/test/es-module/test-esm-process.mjs @@ -1,5 +1,6 @@ // Flags: --experimental-modules -import '../common'; +/* eslint-disable node-core/required-modules */ +import '../common/index.mjs'; import assert from 'assert'; import process from 'process'; diff --git a/test/es-module/test-esm-require-cache.mjs b/test/es-module/test-esm-require-cache.mjs index ff32cde36f..09030e0578 100644 --- a/test/es-module/test-esm-require-cache.mjs +++ b/test/es-module/test-esm-require-cache.mjs @@ -1,7 +1,12 @@ // Flags: --experimental-modules -import '../common'; -import '../fixtures/es-module-require-cache/preload.js'; -import '../fixtures/es-module-require-cache/counter.js'; +/* eslint-disable node-core/required-modules */ +import { createRequire } from '../common/index.mjs'; import assert from 'assert'; +// +const require = createRequire(import.meta.url); + +require('../fixtures/es-module-require-cache/preload.js'); +require('../fixtures/es-module-require-cache/counter.js'); + assert.strictEqual(global.counter, 1); delete global.counter; diff --git a/test/es-module/test-esm-resolve-hook.mjs b/test/es-module/test-esm-resolve-hook.mjs deleted file mode 100644 index e326d20b6d..0000000000 --- a/test/es-module/test-esm-resolve-hook.mjs +++ /dev/null @@ -1,8 +0,0 @@ -// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/js-loader.mjs -/* eslint-disable node-core/required-modules */ -import { namedExport } from '../fixtures/es-module-loaders/js-as-esm.js'; -import assert from 'assert'; -import ok from '../fixtures/es-modules/test-esm-ok.mjs'; - -assert(ok); -assert(namedExport); diff --git a/test/es-module/test-esm-shared-loader-dep.mjs b/test/es-module/test-esm-shared-loader-dep.mjs deleted file mode 100644 index 5c274d835c..0000000000 --- a/test/es-module/test-esm-shared-loader-dep.mjs +++ /dev/null @@ -1,7 +0,0 @@ -// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-shared-dep.mjs -import '../common'; -import assert from 'assert'; -import '../fixtures/es-modules/test-esm-ok.mjs'; -import dep from '../fixtures/es-module-loaders/loader-dep.js'; - -assert.strictEqual(dep.format, 'esm'); diff --git a/test/es-module/test-esm-shebang.mjs b/test/es-module/test-esm-shebang.mjs index d5faace479..486e04dade 100644 --- a/test/es-module/test-esm-shebang.mjs +++ b/test/es-module/test-esm-shebang.mjs @@ -1,6 +1,7 @@ #! }]) // isn't js // Flags: --experimental-modules -import '../common'; +/* eslint-disable node-core/required-modules */ +import '../common/index.mjs'; const isJs = true; export default isJs; diff --git a/test/es-module/test-esm-snapshot.mjs b/test/es-module/test-esm-snapshot.mjs deleted file mode 100644 index 3d4b44bbdd..0000000000 --- a/test/es-module/test-esm-snapshot.mjs +++ /dev/null @@ -1,7 +0,0 @@ -// Flags: --experimental-modules -import '../common'; -import '../fixtures/es-modules/esm-snapshot-mutator'; -import one from '../fixtures/es-modules/esm-snapshot'; -import assert from 'assert'; - -assert.strictEqual(one, 1); diff --git a/test/es-module/test-esm-symlink-main.js b/test/es-module/test-esm-symlink-main.js index f7631ef2e5..871180f5cc 100644 --- a/test/es-module/test-esm-symlink-main.js +++ b/test/es-module/test-esm-symlink-main.js @@ -9,7 +9,7 @@ const fs = require('fs'); tmpdir.refresh(); const realPath = path.resolve(__dirname, '../fixtures/es-modules/symlink.mjs'); -const symlinkPath = path.resolve(tmpdir.path, 'symlink.js'); +const symlinkPath = path.resolve(tmpdir.path, 'symlink.mjs'); try { fs.symlinkSync(realPath, symlinkPath); diff --git a/test/es-module/test-esm-symlink.js b/test/es-module/test-esm-symlink.js index 232925a52e..9b9eb98cd9 100644 --- a/test/es-module/test-esm-symlink.js +++ b/test/es-module/test-esm-symlink.js @@ -12,8 +12,8 @@ const tmpDir = tmpdir.path; const entry = path.join(tmpDir, 'entry.mjs'); const real = path.join(tmpDir, 'index.mjs'); -const link_absolute_path = path.join(tmpDir, 'absolute'); -const link_relative_path = path.join(tmpDir, 'relative'); +const link_absolute_path = path.join(tmpDir, 'absolute.mjs'); +const link_relative_path = path.join(tmpDir, 'relative.mjs'); const link_ignore_extension = path.join(tmpDir, 'ignore_extension.json'); const link_directory = path.join(tmpDir, 'directory'); @@ -22,15 +22,13 @@ fs.writeFileSync(real, 'export default [];'); fs.writeFileSync(entry, ` import assert from 'assert'; import real from './index.mjs'; -import absolute from './absolute'; -import relative from './relative'; +import absolute from './absolute.mjs'; +import relative from './relative.mjs'; import ignoreExtension from './ignore_extension.json'; -import directory from './directory'; assert.strictEqual(absolute, real); assert.strictEqual(relative, real); assert.strictEqual(ignoreExtension, real); -assert.strictEqual(directory, real); `); try { diff --git a/test/es-module/test-esm-throw-undefined.mjs b/test/es-module/test-esm-throw-undefined.mjs deleted file mode 100644 index 541127eee5..0000000000 --- a/test/es-module/test-esm-throw-undefined.mjs +++ /dev/null @@ -1,14 +0,0 @@ -// Flags: --experimental-modules -import '../common'; -import assert from 'assert'; - -async function doTest() { - await assert.rejects( - async () => { - await import('../fixtures/es-module-loaders/throw-undefined'); - }, - (e) => e === undefined - ); -} - -doTest(); diff --git a/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs b/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs deleted file mode 100644 index a944c4fd5e..0000000000 --- a/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs +++ /dev/null @@ -1,24 +0,0 @@ -import module from 'module'; - -export function dynamicInstantiate(url) { - const builtinInstance = module._load(url.substr(5)); - const builtinExports = ['default', ...Object.keys(builtinInstance)]; - return { - exports: builtinExports, - execute: exports => { - for (let name of builtinExports) - exports[name].set(builtinInstance[name]); - exports.default.set(builtinInstance); - } - }; -} - -export function resolve(specifier, base, defaultResolver) { - if (module.builtinModules.includes(specifier)) { - return { - url: `node:${specifier}`, - format: 'dynamic' - }; - } - return defaultResolver(specifier, base); -} diff --git a/test/fixtures/es-module-loaders/example-loader.mjs b/test/fixtures/es-module-loaders/example-loader.mjs deleted file mode 100644 index a7cf276d4a..0000000000 --- a/test/fixtures/es-module-loaders/example-loader.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import url from 'url'; -import path from 'path'; -import process from 'process'; -import { builtinModules } from 'module'; - -const JS_EXTENSIONS = new Set(['.js', '.mjs']); - -const baseURL = new url.URL('file://'); -baseURL.pathname = process.cwd() + '/'; - -export function resolve(specifier, parentModuleURL = baseURL /*, defaultResolve */) { - if (builtinModules.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.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' - }; -} diff --git a/test/fixtures/es-module-loaders/js-as-esm.js b/test/fixtures/es-module-loaders/js-as-esm.js deleted file mode 100644 index b4d2741b2f..0000000000 --- a/test/fixtures/es-module-loaders/js-as-esm.js +++ /dev/null @@ -1 +0,0 @@ -export const namedExport = 'named-export'; diff --git a/test/fixtures/es-module-loaders/js-loader.mjs b/test/fixtures/es-module-loaders/js-loader.mjs deleted file mode 100644 index 2ac959a464..0000000000 --- a/test/fixtures/es-module-loaders/js-loader.mjs +++ /dev/null @@ -1,20 +0,0 @@ -import { URL } from 'url'; -import { builtinModules } from 'module'; - -const baseURL = new URL('file://'); -baseURL.pathname = process.cwd() + '/'; - -export function resolve (specifier, base = baseURL) { - if (builtinModules.includes(specifier)) { - return { - url: specifier, - format: 'builtin' - }; - } - // load all dependencies as esm, regardless of file extension - const url = new URL(specifier, base).href; - return { - url, - format: 'esm' - }; -} diff --git a/test/fixtures/es-module-loaders/loader-dep.js b/test/fixtures/es-module-loaders/loader-dep.js deleted file mode 100644 index cf821afec1..0000000000 --- a/test/fixtures/es-module-loaders/loader-dep.js +++ /dev/null @@ -1 +0,0 @@ -exports.format = 'esm'; diff --git a/test/fixtures/es-module-loaders/loader-invalid-format.mjs b/test/fixtures/es-module-loaders/loader-invalid-format.mjs deleted file mode 100644 index 17a0dcd04d..0000000000 --- a/test/fixtures/es-module-loaders/loader-invalid-format.mjs +++ /dev/null @@ -1,8 +0,0 @@ -export async function resolve(specifier, parentModuleURL, defaultResolve) { - if (parentModuleURL && specifier === '../fixtures/es-modules/test-esm-ok.mjs') { - return { - url: 'file:///asdf' - }; - } - return defaultResolve(specifier, parentModuleURL); -} diff --git a/test/fixtures/es-module-loaders/loader-invalid-url.mjs b/test/fixtures/es-module-loaders/loader-invalid-url.mjs deleted file mode 100644 index 12efbb5021..0000000000 --- a/test/fixtures/es-module-loaders/loader-invalid-url.mjs +++ /dev/null @@ -1,9 +0,0 @@ -export async function resolve(specifier, parentModuleURL, defaultResolve) { - if (parentModuleURL && specifier === '../fixtures/es-modules/test-esm-ok.mjs') { - return { - url: specifier, - format: 'esm' - }; - } - return defaultResolve(specifier, parentModuleURL); -} diff --git a/test/fixtures/es-module-loaders/loader-shared-dep.mjs b/test/fixtures/es-module-loaders/loader-shared-dep.mjs deleted file mode 100644 index 1a19e4c892..0000000000 --- a/test/fixtures/es-module-loaders/loader-shared-dep.mjs +++ /dev/null @@ -1,7 +0,0 @@ -import dep from './loader-dep.js'; -import assert from 'assert'; - -export function resolve(specifier, base, defaultResolve) { - assert.strictEqual(dep.format, 'esm'); - return defaultResolve(specifier, base); -} diff --git a/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs b/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs deleted file mode 100644 index e7c6c8ff34..0000000000 --- a/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs +++ /dev/null @@ -1,6 +0,0 @@ -export async function resolve(specifier, parent, defaultResolve) { - if (specifier === 'unknown-builtin-module') { - return { url: 'unknown-builtin-module', format: 'builtin' }; - } - return defaultResolve(specifier, parent); -} \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/loader-with-dep.mjs b/test/fixtures/es-module-loaders/loader-with-dep.mjs deleted file mode 100644 index 944e6e438c..0000000000 --- a/test/fixtures/es-module-loaders/loader-with-dep.mjs +++ /dev/null @@ -1,7 +0,0 @@ -import dep from './loader-dep.js'; -export function resolve (specifier, base, defaultResolve) { - return { - url: defaultResolve(specifier, base).url, - format: dep.format - }; -} diff --git a/test/fixtures/es-module-loaders/missing-dynamic-instantiate-hook.mjs b/test/fixtures/es-module-loaders/missing-dynamic-instantiate-hook.mjs deleted file mode 100644 index 6993747fcc..0000000000 --- a/test/fixtures/es-module-loaders/missing-dynamic-instantiate-hook.mjs +++ /dev/null @@ -1,6 +0,0 @@ -export function resolve(specifier, parentModule, defaultResolver) { - if (specifier !== 'test') { - return defaultResolver(specifier, parentModule); - } - return { url: 'file://', format: 'dynamic' }; -} diff --git a/test/fixtures/es-module-loaders/module-named-exports.mjs b/test/fixtures/es-module-loaders/module-named-exports.mjs deleted file mode 100644 index 04f7f43ebd..0000000000 --- a/test/fixtures/es-module-loaders/module-named-exports.mjs +++ /dev/null @@ -1,2 +0,0 @@ -export const foo = 'foo'; -export const bar = 'bar'; diff --git a/test/fixtures/es-module-loaders/not-found-assert-loader.mjs b/test/fixtures/es-module-loaders/not-found-assert-loader.mjs deleted file mode 100644 index d15f294fe6..0000000000 --- a/test/fixtures/es-module-loaders/not-found-assert-loader.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import assert from 'assert'; - -// a loader that asserts that the defaultResolve will throw "not found" -// (skipping the top-level main of course) -let mainLoad = true; -export async function resolve (specifier, base, defaultResolve) { - if (mainLoad) { - mainLoad = false; - return defaultResolve(specifier, base); - } - try { - await defaultResolve(specifier, base); - } - catch (e) { - assert.strictEqual(e.code, 'MODULE_NOT_FOUND'); - return { - format: 'builtin', - url: 'fs' - }; - } - assert.fail(`Module resolution for ${specifier} should be throw MODULE_NOT_FOUND`); -} diff --git a/test/fixtures/es-module-loaders/syntax-error-import.mjs b/test/fixtures/es-module-loaders/syntax-error-import.mjs deleted file mode 100644 index 9cad68c7ce..0000000000 --- a/test/fixtures/es-module-loaders/syntax-error-import.mjs +++ /dev/null @@ -1 +0,0 @@ -import { foo, notfound } from './module-named-exports'; diff --git a/test/fixtures/es-module-loaders/syntax-error.mjs b/test/fixtures/es-module-loaders/syntax-error.mjs deleted file mode 100644 index bda4a7e6eb..0000000000 --- a/test/fixtures/es-module-loaders/syntax-error.mjs +++ /dev/null @@ -1,2 +0,0 @@ -'use strict'; -await async () => 0; diff --git a/test/fixtures/es-module-loaders/throw-undefined.mjs b/test/fixtures/es-module-loaders/throw-undefined.mjs deleted file mode 100644 index f062276767..0000000000 --- a/test/fixtures/es-module-loaders/throw-undefined.mjs +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -throw undefined; diff --git a/test/fixtures/es-modules/esm-snapshot-mutator.js b/test/fixtures/es-modules/esm-snapshot-mutator.js deleted file mode 100644 index ee52c270f6..0000000000 --- a/test/fixtures/es-modules/esm-snapshot-mutator.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; -const shouldSnapshotFilePath = require.resolve('./esm-snapshot.js'); -require('./esm-snapshot.js'); -require.cache[shouldSnapshotFilePath].exports++; diff --git a/test/fixtures/es-modules/esm-snapshot.js b/test/fixtures/es-modules/esm-snapshot.js deleted file mode 100644 index 329a0ca3f4..0000000000 --- a/test/fixtures/es-modules/esm-snapshot.js +++ /dev/null @@ -1,2 +0,0 @@ -'use strict'; -module.exports = 1; diff --git a/test/fixtures/es-modules/json.json b/test/fixtures/es-modules/json.json deleted file mode 100644 index 8288d42e2b..0000000000 --- a/test/fixtures/es-modules/json.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "val": 42 -} diff --git a/test/fixtures/es-modules/loop.mjs b/test/fixtures/es-modules/loop.mjs index 1b5cab10ed..3d2ddd2eb7 100644 --- a/test/fixtures/es-modules/loop.mjs +++ b/test/fixtures/es-modules/loop.mjs @@ -1,4 +1,4 @@ -import { message } from './message'; +import { message } from './message.mjs'; var t = 1; var k = 1; diff --git a/test/fixtures/es-modules/pjson-main/main.js b/test/fixtures/es-modules/pjson-main/main.js deleted file mode 100644 index dfdd47b877..0000000000 --- a/test/fixtures/es-modules/pjson-main/main.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = 'main'; diff --git a/test/fixtures/es-modules/pjson-main/main.mjs b/test/fixtures/es-modules/pjson-main/main.mjs new file mode 100644 index 0000000000..9eb0aade18 --- /dev/null +++ b/test/fixtures/es-modules/pjson-main/main.mjs @@ -0,0 +1 @@ +export const main = 'main' diff --git a/test/fixtures/es-modules/pjson-main/package.json b/test/fixtures/es-modules/pjson-main/package.json index c13b8cf6ac..ea9b784692 100644 --- a/test/fixtures/es-modules/pjson-main/package.json +++ b/test/fixtures/es-modules/pjson-main/package.json @@ -1,3 +1,3 @@ { - "main": "main.js" + "main": "main.mjs" } diff --git a/test/fixtures/es-modules/test-esm-double-encoding-native%20.js b/test/fixtures/es-modules/test-esm-double-encoding-native%20.mjs similarity index 86% rename from test/fixtures/es-modules/test-esm-double-encoding-native%20.js rename to test/fixtures/es-modules/test-esm-double-encoding-native%20.mjs index ea1caa81be..a3bfe972e5 100644 --- a/test/fixtures/es-modules/test-esm-double-encoding-native%20.js +++ b/test/fixtures/es-modules/test-esm-double-encoding-native%20.mjs @@ -3,4 +3,4 @@ // Trivial test to assert we can load files with `%` in their pathname. // Imported by `test-esm-double-encoding.mjs`. -module.exports = 42; +export default 42; diff --git a/test/fixtures/syntax/bad_syntax.mjs b/test/fixtures/syntax/bad_syntax.mjs new file mode 100644 index 0000000000..c2cd118b23 --- /dev/null +++ b/test/fixtures/syntax/bad_syntax.mjs @@ -0,0 +1 @@ +var foo bar; diff --git a/test/message/esm_display_syntax_error_import.mjs b/test/message/esm_display_syntax_error_import.mjs deleted file mode 100644 index 87cedf1d4e..0000000000 --- a/test/message/esm_display_syntax_error_import.mjs +++ /dev/null @@ -1,7 +0,0 @@ -// Flags: --experimental-modules -/* eslint-disable no-unused-vars */ -import '../common'; -import { - foo, - notfound -} from '../fixtures/es-module-loaders/module-named-exports'; diff --git a/test/message/esm_display_syntax_error_import.out b/test/message/esm_display_syntax_error_import.out deleted file mode 100644 index 31ee2b6f4b..0000000000 --- a/test/message/esm_display_syntax_error_import.out +++ /dev/null @@ -1,6 +0,0 @@ -(node:*) ExperimentalWarning: The ESM module loader is experimental. -file:///*/test/message/esm_display_syntax_error_import.mjs:6 - notfound - ^^^^^^^^ -SyntaxError: The requested module '../fixtures/es-module-loaders/module-named-exports' does not provide an export named 'notfound' - at ModuleJob._instantiate (internal/modules/esm/module_job.js:*:*) diff --git a/test/message/esm_display_syntax_error_import_module.mjs b/test/message/esm_display_syntax_error_import_module.mjs deleted file mode 100644 index 32c0edb350..0000000000 --- a/test/message/esm_display_syntax_error_import_module.mjs +++ /dev/null @@ -1,3 +0,0 @@ -// Flags: --experimental-modules -import '../common'; -import '../fixtures/es-module-loaders/syntax-error-import'; diff --git a/test/message/esm_display_syntax_error_import_module.out b/test/message/esm_display_syntax_error_import_module.out deleted file mode 100644 index b067a77942..0000000000 --- a/test/message/esm_display_syntax_error_import_module.out +++ /dev/null @@ -1,6 +0,0 @@ -(node:*) ExperimentalWarning: The ESM module loader is experimental. -file:///*/test/fixtures/es-module-loaders/syntax-error-import.mjs:1 -import { foo, notfound } from './module-named-exports'; - ^^^^^^^^ -SyntaxError: The requested module './module-named-exports' does not provide an export named 'notfound' - at ModuleJob._instantiate (internal/modules/esm/module_job.js:*:*) diff --git a/test/message/esm_display_syntax_error_module.mjs b/test/message/esm_display_syntax_error_module.mjs deleted file mode 100644 index e74b70bec8..0000000000 --- a/test/message/esm_display_syntax_error_module.mjs +++ /dev/null @@ -1,3 +0,0 @@ -// Flags: --experimental-modules -import '../common'; -import '../fixtures/es-module-loaders/syntax-error'; diff --git a/test/message/esm_display_syntax_error_module.out b/test/message/esm_display_syntax_error_module.out deleted file mode 100644 index e636abad9e..0000000000 --- a/test/message/esm_display_syntax_error_module.out +++ /dev/null @@ -1,6 +0,0 @@ -(node:*) ExperimentalWarning: The ESM module loader is experimental. -file:///*/test/fixtures/es-module-loaders/syntax-error.mjs:2 -await async () => 0; -^^^^^ -SyntaxError: Unexpected reserved word - at translators.set (internal/modules/esm/translators.js:*:*) diff --git a/test/parallel/test-loaders-unknown-builtin-module.mjs b/test/parallel/test-loaders-unknown-builtin-module.mjs deleted file mode 100644 index db3cfa3582..0000000000 --- a/test/parallel/test-loaders-unknown-builtin-module.mjs +++ /dev/null @@ -1,12 +0,0 @@ -// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs -import { expectsError, mustCall } from '../common'; -import assert from 'assert'; - -const unknownBuiltinModule = 'unknown-builtin-module'; - -import(unknownBuiltinModule) -.then(assert.fail, expectsError({ - code: 'ERR_UNKNOWN_BUILTIN_MODULE', - message: `No such built-in module: ${unknownBuiltinModule}` -})) -.then(mustCall()); diff --git a/test/parallel/test-module-main-extension-lookup.js b/test/parallel/test-module-main-extension-lookup.js index 3d20316647..9e7eab295e 100644 --- a/test/parallel/test-module-main-extension-lookup.js +++ b/test/parallel/test-module-main-extension-lookup.js @@ -6,6 +6,6 @@ const { execFileSync } = require('child_process'); const node = process.argv[0]; execFileSync(node, ['--experimental-modules', - fixtures.path('es-modules', 'test-esm-ok')]); + fixtures.path('es-modules', 'test-esm-ok.mjs')]); execFileSync(node, ['--experimental-modules', fixtures.path('es-modules', 'noext')]); From d95c33d3c52fed6fa6e1990df20f8ed8ac91bfe4 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Thu, 24 Jan 2019 19:33:28 +0200 Subject: [PATCH 2/5] esm: irp type implementation Refs: https://github.com/GeoffreyBooth/node-import-file-specifier-resolution-proposal PR-URL: https://github.com/nodejs/ecmascript-modules/pull/28 --- .eslintrc.js | 2 + doc/api/cli.md | 12 + doc/api/errors.md | 32 +- doc/api/esm.md | 38 +- doc/node.1 | 3 + lib/internal/errors.js | 32 +- lib/internal/main/check_syntax.js | 36 +- lib/internal/main/eval_stdin.js | 6 +- lib/internal/main/eval_string.js | 7 +- lib/internal/main/repl.js | 7 + lib/internal/modules/cjs/loader.js | 26 +- lib/internal/modules/esm/default_resolve.js | 142 +++++--- lib/internal/modules/esm/loader.js | 29 +- lib/internal/modules/esm/module_job.js | 5 +- lib/internal/modules/esm/translators.js | 13 +- lib/internal/process/esm_loader.js | 18 +- lib/internal/process/execution.js | 19 + src/env.h | 26 +- src/module_wrap.cc | 335 ++++++++++++++++-- src/module_wrap.h | 4 - src/node_errors.h | 2 + src/node_options.cc | 11 +- src/node_options.h | 1 + test/es-module/test-esm-dynamic-import.js | 2 +- test/es-module/test-esm-loader-modulemap.js | 2 +- test/es-module/test-esm-main-lookup.mjs | 2 +- test/es-module/test-esm-package-scope.mjs | 12 + test/es-module/test-esm-symlink-type.js | 77 ++++ test/es-module/test-esm-type-flag-alias.mjs | 6 + test/es-module/test-esm-type-flag-errors.js | 57 +++ test/es-module/test-esm-type-flag.mjs | 11 + test/fixtures/es-modules/cjs-file.cjs | 1 + test/fixtures/es-modules/mjs-file.mjs | 1 + .../es-modules/package-type-commonjs/index.js | 3 + .../package-type-commonjs/package.json | 4 + .../es-modules/package-type-module/index.js | 3 + .../package-type-module/package.json | 4 + .../es-modules/package-without-type/index.js | 3 + .../package-without-type/package.json | 3 + .../esm-package-scope/legacy-loader/a.js | 1 + .../esm-package-scope/legacy-loader/b.mjs | 1 + .../esm-package-scope/legacy-loader/c.cjs | 5 + .../esm-package-scope/legacy-loader/index.mjs | 21 ++ .../legacy-loader/package.json | 13 + .../esm-package-scope/new-loader/a.js | 1 + .../esm-package-scope/new-loader/b.mjs | 1 + .../esm-package-scope/new-loader/c.cjs | 5 + .../esm-package-scope/new-loader/index.js | 21 ++ .../esm-package-scope/new-loader/package.json | 13 + test/message/esm_display_syntax_error.out | 2 +- test/parallel/test-cli-syntax-piped-bad.js | 33 +- test/parallel/test-cli-syntax-piped-good.js | 24 +- 52 files changed, 962 insertions(+), 176 deletions(-) create mode 100644 test/es-module/test-esm-package-scope.mjs create mode 100644 test/es-module/test-esm-symlink-type.js create mode 100644 test/es-module/test-esm-type-flag-alias.mjs create mode 100644 test/es-module/test-esm-type-flag-errors.js create mode 100644 test/es-module/test-esm-type-flag.mjs create mode 100644 test/fixtures/es-modules/cjs-file.cjs create mode 100644 test/fixtures/es-modules/mjs-file.mjs create mode 100644 test/fixtures/es-modules/package-type-commonjs/index.js create mode 100644 test/fixtures/es-modules/package-type-commonjs/package.json create mode 100644 test/fixtures/es-modules/package-type-module/index.js create mode 100644 test/fixtures/es-modules/package-type-module/package.json create mode 100644 test/fixtures/es-modules/package-without-type/index.js create mode 100644 test/fixtures/es-modules/package-without-type/package.json create mode 100644 test/fixtures/esm-package-scope/legacy-loader/a.js create mode 100644 test/fixtures/esm-package-scope/legacy-loader/b.mjs create mode 100644 test/fixtures/esm-package-scope/legacy-loader/c.cjs create mode 100644 test/fixtures/esm-package-scope/legacy-loader/index.mjs create mode 100644 test/fixtures/esm-package-scope/legacy-loader/package.json create mode 100644 test/fixtures/esm-package-scope/new-loader/a.js create mode 100644 test/fixtures/esm-package-scope/new-loader/b.mjs create mode 100644 test/fixtures/esm-package-scope/new-loader/c.cjs create mode 100644 test/fixtures/esm-package-scope/new-loader/index.js create mode 100644 test/fixtures/esm-package-scope/new-loader/package.json diff --git a/.eslintrc.js b/.eslintrc.js index 750c230ca1..e2b48b34f5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -38,6 +38,8 @@ module.exports = { { files: [ 'doc/api/esm.md', + 'test/es-module/test-esm-type-flag.js', + 'test/es-module/test-esm-type-flag-alias.js', '*.mjs', ], parserOptions: { sourceType: 'module' }, diff --git a/doc/api/cli.md b/doc/api/cli.md index f8fa53a5cc..de822b6a06 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -504,6 +504,18 @@ added: v2.4.0 Track heap object allocations for heap snapshots. +### `-m`, `--type=type` + +When using `--experimental-modules`, this informs the module resolution type +to interpret the top-level entry into Node.js. + +Works with stdin, `--eval`, `--print` as well as standard execution. + +Valid values are `"commonjs"` and `"module"`, where the default is to infer +from the file extension and package type boundary. + +`-m` is an alias for `--type=module`. + ### `--use-bundled-ca`, `--use-openssl-ca` + +Specify the `file` of the custom [experimental ECMAScript Module][] loader. + ### `--max-http-header-size=size` + +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 | +| --- | --- | +| `'module'` | Load a standard JavaScript module | +| `'commonjs'` | Load a Node.js CommonJS module | +| `'builtin'` | Load a Node.js builtin module | +| `'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. + [Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md +[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/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js index e405dc8b30..803c854d9a 100644 --- a/lib/internal/process/esm_loader.js +++ b/lib/internal/process/esm_loader.js @@ -7,6 +7,7 @@ const { ERR_INVALID_TYPE_FLAG, ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING, } = require('internal/errors').codes; +const { emitExperimentalWarning } = require('internal/util'); const type = require('internal/options').getOptionValue('--type'); if (type && type !== 'commonjs' && type !== 'module') @@ -14,6 +15,7 @@ if (type && type !== 'commonjs' && type !== 'module') exports.typeFlag = type; const { Loader } = require('internal/modules/esm/loader'); +const { pathToFileURL } = require('internal/url'); const { wrapToModuleMap, } = require('internal/vm/source_text_module'); @@ -44,8 +46,16 @@ exports.loaderPromise = new Promise((resolve) => loaderResolve = resolve); exports.ESMLoader = undefined; exports.initializeLoader = function(cwd, userLoader) { - const ESMLoader = new Loader(); + let ESMLoader = new Loader(); const loaderPromise = (async () => { + if (userLoader) { + emitExperimentalWarning('--loader'); + const hooks = await ESMLoader.import( + userLoader, pathToFileURL(`${cwd}/`).href); + ESMLoader = new Loader(); + ESMLoader.hook(hooks); + exports.ESMLoader = ESMLoader; + } return ESMLoader; })(); loaderResolve(loaderPromise); diff --git a/src/node_options.cc b/src/node_options.cc index 7fb6eeac8c..2b241273fd 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -101,6 +101,10 @@ void PerIsolateOptions::CheckOptions(std::vector* errors) { } void EnvironmentOptions::CheckOptions(std::vector* errors) { + if (!userland_loader.empty() && !experimental_modules) { + errors->push_back("--loader requires --experimental-modules be enabled"); + } + if (syntax_check_only && has_eval_string) { errors->push_back("either --check or --eval can be used, not both"); } @@ -242,6 +246,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "(default: llhttp).", &EnvironmentOptions::http_parser, kAllowedInEnvironment); + AddOption("--loader", + "(with --experimental-modules) use the specified file as a " + "custom loader", + &EnvironmentOptions::userland_loader, + kAllowedInEnvironment); AddOption("--no-deprecation", "silence deprecation warnings", &EnvironmentOptions::no_deprecation, diff --git a/src/node_options.h b/src/node_options.h index ee4cc98c9b..83c3be8674 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -105,6 +105,7 @@ class EnvironmentOptions : public Options { bool trace_deprecation = false; bool trace_sync_io = false; bool trace_warnings = false; + std::string userland_loader; bool syntax_check_only = false; bool has_eval_string = false; diff --git a/test/es-module/test-esm-example-loader.js b/test/es-module/test-esm-example-loader.js new file mode 100644 index 0000000000..0b0001acea --- /dev/null +++ b/test/es-module/test-esm-example-loader.js @@ -0,0 +1,6 @@ +// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/example-loader.mjs +/* eslint-disable node-core/required-modules */ +import assert from 'assert'; +import ok from '../fixtures/es-modules/test-esm-ok.mjs'; + +assert(ok); diff --git a/test/es-module/test-esm-loader-dependency.mjs b/test/es-module/test-esm-loader-dependency.mjs new file mode 100644 index 0000000000..1ed8685a6f --- /dev/null +++ b/test/es-module/test-esm-loader-dependency.mjs @@ -0,0 +1,5 @@ +// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-with-dep.mjs +/* eslint-disable node-core/required-modules */ +import '../fixtures/es-modules/test-esm-ok.mjs'; + +// We just test that this module doesn't fail loading diff --git a/test/es-module/test-esm-loader-invalid-format.mjs b/test/es-module/test-esm-loader-invalid-format.mjs new file mode 100644 index 0000000000..c3f3a87407 --- /dev/null +++ b/test/es-module/test-esm-loader-invalid-format.mjs @@ -0,0 +1,12 @@ +// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-invalid-format.mjs +/* eslint-disable node-core/required-modules */ +import { expectsError, mustCall } from '../common/index.mjs'; +import assert from 'assert'; + +import('../fixtures/es-modules/test-esm-ok.mjs') +.then(assert.fail, expectsError({ + code: 'ERR_INVALID_RETURN_PROPERTY_VALUE', + message: 'Expected string to be returned for the "format" from the ' + + '"loader resolve" function but got type undefined.' +})) +.then(mustCall()); diff --git a/test/es-module/test-esm-loader-invalid-url.mjs b/test/es-module/test-esm-loader-invalid-url.mjs new file mode 100644 index 0000000000..9cf17b2478 --- /dev/null +++ b/test/es-module/test-esm-loader-invalid-url.mjs @@ -0,0 +1,14 @@ +// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-invalid-url.mjs +/* eslint-disable node-core/required-modules */ + +import { expectsError, mustCall } from '../common/index.mjs'; +import assert from 'assert'; + +import('../fixtures/es-modules/test-esm-ok.mjs') +.then(assert.fail, expectsError({ + code: 'ERR_INVALID_RETURN_PROPERTY', + message: 'Expected a valid url to be returned for the "url" from the ' + + '"loader resolve" function but got ' + + '../fixtures/es-modules/test-esm-ok.mjs.' +})) +.then(mustCall()); diff --git a/test/es-module/test-esm-loader-missing-dynamic-instantiate-hook.mjs b/test/es-module/test-esm-loader-missing-dynamic-instantiate-hook.mjs new file mode 100644 index 0000000000..ab2da7adce --- /dev/null +++ b/test/es-module/test-esm-loader-missing-dynamic-instantiate-hook.mjs @@ -0,0 +1,10 @@ +// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/missing-dynamic-instantiate-hook.mjs +/* eslint-disable node-core/required-modules */ + +import { expectsError } from '../common/index.mjs'; + +import('test').catch(expectsError({ + code: 'ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK', + message: 'The ES Module loader may not return a format of \'dynamic\' ' + + 'when no dynamicInstantiate function was provided' +})); diff --git a/test/es-module/test-esm-named-exports.mjs b/test/es-module/test-esm-named-exports.mjs new file mode 100644 index 0000000000..e235f598cb --- /dev/null +++ b/test/es-module/test-esm-named-exports.mjs @@ -0,0 +1,9 @@ +// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs +/* eslint-disable node-core/required-modules */ +import '../common/index.mjs'; +import { readFile } from 'fs'; +import assert from 'assert'; +import ok from '../fixtures/es-modules/test-esm-ok.mjs'; + +assert(ok); +assert(readFile); diff --git a/test/es-module/test-esm-preserve-symlinks-not-found-plain.mjs b/test/es-module/test-esm-preserve-symlinks-not-found-plain.mjs new file mode 100644 index 0000000000..2ca0f56581 --- /dev/null +++ b/test/es-module/test-esm-preserve-symlinks-not-found-plain.mjs @@ -0,0 +1,3 @@ +// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/not-found-assert-loader.mjs +/* eslint-disable node-core/required-modules */ +import './not-found.js'; diff --git a/test/es-module/test-esm-preserve-symlinks-not-found.mjs b/test/es-module/test-esm-preserve-symlinks-not-found.mjs new file mode 100644 index 0000000000..b5be2d7e63 --- /dev/null +++ b/test/es-module/test-esm-preserve-symlinks-not-found.mjs @@ -0,0 +1,3 @@ +// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/not-found-assert-loader.mjs +/* eslint-disable node-core/required-modules */ +import './not-found.mjs'; diff --git a/test/es-module/test-esm-resolve-hook.mjs b/test/es-module/test-esm-resolve-hook.mjs new file mode 100644 index 0000000000..e326d20b6d --- /dev/null +++ b/test/es-module/test-esm-resolve-hook.mjs @@ -0,0 +1,8 @@ +// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/js-loader.mjs +/* eslint-disable node-core/required-modules */ +import { namedExport } from '../fixtures/es-module-loaders/js-as-esm.js'; +import assert from 'assert'; +import ok from '../fixtures/es-modules/test-esm-ok.mjs'; + +assert(ok); +assert(namedExport); diff --git a/test/es-module/test-esm-shared-loader-dep.mjs b/test/es-module/test-esm-shared-loader-dep.mjs new file mode 100644 index 0000000000..b8953ab1ec --- /dev/null +++ b/test/es-module/test-esm-shared-loader-dep.mjs @@ -0,0 +1,11 @@ +// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-shared-dep.mjs +/* eslint-disable node-core/required-modules */ +import { createRequire } from '../common/index.mjs'; + +import assert from 'assert'; +import '../fixtures/es-modules/test-esm-ok.mjs'; + +const require = createRequire(import.meta.url); +const dep = require('../fixtures/es-module-loaders/loader-dep.js'); + +assert.strictEqual(dep.format, 'module'); diff --git a/test/es-module/test-esm-throw-undefined.mjs b/test/es-module/test-esm-throw-undefined.mjs new file mode 100644 index 0000000000..97e917da5e --- /dev/null +++ b/test/es-module/test-esm-throw-undefined.mjs @@ -0,0 +1,16 @@ +// Flags: --experimental-modules +/* eslint-disable node-core/required-modules */ + +import '../common/index.mjs'; +import assert from 'assert'; + +async function doTest() { + await assert.rejects( + async () => { + await import('../fixtures/es-module-loaders/throw-undefined.mjs'); + }, + (e) => e === undefined + ); +} + +doTest(); diff --git a/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs b/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs new file mode 100644 index 0000000000..a944c4fd5e --- /dev/null +++ b/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs @@ -0,0 +1,24 @@ +import module from 'module'; + +export function dynamicInstantiate(url) { + const builtinInstance = module._load(url.substr(5)); + const builtinExports = ['default', ...Object.keys(builtinInstance)]; + return { + exports: builtinExports, + execute: exports => { + for (let name of builtinExports) + exports[name].set(builtinInstance[name]); + exports.default.set(builtinInstance); + } + }; +} + +export function resolve(specifier, base, defaultResolver) { + if (module.builtinModules.includes(specifier)) { + return { + url: `node:${specifier}`, + format: 'dynamic' + }; + } + return defaultResolver(specifier, base); +} diff --git a/test/fixtures/es-module-loaders/example-loader.mjs b/test/fixtures/es-module-loaders/example-loader.mjs new file mode 100644 index 0000000000..d8e0ddcba3 --- /dev/null +++ b/test/fixtures/es-module-loaders/example-loader.mjs @@ -0,0 +1,34 @@ +import url from 'url'; +import path from 'path'; +import process from 'process'; +import { builtinModules } from 'module'; + +const JS_EXTENSIONS = new Set(['.js', '.mjs']); + +const baseURL = new url.URL('file://'); +baseURL.pathname = process.cwd() + '/'; + +export function resolve(specifier, parentModuleURL = baseURL /*, defaultResolve */) { + if (builtinModules.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.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: 'module' + }; +} diff --git a/test/fixtures/es-module-loaders/js-as-esm.js b/test/fixtures/es-module-loaders/js-as-esm.js new file mode 100644 index 0000000000..b4d2741b2f --- /dev/null +++ b/test/fixtures/es-module-loaders/js-as-esm.js @@ -0,0 +1 @@ +export const namedExport = 'named-export'; diff --git a/test/fixtures/es-module-loaders/js-loader.mjs b/test/fixtures/es-module-loaders/js-loader.mjs new file mode 100644 index 0000000000..4b8a0fc365 --- /dev/null +++ b/test/fixtures/es-module-loaders/js-loader.mjs @@ -0,0 +1,20 @@ +import { URL } from 'url'; +import { builtinModules } from 'module'; + +const baseURL = new URL('file://'); +baseURL.pathname = process.cwd() + '/'; + +export function resolve (specifier, base = baseURL) { + if (builtinModules.includes(specifier)) { + return { + url: specifier, + format: 'builtin' + }; + } + // load all dependencies as esm, regardless of file extension + const url = new URL(specifier, base).href; + return { + url, + format: 'module' + }; +} diff --git a/test/fixtures/es-module-loaders/loader-dep.js b/test/fixtures/es-module-loaders/loader-dep.js new file mode 100644 index 0000000000..c8154ac5db --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-dep.js @@ -0,0 +1 @@ +exports.format = 'module'; diff --git a/test/fixtures/es-module-loaders/loader-invalid-format.mjs b/test/fixtures/es-module-loaders/loader-invalid-format.mjs new file mode 100644 index 0000000000..17a0dcd04d --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-invalid-format.mjs @@ -0,0 +1,8 @@ +export async function resolve(specifier, parentModuleURL, defaultResolve) { + if (parentModuleURL && specifier === '../fixtures/es-modules/test-esm-ok.mjs') { + return { + url: 'file:///asdf' + }; + } + return defaultResolve(specifier, parentModuleURL); +} diff --git a/test/fixtures/es-module-loaders/loader-invalid-url.mjs b/test/fixtures/es-module-loaders/loader-invalid-url.mjs new file mode 100644 index 0000000000..f653155899 --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-invalid-url.mjs @@ -0,0 +1,10 @@ +/* eslint-disable node-core/required-modules */ +export async function resolve(specifier, parentModuleURL, defaultResolve) { + if (parentModuleURL && specifier === '../fixtures/es-modules/test-esm-ok.mjs') { + return { + url: specifier, + format: 'esm' + }; + } + return defaultResolve(specifier, parentModuleURL); +} diff --git a/test/fixtures/es-module-loaders/loader-shared-dep.mjs b/test/fixtures/es-module-loaders/loader-shared-dep.mjs new file mode 100644 index 0000000000..3acafcce1e --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-shared-dep.mjs @@ -0,0 +1,11 @@ +import assert from 'assert'; + +import {createRequire} from '../../common/index.mjs'; + +const require = createRequire(import.meta.url); +const dep = require('./loader-dep.js'); + +export function resolve(specifier, base, defaultResolve) { + assert.strictEqual(dep.format, 'module'); + return defaultResolve(specifier, base); +} diff --git a/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs b/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs new file mode 100644 index 0000000000..e7c6c8ff34 --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs @@ -0,0 +1,6 @@ +export async function resolve(specifier, parent, defaultResolve) { + if (specifier === 'unknown-builtin-module') { + return { url: 'unknown-builtin-module', format: 'builtin' }; + } + return defaultResolve(specifier, parent); +} \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/loader-with-dep.mjs b/test/fixtures/es-module-loaders/loader-with-dep.mjs new file mode 100644 index 0000000000..5afd3b2e21 --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-with-dep.mjs @@ -0,0 +1,11 @@ +import {createRequire} from '../../common/index.mjs'; + +const require = createRequire(import.meta.url); +const dep = require('./loader-dep.js'); + +export function resolve (specifier, base, defaultResolve) { + return { + url: defaultResolve(specifier, base).url, + format: dep.format + }; +} diff --git a/test/fixtures/es-module-loaders/missing-dynamic-instantiate-hook.mjs b/test/fixtures/es-module-loaders/missing-dynamic-instantiate-hook.mjs new file mode 100644 index 0000000000..6993747fcc --- /dev/null +++ b/test/fixtures/es-module-loaders/missing-dynamic-instantiate-hook.mjs @@ -0,0 +1,6 @@ +export function resolve(specifier, parentModule, defaultResolver) { + if (specifier !== 'test') { + return defaultResolver(specifier, parentModule); + } + return { url: 'file://', format: 'dynamic' }; +} diff --git a/test/fixtures/es-module-loaders/module-named-exports.mjs b/test/fixtures/es-module-loaders/module-named-exports.mjs new file mode 100644 index 0000000000..04f7f43ebd --- /dev/null +++ b/test/fixtures/es-module-loaders/module-named-exports.mjs @@ -0,0 +1,2 @@ +export const foo = 'foo'; +export const bar = 'bar'; diff --git a/test/fixtures/es-module-loaders/not-found-assert-loader.mjs b/test/fixtures/es-module-loaders/not-found-assert-loader.mjs new file mode 100644 index 0000000000..d3eebcd47e --- /dev/null +++ b/test/fixtures/es-module-loaders/not-found-assert-loader.mjs @@ -0,0 +1,22 @@ +import assert from 'assert'; + +// a loader that asserts that the defaultResolve will throw "not found" +// (skipping the top-level main of course) +let mainLoad = true; +export async function resolve (specifier, base, defaultResolve) { + if (mainLoad) { + mainLoad = false; + return defaultResolve(specifier, base); + } + try { + await defaultResolve(specifier, base); + } + catch (e) { + assert.strictEqual(e.code, 'ERR_MODULE_NOT_FOUND'); + return { + format: 'builtin', + url: 'fs' + }; + } + assert.fail(`Module resolution for ${specifier} should be throw ERR_MODULE_NOT_FOUND`); +} diff --git a/test/fixtures/es-module-loaders/syntax-error-import.mjs b/test/fixtures/es-module-loaders/syntax-error-import.mjs new file mode 100644 index 0000000000..3a6bc5effc --- /dev/null +++ b/test/fixtures/es-module-loaders/syntax-error-import.mjs @@ -0,0 +1 @@ +import { foo, notfound } from './module-named-exports.mjs'; diff --git a/test/fixtures/es-module-loaders/syntax-error.mjs b/test/fixtures/es-module-loaders/syntax-error.mjs new file mode 100644 index 0000000000..bda4a7e6eb --- /dev/null +++ b/test/fixtures/es-module-loaders/syntax-error.mjs @@ -0,0 +1,2 @@ +'use strict'; +await async () => 0; diff --git a/test/fixtures/es-module-loaders/throw-undefined.mjs b/test/fixtures/es-module-loaders/throw-undefined.mjs new file mode 100644 index 0000000000..0349ae112d --- /dev/null +++ b/test/fixtures/es-module-loaders/throw-undefined.mjs @@ -0,0 +1,4 @@ +'use strict'; +/* eslint-disable node-core/required-modules */ + +throw undefined; diff --git a/test/message/esm_display_syntax_error.out b/test/message/esm_display_syntax_error.out index 2700fd894c..8f17d5cd7a 100644 --- a/test/message/esm_display_syntax_error.out +++ b/test/message/esm_display_syntax_error.out @@ -2,5 +2,6 @@ file:///*/test/message/esm_display_syntax_error.mjs:3 await async () => 0; ^^^^^ + SyntaxError: Unexpected reserved word at Loader. (internal/modules/esm/translators.js:*:*) diff --git a/test/message/esm_display_syntax_error_import.mjs b/test/message/esm_display_syntax_error_import.mjs new file mode 100644 index 0000000000..12d10270e9 --- /dev/null +++ b/test/message/esm_display_syntax_error_import.mjs @@ -0,0 +1,7 @@ +// Flags: --experimental-modules +/* eslint-disable no-unused-vars, node-core/required-modules */ +import '../common/index.mjs'; +import { + foo, + notfound +} from '../fixtures/es-module-loaders/module-named-exports.mjs'; diff --git a/test/message/esm_display_syntax_error_import.out b/test/message/esm_display_syntax_error_import.out new file mode 100644 index 0000000000..48f2e2fb74 --- /dev/null +++ b/test/message/esm_display_syntax_error_import.out @@ -0,0 +1,6 @@ +(node:*) ExperimentalWarning: The ESM module loader is experimental. +file:///*/test/message/esm_display_syntax_error_import.mjs:6 + notfound + ^^^^^^^^ +SyntaxError: The requested module '../fixtures/es-module-loaders/module-named-exports.mjs' does not provide an export named 'notfound' + at ModuleJob._instantiate (internal/modules/esm/module_job.js:*:*) diff --git a/test/message/esm_display_syntax_error_import_module.mjs b/test/message/esm_display_syntax_error_import_module.mjs new file mode 100644 index 0000000000..a53bbbcd19 --- /dev/null +++ b/test/message/esm_display_syntax_error_import_module.mjs @@ -0,0 +1,4 @@ +// Flags: --experimental-modules +/* eslint-disable node-core/required-modules */ +import '../common/index.mjs'; +import '../fixtures/es-module-loaders/syntax-error-import.mjs'; diff --git a/test/message/esm_display_syntax_error_import_module.out b/test/message/esm_display_syntax_error_import_module.out new file mode 100644 index 0000000000..3e1024db8a --- /dev/null +++ b/test/message/esm_display_syntax_error_import_module.out @@ -0,0 +1,6 @@ +(node:*) ExperimentalWarning: The ESM module loader is experimental. +file:///*/test/fixtures/es-module-loaders/syntax-error-import.mjs:1 +import { foo, notfound } from './module-named-exports.mjs'; + ^^^^^^^^ +SyntaxError: The requested module './module-named-exports.mjs' does not provide an export named 'notfound' + at ModuleJob._instantiate (internal/modules/esm/module_job.js:*:*) diff --git a/test/message/esm_display_syntax_error_module.mjs b/test/message/esm_display_syntax_error_module.mjs new file mode 100644 index 0000000000..5905d2a954 --- /dev/null +++ b/test/message/esm_display_syntax_error_module.mjs @@ -0,0 +1,4 @@ +// Flags: --experimental-modules +/* eslint-disable node-core/required-modules */ +import '../common/index.mjs'; +import '../fixtures/es-module-loaders/syntax-error.mjs'; diff --git a/test/message/esm_display_syntax_error_module.out b/test/message/esm_display_syntax_error_module.out new file mode 100644 index 0000000000..7dbbd32f1f --- /dev/null +++ b/test/message/esm_display_syntax_error_module.out @@ -0,0 +1,7 @@ +(node:*) ExperimentalWarning: The ESM module loader is experimental. +file:///*/test/fixtures/es-module-loaders/syntax-error.mjs:2 +await async () => 0; +^^^^^ + +SyntaxError: Unexpected reserved word + at Loader. (internal/modules/esm/translators.js:*:*) diff --git a/test/parallel/test-loaders-unknown-builtin-module.mjs b/test/parallel/test-loaders-unknown-builtin-module.mjs new file mode 100644 index 0000000000..5f47f191f5 --- /dev/null +++ b/test/parallel/test-loaders-unknown-builtin-module.mjs @@ -0,0 +1,13 @@ +// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs +/* eslint-disable node-core/required-modules */ +import { expectsError, mustCall } from '../common/index.mjs'; +import assert from 'assert'; + +const unknownBuiltinModule = 'unknown-builtin-module'; + +import(unknownBuiltinModule) +.then(assert.fail, expectsError({ + code: 'ERR_UNKNOWN_BUILTIN_MODULE', + message: `No such built-in module: ${unknownBuiltinModule}` +})) +.then(mustCall()); From 97bfb31abc0f4b158a51b73eca90a32b9d661c93 Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Mon, 4 Mar 2019 21:02:08 -0800 Subject: [PATCH 4/5] doc: esm flags, package scope and file extensions rules --- doc/api/cli.md | 10 +- doc/api/esm.md | 571 +++++++++++++++++++++++++++++++++++-------------- 2 files changed, 417 insertions(+), 164 deletions(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index 62e13496c0..4939574e88 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -513,13 +513,13 @@ Track heap object allocations for heap snapshots. ### `-m`, `--type=type` -When using `--experimental-modules`, this informs the module resolution type -to interpret the top-level entry into Node.js. +Used with `--experimental-modules`, this configures Node.js to interpret the +initial entry point as CommonJS or as an ES module. -Works with stdin, `--eval`, `--print` as well as standard execution. +Valid values are `"commonjs"` and `"module"`. The default is to infer from +the file extension and the `"type"` field in the nearest parent `package.json`. -Valid values are `"commonjs"` and `"module"`, where the default is to infer -from the file extension and package type boundary. +Works for executing a file as well as `--eval`, `--print`, `STDIN`. `-m` is an alias for `--type=module`. diff --git a/doc/api/esm.md b/doc/api/esm.md index 25c2e82673..5a144b09a6 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -8,37 +8,223 @@ Node.js contains support for ES Modules based upon the -[Node.js EP for ES Modules][] and the [ESM Minimal Kernel][]. +[Node.js EP for ES Modules][] and the [ecmascript-modules implementation][]. -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. +Expect major changes in the implementation including interoperability support, +specifier resolution, and default behavior. ## Enabling -The `--experimental-modules` flag can be used to enable features for loading -ESM modules. +The `--experimental-modules` flag can be used to enable support for +ECMAScript modules (ES modules). -Once this has been set, files ending with `.mjs` will be able to be loaded -as ES Modules. +## Running Node.js with an ECMAScript Module + +There are a few ways to start Node.js with an ES module as its input. + +### Initial entry point with an .mjs extension + +A file ending with `.mjs` passed to Node.js as an initial entry point will be +loaded as an ES module. ```sh node --experimental-modules my-app.mjs ``` -## Features +### --type=module / -m flag - +Files ending with `.js` or `.mjs`, or lacking any extension, +will be loaded as ES modules when the `--type=module` flag is set. +This flag also has a shorthand alias `-m`. + +```sh +node --experimental-modules --type=module my-app.js +# or +node --experimental-modules -m my-app.js +``` + +For completeness there is also `--type=commonjs`, for explicitly running a `.js` +file as CommonJS. This is the default behavior if `--type` or `-m` is +unspecified. + +The `--type=module` or `-m` flags can also be used to configure Node.js to treat +as an ES module input sent in via `--eval` or `--print` (or `-e` or `-p`) or +piped to Node.js via `STDIN`. + +```sh +node --experimental-modules --type=module --eval \ + "import { sep } from 'path'; console.log(sep);" + +echo "import { sep } from 'path'; console.log(sep);" | \ + node --experimental-modules --type=module +``` + +### package.json "type" field + +Files ending with `.js` or `.mjs`, or lacking any extension, +will be loaded as ES modules when the nearest parent `package.json` file +contains a top-level field `"type"` with a value of `"module"`. + +The nearest parent `package.json` is defined as the first `package.json` found +when searching in the current folder, that folder’s parent, and so on up +until the root of the volume is reached. + + +```js +// package.json +{ + "type": "module" +} +``` + +```sh +# In same folder as above package.json +node --experimental-modules my-app.js # Runs as ES module +``` + +If the nearest parent `package.json` lacks a `"type"` field, or contains +`"type": "commonjs"`, extensionless and `.js` files are treated as CommonJS. +If the volume root is reached and no `package.json` is found, +Node.js defers to the deafult, a `package.json` with no `"type"` +field. + +## Package Scope and File Extensions + +A folder containing a `package.json` file, and all subfolders below that +folder down until the next folder containing another `package.json`, is +considered a _package scope_. The `"type"` field defines how `.js` and +extensionless files should be treated within a particular `package.json` file’s +package scope. Every package in a project’s `node_modules` folder contains its +own `package.json` file, so each project’s dependencies have their own package +scopes. A `package.json` lacking a `"type"` field is treated as if it contained +`"type": "commonjs"`. + +The package scope applies not only to initial entry points (`node +--experimental-modules my-app.js`) but also to files referenced by `import` +statements and `import()` expressions. -### Supported +```js +// my-app.js, in an ES module package scope because there is a package.json +// file in the same folder with "type": "module" + +import './startup/init.js'; +// Loaded as ES module since ./startup contains no package.json file, +// and therefore inherits the ES module package scope from one level up + +import 'commonjs-package'; +// Loaded as CommonJS since ./node_modules/commonjs-package/package.json +// lacks a "type" field or contains "type": "commonjs" + +import './node_modules/commonjs-package/index.js'; +// Loaded as CommonJS since ./node_modules/commonjs-package/package.json +// lacks a "type" field or contains "type": "commonjs" +``` -Only the CLI argument for the main entry point to the program can be an entry -point into an ESM graph. Dynamic import can also be used to create entry points -into ESM graphs at runtime. +Files ending with `.mjs` are always loaded as ES modules regardless of package +scope. -#### import.meta +Files ending with `.cjs` are always loaded as CommonJS regardless of package +scope. + +```js +import './legacy-file.cjs'; +// Loaded as CommonJS since .cjs is always loaded as CommonJS + +import 'commonjs-package/src/index.mjs'; +// Loaded as ES module since .mjs is always loaded as ES module +``` + +You can use the `.mjs` and `.cjs` extensions to mix types within the same +package scope: + +- Within a `"type": "module"` package scope, Node.js can be instructed to + interpret a particular file as CommonJS by naming it with a `.cjs` extension + (since both `.js` and `.mjs` files are treated as ES modules within a + `"module"` package scope). + +- Within a `"type": "commonjs"` package scope, Node.js can be instructed to + interpret a particular file as an ES module by naming it with an `.mjs` + extension (since both `.js` and `.cjs` files are treated as CommonJS within a + `"commonjs"` package scope). + +## Package Entry Points + +The `package.json` `"main"` field defines the entry point for a package, +whether the package is included into CommonJS via `require` or into an ES +module via `import`. + + +```js +// ./node_modules/es-module-package/package.json +{ + "type": "module", + "main": "./src/index.js" +} +``` +```js +// ./my-app.mjs + +import { something } from 'es-module-package'; +// Loads from ./node_modules/es-module-package/src/index.js +``` + +An attempt to `require` the above `es-module-package` would attempt to load +`./node_modules/es-module-package/src/index.js` as CommonJS, which would throw +an error as Node.js would not be able to parse the `export` statement in +CommonJS. + +As with `import` statements, for ES module usage the value of `"main"` must be +a full path including extension: `"./index.mjs"`, not `"./index"`. + +If the `package.json` `"type"` field is omitted, a `.js` file in `"main"` will +be interpreted as CommonJS. + +> Currently a package can define _either_ a CommonJS entry point **or** an ES +> module entry point; there is no way to specify separate entry points for +> CommonJS and ES module usage. This means that a package entry point can be +> included via `require` or via `import` but not both. +> +> Such a limitation makes it difficult for packages to support both new versions +> of Node.js that understand ES modules and older versions of Node.js that +> understand only CommonJS. There is work ongoing to remove this limitation, and +> it will very likely entail changes to the behavior of `"main"` as defined +> here. + +## import Specifiers + +### Terminology + +The _specifier_ of an `import` statement is the string after the `from` keyword, +e.g. `'path'` in `import { sep } from 'path'`. Specifiers are also used in +`export from` statements, and as the argument to an `import()` expression. + +There are four types of specifiers: + +- _Bare specifiers_ like `'some-package'`. They refer to an entry point of a + package by the package name. + +- _Deep import specifiers_ like `'some-package/lib/shuffle.mjs'`. They refer to + a path within a package prefixed by the package name. + +- _Relative specifiers_ like `'./startup.js'` or `'../config.mjs'`. They refer + to a path relative to the location of the importing file. + +- _Absolute specifiers_ like `'file:///opt/nodejs/config.js'`. They refer + directly and explicitly to a full path. + +Bare specifiers, and the bare specifier portion of deep import specifiers, are +strings; but everything else in a specifier is a URL. + +Only `file://` URLs are supported. A specifier like +`'https://example.com/app.js'` may be supported by browsers but it is not +supported in Node.js. + +Specifiers may not begin with `/` or `//`. These are reserved for potential +future use. The root of the current volume may be referenced via `file:///`. + +## import.meta * {Object} @@ -47,37 +233,44 @@ property: * `url` {string} The absolute `file:` URL of the module. -### Unsupported - -| Feature | Reason | -| --- | --- | -| `require('./foo.mjs')` | ES Modules have differing resolution and timing, use dynamic import | - -## Notable differences between `import` and `require` +## Differences Between ES Modules and CommonJS ### Mandatory file extensions -You must provide a file extension when using the `import` keyword. +You must provide a file extension when using the `import` keyword. Directory +indexes (e.g. `'./startup/index.js'`) must also be fully specified. + +This behavior matches how `import` behaves in browser environments, assuming a +typically configured server. -### No NODE_PATH +### No NODE_PATH `NODE_PATH` is not part of resolving `import` specifiers. Please use symlinks if this behavior is desired. -### No `require.extensions` +### No require, exports, module.exports, \_\_filename, \_\_dirname + +These CommonJS variables are not available in ES modules. + +`require` can be imported into an ES module using +[`module.createRequireFromPath()`][]. + +An equivalent for `__filename` and `__dirname` is [`import.meta.url`][]. + +### No require.extensions `require.extensions` is not used by `import`. The expectation is that loader hooks can provide this workflow in the future. -### No `require.cache` +### No require.cache `require.cache` is not used by `import`. It has a separate cache. -### URL based paths +### URL-based paths -ESM are resolved and cached based upon [URL](https://url.spec.whatwg.org/) -semantics. This means that files containing special characters such as `#` and -`?` need to be escaped. +ES modules are resolved and cached based upon +[URL](https://url.spec.whatwg.org/) semantics. This means that files containing +special characters such as `#` and `?` need to be escaped. Modules will be loaded multiple times if the `import` specifier used to resolve them have a different query or fragment. @@ -89,6 +282,59 @@ import './foo.mjs?query=2'; // loads ./foo.mjs with query of "?query=2" For now, only modules using the `file:` protocol can be loaded. +## Interoperability with CommonJS + +### require + +`require` always treats the files it references as CommonJS. This applies +whether `require` is used the traditional way within a CommonJS environment, or +in an ES module environment using [`module.createRequireFromPath()`][]. + +To include an ES module into CommonJS, use [`import()`][]. + +### import statements + +An `import` statement can reference either ES module or CommonJS JavaScript. +Other file types such as JSON and Native modules are not supported. For those, +use [`module.createRequireFromPath()`][]. + +`import` statements are permitted only in ES modules. For similar functionality +in CommonJS, see [`import()`][]. + +The _specifier_ of an `import` statement (the string after the `from` keyword) +can either be an URL-style relative path like `'./file.mjs'` or a package name +like `'fs'`. + +Like in CommonJS, files within packages can be accessed by appending a path to +the package name. + +```js +import { sin, cos } from 'geometry/trigonometry-functions.mjs'; +``` + +> Currently only the “default export” is supported for CommonJS files or +> packages: +> +> +> ```js +> import packageMain from 'commonjs-package'; // Works +> +> import { method } from 'commonjs-package'; // Errors +> ``` +> +> There are ongoing efforts to make the latter code possible. + +### import() expressions + +Dynamic `import()` is supported in both CommonJS and ES modules. It can be used +to include ES module files from CommonJS code. + +```js +(async () => { + await import('./my-app.mjs'); +})(); +``` + ## CommonJS, JSON, and Native Modules CommonJS, JSON, and Native modules can be used with [`module.createRequireFromPath()`][]. @@ -99,9 +345,9 @@ module.exports = 'cjs'; // esm.mjs import { createRequireFromPath as createRequire } from 'module'; -import { fileURLToPath as fromPath } from 'url'; +import { fileURLToPath as fromURL } from 'url'; -const require = createRequire(fromPath(import.meta.url)); +const require = createRequire(fromURL(import.meta.url)); const cjs = require('./cjs'); cjs === 'cjs'; // true @@ -138,6 +384,127 @@ fs.readFileSync = () => Buffer.from('Hello, ESM'); fs.readFileSync === readFileSync; ``` +## Experimental Loader hooks + +**Note: This API is currently being redesigned and will still change.**. + + + +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 | +| --- | --- | +| `'module'` | Load a standard JavaScript module | +| `'commonjs'` | Load a Node.js CommonJS module | +| `'builtin'` | Load a Node.js builtin module | +| `'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 @@ -174,6 +541,9 @@ entirely for the CommonJS loader. If the top-level `--type` is _"module"_, then the ESM resolver is used as described here, with the conditional `--type` check in **ESM_FORMAT**. +
+Resolver algorithm psuedocode + **ESM_RESOLVE(_specifier_, _parentURL_, _isMain_)** > 1. Let _resolvedURL_ be **undefined**. > 1. If _specifier_ is a valid URL, then @@ -256,7 +626,7 @@ PACKAGE_MAIN_RESOLVE(_packageURL_, _pjson_) > 1. If _url_ ends with _".cjs"_, then > 1. Throw a _Type Mismatch_ error. > 1. Return _"module"_. -> 1. Let _pjson_ be the result of **READ_PACKAGE_BOUNDARY**(_url_). +> 1. Let _pjson_ be the result of **READ_PACKAGE_SCOPE**(_url_). > 1. If _pjson_ is **null** and _isMain_ is **true**, then > 1. If _url_ ends in _".mjs"_, then > 1. Return _"module"_. @@ -272,13 +642,13 @@ PACKAGE_MAIN_RESOLVE(_packageURL_, _pjson_) > 1. Throw an _Unsupported File Extension_ error. > 1. Return _"commonjs"_. -READ_PACKAGE_BOUNDARY(_url_) -> 1. Let _boundaryURL_ be _url_. -> 1. While _boundaryURL_ is not the file system root, -> 1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_boundaryURL_). +READ_PACKAGE_SCOPE(_url_) +> 1. Let _scopeURL_ be _url_. +> 1. While _scopeURL_ is not the file system root, +> 1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_scopeURL_). > 1. If _pjson_ is not **null**, then > 1. Return _pjson_. -> 1. Set _boundaryURL_ to the parent URL of _boundaryURL_. +> 1. Set _scopeURL_ to the parent URL of _scopeURL_. > 1. Return **null**. READ_PACKAGE_JSON(_packageURL_) @@ -289,128 +659,11 @@ READ_PACKAGE_JSON(_packageURL_) > 1. Throw an _Invalid Package Configuration_ error. > 1. Return the parsed JSON source of the file at _pjsonURL_. -## Experimental Loader hooks - -**Note: This API is currently being redesigned and will still change.**. - - - -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 | -| --- | --- | -| `'module'` | Load a standard JavaScript module | -| `'commonjs'` | Load a Node.js CommonJS module | -| `'builtin'` | Load a Node.js builtin module | -| `'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. +
[Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md [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 +[`import.meta.url`]: esm.html#importmeta +[`import()`]: esm.html#import-expressions +[ecmascript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md From ea592210c61818ca4eb417f1945b191704ed2c18 Mon Sep 17 00:00:00 2001 From: Myles Borins Date: Tue, 5 Mar 2019 04:20:15 -0500 Subject: [PATCH 5/5] esm: add --es-module-specifier-resolution There are currently two supported values "explicit" and "node" --- doc/api/esm.md | 4 +- src/module_wrap.cc | 120 +++++++++++++----- src/node_options.cc | 5 + src/node_options.h | 1 + test/es-module/test-esm-package-scope.mjs | 12 -- test/es-module/test-esm-specifiers.mjs | 35 +++++ test/fixtures/es-module-specifiers/index.mjs | 10 ++ .../node_modules/explicit-main/entry.mjs | 1 + .../node_modules/explicit-main/package.json | 3 + .../implicit-main-type-commonjs/entry.mjs | 1 + .../implicit-main-type-commonjs/package.json | 4 + .../implicit-main-type-module/entry.js | 1 + .../implicit-main-type-module/entry.mjs | 1 + .../implicit-main-type-module/package.json | 4 + .../node_modules/implicit-main/entry.js | 1 + .../node_modules/implicit-main/entry.mjs | 1 + .../node_modules/implicit-main/package.json | 3 + .../package-type-commonjs}/a.js | 0 .../package-type-commonjs}/b.mjs | 0 .../package-type-commonjs}/c.cjs | 0 .../package-type-commonjs}/index.mjs | 4 +- .../package-type-commonjs/package.json | 3 + .../package-type-module}/a.js | 0 .../package-type-module}/b.mjs | 0 .../package-type-module}/c.cjs | 0 .../package-type-module}/index.js | 4 +- .../package-type-module/package.json | 3 + .../es-module-specifiers/package.json | 1 + .../legacy-loader/package.json | 13 -- .../esm-package-scope/new-loader/package.json | 13 -- 30 files changed, 172 insertions(+), 76 deletions(-) delete mode 100644 test/es-module/test-esm-package-scope.mjs create mode 100644 test/es-module/test-esm-specifiers.mjs create mode 100644 test/fixtures/es-module-specifiers/index.mjs create mode 100644 test/fixtures/es-module-specifiers/node_modules/explicit-main/entry.mjs create mode 100644 test/fixtures/es-module-specifiers/node_modules/explicit-main/package.json create mode 100644 test/fixtures/es-module-specifiers/node_modules/implicit-main-type-commonjs/entry.mjs create mode 100644 test/fixtures/es-module-specifiers/node_modules/implicit-main-type-commonjs/package.json create mode 100644 test/fixtures/es-module-specifiers/node_modules/implicit-main-type-module/entry.js create mode 100644 test/fixtures/es-module-specifiers/node_modules/implicit-main-type-module/entry.mjs create mode 100644 test/fixtures/es-module-specifiers/node_modules/implicit-main-type-module/package.json create mode 100644 test/fixtures/es-module-specifiers/node_modules/implicit-main/entry.js create mode 100644 test/fixtures/es-module-specifiers/node_modules/implicit-main/entry.mjs create mode 100644 test/fixtures/es-module-specifiers/node_modules/implicit-main/package.json rename test/fixtures/{esm-package-scope/legacy-loader => es-module-specifiers/package-type-commonjs}/a.js (100%) rename test/fixtures/{esm-package-scope/legacy-loader => es-module-specifiers/package-type-commonjs}/b.mjs (100%) rename test/fixtures/{esm-package-scope/legacy-loader => es-module-specifiers/package-type-commonjs}/c.cjs (100%) rename test/fixtures/{esm-package-scope/legacy-loader => es-module-specifiers/package-type-commonjs}/index.mjs (82%) create mode 100644 test/fixtures/es-module-specifiers/package-type-commonjs/package.json rename test/fixtures/{esm-package-scope/new-loader => es-module-specifiers/package-type-module}/a.js (100%) rename test/fixtures/{esm-package-scope/new-loader => es-module-specifiers/package-type-module}/b.mjs (100%) rename test/fixtures/{esm-package-scope/new-loader => es-module-specifiers/package-type-module}/c.cjs (100%) rename test/fixtures/{esm-package-scope/new-loader => es-module-specifiers/package-type-module}/index.js (82%) create mode 100644 test/fixtures/es-module-specifiers/package-type-module/package.json create mode 100644 test/fixtures/es-module-specifiers/package.json delete mode 100644 test/fixtures/esm-package-scope/legacy-loader/package.json delete mode 100644 test/fixtures/esm-package-scope/new-loader/package.json diff --git a/doc/api/esm.md b/doc/api/esm.md index 5a144b09a6..5b40293830 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -664,6 +664,6 @@ READ_PACKAGE_JSON(_packageURL_) [Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md [dynamic instantiate hook]: #esm_dynamic_instantiate_hook [`module.createRequireFromPath()`]: modules.html#modules_module_createrequirefrompath_filename -[`import.meta.url`]: esm.html#importmeta -[`import()`]: esm.html#import-expressions +[`import.meta.url`]: #esm_import_meta +[`import()`]: #esm_import-expressions [ecmascript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 507ae11a6b..f505c09cec 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -43,6 +43,14 @@ using v8::String; using v8::Undefined; using v8::Value; +static const char* const EXTENSIONS[] = { + ".mjs", + ".cjs", + ".js", + ".json", + ".node" +}; + ModuleWrap::ModuleWrap(Environment* env, Local object, Local module, @@ -667,13 +675,57 @@ Maybe LegacyMainResolve(const URL& pjson_url, return Nothing(); } +enum ResolveExtensionsOptions { + TRY_EXACT_NAME, + ONLY_VIA_EXTENSIONS +}; + +template +Maybe ResolveExtensions(const URL& search) { + if (options == TRY_EXACT_NAME) { + if (FileExists(search)) { + return Just(search); + } + } + + for (const char* extension : EXTENSIONS) { + URL guess(search.path() + extension, &search); + if (FileExists(guess)) { + return Just(guess); + } + } + + return Nothing(); +} + +inline Maybe ResolveIndex(const URL& search) { + return ResolveExtensions(URL("index", search)); +} + Maybe FinalizeResolution(Environment* env, - const URL& resolved, - const URL& base, - bool check_exists) { - const std::string& path = resolved.ToFilePath(); + const URL& resolved, + const URL& base) { + if (env->options()->es_module_specifier_resolution == "node") { + Maybe file = ResolveExtensions(resolved); + if (!file.IsNothing()) { + return file; + } + if (resolved.path().back() != '/') { + file = ResolveIndex(URL(resolved.path() + "/", &base)); + } else { + file = ResolveIndex(resolved); + } + if (!file.IsNothing()) { + return file; + } + std::string msg = "Cannot find module '" + resolved.path() + + "' imported from " + base.ToFilePath(); + node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str()); + return Nothing(); + } - if (check_exists && CheckDescriptorAtPath(path) != FILE) { + const std::string& path = resolved.ToFilePath(); + if (CheckDescriptorAtPath(path) != FILE) { std::string msg = "Cannot find module '" + path + "' imported from " + base.ToFilePath(); node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str()); @@ -684,32 +736,36 @@ Maybe FinalizeResolution(Environment* env, } Maybe PackageMainResolve(Environment* env, - const URL& pjson_url, - const PackageConfig& pcfg, - const URL& base) { - if (pcfg.exists == Exists::No || ( - pcfg.esm == IsESM::Yes && pcfg.has_main == HasMain::No)) { - std::string msg = "Cannot find main entry point for '" + - URL(".", pjson_url).ToFilePath() + "' imported from " + - base.ToFilePath(); - node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str()); - return Nothing(); - } - if (pcfg.has_main == HasMain::Yes && - pcfg.main.substr(pcfg.main.length() - 4, 4) == ".mjs") { - return FinalizeResolution(env, URL(pcfg.main, pjson_url), base, true); - } - if (pcfg.esm == IsESM::Yes && - pcfg.main.substr(pcfg.main.length() - 3, 3) == ".js") { - return FinalizeResolution(env, URL(pcfg.main, pjson_url), base, true); - } - - Maybe resolved = LegacyMainResolve(pjson_url, pcfg); - // Legacy main resolution error - if (resolved.IsNothing()) { - return Nothing(); + const URL& pjson_url, + const PackageConfig& pcfg, + const URL& base) { + if (pcfg.exists == Exists::Yes) { + if (pcfg.has_main == HasMain::Yes) { + URL resolved(pcfg.main, pjson_url); + const std::string& path = resolved.ToFilePath(); + if (CheckDescriptorAtPath(path) == FILE) { + return Just(resolved); + } + } + if (env->options()->es_module_specifier_resolution == "node") { + if (pcfg.has_main == HasMain::Yes) { + return FinalizeResolution(env, URL(pcfg.main, pjson_url), base); + } else { + return FinalizeResolution(env, URL("index", pjson_url), base); + } + } + if (pcfg.esm == IsESM::No) { + Maybe resolved = LegacyMainResolve(pjson_url, pcfg); + if (!resolved.IsNothing()) { + return resolved; + } + } } - return resolved; + std::string msg = "Cannot find main entry point for '" + + URL(".", pjson_url).ToFilePath() + "' imported from " + + base.ToFilePath(); + node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str()); + return Nothing(); } Maybe PackageResolve(Environment* env, @@ -759,7 +815,7 @@ Maybe PackageResolve(Environment* env, if (!pkg_subpath.length()) { return PackageMainResolve(env, pjson_url, *pcfg.FromJust(), base); } else { - return FinalizeResolution(env, URL(pkg_subpath, pjson_url), base, true); + return FinalizeResolution(env, URL(pkg_subpath, pjson_url), base); } CHECK(false); // Cross-platform root check. @@ -789,7 +845,7 @@ Maybe Resolve(Environment* env, return PackageResolve(env, specifier, base); } } - return FinalizeResolution(env, resolved, base, true); + return FinalizeResolution(env, resolved, base); } void ModuleWrap::Resolve(const FunctionCallbackInfo& args) { diff --git a/src/node_options.cc b/src/node_options.cc index 2b241273fd..04f017ba5a 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -251,6 +251,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "custom loader", &EnvironmentOptions::userland_loader, kAllowedInEnvironment); + AddOption("--es-module-specifier-resolution", + "Select extension resolution algorithm for es modules; " + "either 'explicit' (default) or 'node'", + &EnvironmentOptions::es_module_specifier_resolution, + kAllowedInEnvironment); AddOption("--no-deprecation", "silence deprecation warnings", &EnvironmentOptions::no_deprecation, diff --git a/src/node_options.h b/src/node_options.h index 83c3be8674..862514f2a7 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -86,6 +86,7 @@ class EnvironmentOptions : public Options { public: bool abort_on_uncaught_exception = false; bool experimental_modules = false; + std::string es_module_specifier_resolution = "explicit"; std::string module_type; std::string experimental_policy; bool experimental_repl_await = false; diff --git a/test/es-module/test-esm-package-scope.mjs b/test/es-module/test-esm-package-scope.mjs deleted file mode 100644 index 6e07a307e0..0000000000 --- a/test/es-module/test-esm-package-scope.mjs +++ /dev/null @@ -1,12 +0,0 @@ -// Flags: --experimental-modules -/* eslint-disable node-core/required-modules */ - -import '../common/index.mjs'; -import assert from 'assert'; - -import legacyLoader from - '../fixtures/esm-package-scope/legacy-loader/index.mjs'; -import newLoader from '../fixtures/esm-package-scope/new-loader/index.js'; - -assert.strictEqual(legacyLoader, 'legacy-loader'); -assert.strictEqual(newLoader, 'new-loader'); diff --git a/test/es-module/test-esm-specifiers.mjs b/test/es-module/test-esm-specifiers.mjs new file mode 100644 index 0000000000..b386dcb8e9 --- /dev/null +++ b/test/es-module/test-esm-specifiers.mjs @@ -0,0 +1,35 @@ +// Flags: --experimental-modules --es-module-specifier-resolution=node +import { mustNotCall } from '../common'; +import assert from 'assert'; + +// commonJS index.js +import commonjs from '../fixtures/es-module-specifiers/package-type-commonjs'; +// esm index.js +import module from '../fixtures/es-module-specifiers/package-type-module'; +// notice the trailing slash +import success, { explicit, implicit, implicitModule, getImplicitCommonjs } + from '../fixtures/es-module-specifiers/'; + +assert.strictEqual(commonjs, 'commonjs'); +assert.strictEqual(module, 'module'); +assert.strictEqual(success, 'success'); +assert.strictEqual(explicit, 'esm'); +assert.strictEqual(implicit, 'esm'); +assert.strictEqual(implicitModule, 'esm'); + +async function main() { + try { + await import('../fixtures/es-module-specifiers/do-not-exist.js'); + } catch (e) { + // Files that do not exist should throw + assert.strictEqual(e.name, 'Error'); + } + try { + await getImplicitCommonjs(); + } catch (e) { + // Legacy loader cannot resolve .mjs automatically from main + assert.strictEqual(e.name, 'Error'); + } +} + +main().catch(mustNotCall); diff --git a/test/fixtures/es-module-specifiers/index.mjs b/test/fixtures/es-module-specifiers/index.mjs new file mode 100644 index 0000000000..2be7048513 --- /dev/null +++ b/test/fixtures/es-module-specifiers/index.mjs @@ -0,0 +1,10 @@ +import explicit from 'explicit-main'; +import implicit from 'implicit-main'; +import implicitModule from 'implicit-main-type-module'; + +function getImplicitCommonjs () { + return import('implicit-main-type-commonjs'); +} + +export {explicit, implicit, implicitModule, getImplicitCommonjs}; +export default 'success'; diff --git a/test/fixtures/es-module-specifiers/node_modules/explicit-main/entry.mjs b/test/fixtures/es-module-specifiers/node_modules/explicit-main/entry.mjs new file mode 100644 index 0000000000..914e3a97d5 --- /dev/null +++ b/test/fixtures/es-module-specifiers/node_modules/explicit-main/entry.mjs @@ -0,0 +1 @@ +export default 'esm'; diff --git a/test/fixtures/es-module-specifiers/node_modules/explicit-main/package.json b/test/fixtures/es-module-specifiers/node_modules/explicit-main/package.json new file mode 100644 index 0000000000..e9457582ac --- /dev/null +++ b/test/fixtures/es-module-specifiers/node_modules/explicit-main/package.json @@ -0,0 +1,3 @@ +{ + "main": "entry.mjs" +} \ No newline at end of file diff --git a/test/fixtures/es-module-specifiers/node_modules/implicit-main-type-commonjs/entry.mjs b/test/fixtures/es-module-specifiers/node_modules/implicit-main-type-commonjs/entry.mjs new file mode 100644 index 0000000000..914e3a97d5 --- /dev/null +++ b/test/fixtures/es-module-specifiers/node_modules/implicit-main-type-commonjs/entry.mjs @@ -0,0 +1 @@ +export default 'esm'; diff --git a/test/fixtures/es-module-specifiers/node_modules/implicit-main-type-commonjs/package.json b/test/fixtures/es-module-specifiers/node_modules/implicit-main-type-commonjs/package.json new file mode 100644 index 0000000000..663dad4f46 --- /dev/null +++ b/test/fixtures/es-module-specifiers/node_modules/implicit-main-type-commonjs/package.json @@ -0,0 +1,4 @@ +{ + "main": "entry", + "type": "commonjs" +} \ No newline at end of file diff --git a/test/fixtures/es-module-specifiers/node_modules/implicit-main-type-module/entry.js b/test/fixtures/es-module-specifiers/node_modules/implicit-main-type-module/entry.js new file mode 100644 index 0000000000..5d7af588fd --- /dev/null +++ b/test/fixtures/es-module-specifiers/node_modules/implicit-main-type-module/entry.js @@ -0,0 +1 @@ +export default 'nope'; diff --git a/test/fixtures/es-module-specifiers/node_modules/implicit-main-type-module/entry.mjs b/test/fixtures/es-module-specifiers/node_modules/implicit-main-type-module/entry.mjs new file mode 100644 index 0000000000..914e3a97d5 --- /dev/null +++ b/test/fixtures/es-module-specifiers/node_modules/implicit-main-type-module/entry.mjs @@ -0,0 +1 @@ +export default 'esm'; diff --git a/test/fixtures/es-module-specifiers/node_modules/implicit-main-type-module/package.json b/test/fixtures/es-module-specifiers/node_modules/implicit-main-type-module/package.json new file mode 100644 index 0000000000..c34ab42042 --- /dev/null +++ b/test/fixtures/es-module-specifiers/node_modules/implicit-main-type-module/package.json @@ -0,0 +1,4 @@ +{ + "main": "entry", + "type": "module" +} \ No newline at end of file diff --git a/test/fixtures/es-module-specifiers/node_modules/implicit-main/entry.js b/test/fixtures/es-module-specifiers/node_modules/implicit-main/entry.js new file mode 100644 index 0000000000..b2825bd3c9 --- /dev/null +++ b/test/fixtures/es-module-specifiers/node_modules/implicit-main/entry.js @@ -0,0 +1 @@ +module.exports = 'cjs'; diff --git a/test/fixtures/es-module-specifiers/node_modules/implicit-main/entry.mjs b/test/fixtures/es-module-specifiers/node_modules/implicit-main/entry.mjs new file mode 100644 index 0000000000..914e3a97d5 --- /dev/null +++ b/test/fixtures/es-module-specifiers/node_modules/implicit-main/entry.mjs @@ -0,0 +1 @@ +export default 'esm'; diff --git a/test/fixtures/es-module-specifiers/node_modules/implicit-main/package.json b/test/fixtures/es-module-specifiers/node_modules/implicit-main/package.json new file mode 100644 index 0000000000..bf2e35593b --- /dev/null +++ b/test/fixtures/es-module-specifiers/node_modules/implicit-main/package.json @@ -0,0 +1,3 @@ +{ + "main": "entry" +} \ No newline at end of file diff --git a/test/fixtures/esm-package-scope/legacy-loader/a.js b/test/fixtures/es-module-specifiers/package-type-commonjs/a.js similarity index 100% rename from test/fixtures/esm-package-scope/legacy-loader/a.js rename to test/fixtures/es-module-specifiers/package-type-commonjs/a.js diff --git a/test/fixtures/esm-package-scope/legacy-loader/b.mjs b/test/fixtures/es-module-specifiers/package-type-commonjs/b.mjs similarity index 100% rename from test/fixtures/esm-package-scope/legacy-loader/b.mjs rename to test/fixtures/es-module-specifiers/package-type-commonjs/b.mjs diff --git a/test/fixtures/esm-package-scope/legacy-loader/c.cjs b/test/fixtures/es-module-specifiers/package-type-commonjs/c.cjs similarity index 100% rename from test/fixtures/esm-package-scope/legacy-loader/c.cjs rename to test/fixtures/es-module-specifiers/package-type-commonjs/c.cjs diff --git a/test/fixtures/esm-package-scope/legacy-loader/index.mjs b/test/fixtures/es-module-specifiers/package-type-commonjs/index.mjs similarity index 82% rename from test/fixtures/esm-package-scope/legacy-loader/index.mjs rename to test/fixtures/es-module-specifiers/package-type-commonjs/index.mjs index 1c78c389a2..ef2b30b19b 100644 --- a/test/fixtures/esm-package-scope/legacy-loader/index.mjs +++ b/test/fixtures/es-module-specifiers/package-type-commonjs/index.mjs @@ -5,7 +5,7 @@ import {b} from './b.mjs'; // import 'c.cjs'; import cjs from './c.cjs'; // proves cross boundary fun bits -import jsAsEsm from '../new-loader/a.js'; +import jsAsEsm from '../package-type-module/a.js'; // named export from core import {strictEqual, deepStrictEqual} from 'assert'; @@ -18,4 +18,4 @@ deepStrictEqual(cjs, { three: 3 }); -export default 'legacy-loader'; +export default 'commonjs'; diff --git a/test/fixtures/es-module-specifiers/package-type-commonjs/package.json b/test/fixtures/es-module-specifiers/package-type-commonjs/package.json new file mode 100644 index 0000000000..5bbefffbab --- /dev/null +++ b/test/fixtures/es-module-specifiers/package-type-commonjs/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/test/fixtures/esm-package-scope/new-loader/a.js b/test/fixtures/es-module-specifiers/package-type-module/a.js similarity index 100% rename from test/fixtures/esm-package-scope/new-loader/a.js rename to test/fixtures/es-module-specifiers/package-type-module/a.js diff --git a/test/fixtures/esm-package-scope/new-loader/b.mjs b/test/fixtures/es-module-specifiers/package-type-module/b.mjs similarity index 100% rename from test/fixtures/esm-package-scope/new-loader/b.mjs rename to test/fixtures/es-module-specifiers/package-type-module/b.mjs diff --git a/test/fixtures/esm-package-scope/new-loader/c.cjs b/test/fixtures/es-module-specifiers/package-type-module/c.cjs similarity index 100% rename from test/fixtures/esm-package-scope/new-loader/c.cjs rename to test/fixtures/es-module-specifiers/package-type-module/c.cjs diff --git a/test/fixtures/esm-package-scope/new-loader/index.js b/test/fixtures/es-module-specifiers/package-type-module/index.js similarity index 82% rename from test/fixtures/esm-package-scope/new-loader/index.js rename to test/fixtures/es-module-specifiers/package-type-module/index.js index 98c536cc34..a8baacb7c9 100644 --- a/test/fixtures/esm-package-scope/new-loader/index.js +++ b/test/fixtures/es-module-specifiers/package-type-module/index.js @@ -5,7 +5,7 @@ import {b} from './b.mjs'; // import 'c.cjs'; import cjs from './c.cjs'; // import across boundaries -import jsAsCjs from '../legacy-loader/a.js' +import jsAsCjs from '../package-type-commonjs/a.js' // named export from core import {strictEqual, deepStrictEqual} from 'assert'; @@ -18,4 +18,4 @@ deepStrictEqual(cjs, { three: 3 }); -export default 'new-loader'; +export default 'module'; diff --git a/test/fixtures/es-module-specifiers/package-type-module/package.json b/test/fixtures/es-module-specifiers/package-type-module/package.json new file mode 100644 index 0000000000..3dbc1ca591 --- /dev/null +++ b/test/fixtures/es-module-specifiers/package-type-module/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/test/fixtures/es-module-specifiers/package.json b/test/fixtures/es-module-specifiers/package.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/test/fixtures/es-module-specifiers/package.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/fixtures/esm-package-scope/legacy-loader/package.json b/test/fixtures/esm-package-scope/legacy-loader/package.json deleted file mode 100644 index 215a962248..0000000000 --- a/test/fixtures/esm-package-scope/legacy-loader/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "legacy-loader", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "Myles Borins ", - "license": "Apache-2.0", - "type": "commonjs" -} diff --git a/test/fixtures/esm-package-scope/new-loader/package.json b/test/fixtures/esm-package-scope/new-loader/package.json deleted file mode 100644 index 1f2c704322..0000000000 --- a/test/fixtures/esm-package-scope/new-loader/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "new-loader", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "Myles Borins ", - "license": "Apache-2.0", - "type": "module" -}