diff --git a/.eslintrc.js b/.eslintrc.js index 54595a8b74d9a9..d3a6c73241f1ff 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', 'test/es-module/test-esm-example-loader.js', ], diff --git a/doc/api/cli.md b/doc/api/cli.md index 4c5c8fb61853cf..ad7b65e90a827e 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -131,9 +131,43 @@ conjunction with native stack and other runtime environment data. added: v6.0.0 --> +### `--entry-type=type` + + +Used with `--experimental-modules`, this configures Node.js to interpret the +initial entry point as CommonJS or as an ES module. + +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`. + +Works for executing a file as well as `--eval`, `--print`, `STDIN`. + Enable FIPS-compliant crypto at startup. (Requires Node.js to be built with `./configure --openssl-fips`.) +### `--es-module-specifier-resolution=mode` + + +To be used in conjunction with `--experimental-modules`. Sets the resolution +algorithm for resolving specifiers. Valid options are `explicit` and `node`. + +The default is `explicit`, which requires providing the full path to a +module. The `node` mode will enable support for optional file extensions and +the ability to import a directory that has an index file. + +Please see [customizing esm specifier resolution][] for example usage. + +### `--experimental-json-modules` + + +Enable experimental JSON support for the ES Module loader. + ### `--experimental-modules` +ECMAScript modules are [the official standard format][] to package JavaScript +code for reuse. Modules are defined using a variety of [`import`][] and +[`export`][] statements. + +Node.js fully supports ECMAScript modules as they are currently specified and +provides limited interoperability between them and the existing module format, +[CommonJS][]. + Node.js contains support for ES Modules based upon the -[Node.js EP for ES Modules][]. +[Node.js EP for ES Modules][] and the [ECMAScript-modules implementation][]. -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. +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). + +## Running Node.js with an ECMAScript Module -Once this has been set, files ending with `.mjs` will be able to be loaded -as ES Modules. +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 +### --entry-type=module flag - +Files ending with `.js` or `.mjs`, or lacking any extension, +will be loaded as ES modules when the `--entry-type=module` flag is set. + +```sh +node --experimental-modules --entry-type=module my-app.js +``` + +For completeness there is also `--entry-type=commonjs`, for explicitly running +a `.js` file as CommonJS. This is the default behavior if `--entry-type` is +unspecified. + +The `--entry-type=module` flag 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 --entry-type=module --eval \ + "import { sep } from 'path'; console.log(sep);" + +echo "import { sep } from 'path'; console.log(sep);" | \ + node --experimental-modules --entry-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 default, 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. + +```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". +``` + +Files ending with `.mjs` are always loaded as ES modules regardless of package +scope. + +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. +``` + +The `.mjs` and `.cjs` extensions may be used 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. -### Supported +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. -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. +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 +## import.meta * {Object} @@ -46,63 +240,128 @@ property: * `url` {string} The absolute `file:` URL of the module. -### Unsupported +## Differences Between ES Modules and CommonJS -| Feature | Reason | -| --- | --- | -| `require('./foo.mjs')` | ES Modules have differing resolution and timing, use dynamic import | +### Mandatory file extensions + +A file extension must be provided when using the `import` keyword. Directory +indexes (e.g. `'./startup/index.js'`) must also be fully specified. -## Notable differences between `import` and `require` +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. ```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 +## Interoperability with CommonJS + +### require -All CommonJS, JSON, and C++ modules can be used with `import`. +`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()`][]. -Modules loaded this way will only be loaded once, even if their query -or fragment string differs between `import` statements. +To include an ES module into CommonJS, use [`import()`][]. -When loaded via `import` these modules will provide a single `default` export -representing the value of `module.exports` at the time they finished evaluating. +### 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 -// foo.js -module.exports = { one: 1 }; +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. -// bar.mjs -import foo from './foo.js'; -foo.one === 1; // true +### 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()`][]. + +```js +// cjs.js +module.exports = 'cjs'; + +// esm.mjs +import { createRequireFromPath as createRequire } from 'module'; +import { fileURLToPath as fromURL } from 'url'; + +const require = createRequire(fromURL(import.meta.url)); + +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,7 +391,41 @@ fs.readFileSync = () => Buffer.from('Hello, ESM'); fs.readFileSync === readFileSync; ``` -## Loader hooks +## Experimental JSON Modules + +**Note: This API is still being designed and is subject to change.** + +Currently importing JSON modules are only supported in the `commonjs` mode +and are loaded using the CJS loader. [WHATWG JSON modules][] are currently +being standardized, and are experimentally supported by including the +additional flag `--experimental-json-modules` when running Node.js. + +When the `--experimental-json-modules` flag is included both the +`commonjs` and `module` mode will use the new experimental JSON +loader. The imported JSON only exposes a `default`, there is no +support for named exports. A cache entry is created in the CommonJS +cache, to avoid duplication. The same object will be returned in +CommonJS if the JSON module has already been imported from the +same path. + +Assuming an `index.js` with + + +```js +import packageConfig from './package.json'; +``` + +The `--experimental-json-modules` flag is needed for the module +to work. + +```bash +node --experimental-modules --entry-type=module index.js # fails +node --experimental-modules --entry-type=module --experimental-json-modules index.js # works +``` + +## Experimental Loader hooks + +**Note: This API is currently being redesigned and will still change.** @@ -173,11 +466,10 @@ 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 | +| `'module'` | Load a standard JavaScript module | +| `'commonjs'` | Load a Node.js CommonJS module | +| `'builtin'` | Load a Node.js builtin 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 @@ -253,6 +545,184 @@ 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 _"module"_ format is returned for an ECMAScript Module, while the +_"commonjs"_ 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 propagated as errors +of these top-level routines. + +_isMain_ is **true** when resolving the Node.js application entry point. + +When using the `--entry-type` flag, it overrides the ESM_FORMAT result while +providing errors in the case of explicit conflicts. + +
+Resolver algorithm specification + +**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. Set _resolvedURL_ to the real path of _resolvedURL_. +> 1. Let _format_ be the result of **ESM_FORMAT**(_resolvedURL_, _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. Let _packageURL_ be the URL resolution of "node_modules/" +> concatenated with _packageSpecifier_, relative to _parentURL_. +> 1. Set _parentURL_ to the parent folder URL of _parentURL_. +> 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 _"module"_, 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_SCOPE**(_url_). +> 1. If _url_ ends in _".mjs"_, then +> 1. Return _"module"_. +> 1. If _url_ ends in _".cjs"_, then +> 1. Return _"commonjs"_. +> 1. If _pjson?.type_ exists and is _"module"_, then +> 1. If _isMain_ is **true** or _url_ ends in _".js"_, then +> 1. Return _"module"_. +> 1. Throw an _Unsupported File Extension_ error. +> 1. Otherwise, +> 1. If _isMain_ is **true** or _url_ ends in _".js"_, _".json"_ or +> _".node"_, then +> 1. Return _"commonjs"_. +> 1. Throw an _Unsupported File Extension_ error. + +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 _scopeURL_ to the parent URL of _scopeURL_. +> 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_. + +
+ +### Customizing ESM specifier resolution algorithm + +The current specifier resolution does not support all default behavior of +the CommonJS loader. One of the behavior differences is automatic resolution +of file extensions and the ability to import directories that have an index +file. + +The `--es-module-specifier-resolution=[mode]` flag can be used to customize +the extension resolution algorithm. The default mode is `explicit`, which +requires the full path to a module be provided to the loader. To enable the +automatic extension resolution and importing from directories that include an +index file use the `node` mode. + +```bash +$ node --experimental-modules index.mjs +success! +$ node --experimental-modules index #Failure! +Error: Cannot find module +$ node --experimental-modules --es-module-specifier-resolution=node index +success! +``` + +[`export`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export +[`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import +[`import()`]: #esm_import-expressions +[`import.meta.url`]: #esm_import_meta +[`module.createRequireFromPath()`]: modules.html#modules_module_createrequirefrompath_filename +[CommonJS]: modules.html +[ECMAScript-modules implementation]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md [Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md -[addons]: addons.html +[WHATWG JSON modules]: https://github.com/whatwg/html/issues/4315 [dynamic instantiate hook]: #esm_dynamic_instantiate_hook +[the official standard format]: https://tc39.github.io/ecma262/#sec-modules diff --git a/doc/node.1 b/doc/node.1 index 7bcb1edc59ab5d..e2ac998cab2973 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -119,6 +119,15 @@ Enable FIPS-compliant crypto at startup. Requires Node.js to be built with .Sy ./configure --openssl-fips . . +.It Fl -entry-type Ns = Ns Ar type +Set the top-level module resolution type. +. +.It Fl -es-module-specifier-resolution +Select extension resolution algorithm for ES Modules; either 'explicit' (default) or 'node' +. +.It Fl -experimental-json-modules +Enable experimental JSON interop support for the ES Module loader. +. .It Fl -experimental-modules Enable experimental ES module support and caching modules. . diff --git a/lib/internal/bootstrap/pre_execution.js b/lib/internal/bootstrap/pre_execution.js index 820d931575b1bc..20b362f2136583 100644 --- a/lib/internal/bootstrap/pre_execution.js +++ b/lib/internal/bootstrap/pre_execution.js @@ -323,6 +323,10 @@ function initializeESMLoader() { const userLoader = getOptionValue('--loader'); // If --loader is specified, create a loader with user hooks. Otherwise // create the default loader. + if (userLoader) { + const { emitExperimentalWarning } = require('internal/util'); + emitExperimentalWarning('--loader'); + } esm.initializeLoader(process.cwd(), userLoader); } } diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 42775691c37251..e3347374cc702b 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -678,6 +678,24 @@ E('ERR_ENCODING_INVALID_ENCODED_DATA', function(encoding, ret) { }, TypeError); E('ERR_ENCODING_NOT_SUPPORTED', 'The "%s" encoding is not supported', RangeError); +E('ERR_ENTRY_TYPE_MISMATCH', (filename, ext, typeFlag, conflict) => { + const typeString = + typeFlag === 'module' ? '--entry-type=module' : '--entry-type=commonjs'; + // --entry-type mismatches file extension + if (conflict === 'extension') { + return `Extension ${ext} is not supported for ` + + `${typeString} loading ${filename}`; + } + assert( + conflict === 'scope', + '"conflict" value unknown. Set this argument to "extension" or "scope"' + ); + // --entry-type mismatches package.json "type" + return `Cannot use ${typeString} because nearest parent package.json ` + + ((typeFlag === 'module') ? + 'includes "type": "commonjs"' : 'includes "type": "module",') + + ` which controls the type to use for ${filename}`; +}, TypeError); E('ERR_FALSY_VALUE_REJECTION', function(reason) { this.reason = reason; return 'Promise was rejected with falsy value'; @@ -864,6 +882,8 @@ E('ERR_INVALID_OPT_VALUE', (name, value) => RangeError); E('ERR_INVALID_OPT_VALUE_ENCODING', 'The value "%s" is invalid for option "encoding"', TypeError); +E('ERR_INVALID_PACKAGE_CONFIG', + 'Invalid package config in \'%s\' imported from %s', Error); E('ERR_INVALID_PERFORMANCE_MARK', 'The "%s" performance mark has not been set', Error); E('ERR_INVALID_PROTOCOL', @@ -958,11 +978,6 @@ 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_MULTIPLE_CALLBACK', 'Callback called multiple times', Error); E('ERR_NAPI_CONS_FUNCTION', 'Constructor must be a function', TypeError); E('ERR_NAPI_INVALID_DATAVIEW_ARGS', @@ -1067,9 +1082,7 @@ E('ERR_UNHANDLED_ERROR', E('ERR_UNKNOWN_BUILTIN_MODULE', 'No such built-in module: %s', Error); 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', TypeError); E('ERR_UNKNOWN_MODULE_FORMAT', 'Unknown module format: %s', RangeError); E('ERR_UNKNOWN_SIGNAL', 'Unknown signal: %s', TypeError); diff --git a/lib/internal/main/check_syntax.js b/lib/internal/main/check_syntax.js index 7df70b272091ff..8c73d522edbf69 100644 --- a/lib/internal/main/check_syntax.js +++ b/lib/internal/main/check_syntax.js @@ -11,12 +11,18 @@ const { readStdin } = require('internal/process/execution'); -const CJSModule = require('internal/modules/cjs/loader'); +const { pathToFileURL } = require('url'); + const vm = require('vm'); const { stripShebang, stripBOM } = require('internal/modules/cjs/helpers'); +let CJSModule; +function CJSModuleInit() { + if (!CJSModule) + CJSModule = require('internal/modules/cjs/loader'); +} if (process.argv[1] && process.argv[1] !== '-') { // Expand process.argv[1] into a full path. @@ -25,7 +31,7 @@ if (process.argv[1] && process.argv[1] !== '-') { // TODO(joyeecheung): not every one of these are necessary prepareMainThreadExecution(); - + CJSModuleInit(); // Read the source. const filename = CJSModule._resolveFilename(process.argv[1]); @@ -34,20 +40,40 @@ if (process.argv[1] && process.argv[1] !== '-') { markBootstrapComplete(); - checkScriptSyntax(source, filename); + checkSyntax(source, filename); } else { // TODO(joyeecheung): not every one of these are necessary prepareMainThreadExecution(); + CJSModuleInit(); markBootstrapComplete(); readStdin((code) => { - checkScriptSyntax(code, '[stdin]'); + checkSyntax(code, '[stdin]'); }); } -function checkScriptSyntax(source, filename) { +function checkSyntax(source, filename) { // Remove Shebang. source = stripShebang(source); + + const { getOptionValue } = require('internal/options'); + const experimentalModules = getOptionValue('--experimental-modules'); + if (experimentalModules) { + let isModule = false; + if (filename === '[stdin]' || filename === '[eval]') { + isModule = getOptionValue('--entry-type') === 'module'; + } else { + const resolve = require('internal/modules/esm/default_resolve'); + const { format } = resolve(pathToFileURL(filename).toString()); + isModule = format === 'module'; + } + if (isModule) { + const { ModuleWrap } = internalBinding('module_wrap'); + new ModuleWrap(source, filename); + return; + } + } + // Remove BOM. source = stripBOM(source); // Wrap it. diff --git a/lib/internal/main/eval_stdin.js b/lib/internal/main/eval_stdin.js index 2a2ef6d38a1fe8..4face9e61e7e64 100644 --- a/lib/internal/main/eval_stdin.js +++ b/lib/internal/main/eval_stdin.js @@ -7,6 +7,7 @@ const { } = require('internal/bootstrap/pre_execution'); const { + evalModule, evalScript, readStdin } = require('internal/process/execution'); @@ -16,5 +17,8 @@ markBootstrapComplete(); readStdin((code) => { process._eval = code; - evalScript('[stdin]', process._eval, process._breakFirstLine); + if (require('internal/options').getOptionValue('--entry-type') === 'module') + evalModule(process._eval); + else + evalScript('[stdin]', process._eval, process._breakFirstLine); }); diff --git a/lib/internal/main/eval_string.js b/lib/internal/main/eval_string.js index 953fab386d0671..b0322819251e76 100644 --- a/lib/internal/main/eval_string.js +++ b/lib/internal/main/eval_string.js @@ -6,11 +6,15 @@ const { prepareMainThreadExecution } = require('internal/bootstrap/pre_execution'); -const { evalScript } = require('internal/process/execution'); +const { evalModule, evalScript } = require('internal/process/execution'); const { addBuiltinLibsToObject } = require('internal/modules/cjs/helpers'); -const source = require('internal/options').getOptionValue('--eval'); +const { getOptionValue } = require('internal/options'); +const source = getOptionValue('--eval'); prepareMainThreadExecution(); addBuiltinLibsToObject(global); markBootstrapComplete(); -evalScript('[eval]', source, process._breakFirstLine); +if (getOptionValue('--entry-type') === 'module') + evalModule(source); +else + evalScript('[eval]', source, process._breakFirstLine); diff --git a/lib/internal/main/repl.js b/lib/internal/main/repl.js index e6b98853511d11..c2bf54f8bb6981 100644 --- a/lib/internal/main/repl.js +++ b/lib/internal/main/repl.js @@ -13,6 +13,12 @@ const { prepareMainThreadExecution(); +// --entry-type flag not supported in REPL +if (require('internal/options').getOptionValue('--entry-type')) { + console.error('Cannot specify --entry-type for REPL'); + process.exit(1); +} + const cliRepl = require('internal/repl'); cliRepl.createInternalRepl(process.env, (err, repl) => { if (err) { diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 3f280dd4beac35..42f143be945175 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -872,9 +872,11 @@ Module.runMain = function() { .catch((e) => { internalBinding('task_queue').triggerFatalException(e); }); - } else { - Module._load(process.argv[1], null, true); + // Handle any nextTicks added in the first tick of the program + process._tickCallback(); + return; } + Module._load(process.argv[1], null, true); // Handle any nextTicks added in the first tick of the program process._tickCallback(); }; diff --git a/lib/internal/modules/esm/default_resolve.js b/lib/internal/modules/esm/default_resolve.js index 33366f0069a8f1..8da24cf5b3afab 100644 --- a/lib/internal/modules/esm/default_resolve.js +++ b/lib/internal/modules/esm/default_resolve.js @@ -1,57 +1,54 @@ 'use strict'; -const { URL } = require('url'); -const CJSmodule = require('internal/modules/cjs/loader'); const internalFS = require('internal/fs/utils'); const { NativeModule } = require('internal/bootstrap/loaders'); const { extname } = require('path'); const { realpathSync } = require('fs'); 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_UNKNOWN_FILE_EXTENSION -} = require('internal/errors').codes; -const { resolve: moduleWrapResolve } = internalBinding('module_wrap'); -const StringStartsWith = Function.call.bind(String.prototype.startsWith); +const experimentalJsonModules = getOptionValue('--experimental-json-modules'); +const typeFlag = getOptionValue('--entry-type'); + +const { resolve: moduleWrapResolve, + getPackageType } = internalBinding('module_wrap'); const { pathToFileURL, fileURLToPath } = require('internal/url'); +const { ERR_ENTRY_TYPE_MISMATCH, + ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes; 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) { - e.stack; // cause V8 to generate stack before rethrow - let error = e; - try { - const questionedBase = new URL(base); - const tmpMod = new CJSmodule(questionedBase.pathname, null); - tmpMod.paths = CJSmodule._nodeModulePaths( - new URL('./', questionedBase).pathname); - const found = CJSmodule._resolveFilename(target, tmpMod); - error = new ERR_MODULE_RESOLUTION_LEGACY(target, base, found); - } catch { - // ignore - } - throw error; - } -} +// const TYPE_NONE = 0; +const TYPE_COMMONJS = 1; +const TYPE_MODULE = 2; const extensionFormatMap = { '__proto__': null, - '.mjs': 'esm', - '.json': 'json', - '.node': 'addon', - '.js': 'cjs' + '.cjs': 'commonjs', + '.js': 'module', + '.mjs': 'module' }; +const legacyExtensionFormatMap = { + '__proto__': null, + '.cjs': 'commonjs', + '.js': 'commonjs', + '.json': 'commonjs', + '.mjs': 'module', + '.node': 'commonjs' +}; + +if (experimentalJsonModules) { + // This is a total hack + Object.assign(extensionFormatMap, { + '.json': 'json' + }); + Object.assign(legacyExtensionFormatMap, { + '.json': 'json' + }); +} + function resolve(specifier, parentURL) { if (NativeModule.canBeRequiredByUsers(specifier)) { return { @@ -60,21 +57,11 @@ function resolve(specifier, parentURL) { }; } - let url; - try { - url = search(specifier, - parentURL || pathToFileURL(`${process.cwd()}/`).href); - } catch (e) { - if (typeof e.message === 'string' && - StringStartsWith(e.message, 'Cannot find module')) { - e.code = 'MODULE_NOT_FOUND'; - // TODO: also add e.requireStack to match behavior with CJS - // MODULE_NOT_FOUND. - } - throw e; - } - const isMain = parentURL === undefined; + if (isMain) + parentURL = pathToFileURL(`${process.cwd()}/`).href; + + let url = moduleWrapResolve(specifier, parentURL); if (isMain ? !preserveSymlinksMain : !preserveSymlinks) { const real = realpathSync(fileURLToPath(url), { @@ -86,19 +73,40 @@ function resolve(specifier, parentURL) { url.hash = old.hash; } + const type = getPackageType(url.href); + const ext = extname(url.pathname); + const extMap = + type !== TYPE_MODULE ? legacyExtensionFormatMap : extensionFormatMap; + let format = extMap[ext]; - let format = extensionFormatMap[ext]; + if (isMain && typeFlag) { + // Conflict between explicit extension (.mjs, .cjs) and --entry-type + if (ext === '.cjs' && typeFlag === 'module' || + ext === '.mjs' && typeFlag === 'commonjs') { + throw new ERR_ENTRY_TYPE_MISMATCH( + fileURLToPath(url), ext, typeFlag, 'extension'); + } + + // Conflict between package scope type and --entry-type + if (ext === '.js') { + if (type === TYPE_MODULE && typeFlag === 'commonjs' || + type === TYPE_COMMONJS && typeFlag === 'module') { + throw new ERR_ENTRY_TYPE_MISMATCH( + fileURLToPath(url), ext, typeFlag, 'scope'); + } + } + } if (!format) { - if (isMain) - format = 'cjs'; + if (isMain && typeFlag) + format = typeFlag; + else if (isMain) + format = type === TYPE_MODULE ? 'module' : 'commonjs'; else - throw new ERR_UNKNOWN_FILE_EXTENSION(url.pathname); + throw new ERR_UNKNOWN_FILE_EXTENSION(fileURLToPath(url), + fileURLToPath(parentURL)); } - return { url: `${url}`, format }; } module.exports = resolve; -// exported for tests -module.exports.search = search; diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index aff12113688b44..465775f56ffc72 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -11,10 +11,12 @@ const { URL } = require('url'); const { validateString } = require('internal/validators'); const ModuleMap = require('internal/modules/esm/module_map'); const ModuleJob = require('internal/modules/esm/module_job'); + const defaultResolve = require('internal/modules/esm/default_resolve'); const createDynamicModule = require( 'internal/modules/esm/create_dynamic_module'); -const translators = require('internal/modules/esm/translators'); +const { translators } = require('internal/modules/esm/translators'); +const { ModuleWrap } = internalBinding('module_wrap'); const FunctionBind = Function.call.bind(Function.prototype.bind); @@ -32,6 +34,9 @@ class Loader { // Registry of loaded modules, akin to `require.cache` this.moduleMap = new ModuleMap(); + // Map of already-loaded CJS modules to use + this.cjsCache = new Map(); + // The resolver has the signature // (specifier : string, parentURL : string, defaultResolve) // -> Promise<{ url : string, format: string }> @@ -48,6 +53,8 @@ class Loader { // an object with the same keys as `exports`, whose values are get/set // functions for the actual exported values. this._dynamicInstantiate = undefined; + // The index for assigning unique URLs to anonymous module evaluation + this.evalIndex = 0; } async resolve(specifier, parentURL) { @@ -95,9 +102,25 @@ class Loader { return { url, format }; } + async eval(source, url = `eval:${++this.evalIndex}`) { + const evalInstance = async (url) => { + return { + module: new ModuleWrap(source, url), + reflect: undefined + }; + }; + const job = new ModuleJob(this, url, evalInstance, false); + this.moduleMap.set(url, job); + const { module, result } = await job.run(); + return { + namespace: module.namespace(), + result + }; + } + async import(specifier, parent) { const job = await this.getModuleJob(specifier, parent); - const module = await job.run(); + const { module } = await job.run(); return module.namespace(); } @@ -143,4 +166,4 @@ class Loader { Object.setPrototypeOf(Loader.prototype, null); -module.exports = Loader; +exports.Loader = Loader; diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index 016495096c3b33..5666032df1d6d8 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -23,7 +23,7 @@ class ModuleJob { // This is a Promise<{ module, reflect }>, whose fields will be copied // onto `this` by `link()` below once it has been resolved. - this.modulePromise = moduleProvider(url, isMain); + this.modulePromise = moduleProvider.call(loader, url, isMain); this.module = undefined; this.reflect = undefined; @@ -101,8 +101,9 @@ class ModuleJob { async run() { const module = await this.instantiate(); - module.evaluate(-1, false); - return module; + const timeout = -1; + const breakOnSigint = false; + return { module, result: module.evaluate(timeout, breakOnSigint) }; } } Object.setPrototypeOf(ModuleJob.prototype, null); diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index de09910872e133..172569fb042c69 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -11,14 +11,10 @@ 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 { fileURLToPath, URL } = require('url'); const { debuglog } = require('internal/util/debuglog'); const { promisify } = require('internal/util'); const esmLoader = require('internal/process/esm_loader'); @@ -26,14 +22,13 @@ const { ERR_UNKNOWN_BUILTIN_MODULE } = require('internal/errors').codes; const readFileAsync = promisify(fs.readFile); -const readFileSync = fs.readFileSync; -const StringReplace = FunctionPrototype.call.bind(StringPrototype.replace); +const StringReplace = Function.call.bind(String.prototype.replace); const JsonParse = JSON.parse; const debug = debuglog('esm'); const translators = new SafeMap(); -module.exports = translators; +exports.translators = translators; function initializeImportMeta(meta, { url }) { meta.url = url; @@ -45,7 +40,7 @@ async function importModuleDynamically(specifier, { url }) { } // Strategy for loading a standard JavaScript module -translators.set('esm', async (url) => { +translators.set('module', async function moduleStrategy(url) { const source = `${await readFileAsync(new URL(url))}`; debug(`Translating StandardModule ${url}`); const module = new ModuleWrap(stripShebang(source), url); @@ -62,9 +57,14 @@ translators.set('esm', async (url) => { // Strategy for loading a node-style CommonJS module const isWindows = process.platform === 'win32'; const winSepRegEx = /\//g; -translators.set('cjs', async (url, isMain) => { +translators.set('commonjs', async function commonjsStrategy(url, isMain) { debug(`Translating CJSModule ${url}`); const pathname = internalURLModule.fileURLToPath(new URL(url)); + const cached = this.cjsCache.get(url); + if (cached) { + this.cjsCache.delete(url); + return cached; + } const module = CJSModule._cache[ isWindows ? StringReplace(pathname, winSepRegEx, '\\') : pathname]; if (module && module.loaded) { @@ -84,7 +84,7 @@ translators.set('cjs', async (url, isMain) => { // Strategy for loading a node builtin CommonJS module that isn't // through normal resolution -translators.set('builtin', async (url) => { +translators.set('builtin', async function builtinStrategy(url) { debug(`Translating BuiltinModule ${url}`); // Slice 'node:' scheme const id = url.slice(5); @@ -103,31 +103,38 @@ translators.set('builtin', async (url) => { }); }); -// 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) => { +translators.set('json', async function jsonStrategy(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)); + debug(`Loading JSONModule ${url}`); + const pathname = fileURLToPath(url); + const modulePath = isWindows ? + StringReplace(pathname, winSepRegEx, '\\') : pathname; + let module = CJSModule._cache[modulePath]; + if (module && module.loaded) { + const exports = module.exports; + return createDynamicModule(['default'], url, (reflect) => { reflect.exports.default.set(exports); - } catch (err) { - err.message = pathname + ': ' + err.message; - throw err; - } + }); + } + const content = await readFileAsync(pathname, 'utf-8'); + try { + const exports = JsonParse(stripBOM(content)); + module = { + exports, + loaded: true + }; + } catch (err) { + // TODO (BridgeAR): We could add a NodeCore error that wraps the JSON + // parse error instead of just manipulating the original error message. + // That would allow to add further properties and maybe additional + // debugging information. + err.message = pathname + ': ' + err.message; + throw err; + } + CJSModule._cache[modulePath] = module; + return createDynamicModule(['default'], url, (reflect) => { + debug(`Parsing JSONModule ${url}`); + reflect.exports.default.set(module.exports); }); }); diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js index 0b7f1be6ff0595..6225ea81ab1364 100644 --- a/lib/internal/process/esm_loader.js +++ b/lib/internal/process/esm_loader.js @@ -3,15 +3,15 @@ const { callbackMap, } = internalBinding('module_wrap'); +const { + ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING, +} = require('internal/errors').codes; +const { Loader } = require('internal/modules/esm/loader'); const { pathToFileURL } = require('internal/url'); -const Loader = require('internal/modules/esm/loader'); const { wrapToModuleMap, } = require('internal/vm/source_text_module'); -const { - ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING, -} = require('internal/errors').codes; exports.initializeImportMetaObject = function(wrap, meta) { if (callbackMap.has(wrap)) { @@ -34,9 +34,7 @@ exports.importModuleDynamicallyCallback = async function(wrap, specifier) { }; let loaderResolve; -exports.loaderPromise = new Promise((resolve, reject) => { - loaderResolve = resolve; -}); +exports.loaderPromise = new Promise((resolve) => loaderResolve = resolve); exports.ESMLoader = undefined; diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js index 7118dbf3ad50d2..070410ef6faf21 100644 --- a/lib/internal/process/execution.js +++ b/lib/internal/process/execution.js @@ -33,6 +33,24 @@ function tryGetCwd() { } } +function evalModule(source) { + const { decorateErrorStack } = require('internal/util'); + const asyncESM = require('internal/process/esm_loader'); + asyncESM.loaderPromise.then(async (loader) => { + const { result } = await loader.eval(source); + if (require('internal/options').getOptionValue('--print')) { + console.log(result); + } + }) + .catch((e) => { + decorateErrorStack(e); + console.error(e); + process.exit(1); + }); + // Handle any nextTicks added in the first tick of the program. + process._tickCallback(); +} + function evalScript(name, body, breakFirstLine) { const CJSModule = require('internal/modules/cjs/loader'); const { kVmBreakFirstLineSymbol } = require('internal/util'); @@ -176,6 +194,7 @@ function readStdin(callback) { module.exports = { readStdin, tryGetCwd, + evalModule, evalScript, fatalException: createFatalException(), setUncaughtExceptionCaptureCallback, diff --git a/src/env.h b/src/env.h index 55018b6d40d1ca..743c236827b07d 100644 --- a/src/env.h +++ b/src/env.h @@ -77,11 +77,13 @@ struct PackageConfig { enum class Exists { Yes, No }; enum class IsValid { Yes, No }; enum class HasMain { Yes, No }; + enum PackageType : uint32_t { None = 0, CommonJS, Module }; - Exists exists; - IsValid is_valid; - HasMain has_main; - std::string main; + const Exists exists; + const IsValid is_valid; + const HasMain has_main; + const std::string main; + const PackageType type; }; } // namespace loader @@ -141,6 +143,7 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2; V(channel_string, "channel") \ V(chunks_sent_since_last_write_string, "chunksSentSinceLastWrite") \ V(code_string, "code") \ + V(commonjs_string, "commonjs") \ V(config_string, "config") \ V(constants_string, "constants") \ V(crypto_dsa_string, "dsa") \ diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 56149d0cc759c9..622deed45ff668 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -29,7 +29,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; @@ -46,7 +45,13 @@ using v8::String; using v8::Undefined; using v8::Value; -static const char* const EXTENSIONS[] = {".mjs", ".js", ".json", ".node"}; +static const char* const EXTENSIONS[] = { + ".mjs", + ".cjs", + ".js", + ".json", + ".node" +}; ModuleWrap::ModuleWrap(Environment* env, Local object, @@ -471,100 +476,233 @@ std::string ReadFile(uv_file file) { return contents; } -enum CheckFileOptions { - LEAVE_OPEN_AFTER_CHECK, - CLOSE_AFTER_CHECK +enum DescriptorType { + FILE, + DIRECTORY, + NONE }; -Maybe CheckFile(const std::string& path, - CheckFileOptions opt = CLOSE_AFTER_CHECK) { +// When DescriptorType cache is added, this can also return +// Nothing for the "null" cache entries. +inline Maybe OpenDescriptor(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(); + return Just(fd); +} - if (fd < 0) { - return Nothing(); - } - - uv_fs_fstat(nullptr, &fs_req, fd, nullptr); - uint64_t is_directory = fs_req.statbuf.st_mode & S_IFDIR; +inline void CloseDescriptor(uv_file fd) { + uv_fs_t fs_req; + uv_fs_close(nullptr, &fs_req, fd, nullptr); uv_fs_req_cleanup(&fs_req); +} - if (is_directory) { - CHECK_EQ(0, uv_fs_close(nullptr, &fs_req, fd, nullptr)); +inline DescriptorType CheckDescriptorAtFile(uv_file fd) { + uv_fs_t fs_req; + int rc = uv_fs_fstat(nullptr, &fs_req, fd, nullptr); + if (rc == 0) { + uint64_t is_directory = fs_req.statbuf.st_mode & S_IFDIR; uv_fs_req_cleanup(&fs_req); - return Nothing(); + return is_directory ? DIRECTORY : FILE; } + uv_fs_req_cleanup(&fs_req); + return NONE; +} - if (opt == CLOSE_AFTER_CHECK) { - CHECK_EQ(0, uv_fs_close(nullptr, &fs_req, fd, nullptr)); - uv_fs_req_cleanup(&fs_req); - } +// TODO(@guybedford): Add a DescriptorType cache layer here. +// Should be directory based -> if path/to/dir doesn't exist +// then the cache should early-fail any path/to/dir/file check. +DescriptorType CheckDescriptorAtPath(const std::string& path) { + Maybe fd = OpenDescriptor(path); + if (fd.IsNothing()) return NONE; + DescriptorType type = CheckDescriptorAtFile(fd.FromJust()); + CloseDescriptor(fd.FromJust()); + return type; +} - return Just(fd); +Maybe ReadIfFile(const std::string& path) { + Maybe fd = OpenDescriptor(path); + if (fd.IsNothing()) return Nothing(); + DescriptorType type = CheckDescriptorAtFile(fd.FromJust()); + if (type != FILE) return Nothing(); + std::string source = ReadFile(fd.FromJust()); + CloseDescriptor(fd.FromJust()); + return Just(source); } using Exists = PackageConfig::Exists; using IsValid = PackageConfig::IsValid; using HasMain = PackageConfig::HasMain; +using PackageType = PackageConfig::PackageType; -const PackageConfig& GetPackageConfig(Environment* env, - const std::string& path) { +Maybe GetPackageConfig(Environment* env, + const std::string& path, + const URL& base) { auto existing = env->package_json_cache.find(path); if (existing != env->package_json_cache.end()) { - return existing->second; + const PackageConfig* pcfg = &existing->second; + if (pcfg->is_valid == IsValid::No) { + std::string msg = "Invalid JSON in '" + path + + "' imported from " + base.ToFilePath(); + node::THROW_ERR_INVALID_PACKAGE_CONFIG(env, msg.c_str()); + return Nothing(); + } + return Just(pcfg); } - Maybe check = CheckFile(path, LEAVE_OPEN_AFTER_CHECK); - if (check.IsNothing()) { + + Maybe source = ReadIfFile(path); + + if (source.IsNothing()) { auto entry = env->package_json_cache.emplace(path, - PackageConfig { Exists::No, IsValid::Yes, HasMain::No, "" }); - return entry.first->second; + PackageConfig { Exists::No, IsValid::Yes, HasMain::No, "", + PackageType::None }); + return Just(&entry.first->second); } + std::string pkg_src = source.FromJust(); + 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 src; + Local pkg_json_v; + Local context = env->context(); + + if (!ToV8Value(context, pkg_src).ToLocal(&src) || + !v8::JSON::Parse(context, src.As()).ToLocal(&pkg_json_v) || + !pkg_json_v->ToObject(context).ToLocal(&pkg_json)) { + env->package_json_cache.emplace(path, + PackageConfig { Exists::Yes, IsValid::No, HasMain::No, "", + PackageType::None }); + std::string msg = "Invalid JSON in '" + path + + "' imported from " + base.ToFilePath(); + node::THROW_ERR_INVALID_PACKAGE_CONFIG(env, msg.c_str()); + return Nothing(); + } } 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; + if (pkg_main->IsString()) { + has_main = HasMain::Yes; + } Utf8Value main_utf8(isolate, pkg_main); main_std.assign(std::string(*main_utf8, main_utf8.length())); } + PackageType pkg_type = PackageType::None; + Local type_v; + if (pkg_json->Get(env->context(), env->type_string()).ToLocal(&type_v)) { + if (type_v->StrictEquals(env->module_string())) { + pkg_type = PackageType::Module; + } else if (type_v->StrictEquals(env->commonjs_string())) { + pkg_type = PackageType::CommonJS; + } + // ignore unknown types for forwards compatibility + } + + Local exports_v; + if (pkg_json->Get(env->context(), + env->exports_string()).ToLocal(&exports_v) && + (exports_v->IsObject() || exports_v->IsString() || + exports_v->IsBoolean())) { + Persistent exports; + exports.Reset(env->isolate(), exports_v); + + auto entry = env->package_json_cache.emplace(path, + PackageConfig { Exists::Yes, IsValid::Yes, has_main, main_std, + pkg_type }); + return Just(&entry.first->second); + } + auto entry = env->package_json_cache.emplace(path, - PackageConfig { Exists::Yes, IsValid::Yes, has_main, main_std }); - return entry.first->second; + PackageConfig { Exists::Yes, IsValid::Yes, has_main, main_std, + pkg_type }); + return Just(&entry.first->second); +} + +Maybe GetPackageScopeConfig(Environment* env, + const URL& resolved, + const URL& base) { + URL pjson_url("./package.json", &resolved); + while (true) { + Maybe pkg_cfg = + GetPackageConfig(env, pjson_url.ToFilePath(), base); + if (pkg_cfg.IsNothing()) return pkg_cfg; + if (pkg_cfg.FromJust()->exists == Exists::Yes) return pkg_cfg; + + URL last_pjson_url = pjson_url; + pjson_url = URL("../package.json", pjson_url); + + // Terminates at root where ../package.json equals ../../package.json + // (can't just check "/package.json" for Windows support). + if (pjson_url.path() == last_pjson_url.path()) { + auto entry = env->package_json_cache.emplace(pjson_url.ToFilePath(), + PackageConfig { Exists::No, IsValid::Yes, HasMain::No, "", + PackageType::None }); + const PackageConfig* pcfg = &entry.first->second; + return Just(pcfg); + } + } +} + +/* + * Legacy CommonJS main resolution: + * 1. let M = pkg_url + (json main field) + * 2. TRY(M, M.js, M.json, M.node) + * 3. TRY(M/index.js, M/index.json, M/index.node) + * 4. TRY(pkg_url/index.js, pkg_url/index.json, pkg_url/index.node) + * 5. NOT_FOUND + */ +inline bool FileExists(const URL& url) { + return CheckDescriptorAtPath(url.ToFilePath()) == FILE; +} +Maybe LegacyMainResolve(const URL& pjson_url, + const PackageConfig& pcfg) { + URL guess; + if (pcfg.has_main == HasMain::Yes) { + // Note: fs check redundances will be handled by Descriptor cache here. + if (FileExists(guess = URL("./" + pcfg.main, pjson_url))) { + return Just(guess); + } + if (FileExists(guess = URL("./" + pcfg.main + ".js", pjson_url))) { + return Just(guess); + } + if (FileExists(guess = URL("./" + pcfg.main + ".json", pjson_url))) { + return Just(guess); + } + if (FileExists(guess = URL("./" + pcfg.main + ".node", pjson_url))) { + return Just(guess); + } + if (FileExists(guess = URL("./" + pcfg.main + "/index.js", pjson_url))) { + return Just(guess); + } + // Such stat. + if (FileExists(guess = URL("./" + pcfg.main + "/index.json", pjson_url))) { + return Just(guess); + } + if (FileExists(guess = URL("./" + pcfg.main + "/index.node", pjson_url))) { + return Just(guess); + } + // Fallthrough. + } + if (FileExists(guess = URL("./index.js", pjson_url))) { + return Just(guess); + } + // So fs. + if (FileExists(guess = URL("./index.json", pjson_url))) { + return Just(guess); + } + if (FileExists(guess = URL("./index.node", pjson_url))) { + return Just(guess); + } + // Not found. + return Nothing(); } enum ResolveExtensionsOptions { @@ -575,17 +713,14 @@ enum ResolveExtensionsOptions { template Maybe ResolveExtensions(const URL& search) { if (options == TRY_EXACT_NAME) { - std::string filePath = search.ToFilePath(); - Maybe check = CheckFile(filePath); - if (!check.IsNothing()) { + if (FileExists(search)) { return Just(search); } } for (const char* extension : EXTENSIONS) { URL guess(search.path() + extension, &search); - Maybe check = CheckFile(guess.ToFilePath()); - if (!check.IsNothing()) { + if (FileExists(guess)) { return Just(guess); } } @@ -597,93 +732,150 @@ 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) { +Maybe FinalizeResolution(Environment* env, + 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 (!ShouldBeTreatedAsRelativeOrAbsolutePath(pjson.main)) { - return Resolve(env, "./" + pjson.main, search, IgnoreMain); + + 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()); + return Nothing(); } - return Resolve(env, pjson.main, search, IgnoreMain); + + return Just(resolved); } -Maybe ResolveModule(Environment* env, - const std::string& specifier, - const URL& base) { - URL parent(".", base); - URL dir(""); - 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(); +Maybe PackageMainResolve(Environment* env, + 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.type != PackageType::Module) { + Maybe resolved = LegacyMainResolve(pjson_url, pcfg); + if (!resolved.IsNothing()) { + return resolved; } - return check; - } else { - // TODO(bmeck) PREVENT FALLTHROUGH } - parent = URL("..", &dir); - } while (parent.path() != dir.path()); + } + 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 ResolveDirectory(Environment* env, - const URL& search, - PackageMainCheck check_pjson_main) { - if (check_pjson_main) { - Maybe main = ResolveMain(env, search); - if (!main.IsNothing()) - return main; +Maybe PackageResolve(Environment* env, + const std::string& specifier, + const URL& base) { + size_t sep_index = specifier.find('/'); + if (specifier[0] == '@' && (sep_index == std::string::npos || + specifier.length() == 0)) { + std::string msg = "Invalid package name '" + specifier + + "' imported from " + base.ToFilePath(); + node::THROW_ERR_INVALID_MODULE_SPECIFIER(env, msg.c_str()); + return Nothing(); + } + bool scope = false; + if (specifier[0] == '@') { + scope = true; + sep_index = specifier.find('/', sep_index + 1); } - return ResolveIndex(search); + std::string pkg_name = specifier.substr(0, + sep_index == std::string::npos ? std::string::npos : sep_index); + std::string pkg_subpath; + if ((sep_index == std::string::npos || + sep_index == specifier.length() - 1)) { + pkg_subpath = ""; + } else { + pkg_subpath = "." + specifier.substr(sep_index); + } + URL pjson_url("./node_modules/" + pkg_name + "/package.json", &base); + std::string pjson_path = pjson_url.ToFilePath(); + std::string last_path; + do { + DescriptorType check = + CheckDescriptorAtPath(pjson_path.substr(0, pjson_path.length() - 13)); + if (check != DIRECTORY) { + last_path = pjson_path; + pjson_url = URL((scope ? + "../../../../node_modules/" : "../../../node_modules/") + + pkg_name + "/package.json", &pjson_url); + pjson_path = pjson_url.ToFilePath(); + continue; + } + + // Package match. + Maybe pcfg = GetPackageConfig(env, pjson_path, base); + // Invalid package configuration error. + if (pcfg.IsNothing()) return Nothing(); + if (!pkg_subpath.length()) { + return PackageMainResolve(env, pjson_url, *pcfg.FromJust(), base); + } else { + return FinalizeResolution(env, URL(pkg_subpath, pjson_url), base); + } + CHECK(false); + // Cross-platform root check. + } while (pjson_path.length() != last_path.length()); + + std::string msg = "Cannot find package '" + pkg_name + + "' imported from " + base.ToFilePath(); + node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str()); + return Nothing(); } } // 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(); - } - return Just(pure_url); - } - if (specifier.length() == 0) { - 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)) { - 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); + resolved = URL(specifier, base); } else { - return ResolveModule(env, specifier, base); + URL pure_url(specifier); + if (!(pure_url.flags() & URL_FLAGS_FAILED)) { + resolved = pure_url; + } else { + return PackageResolve(env, specifier, base); + } } + return FinalizeResolution(env, resolved, base); } void ModuleWrap::Resolve(const FunctionCallbackInfo& args) { @@ -705,15 +897,40 @@ void ModuleWrap::Resolve(const FunctionCallbackInfo& args) { env, "second argument is not a URL string"); } - 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()); + Maybe result = + node::loader::Resolve(env, + specifier_std, + url); + if (result.IsNothing()) { + return; + } + + URL resolution = result.FromJust(); + CHECK(!(resolution.flags() & URL_FLAGS_FAILED)); + + Local resolution_obj; + if (resolution.ToObject(env).ToLocal(&resolution_obj)) + args.GetReturnValue().Set(resolution_obj); +} + +void ModuleWrap::GetPackageType(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + // module.getPackageType(url) + CHECK_EQ(args.Length(), 1); + + CHECK(args[0]->IsString()); + Utf8Value url_utf8(env->isolate(), args[0]); + URL url(*url_utf8, url_utf8.length()); + + PackageType pkg_type = PackageType::None; + Maybe pcfg = + GetPackageScopeConfig(env, url, url); + if (!pcfg.IsNothing()) { + pkg_type = pcfg.FromJust()->type; } - MaybeLocal obj = result.FromJust().ToObject(env); - if (!obj.IsEmpty()) - args.GetReturnValue().Set(obj.ToLocalChecked()); + args.GetReturnValue().Set(Integer::New(env->isolate(), pkg_type)); } static MaybeLocal ImportModuleDynamically( @@ -849,6 +1066,7 @@ void ModuleWrap::Initialize(Local target, target->Set(env->context(), FIXED_ONE_BYTE_STRING(isolate, "ModuleWrap"), tpl->GetFunction(context).ToLocalChecked()).FromJust(); env->SetMethod(target, "resolve", Resolve); + env->SetMethod(target, "getPackageType", GetPackageType); env->SetMethod(target, "setImportModuleDynamicallyCallback", SetImportModuleDynamicallyCallback); diff --git a/src/module_wrap.h b/src/module_wrap.h index dc34685fedadbc..6b7025f8a9452d 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, @@ -29,11 +24,6 @@ enum HostDefinedOptions : int { kLength = 10, }; -v8::Maybe Resolve(Environment* env, - const std::string& specifier, - const url::URL& base, - PackageMainCheck read_pkg_json = CheckMain); - class ModuleWrap : public BaseObject { public: static const std::string EXTENSIONS[]; @@ -75,6 +65,7 @@ class ModuleWrap : public BaseObject { const v8::FunctionCallbackInfo& args); static void Resolve(const v8::FunctionCallbackInfo& args); + static void GetPackageType(const v8::FunctionCallbackInfo& args); static void SetImportModuleDynamicallyCallback( const v8::FunctionCallbackInfo& args); static void SetInitializeImportMetaObjectCallback( diff --git a/src/node_errors.h b/src/node_errors.h index 835794b17820cc..9d3f2ead71279d 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -45,12 +45,14 @@ void FatalException(v8::Isolate* isolate, V(ERR_CONSTRUCT_CALL_REQUIRED, Error) \ V(ERR_INVALID_ARG_VALUE, TypeError) \ V(ERR_INVALID_ARG_TYPE, TypeError) \ + V(ERR_INVALID_MODULE_SPECIFIER, TypeError) \ + V(ERR_INVALID_PACKAGE_CONFIG, SyntaxError) \ V(ERR_INVALID_TRANSFER_OBJECT, TypeError) \ 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 5687fb327b5663..bd20b6385d0c05 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -107,6 +107,32 @@ void EnvironmentOptions::CheckOptions(std::vector* errors) { errors->push_back("--loader requires --experimental-modules be enabled"); } + if (!module_type.empty()) { + if (!experimental_modules) { + errors->push_back("--entry-type requires " + "--experimental-modules to be enabled"); + } + if (module_type != "commonjs" && module_type != "module") { + errors->push_back("--entry-type must \"module\" or \"commonjs\""); + } + } + + if (experimental_json_modules && !experimental_modules) { + errors->push_back("--experimental-json-modules requires " + "--experimental-modules be enabled"); + } + + if (!es_module_specifier_resolution.empty()) { + if (!experimental_modules) { + errors->push_back("--es-module-specifier-resolution requires " + "--experimental-modules be enabled"); + } + if (es_module_specifier_resolution != "node" && + es_module_specifier_resolution != "explicit") { + errors->push_back("invalid value for --es-module-specifier-resolution"); + } + } + if (syntax_check_only && has_eval_string) { errors->push_back("either --check or --eval can be used, not both"); } @@ -214,6 +240,10 @@ DebugOptionsParser::DebugOptionsParser() { } EnvironmentOptionsParser::EnvironmentOptionsParser() { + AddOption("--experimental-json-modules", + "experimental JSON interop support for the ES Module loader", + &EnvironmentOptions::experimental_json_modules, + kAllowedInEnvironment); AddOption("--experimental-modules", "experimental ES Module support and caching modules", &EnvironmentOptions::experimental_modules, @@ -253,6 +283,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, @@ -271,10 +306,12 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { kAllowedInEnvironment); AddOption("--preserve-symlinks", "preserve symbolic links when resolving", - &EnvironmentOptions::preserve_symlinks); + &EnvironmentOptions::preserve_symlinks, + kAllowedInEnvironment); AddOption("--preserve-symlinks-main", "preserve symbolic links when resolving the main module", - &EnvironmentOptions::preserve_symlinks_main); + &EnvironmentOptions::preserve_symlinks_main, + kAllowedInEnvironment); AddOption("--prof-process", "process V8 profiler output generated using --prof", &EnvironmentOptions::prof_process); @@ -301,6 +338,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "show stack traces on process warnings", &EnvironmentOptions::trace_warnings, kAllowedInEnvironment); + AddOption("--entry-type", + "set module type name of the entry point", + &EnvironmentOptions::module_type, + kAllowedInEnvironment); AddOption("--check", "syntax check script without executing", diff --git a/src/node_options.h b/src/node_options.h index bcd6d2457dc554..e07ee7fb353065 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -91,7 +91,10 @@ class DebugOptions : public Options { class EnvironmentOptions : public Options { public: bool abort_on_uncaught_exception = false; + bool experimental_json_modules = false; bool experimental_modules = false; + std::string es_module_specifier_resolution; + std::string module_type; std::string experimental_policy; bool experimental_repl_await = false; bool experimental_vm_modules = false; diff --git a/test/addons/hello-world-esm/binding.cc b/test/addons/hello-world-esm/binding.cc deleted file mode 100644 index 02eecec099807a..00000000000000 --- 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 55fbe7050f18e4..00000000000000 --- 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 d0faf655400f13..00000000000000 --- 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 d98de5bf87c771..00000000000000 --- 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 de9119f37e2f3d..41592098eb50d5 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 78a4106f94497c..d9bb22be0a5f12 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 c8dfff919c2f7e..a207efc73ecb0a 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 c81d0530d3327b..9366d4bd6bcc68 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 b271d43c805d0e..ca9c99007b5b1b 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'); @@ -17,7 +18,7 @@ function expectErrorProperty(result, propertyKey, value) { } function expectMissingModuleError(result) { - expectErrorProperty(result, 'code', 'MODULE_NOT_FOUND'); + expectErrorProperty(result, 'code', 'ERR_MODULE_NOT_FOUND'); } function expectOkNamespace(result) { diff --git a/test/es-module/test-esm-encoded-path.mjs b/test/es-module/test-esm-encoded-path.mjs index 365a425afa5000..2cabfdacff3954 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 98244615eff2c6..79f76357eca3ea 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-forbidden-globals.mjs b/test/es-module/test-esm-forbidden-globals.mjs index 4e777412a346fd..cf110ff2900eff 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 c17e0e20d49b4d..4c34b337fb8914 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-cache.mjs b/test/es-module/test-esm-json-cache.mjs new file mode 100644 index 00000000000000..ecd27c5488c7dc --- /dev/null +++ b/test/es-module/test-esm-json-cache.mjs @@ -0,0 +1,26 @@ +// Flags: --experimental-modules --experimental-json-modules +/* eslint-disable node-core/required-modules */ +import '../common/index.mjs'; + +import { strictEqual, deepStrictEqual } from 'assert'; + +import { createRequireFromPath as createRequire } from 'module'; +import { fileURLToPath as fromURL } from 'url'; + +import mod from '../fixtures/es-modules/json-cache/mod.cjs'; +import another from '../fixtures/es-modules/json-cache/another.cjs'; +import test from '../fixtures/es-modules/json-cache/test.json'; + +const require = createRequire(fromURL(import.meta.url)); + +const modCjs = require('../fixtures/es-modules/json-cache/mod.cjs'); +const anotherCjs = require('../fixtures/es-modules/json-cache/another.cjs'); +const testCjs = require('../fixtures/es-modules/json-cache/test.json'); + +strictEqual(mod.one, 1); +strictEqual(another.one, 'zalgo'); +strictEqual(test.one, 'it comes'); + +deepStrictEqual(mod, modCjs); +deepStrictEqual(another, anotherCjs); +deepStrictEqual(test, testCjs); diff --git a/test/es-module/test-esm-json.mjs b/test/es-module/test-esm-json.mjs index a7146d19a9ad8e..b140d031cae8be 100644 --- a/test/es-module/test-esm-json.mjs +++ b/test/es-module/test-esm-json.mjs @@ -1,8 +1,9 @@ -// 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'; +// Flags: --experimental-modules --experimental-json-modules +/* eslint-disable node-core/required-modules */ -assert(ok); -assert.strictEqual(json.val, 42); +import '../common/index.mjs'; +import { strictEqual } from 'assert'; + +import secret from '../fixtures/experimental.json'; + +strictEqual(secret.ofLife, 42); diff --git a/test/es-module/test-esm-live-binding.mjs b/test/es-module/test-esm-live-binding.mjs index d151e004dff4b2..880a6c389b422c 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-invalid-format.mjs b/test/es-module/test-esm-loader-invalid-format.mjs index f8714d4aa11d59..c3f3a874079fa7 100644 --- a/test/es-module/test-esm-loader-invalid-format.mjs +++ b/test/es-module/test-esm-loader-invalid-format.mjs @@ -1,5 +1,6 @@ // Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-invalid-format.mjs -import { expectsError, mustCall } from '../common'; +/* 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') diff --git a/test/es-module/test-esm-loader-invalid-url.mjs b/test/es-module/test-esm-loader-invalid-url.mjs index 43971a2e6e3b71..9cf17b2478ef13 100644 --- a/test/es-module/test-esm-loader-invalid-url.mjs +++ b/test/es-module/test-esm-loader-invalid-url.mjs @@ -1,5 +1,7 @@ // Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-invalid-url.mjs -import { expectsError, mustCall } from '../common'; +/* 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') 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 index f2b37f7e8a4db6..ab2da7adcedb52 100644 --- a/test/es-module/test-esm-loader-missing-dynamic-instantiate-hook.mjs +++ b/test/es-module/test-esm-loader-missing-dynamic-instantiate-hook.mjs @@ -1,6 +1,7 @@ // Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/missing-dynamic-instantiate-hook.mjs +/* eslint-disable node-core/required-modules */ -import { expectsError } from '../common'; +import { expectsError } from '../common/index.mjs'; import('test').catch(expectsError({ code: 'ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK', diff --git a/test/es-module/test-esm-loader-modulemap.js b/test/es-module/test-esm-loader-modulemap.js index 946d54ffaaa5aa..5493c6c47c9643 100644 --- a/test/es-module/test-esm-loader-modulemap.js +++ b/test/es-module/test-esm-loader-modulemap.js @@ -7,7 +7,7 @@ const common = require('../common'); const { URL } = require('url'); -const Loader = require('internal/modules/esm/loader'); +const { Loader } = require('internal/modules/esm/loader'); const ModuleMap = require('internal/modules/esm/module_map'); const ModuleJob = require('internal/modules/esm/module_job'); const createDynamicModule = require( diff --git a/test/es-module/test-esm-loader-search.js b/test/es-module/test-esm-loader-search.js index 0ca8990cb723ad..b5e0d8d656f65f 100644 --- a/test/es-module/test-esm-loader-search.js +++ b/test/es-module/test-esm-loader-search.js @@ -5,13 +5,13 @@ const common = require('../common'); -const { search } = require('internal/modules/esm/default_resolve'); +const resolve = require('internal/modules/esm/default_resolve'); common.expectsError( - () => search('target', undefined), + () => resolve('target', undefined), { - code: 'ERR_MISSING_MODULE', + code: 'ERR_MODULE_NOT_FOUND', type: Error, - message: 'Cannot find module target' + message: /Cannot find package 'target'/ } ); diff --git a/test/es-module/test-esm-main-lookup.mjs b/test/es-module/test-esm-main-lookup.mjs index ca313a1d264485..19c025beab9ea9 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, 'ERR_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 index 3aae9230de36a7..e235f598cb1d34 100644 --- a/test/es-module/test-esm-named-exports.mjs +++ b/test/es-module/test-esm-named-exports.mjs @@ -1,5 +1,6 @@ // Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs -import '../common'; +/* 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'; diff --git a/test/es-module/test-esm-namespace.mjs b/test/es-module/test-esm-namespace.mjs index da1286d0f40e67..38b7ef12d585fc 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-no-extension.js b/test/es-module/test-esm-no-extension.js new file mode 100644 index 00000000000000..3e9ffb2bbca8df --- /dev/null +++ b/test/es-module/test-esm-no-extension.js @@ -0,0 +1,35 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const { spawn } = require('child_process'); +const assert = require('assert'); + +const entry = fixtures.path('/es-modules/noext-esm'); + +// Run a module that does not have extension +// This is to ensure the --entry-type works as expected + +const child = spawn(process.execPath, [ + '--experimental-modules', + '--entry-type=module', + entry +]); + +let stderr = ''; +child.stderr.setEncoding('utf8'); +child.stderr.on('data', (data) => { + stderr += data; +}); +let stdout = ''; +child.stdout.setEncoding('utf8'); +child.stdout.on('data', (data) => { + stdout += data; +}); +child.on('close', common.mustCall((code, signal) => { + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + assert.strictEqual(stdout, 'executed\n'); + assert.strictEqual(stderr, `(node:${child.pid}) ` + + 'ExperimentalWarning: The ESM module loader is experimental.\n'); +})); diff --git a/test/es-module/test-esm-preserve-symlinks-not-found.mjs b/test/es-module/test-esm-preserve-symlinks-not-found.mjs index 5119957bae7c6a..b5be2d7e63b54f 100644 --- a/test/es-module/test-esm-preserve-symlinks-not-found.mjs +++ b/test/es-module/test-esm-preserve-symlinks-not-found.mjs @@ -1,3 +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'; +import './not-found.mjs'; diff --git a/test/es-module/test-esm-process.mjs b/test/es-module/test-esm-process.mjs index ea9b4b4936a313..3a23573d33b8b1 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 ff32cde36ff2a8..09030e0578e8c5 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-shared-loader-dep.mjs b/test/es-module/test-esm-shared-loader-dep.mjs index 5c274d835c8a31..b8953ab1ec1a72 100644 --- a/test/es-module/test-esm-shared-loader-dep.mjs +++ b/test/es-module/test-esm-shared-loader-dep.mjs @@ -1,7 +1,11 @@ // Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-shared-dep.mjs -import '../common'; +/* eslint-disable node-core/required-modules */ +import { createRequire } from '../common/index.mjs'; + 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'); +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-shebang.mjs b/test/es-module/test-esm-shebang.mjs index d5faace4792f45..486e04dadece61 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 index 3d4b44bbdd5bcf..3997e24ed703c6 100644 --- a/test/es-module/test-esm-snapshot.mjs +++ b/test/es-module/test-esm-snapshot.mjs @@ -1,7 +1,8 @@ // Flags: --experimental-modules -import '../common'; -import '../fixtures/es-modules/esm-snapshot-mutator'; -import one from '../fixtures/es-modules/esm-snapshot'; +/* eslint-disable node-core/required-modules */ +import '../common/index.mjs'; +import '../fixtures/es-modules/esm-snapshot-mutator.js'; +import one from '../fixtures/es-modules/esm-snapshot.js'; import assert from 'assert'; assert.strictEqual(one, 1); diff --git a/test/es-module/test-esm-specifiers.mjs b/test/es-module/test-esm-specifiers.mjs new file mode 100644 index 00000000000000..0c5e1ac04ad1a8 --- /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/es-module/test-esm-symlink-main.js b/test/es-module/test-esm-symlink-main.js index f7631ef2e5bdca..871180f5ccf4bb 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-type.js b/test/es-module/test-esm-symlink-type.js new file mode 100644 index 00000000000000..6159ebecd18be4 --- /dev/null +++ b/test/es-module/test-esm-symlink-type.js @@ -0,0 +1,77 @@ +'use strict'; +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const path = require('path'); +const assert = require('assert'); +const exec = require('child_process').execFile; +const fs = require('fs'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); +const tmpDir = tmpdir.path; + +// Check that running the symlink executes the target as the correct type +const symlinks = [ + { + source: 'extensionless-symlink-to-mjs-file', + target: fixtures.path('es-modules/mjs-file.mjs'), + prints: '.mjs file', + errorsWithPreserveSymlinksMain: false + }, { + source: 'extensionless-symlink-to-cjs-file', + target: fixtures.path('es-modules/cjs-file.cjs'), + prints: '.cjs file', + errorsWithPreserveSymlinksMain: false + }, { + source: 'extensionless-symlink-to-file-in-module-scope', + target: fixtures.path('es-modules/package-type-module/index.js'), + prints: 'package-type-module', + // The package scope of the symlinks' sources is commonjs, and this + // symlink's target is a .js file in a module scope, so when the scope + // is evaluated based on the source (commonjs) this esm file should error + errorsWithPreserveSymlinksMain: true + }, { + source: 'extensionless-symlink-to-file-in-explicit-commonjs-scope', + target: fixtures.path('es-modules/package-type-commonjs/index.js'), + prints: 'package-type-commonjs', + errorsWithPreserveSymlinksMain: false + }, { + source: 'extensionless-symlink-to-file-in-implicit-commonjs-scope', + target: fixtures.path('es-modules/package-without-type/index.js'), + prints: 'package-without-type', + errorsWithPreserveSymlinksMain: false + } +]; + +symlinks.forEach((symlink) => { + const mainPath = path.join(tmpDir, symlink.source); + fs.symlinkSync(symlink.target, mainPath); + + const flags = [ + '--experimental-modules', + '--experimental-modules --preserve-symlinks-main' + ]; + flags.forEach((nodeOptions) => { + const opts = { + env: Object.assign({}, process.env, { NODE_OPTIONS: nodeOptions }) + }; + exec(process.execPath, [mainPath], opts, common.mustCall( + (err, stdout) => { + if (nodeOptions.includes('--preserve-symlinks-main')) { + if (symlink.errorsWithPreserveSymlinksMain && + err.toString().includes('Error')) return; + else if (!symlink.errorsWithPreserveSymlinksMain && + stdout.includes(symlink.prints)) return; + assert.fail(`For ${JSON.stringify(symlink)}, ${ + (symlink.errorsWithPreserveSymlinksMain) ? + 'failed to error' : 'errored unexpectedly' + } with --preserve-symlinks-main`); + } else { + if (stdout.includes(symlink.prints)) return; + assert.fail(`For ${JSON.stringify(symlink)}, failed to find ` + + `${symlink.prints} in: <\n${stdout}\n>`); + } + } + )); + }); +}); diff --git a/test/es-module/test-esm-symlink.js b/test/es-module/test-esm-symlink.js index 232925a52e3f64..9b9eb98cd98406 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 index 541127eee5919f..97e917da5e8d44 100644 --- a/test/es-module/test-esm-throw-undefined.mjs +++ b/test/es-module/test-esm-throw-undefined.mjs @@ -1,11 +1,13 @@ // Flags: --experimental-modules -import '../common'; +/* 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'); + await import('../fixtures/es-module-loaders/throw-undefined.mjs'); }, (e) => e === undefined ); diff --git a/test/es-module/test-esm-type-flag-errors.js b/test/es-module/test-esm-type-flag-errors.js new file mode 100644 index 00000000000000..612ada584d3726 --- /dev/null +++ b/test/es-module/test-esm-type-flag-errors.js @@ -0,0 +1,53 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const exec = require('child_process').execFile; + +const mjsFile = require.resolve('../fixtures/es-modules/mjs-file.mjs'); +const cjsFile = require.resolve('../fixtures/es-modules/cjs-file.cjs'); +const packageWithoutTypeMain = + require.resolve('../fixtures/es-modules/package-without-type/index.js'); +const packageTypeCommonJsMain = + require.resolve('../fixtures/es-modules/package-type-commonjs/index.js'); +const packageTypeModuleMain = + require.resolve('../fixtures/es-modules/package-type-module/index.js'); + +// Check that running `node` without options works +expect('', mjsFile, '.mjs file'); +expect('', cjsFile, '.cjs file'); +expect('', packageTypeModuleMain, 'package-type-module'); +expect('', packageTypeCommonJsMain, 'package-type-commonjs'); +expect('', packageWithoutTypeMain, 'package-without-type'); + +// Check that running with --entry-type and no package.json "type" works +expect('--entry-type=commonjs', packageWithoutTypeMain, 'package-without-type'); +expect('--entry-type=module', packageWithoutTypeMain, 'package-without-type'); + +// Check that running with conflicting --entry-type flags throws errors +expect('--entry-type=commonjs', mjsFile, 'ERR_ENTRY_TYPE_MISMATCH', true); +expect('--entry-type=module', cjsFile, 'ERR_ENTRY_TYPE_MISMATCH', true); +expect('--entry-type=commonjs', packageTypeModuleMain, + 'ERR_ENTRY_TYPE_MISMATCH', true); +expect('--entry-type=module', packageTypeCommonJsMain, + 'ERR_ENTRY_TYPE_MISMATCH', true); + +function expect(opt = '', inputFile, want, wantsError = false) { + // TODO: Remove when --experimental-modules is unflagged + opt = `--experimental-modules ${opt}`; + const argv = [inputFile]; + const opts = { + env: Object.assign({}, process.env, { NODE_OPTIONS: opt }), + maxBuffer: 1e6, + }; + exec(process.execPath, argv, opts, common.mustCall((err, stdout, stderr) => { + if (wantsError) { + stdout = stderr; + } else { + assert.ifError(err); + } + if (stdout.includes(want)) return; + + const o = JSON.stringify(opt); + assert.fail(`For ${o}, failed to find ${want} in: <\n${stdout}\n>`); + })); +} diff --git a/test/es-module/test-esm-type-flag.mjs b/test/es-module/test-esm-type-flag.mjs new file mode 100644 index 00000000000000..2f5d0b626ad61b --- /dev/null +++ b/test/es-module/test-esm-type-flag.mjs @@ -0,0 +1,11 @@ +// Flags: --experimental-modules --entry-type=module +/* eslint-disable node-core/required-modules */ +import cjs from '../fixtures/baz.js'; +import '../common/index.mjs'; +import { message } from '../fixtures/es-modules/message.mjs'; +import assert from 'assert'; + +// Assert we loaded esm dependency as ".js" in this mode +assert.strictEqual(message, 'A message'); +// Assert we loaded CommonJS dependency +assert.strictEqual(cjs, 'perhaps I work'); diff --git a/test/fixtures/es-module-loaders/example-loader.mjs b/test/fixtures/es-module-loaders/example-loader.mjs index a7cf276d4ad4c4..d8e0ddcba30699 100644 --- a/test/fixtures/es-module-loaders/example-loader.mjs +++ b/test/fixtures/es-module-loaders/example-loader.mjs @@ -29,6 +29,6 @@ export function resolve(specifier, parentModuleURL = baseURL /*, defaultResolve } return { url: resolved.href, - format: 'esm' + format: 'module' }; } diff --git a/test/fixtures/es-module-loaders/js-loader.mjs b/test/fixtures/es-module-loaders/js-loader.mjs index 2ac959a4643032..4b8a0fc365f3ac 100644 --- a/test/fixtures/es-module-loaders/js-loader.mjs +++ b/test/fixtures/es-module-loaders/js-loader.mjs @@ -15,6 +15,6 @@ export function resolve (specifier, base = baseURL) { const url = new URL(specifier, base).href; return { url, - format: 'esm' + format: 'module' }; } diff --git a/test/fixtures/es-module-loaders/loader-dep.js b/test/fixtures/es-module-loaders/loader-dep.js index cf821afec16e2c..c8154ac5dbe622 100644 --- a/test/fixtures/es-module-loaders/loader-dep.js +++ b/test/fixtures/es-module-loaders/loader-dep.js @@ -1 +1 @@ -exports.format = 'esm'; +exports.format = 'module'; diff --git a/test/fixtures/es-module-loaders/loader-invalid-url.mjs b/test/fixtures/es-module-loaders/loader-invalid-url.mjs index 12efbb5021a4bc..f653155899d6fc 100644 --- a/test/fixtures/es-module-loaders/loader-invalid-url.mjs +++ b/test/fixtures/es-module-loaders/loader-invalid-url.mjs @@ -1,3 +1,4 @@ +/* eslint-disable node-core/required-modules */ export async function resolve(specifier, parentModuleURL, defaultResolve) { if (parentModuleURL && specifier === '../fixtures/es-modules/test-esm-ok.mjs') { return { diff --git a/test/fixtures/es-module-loaders/loader-shared-dep.mjs b/test/fixtures/es-module-loaders/loader-shared-dep.mjs index 1a19e4c8927527..3acafcce1ecf7a 100644 --- a/test/fixtures/es-module-loaders/loader-shared-dep.mjs +++ b/test/fixtures/es-module-loaders/loader-shared-dep.mjs @@ -1,7 +1,11 @@ -import dep from './loader-dep.js'; 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, 'esm'); + assert.strictEqual(dep.format, 'module'); return defaultResolve(specifier, base); } diff --git a/test/fixtures/es-module-loaders/loader-with-dep.mjs b/test/fixtures/es-module-loaders/loader-with-dep.mjs index 944e6e438c5cbd..5afd3b2e212322 100644 --- a/test/fixtures/es-module-loaders/loader-with-dep.mjs +++ b/test/fixtures/es-module-loaders/loader-with-dep.mjs @@ -1,4 +1,8 @@ -import dep from './loader-dep.js'; +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, diff --git a/test/fixtures/es-module-loaders/not-found-assert-loader.mjs b/test/fixtures/es-module-loaders/not-found-assert-loader.mjs index d15f294fe674bb..d3eebcd47ec906 100644 --- a/test/fixtures/es-module-loaders/not-found-assert-loader.mjs +++ b/test/fixtures/es-module-loaders/not-found-assert-loader.mjs @@ -12,11 +12,11 @@ export async function resolve (specifier, base, defaultResolve) { await defaultResolve(specifier, base); } catch (e) { - assert.strictEqual(e.code, 'MODULE_NOT_FOUND'); + assert.strictEqual(e.code, 'ERR_MODULE_NOT_FOUND'); return { format: 'builtin', url: 'fs' }; } - assert.fail(`Module resolution for ${specifier} should be throw MODULE_NOT_FOUND`); + 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 index 9cad68c7ce2a30..3a6bc5effc1940 100644 --- a/test/fixtures/es-module-loaders/syntax-error-import.mjs +++ b/test/fixtures/es-module-loaders/syntax-error-import.mjs @@ -1 +1 @@ -import { foo, notfound } from './module-named-exports'; +import { foo, notfound } from './module-named-exports.mjs'; diff --git a/test/fixtures/es-module-loaders/throw-undefined.mjs b/test/fixtures/es-module-loaders/throw-undefined.mjs index f062276767408d..0349ae112d6b2d 100644 --- a/test/fixtures/es-module-loaders/throw-undefined.mjs +++ b/test/fixtures/es-module-loaders/throw-undefined.mjs @@ -1,3 +1,4 @@ 'use strict'; +/* eslint-disable node-core/required-modules */ throw undefined; diff --git a/test/fixtures/es-module-specifiers/index.mjs b/test/fixtures/es-module-specifiers/index.mjs new file mode 100644 index 00000000000000..2be7048513f287 --- /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 00000000000000..914e3a97d5dd82 --- /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 00000000000000..e9457582ac9cf0 --- /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 00000000000000..914e3a97d5dd82 --- /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 00000000000000..9093a7cc149ca8 --- /dev/null +++ b/test/fixtures/es-module-specifiers/node_modules/implicit-main-type-commonjs/package.json @@ -0,0 +1,4 @@ +{ + "main": "entry", + "type": "commonjs" +} 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 00000000000000..5d7af588fd65b7 --- /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 00000000000000..914e3a97d5dd82 --- /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 00000000000000..5710280badd3fc --- /dev/null +++ b/test/fixtures/es-module-specifiers/node_modules/implicit-main-type-module/package.json @@ -0,0 +1,4 @@ +{ + "main": "entry", + "type": "module" +} 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 00000000000000..b2825bd3c9949b --- /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 00000000000000..914e3a97d5dd82 --- /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 00000000000000..5c07cf41bb8f69 --- /dev/null +++ b/test/fixtures/es-module-specifiers/node_modules/implicit-main/package.json @@ -0,0 +1,3 @@ +{ + "main": "entry" +} diff --git a/test/fixtures/es-module-specifiers/package-type-commonjs/a.js b/test/fixtures/es-module-specifiers/package-type-commonjs/a.js new file mode 100644 index 00000000000000..2e7700bc63e659 --- /dev/null +++ b/test/fixtures/es-module-specifiers/package-type-commonjs/a.js @@ -0,0 +1 @@ +module.exports = 'a'; diff --git a/test/fixtures/es-module-specifiers/package-type-commonjs/b.mjs b/test/fixtures/es-module-specifiers/package-type-commonjs/b.mjs new file mode 100644 index 00000000000000..137b8ce6429ac5 --- /dev/null +++ b/test/fixtures/es-module-specifiers/package-type-commonjs/b.mjs @@ -0,0 +1 @@ +export const b = 'b'; diff --git a/test/fixtures/es-module-specifiers/package-type-commonjs/c.cjs b/test/fixtures/es-module-specifiers/package-type-commonjs/c.cjs new file mode 100644 index 00000000000000..2d5312952fef87 --- /dev/null +++ b/test/fixtures/es-module-specifiers/package-type-commonjs/c.cjs @@ -0,0 +1,5 @@ +module.exports = { + one: 1, + two: 2, + three: 3 +}; diff --git a/test/fixtures/es-module-specifiers/package-type-commonjs/index.mjs b/test/fixtures/es-module-specifiers/package-type-commonjs/index.mjs new file mode 100644 index 00000000000000..ef2b30b19b61c5 --- /dev/null +++ b/test/fixtures/es-module-specifiers/package-type-commonjs/index.mjs @@ -0,0 +1,21 @@ +// js file that is common.js +import a from './a.js'; +// ESM with named export +import {b} from './b.mjs'; +// import 'c.cjs'; +import cjs from './c.cjs'; +// proves cross boundary fun bits +import jsAsEsm from '../package-type-module/a.js'; + +// named export from core +import {strictEqual, deepStrictEqual} from 'assert'; + +strictEqual(a, jsAsEsm); +strictEqual(b, 'b'); +deepStrictEqual(cjs, { + one: 1, + two: 2, + three: 3 +}); + +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 00000000000000..5bbefffbabee39 --- /dev/null +++ b/test/fixtures/es-module-specifiers/package-type-commonjs/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/test/fixtures/es-module-specifiers/package-type-module/a.js b/test/fixtures/es-module-specifiers/package-type-module/a.js new file mode 100644 index 00000000000000..90bd54cd7f2e6d --- /dev/null +++ b/test/fixtures/es-module-specifiers/package-type-module/a.js @@ -0,0 +1 @@ +export default 'a' diff --git a/test/fixtures/es-module-specifiers/package-type-module/b.mjs b/test/fixtures/es-module-specifiers/package-type-module/b.mjs new file mode 100644 index 00000000000000..137b8ce6429ac5 --- /dev/null +++ b/test/fixtures/es-module-specifiers/package-type-module/b.mjs @@ -0,0 +1 @@ +export const b = 'b'; diff --git a/test/fixtures/es-module-specifiers/package-type-module/c.cjs b/test/fixtures/es-module-specifiers/package-type-module/c.cjs new file mode 100644 index 00000000000000..2d5312952fef87 --- /dev/null +++ b/test/fixtures/es-module-specifiers/package-type-module/c.cjs @@ -0,0 +1,5 @@ +module.exports = { + one: 1, + two: 2, + three: 3 +}; diff --git a/test/fixtures/es-module-specifiers/package-type-module/index.js b/test/fixtures/es-module-specifiers/package-type-module/index.js new file mode 100644 index 00000000000000..a8baacb7c998c0 --- /dev/null +++ b/test/fixtures/es-module-specifiers/package-type-module/index.js @@ -0,0 +1,21 @@ +// ESM with only default +import a from './a.js'; +// ESM with named export +import {b} from './b.mjs'; +// import 'c.cjs'; +import cjs from './c.cjs'; +// import across boundaries +import jsAsCjs from '../package-type-commonjs/a.js' + +// named export from core +import {strictEqual, deepStrictEqual} from 'assert'; + +strictEqual(a, jsAsCjs); +strictEqual(b, 'b'); +deepStrictEqual(cjs, { + one: 1, + two: 2, + three: 3 +}); + +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 00000000000000..3dbc1ca591c055 --- /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 00000000000000..9e26dfeeb6e641 --- /dev/null +++ b/test/fixtures/es-module-specifiers/package.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/fixtures/es-modules/cjs-file.cjs b/test/fixtures/es-modules/cjs-file.cjs new file mode 100644 index 00000000000000..3d0637686e9202 --- /dev/null +++ b/test/fixtures/es-modules/cjs-file.cjs @@ -0,0 +1 @@ +console.log('.cjs file'); diff --git a/test/fixtures/es-modules/json-cache/another.cjs b/test/fixtures/es-modules/json-cache/another.cjs new file mode 100644 index 00000000000000..8c8e9f1c0f41fc --- /dev/null +++ b/test/fixtures/es-modules/json-cache/another.cjs @@ -0,0 +1,7 @@ +const test = require('./test.json'); + +module.exports = { + ...test +}; + +test.one = 'it comes'; diff --git a/test/fixtures/es-modules/json-cache/mod.cjs b/test/fixtures/es-modules/json-cache/mod.cjs new file mode 100644 index 00000000000000..047cfb24a45d24 --- /dev/null +++ b/test/fixtures/es-modules/json-cache/mod.cjs @@ -0,0 +1,7 @@ +const test = require('./test.json'); + +module.exports = { + ...test +}; + +test.one = 'zalgo'; diff --git a/test/fixtures/es-modules/json-cache/test.json b/test/fixtures/es-modules/json-cache/test.json new file mode 100644 index 00000000000000..120cbb284045fd --- /dev/null +++ b/test/fixtures/es-modules/json-cache/test.json @@ -0,0 +1,5 @@ +{ + "one": 1, + "two": 2, + "three": 3 +} diff --git a/test/fixtures/es-modules/json.json b/test/fixtures/es-modules/json.json deleted file mode 100644 index 8288d42e2bc3f1..00000000000000 --- 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 1b5cab10edc7bf..3d2ddd2eb7305e 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/mjs-file.mjs b/test/fixtures/es-modules/mjs-file.mjs new file mode 100644 index 00000000000000..489d4ab570209b --- /dev/null +++ b/test/fixtures/es-modules/mjs-file.mjs @@ -0,0 +1 @@ +console.log('.mjs file'); diff --git a/test/fixtures/es-modules/noext-esm b/test/fixtures/es-modules/noext-esm new file mode 100644 index 00000000000000..251d6e538a1fcf --- /dev/null +++ b/test/fixtures/es-modules/noext-esm @@ -0,0 +1,2 @@ +export default 'module'; +console.log('executed'); diff --git a/test/fixtures/es-modules/package-type-commonjs/index.js b/test/fixtures/es-modules/package-type-commonjs/index.js new file mode 100644 index 00000000000000..009431d8518ac6 --- /dev/null +++ b/test/fixtures/es-modules/package-type-commonjs/index.js @@ -0,0 +1,3 @@ +const identifier = 'package-type-commonjs'; +console.log(identifier); +module.exports = identifier; diff --git a/test/fixtures/es-modules/package-type-commonjs/package.json b/test/fixtures/es-modules/package-type-commonjs/package.json new file mode 100644 index 00000000000000..4aaa4a2388d533 --- /dev/null +++ b/test/fixtures/es-modules/package-type-commonjs/package.json @@ -0,0 +1,4 @@ +{ + "type": "commonjs", + "main": "index.js" +} diff --git a/test/fixtures/es-modules/package-type-module/index.js b/test/fixtures/es-modules/package-type-module/index.js new file mode 100644 index 00000000000000..12aba970ef279f --- /dev/null +++ b/test/fixtures/es-modules/package-type-module/index.js @@ -0,0 +1,3 @@ +const identifier = 'package-type-module'; +console.log(identifier); +export default identifier; diff --git a/test/fixtures/es-modules/package-type-module/package.json b/test/fixtures/es-modules/package-type-module/package.json new file mode 100644 index 00000000000000..07aec65d5a4f31 --- /dev/null +++ b/test/fixtures/es-modules/package-type-module/package.json @@ -0,0 +1,4 @@ +{ + "type": "module", + "main": "index.js" +} diff --git a/test/fixtures/es-modules/package-without-type/index.js b/test/fixtures/es-modules/package-without-type/index.js new file mode 100644 index 00000000000000..a547216cb0c23c --- /dev/null +++ b/test/fixtures/es-modules/package-without-type/index.js @@ -0,0 +1,3 @@ +const identifier = 'package-without-type'; +console.log(identifier); +module.exports = identifier; diff --git a/test/fixtures/es-modules/package-without-type/package.json b/test/fixtures/es-modules/package-without-type/package.json new file mode 100644 index 00000000000000..14ab704d8f6395 --- /dev/null +++ b/test/fixtures/es-modules/package-without-type/package.json @@ -0,0 +1,3 @@ +{ + "main": "index.js" +} 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 dfdd47b877319c..00000000000000 --- 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 00000000000000..9eb0aade18297c --- /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 c13b8cf6acfd33..ea9b7846928143 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 ea1caa81bea9fa..a3bfe972e5a853 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/experimental.json b/test/fixtures/experimental.json new file mode 100644 index 00000000000000..12611d2385a5a5 --- /dev/null +++ b/test/fixtures/experimental.json @@ -0,0 +1,3 @@ +{ + "ofLife": 42 +} diff --git a/test/fixtures/syntax/bad_syntax.mjs b/test/fixtures/syntax/bad_syntax.mjs new file mode 100644 index 00000000000000..c2cd118b23b133 --- /dev/null +++ b/test/fixtures/syntax/bad_syntax.mjs @@ -0,0 +1 @@ +var foo bar; diff --git a/test/message/esm_display_syntax_error.out b/test/message/esm_display_syntax_error.out index 1c68ecb952f3e7..5e82a1e1ee8f87 100644 --- a/test/message/esm_display_syntax_error.out +++ b/test/message/esm_display_syntax_error.out @@ -2,6 +2,7 @@ file:///*/test/message/esm_display_syntax_error.mjs:3 await async () => 0; ^^^^^ + SyntaxError: Unexpected reserved word - at internal/modules/esm/translators.js:*:* + at Loader.moduleStrategy (internal/modules/esm/translators.js:*:*) at async link (internal/modules/esm/module_job.js:*:*) diff --git a/test/message/esm_display_syntax_error_import.mjs b/test/message/esm_display_syntax_error_import.mjs index 87cedf1d4edd97..12d10270e96854 100644 --- a/test/message/esm_display_syntax_error_import.mjs +++ b/test/message/esm_display_syntax_error_import.mjs @@ -1,7 +1,7 @@ // Flags: --experimental-modules -/* eslint-disable no-unused-vars */ -import '../common'; +/* eslint-disable no-unused-vars, node-core/required-modules */ +import '../common/index.mjs'; import { foo, notfound -} from '../fixtures/es-module-loaders/module-named-exports'; +} 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 index edbbde9f2d6407..a3601d6cb46fab 100644 --- a/test/message/esm_display_syntax_error_import.out +++ b/test/message/esm_display_syntax_error_import.out @@ -2,7 +2,7 @@ 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' +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:*:*) at async ModuleJob.run (internal/modules/esm/module_job.js:*:*) at async Loader.import (internal/modules/esm/loader.js:*:*) diff --git a/test/message/esm_display_syntax_error_import_module.mjs b/test/message/esm_display_syntax_error_import_module.mjs index 32c0edb3506835..a53bbbcd19243f 100644 --- a/test/message/esm_display_syntax_error_import_module.mjs +++ b/test/message/esm_display_syntax_error_import_module.mjs @@ -1,3 +1,4 @@ // Flags: --experimental-modules -import '../common'; -import '../fixtures/es-module-loaders/syntax-error-import'; +/* 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 index 0512a9ac777f0a..0daaeff5b95daf 100644 --- a/test/message/esm_display_syntax_error_import_module.out +++ b/test/message/esm_display_syntax_error_import_module.out @@ -1,8 +1,8 @@ (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'; +import { foo, notfound } from './module-named-exports.mjs'; ^^^^^^^^ -SyntaxError: The requested module './module-named-exports' does not provide an export named 'notfound' +SyntaxError: The requested module './module-named-exports.mjs' does not provide an export named 'notfound' at ModuleJob._instantiate (internal/modules/esm/module_job.js:*:*) at async ModuleJob.run (internal/modules/esm/module_job.js:*:*) at async Loader.import (internal/modules/esm/loader.js:*:*) diff --git a/test/message/esm_display_syntax_error_module.mjs b/test/message/esm_display_syntax_error_module.mjs index e74b70bec8cc28..5905d2a95478c1 100644 --- a/test/message/esm_display_syntax_error_module.mjs +++ b/test/message/esm_display_syntax_error_module.mjs @@ -1,3 +1,4 @@ // Flags: --experimental-modules -import '../common'; -import '../fixtures/es-module-loaders/syntax-error'; +/* 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 index 4e4cbf2ea33f96..a1498f72c94d57 100644 --- a/test/message/esm_display_syntax_error_module.out +++ b/test/message/esm_display_syntax_error_module.out @@ -2,5 +2,6 @@ file:///*/test/fixtures/es-module-loaders/syntax-error.mjs:2 await async () => 0; ^^^^^ + SyntaxError: Unexpected reserved word - at internal/modules/esm/translators.js:*:* + at Loader.moduleStrategy (internal/modules/esm/translators.js:*:*) \ No newline at end of file diff --git a/test/parallel/test-cli-syntax-piped-bad.js b/test/parallel/test-cli-syntax-piped-bad.js index 4fb24b24f339c7..6d6f800a40414c 100644 --- a/test/parallel/test-cli-syntax-piped-bad.js +++ b/test/parallel/test-cli-syntax-piped-bad.js @@ -8,24 +8,45 @@ const node = process.execPath; // Test both sets of arguments that check syntax const syntaxArgs = [ - ['-c'], - ['--check'] + '-c', + '--check' ]; // Match on the name of the `Error` but not the message as it is different // depending on the JavaScript engine. -const syntaxErrorRE = /^SyntaxError: \b/m; +const syntaxErrorRE = /^SyntaxError: Unexpected identifier\b/m; // Should throw if code piped from stdin with --check has bad syntax // loop each possible option, `-c` or `--check` -syntaxArgs.forEach(function(args) { +syntaxArgs.forEach(function(arg) { const stdin = 'var foo bar;'; - const c = spawnSync(node, args, { encoding: 'utf8', input: stdin }); + const c = spawnSync(node, [arg], { encoding: 'utf8', input: stdin }); // stderr should include '[stdin]' as the filename assert(c.stderr.startsWith('[stdin]'), `${c.stderr} starts with ${stdin}`); - // No stdout or stderr should be produced + // No stdout should be produced + assert.strictEqual(c.stdout, ''); + + // stderr should have a syntax error message + assert(syntaxErrorRE.test(c.stderr), `${syntaxErrorRE} === ${c.stderr}`); + + assert.strictEqual(c.status, 1); +}); + +// Check --entry-type=module +syntaxArgs.forEach(function(arg) { + const stdin = 'export var p = 5; var foo bar;'; + const c = spawnSync( + node, + ['--experimental-modules', '--entry-type=module', '--no-warnings', arg], + { encoding: 'utf8', input: stdin } + ); + + // stderr should include '[stdin]' as the filename + assert(c.stderr.startsWith('[stdin]'), `${c.stderr} starts with ${stdin}`); + + // No stdout should be produced assert.strictEqual(c.stdout, ''); // stderr should have a syntax error message diff --git a/test/parallel/test-cli-syntax-piped-good.js b/test/parallel/test-cli-syntax-piped-good.js index cdadf449d87b41..b2b02172cbc4c3 100644 --- a/test/parallel/test-cli-syntax-piped-good.js +++ b/test/parallel/test-cli-syntax-piped-good.js @@ -8,15 +8,31 @@ const node = process.execPath; // Test both sets of arguments that check syntax const syntaxArgs = [ - ['-c'], - ['--check'] + '-c', + '--check' ]; // Should not execute code piped from stdin with --check. // Loop each possible option, `-c` or `--check`. -syntaxArgs.forEach(function(args) { +syntaxArgs.forEach(function(arg) { const stdin = 'throw new Error("should not get run");'; - const c = spawnSync(node, args, { encoding: 'utf8', input: stdin }); + const c = spawnSync(node, [arg], { encoding: 'utf8', input: stdin }); + + // No stdout or stderr should be produced + assert.strictEqual(c.stdout, ''); + assert.strictEqual(c.stderr, ''); + + assert.strictEqual(c.status, 0); +}); + +// Check --entry-type=module +syntaxArgs.forEach(function(arg) { + const stdin = 'export var p = 5; throw new Error("should not get run");'; + const c = spawnSync( + node, + ['--experimental-modules', '--no-warnings', '--entry-type=module', arg], + { encoding: 'utf8', input: stdin } + ); // No stdout or stderr should be produced assert.strictEqual(c.stdout, ''); diff --git a/test/parallel/test-loaders-unknown-builtin-module.mjs b/test/parallel/test-loaders-unknown-builtin-module.mjs index db3cfa3582e9e2..5f47f191f54094 100644 --- a/test/parallel/test-loaders-unknown-builtin-module.mjs +++ b/test/parallel/test-loaders-unknown-builtin-module.mjs @@ -1,5 +1,6 @@ // Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs -import { expectsError, mustCall } from '../common'; +/* eslint-disable node-core/required-modules */ +import { expectsError, mustCall } from '../common/index.mjs'; import assert from 'assert'; const unknownBuiltinModule = 'unknown-builtin-module'; diff --git a/test/parallel/test-module-main-extension-lookup.js b/test/parallel/test-module-main-extension-lookup.js index 3d20316647c6c0..9e7eab295e8795 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')]);