From baec319c9a4af558a82cee8667de4b5b7892e59d Mon Sep 17 00:00:00 2001
From: guybedford <guybedford@gmail.com>
Date: Tue, 28 Aug 2018 17:28:46 +0200
Subject: [PATCH] esm: irp type implementation

Refs: https://github.com/GeoffreyBooth/node-import-file-specifier-resolution-proposal
Refs: https://github.com/nodejs/modules/pull/180
Refs: https://github.com/nodejs/ecmascript-modules/pull/6
Refs: https://github.com/nodejs/ecmascript-modules/pull/12
Refs: https://github.com/nodejs/ecmascript-modules/pull/28
Co-authored-by: Myles Borins <MylesBorins@google.com>
Co-authored-by: John-David Dalton <john.david.dalton@gmail.com>
---
 .eslintrc.js                                  |   2 +
 doc/api/cli.md                                |  14 +-
 doc/api/errors.md                             |  45 +-
 doc/api/esm.md                                | 237 +++++++---
 doc/node.1                                    |   3 +
 lib/internal/errors.js                        |  29 +-
 lib/internal/main/check_syntax.js             |  36 +-
 lib/internal/main/eval_stdin.js               |   6 +-
 lib/internal/main/eval_string.js              |   7 +-
 lib/internal/main/repl.js                     |   7 +
 lib/internal/modules/cjs/loader.js            |  20 +-
 lib/internal/modules/esm/default_resolve.js   | 151 +++---
 lib/internal/modules/esm/loader.js            |  29 +-
 lib/internal/modules/esm/module_job.js        |   5 +-
 lib/internal/modules/esm/translators.js       |  53 +--
 lib/internal/process/esm_loader.js            |  20 +-
 lib/internal/process/execution.js             |  19 +
 src/env.h                                     |  26 +-
 src/module_wrap.cc                            | 429 +++++++++++-------
 src/module_wrap.h                             |  10 -
 src/node_errors.h                             |   4 +-
 src/node_options.cc                           |  10 +-
 src/node_options.h                            |   1 +
 test/addons/hello-world-esm/binding.cc        |  14 -
 test/addons/hello-world-esm/binding.gyp       |   9 -
 test/addons/hello-world-esm/test.js           |  20 -
 test/addons/hello-world-esm/test.mjs          |   6 -
 test/common/index.mjs                         |  14 +-
 test/es-module/test-esm-basic-imports.mjs     |   3 +-
 .../test-esm-cyclic-dynamic-import.mjs        |   5 +-
 test/es-module/test-esm-double-encoding.mjs   |   5 +-
 test/es-module/test-esm-dynamic-import.js     |   3 +-
 test/es-module/test-esm-encoded-path.mjs      |   3 +-
 test/es-module/test-esm-error-cache.js        |   6 +-
 test/es-module/test-esm-forbidden-globals.mjs |   3 +-
 test/es-module/test-esm-import-meta.mjs       |   3 +-
 test/es-module/test-esm-json.mjs              |   8 -
 test/es-module/test-esm-live-binding.mjs      |   3 +-
 .../test-esm-loader-invalid-format.mjs        |   3 +-
 .../es-module/test-esm-loader-invalid-url.mjs |   4 +-
 ...oader-missing-dynamic-instantiate-hook.mjs |   3 +-
 test/es-module/test-esm-loader-modulemap.js   |   2 +-
 test/es-module/test-esm-loader-search.js      |   8 +-
 test/es-module/test-esm-main-lookup.mjs       |  26 +-
 test/es-module/test-esm-named-exports.mjs     |   3 +-
 test/es-module/test-esm-namespace.mjs         |   4 +-
 test/es-module/test-esm-package-scope.mjs     |  12 +
 .../test-esm-preserve-symlinks-not-found.mjs  |   2 +-
 test/es-module/test-esm-process.mjs           |   3 +-
 test/es-module/test-esm-require-cache.mjs     |  11 +-
 test/es-module/test-esm-shared-loader-dep.mjs |  10 +-
 test/es-module/test-esm-shebang.mjs           |   3 +-
 test/es-module/test-esm-snapshot.mjs          |   7 +-
 test/es-module/test-esm-symlink-main.js       |   2 +-
 test/es-module/test-esm-symlink-type.js       |  77 ++++
 test/es-module/test-esm-symlink.js            |  10 +-
 test/es-module/test-esm-throw-undefined.mjs   |   6 +-
 test/es-module/test-esm-type-flag-errors.js   |  53 +++
 test/es-module/test-esm-type-flag.mjs         |  11 +
 .../es-module-loaders/example-loader.mjs      |   2 +-
 test/fixtures/es-module-loaders/js-loader.mjs |   2 +-
 test/fixtures/es-module-loaders/loader-dep.js |   2 +-
 .../es-module-loaders/loader-invalid-url.mjs  |   1 +
 .../es-module-loaders/loader-shared-dep.mjs   |   8 +-
 .../es-module-loaders/loader-with-dep.mjs     |   6 +-
 .../not-found-assert-loader.mjs               |   4 +-
 .../es-module-loaders/syntax-error-import.mjs |   2 +-
 .../es-module-loaders/throw-undefined.mjs     |   1 +
 test/fixtures/es-modules/cjs-file.cjs         |   1 +
 test/fixtures/es-modules/json.json            |   3 -
 test/fixtures/es-modules/loop.mjs             |   2 +-
 test/fixtures/es-modules/mjs-file.mjs         |   1 +
 .../es-modules/package-type-commonjs/index.js |   3 +
 .../package-type-commonjs/package.json        |   4 +
 .../es-modules/package-type-module/index.js   |   3 +
 .../package-type-module/package.json          |   4 +
 .../es-modules/package-without-type/index.js  |   3 +
 .../package-without-type/package.json         |   3 +
 test/fixtures/es-modules/pjson-main/main.js   |   1 -
 test/fixtures/es-modules/pjson-main/main.mjs  |   1 +
 .../es-modules/pjson-main/package.json        |   2 +-
 ...=> test-esm-double-encoding-native%20.mjs} |   2 +-
 .../esm-package-scope/legacy-loader/a.js      |   1 +
 .../esm-package-scope/legacy-loader/b.mjs     |   1 +
 .../esm-package-scope/legacy-loader/c.cjs     |   5 +
 .../esm-package-scope/legacy-loader/index.mjs |  21 +
 .../legacy-loader/package.json                |  13 +
 .../esm-package-scope/new-loader/a.js         |   1 +
 .../esm-package-scope/new-loader/b.mjs        |   1 +
 .../esm-package-scope/new-loader/c.cjs        |   5 +
 .../esm-package-scope/new-loader/index.js     |  21 +
 .../esm-package-scope/new-loader/package.json |  13 +
 test/fixtures/syntax/bad_syntax.mjs           |   1 +
 test/message/esm_display_syntax_error.out     |   3 +-
 .../esm_display_syntax_error_import.mjs       |   6 +-
 .../esm_display_syntax_error_import.out       |   2 +-
 ...esm_display_syntax_error_import_module.mjs |   5 +-
 ...esm_display_syntax_error_import_module.out |   4 +-
 .../esm_display_syntax_error_module.mjs       |   5 +-
 .../esm_display_syntax_error_module.out       |   3 +-
 test/parallel/test-cli-syntax-piped-bad.js    |  33 +-
 test/parallel/test-cli-syntax-piped-good.js   |  24 +-
 .../test-loaders-unknown-builtin-module.mjs   |   3 +-
 .../test-module-main-extension-lookup.js      |   2 +-
 104 files changed, 1235 insertions(+), 527 deletions(-)
 delete mode 100644 test/addons/hello-world-esm/binding.cc
 delete mode 100644 test/addons/hello-world-esm/binding.gyp
 delete mode 100644 test/addons/hello-world-esm/test.js
 delete mode 100644 test/addons/hello-world-esm/test.mjs
 delete mode 100644 test/es-module/test-esm-json.mjs
 create mode 100644 test/es-module/test-esm-package-scope.mjs
 create mode 100644 test/es-module/test-esm-symlink-type.js
 create mode 100644 test/es-module/test-esm-type-flag-errors.js
 create mode 100644 test/es-module/test-esm-type-flag.mjs
 create mode 100644 test/fixtures/es-modules/cjs-file.cjs
 delete mode 100644 test/fixtures/es-modules/json.json
 create mode 100644 test/fixtures/es-modules/mjs-file.mjs
 create mode 100644 test/fixtures/es-modules/package-type-commonjs/index.js
 create mode 100644 test/fixtures/es-modules/package-type-commonjs/package.json
 create mode 100644 test/fixtures/es-modules/package-type-module/index.js
 create mode 100644 test/fixtures/es-modules/package-type-module/package.json
 create mode 100644 test/fixtures/es-modules/package-without-type/index.js
 create mode 100644 test/fixtures/es-modules/package-without-type/package.json
 delete mode 100644 test/fixtures/es-modules/pjson-main/main.js
 create mode 100644 test/fixtures/es-modules/pjson-main/main.mjs
 rename test/fixtures/es-modules/{test-esm-double-encoding-native%20.js => test-esm-double-encoding-native%20.mjs} (86%)
 create mode 100644 test/fixtures/esm-package-scope/legacy-loader/a.js
 create mode 100644 test/fixtures/esm-package-scope/legacy-loader/b.mjs
 create mode 100644 test/fixtures/esm-package-scope/legacy-loader/c.cjs
 create mode 100644 test/fixtures/esm-package-scope/legacy-loader/index.mjs
 create mode 100644 test/fixtures/esm-package-scope/legacy-loader/package.json
 create mode 100644 test/fixtures/esm-package-scope/new-loader/a.js
 create mode 100644 test/fixtures/esm-package-scope/new-loader/b.mjs
 create mode 100644 test/fixtures/esm-package-scope/new-loader/c.cjs
 create mode 100644 test/fixtures/esm-package-scope/new-loader/index.js
 create mode 100644 test/fixtures/esm-package-scope/new-loader/package.json
 create mode 100644 test/fixtures/syntax/bad_syntax.mjs

diff --git a/.eslintrc.js b/.eslintrc.js
index f8e6e78769..d54be1397d 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 cf28d9b2bf..9f02e4b026 100644
--- a/doc/api/cli.md
+++ b/doc/api/cli.md
@@ -533,6 +533,18 @@ added: v2.4.0
 
 Track heap object allocations for heap snapshots.
 
+### `-m`, `--type=type`
+
+When using `--experimental-modules`, this informs the module resolution type
+to interpret the top-level entry into Node.js.
+
+Works with stdin, `--eval`, `--print` as well as standard execution.
+
+Valid values are `"commonjs"` and `"module"`, where the default is to infer
+from the file extension and package type boundary.
+
+`-m` is an alias for `--type=module`.
+
 ### `--use-bundled-ca`, `--use-openssl-ca`
 <!-- YAML
 added: v6.11.0
@@ -928,6 +940,6 @@ greater than `4` (its current default value). For more information, see the
 [debugger]: debugger.html
 [debugging security implications]: https://nodejs.org/en/docs/guides/debugging-getting-started/#security-implications
 [emit_warning]: process.html#process_process_emitwarning_warning_type_code_ctor
-[experimental ECMAScript Module]: esm.html#esm_loader_hooks
+[experimental ECMAScript Module]: esm.html#esm_experimental_loader_hooks
 [libuv threadpool documentation]: http://docs.libuv.org/en/latest/threadpool.html
 [remote code execution]: https://www.owasp.org/index.php/Code_Injection
diff --git a/doc/api/errors.md b/doc/api/errors.md
index 716dcdaf32..7131e7e4eb 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -1267,6 +1267,11 @@ An invalid or unexpected value was passed in an options object.
 
 An invalid or unknown file encoding was passed.
 
+<a id="ERR_INVALID_PACKAGE_CONFIG"></a>
+### ERR_INVALID_PACKAGE_CONFIG
+
+An invalid `package.json` file was found which failed parsing.
+
 <a id="ERR_INVALID_PERFORMANCE_MARK"></a>
 ### ERR_INVALID_PERFORMANCE_MARK
 
@@ -1449,13 +1454,6 @@ a `dynamicInstantiate` hook.
 A `MessagePort` was found in the object passed to a `postMessage()` call,
 but not provided in the `transferList` for that call.
 
-<a id="ERR_MISSING_MODULE"></a>
-### ERR_MISSING_MODULE
-
-> Stability: 1 - Experimental
-
-An [ES6 module][] could not be resolved.
-
 <a id="ERR_MISSING_PLATFORM_FOR_WORKER"></a>
 ### ERR_MISSING_PLATFORM_FOR_WORKER
 
@@ -1463,12 +1461,12 @@ The V8 platform used by this instance of Node.js does not support creating
 Workers. This is caused by lack of embedder support for Workers. In particular,
 this error will not occur with standard builds of Node.js.
 
-<a id="ERR_MODULE_RESOLUTION_LEGACY"></a>
-### ERR_MODULE_RESOLUTION_LEGACY
+<a id="ERR_MODULE_NOT_FOUND"></a>
+### ERR_MODULE_NOT_FOUND
 
 > Stability: 1 - Experimental
 
-A failure occurred resolving imports in an [ES6 module][].
+An [ESM module][] could not be resolved.
 
 <a id="ERR_MULTIPLE_CALLBACK"></a>
 ### ERR_MULTIPLE_CALLBACK
@@ -2220,6 +2218,32 @@ A non-specific HTTP/2 error has occurred.
 Used in the `repl` in case the old history file is used and an error occurred
 while trying to read and parse it.
 
+<a id="ERR_INVALID_REPL_TYPE"></a>
+### ERR_INVALID_REPL_TYPE
+
+> Stability: 1 - Experimental
+
+The `--type=...` flag is not compatible with the Node.js REPL.
+
+<a id="ERR_TYPE_MISMATCH"></a>
+### ERR_TYPE_MISMATCH
+
+> Stability: 1 - Experimental
+
+The `--type=commonjs` flag was used to attempt to execute an `.mjs` file or
+a `.js` file where the nearest parent `package.json` contains
+`"type": "module"`; or
+the `--type=module` flag was used to attempt to execute a `.cjs` file or
+a `.js` file where the nearest parent `package.json` either lacks a `"type"`
+field or contains `"type": "commonjs"`.
+
+<a id="ERR_INVALID_TYPE_FLAG"></a>
+### ERR_INVALID_TYPE_FLAG
+
+> Stability: 1 - Experimental
+
+An invalid `--type=...` flag value was provided.
+
 <a id="ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK"></a>
 #### ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK
 
@@ -2250,7 +2274,6 @@ size.
 This `Error` is thrown when a read is attempted on a TTY `WriteStream`,
 such as `process.stdout.on('data')`.
 
-
 [`'uncaughtException'`]: process.html#process_event_uncaughtexception
 [`--force-fips`]: cli.html#cli_force_fips
 [`Class: assert.AssertionError`]: assert.html#assert_class_assert_assertionerror
diff --git a/doc/api/esm.md b/doc/api/esm.md
index e81a69c6ed..cb532abbe1 100644
--- a/doc/api/esm.md
+++ b/doc/api/esm.md
@@ -8,10 +8,11 @@
 <!--name=esm-->
 
 Node.js contains support for ES Modules based upon the
-[Node.js EP for ES Modules][].
+[Node.js EP for ES Modules][] and the [ESM Minimal Kernel][].
 
-Not all features of the EP are complete and will be landing as both VM support
-and implementation is ready. Error messages are still being polished.
+The minimal feature set is designed to be compatible with all potential
+future implementations. Expect major changes in the implementation including
+interoperability support, specifier resolution, and default behavior.
 
 ## Enabling
 
@@ -54,6 +55,10 @@ property:
 
 ## Notable differences between `import` and `require`
 
+### Mandatory file extensions
+
+You must provide a file extension when using the `import` keyword.
+
 ### No NODE_PATH
 
 `NODE_PATH` is not part of resolving `import` specifiers. Please use symlinks
@@ -78,31 +83,32 @@ Modules will be loaded multiple times if the `import` specifier used to resolve
 them have a different query or fragment.
 
 ```js
-import './foo?query=1'; // loads ./foo with query of "?query=1"
-import './foo?query=2'; // loads ./foo with query of "?query=2"
+import './foo.mjs?query=1'; // loads ./foo.mjs with query of "?query=1"
+import './foo.mjs?query=2'; // loads ./foo.mjs with query of "?query=2"
 ```
 
 For now, only modules using the `file:` protocol can be loaded.
 
-## Interop with existing modules
+## CommonJS, JSON, and Native Modules
 
-All CommonJS, JSON, and C++ modules can be used with `import`.
+CommonJS, JSON, and Native modules can be used with [`module.createRequireFromPath()`][].
 
-Modules loaded this way will only be loaded once, even if their query
-or fragment string differs between `import` statements.
+```js
+// cjs.js
+module.exports = 'cjs';
 
-When loaded via `import` these modules will provide a single `default` export
-representing the value of `module.exports` at the time they finished evaluating.
+// esm.mjs
+import { createRequireFromPath as createRequire } from 'module';
+import { fileURLToPath as fromPath } from 'url';
 
-```js
-// foo.js
-module.exports = { one: 1 };
+const require = createRequire(fromPath(import.meta.url));
 
-// bar.mjs
-import foo from './foo.js';
-foo.one === 1; // true
+const cjs = require('./cjs');
+cjs === 'cjs'; // true
 ```
 
+## Builtin modules
+
 Builtin modules will provide named exports of their public API, as well as a
 default export which can be used for, among other things, modifying the named
 exports. Named exports of builtin modules are updated when the corresponding
@@ -132,37 +138,161 @@ fs.readFileSync = () => Buffer.from('Hello, ESM');
 fs.readFileSync === readFileSync;
 ```
 
-## Loader hooks
-
-<!-- type=misc -->
-
-To customize the default module resolution, loader hooks can optionally be
-provided via a `--loader ./loader-name.mjs` argument to Node.js.
-
-When hooks are used they only apply to ES module loading and not to any
-CommonJS modules loaded.
-
-### Resolve hook
-
-The resolve hook returns the resolved file URL and module format for a
-given module specifier and parent file URL:
-
-```js
-const baseURL = new URL('file://');
-baseURL.pathname = `${process.cwd()}/`;
-
-export async function resolve(specifier,
-                              parentModuleURL = baseURL,
-                              defaultResolver) {
-  return {
-    url: new URL(specifier, parentModuleURL).href,
-    format: 'esm'
-  };
-}
-```
-
-The `parentModuleURL` is provided as `undefined` when performing main Node.js
-load itself.
+## 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 propogated as errors
+of these top-level routines.
+
+_isMain_ is **true** when resolving the Node.js application entry point.
+
+If the top-level `--type` is _"commonjs"_, then the ESM resolver is skipped
+entirely for the CommonJS loader.
+
+If the top-level `--type` is _"module"_, then the ESM resolver is used
+as described here, with the conditional `--type` check in **ESM_FORMAT**.
+
+<details>
+<summary>Resolver algorithm psuedocode</summary>
+
+**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. Set _parentURL_ to the parent folder URL of _parentURL_.
+>    1. Let _packageURL_ be the URL resolution of the string concatenation of
+>       _parentURL_, _"/node_modules/"_ and _packageSpecifier_.
+>    1. If the folder at _packageURL_ does not exist, then
+>       1. Set _parentURL_ to the parent URL path of _parentURL_.
+>       1. Continue the next loop iteration.
+>    1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_packageURL_).
+>    1. If _packageSubpath_ is empty, then
+>       1. Return the result of **PACKAGE_MAIN_RESOLVE**(_packageURL_,
+>          _pjson_).
+>    1. Otherwise,
+>       1. Return the URL resolution of _packageSubpath_ in _packageURL_.
+> 1. Throw a _Module Not Found_ error.
+
+PACKAGE_MAIN_RESOLVE(_packageURL_, _pjson_)
+> 1. If _pjson_ is **null**, then
+>    1. Throw a _Module Not Found_ error.
+> 1. If _pjson.main_ is a String, then
+>    1. Let _resolvedMain_ be the concatenation of _packageURL_, "/", and
+>       _pjson.main_.
+>    1. If the file at _resolvedMain_ exists, then
+>       1. Return _resolvedMain_.
+> 1. If _pjson.type_ is equal to _"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. If _isMain_ is **true** and the `--type` flag is _"module"_, then
+>    1. If _url_ ends with _".cjs"_, then
+>       1. Throw a _Type Mismatch_ error.
+>    1. Return _"module"_.
+> 1. Let _pjson_ be the result of **READ_PACKAGE_SCOPE**(_url_).
+> 1. If _pjson_ is **null** and _isMain_ is **true**, then
+>    1. If _url_ ends in _".mjs"_, then
+>       1. Return _"module"_.
+>    1. Return _"commonjs"_.
+> 1. If _pjson.type_ exists and is _"module"_, then
+>    1. If _url_ ends in _".cjs"_, then
+>       1. Return _"commonjs"_.
+>    1. Return _"module"_.
+> 1. Otherwise,
+>    1. If _url_ ends in _".mjs"_, then
+>       1. Return _"module"_.
+>    1. If _url_ does not end in _".js"_, then
+>       1. Throw an _Unsupported File Extension_ error.
+>    1. Return _"commonjs"_.
+
+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_.
+
+</details>
 
 The default Node.js ES module resolution function is provided as a third
 argument to the resolver for easy compatibility workflows.
@@ -173,11 +303,9 @@ module. This can be one of the following:
 
 | `format` | Description |
 | --- | --- |
-| `'esm'` | Load a standard JavaScript module |
-| `'cjs'` | Load a node-style CommonJS module |
-| `'builtin'` | Load a node builtin CommonJS module |
-| `'json'` | Load a JSON file |
-| `'addon'` | Load a [C++ Addon][addons] |
+| `'module'` | Load a standard JavaScript module |
+| `'commonjs'` | Load a Node.js CommonJS module |
+| `'builtin'` | Load a Node.js builtin module |
 | `'dynamic'` | Use a [dynamic instantiate hook][] |
 
 For example, a dummy loader to load JavaScript restricted to browser resolution
@@ -254,5 +382,6 @@ then be called at the exact point of module evaluation order for that module
 in the import tree.
 
 [Node.js EP for ES Modules]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md
-[addons]: addons.html
 [dynamic instantiate hook]: #esm_dynamic_instantiate_hook
+[`module.createRequireFromPath()`]: modules.html#modules_module_createrequirefrompath_filename
+[ESM Minimal Kernel]: https://github.com/nodejs/modules/blob/master/doc/plan-for-new-modules-implementation.md
diff --git a/doc/node.1 b/doc/node.1
index 7bcb1edc59..8b5560b1f1 100644
--- a/doc/node.1
+++ b/doc/node.1
@@ -280,6 +280,9 @@ Print stack traces for process warnings (including deprecations).
 .It Fl -track-heap-objects
 Track heap object allocations for heap snapshots.
 .
+.It Fl -type Ns = Ns Ar type
+Set the top-level module resolution type.
+.
 .It Fl -use-bundled-ca , Fl -use-openssl-ca
 Use bundled Mozilla CA store as supplied by current Node.js version or use OpenSSL's default CA store.
 The default store is selectable at build-time.
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 7b00bed316..4dd61ffd69 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -774,6 +774,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',
@@ -781,6 +783,8 @@ E('ERR_INVALID_PROTOCOL',
   TypeError);
 E('ERR_INVALID_REPL_EVAL_CONFIG',
   'Cannot specify both "breakEvalOnSigint" and "eval" for REPL', TypeError);
+E('ERR_INVALID_REPL_TYPE',
+  'Cannot specify --type for REPL', TypeError);
 E('ERR_INVALID_RETURN_PROPERTY', (input, name, prop, value) => {
   return `Expected a valid ${input} to be returned for the "${prop}" from the` +
          ` "${name}" function but got ${value}.`;
@@ -811,6 +815,9 @@ E('ERR_INVALID_SYNC_FORK_INPUT',
   TypeError);
 E('ERR_INVALID_THIS', 'Value of "this" must be of type %s', TypeError);
 E('ERR_INVALID_TUPLE', '%s must be an iterable %s tuple', TypeError);
+E('ERR_INVALID_TYPE_FLAG',
+  'Type flag must be one of "module", "commonjs". Received --type=%s',
+  TypeError);
 E('ERR_INVALID_URI', 'URI malformed', URIError);
 E('ERR_INVALID_URL', 'Invalid URL: %s', TypeError);
 E('ERR_INVALID_URL_SCHEME',
@@ -865,11 +872,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',
@@ -954,6 +956,20 @@ E('ERR_TRANSFORM_ALREADY_TRANSFORMING',
 E('ERR_TRANSFORM_WITH_LENGTH_0',
   'Calling transform done when writableState.length != 0', Error);
 E('ERR_TTY_INIT_FAILED', 'TTY initialization failed', SystemError);
+E('ERR_TYPE_MISMATCH', (filename, ext, typeFlag, conflict) => {
+  const typeString =
+    typeFlag === 'module' ? '--type=module' : '--type=commonjs';
+  // --type mismatches file extension
+  if (conflict === 'extension')
+    return `Extension ${ext} is not supported for ` +
+      `${typeString} loading ${filename}`;
+  // --type mismatches package.json "type"
+  else if (conflict === 'scope')
+    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_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET',
   '`process.setupUncaughtExceptionCapture()` was called while a capture ' +
     'callback was already active',
@@ -970,9 +986,8 @@ 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);
-
+E('ERR_UNKNOWN_FILE_EXTENSION', 'Unknown file extension: %s', TypeError);
 // This should probably be a `TypeError`.
-E('ERR_UNKNOWN_FILE_EXTENSION', 'Unknown file extension: %s', Error);
 E('ERR_UNKNOWN_MODULE_FORMAT', 'Unknown module format: %s', RangeError);
 E('ERR_UNKNOWN_SIGNAL', 'Unknown signal: %s', TypeError);
 
diff --git a/lib/internal/main/check_syntax.js b/lib/internal/main/check_syntax.js
index 7df70b2720..2795f5766e 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 experimentalModules =
+      require('internal/options').getOptionValue('--experimental-modules');
+  if (experimentalModules) {
+    let isModule = false;
+    if (filename === '[stdin]' || filename === '[eval]') {
+      isModule = require('internal/process/esm_loader').typeFlag === '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 2a2ef6d38a..869a3675b6 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/process/esm_loader').typeFlag === '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 953fab386d..9328a114aa 100644
--- a/lib/internal/main/eval_string.js
+++ b/lib/internal/main/eval_string.js
@@ -6,11 +6,14 @@
 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');
 prepareMainThreadExecution();
 addBuiltinLibsToObject(global);
 markBootstrapComplete();
-evalScript('[eval]', source, process._breakFirstLine);
+if (require('internal/process/esm_loader').typeFlag === 'module')
+  evalModule(source);
+else
+  evalScript('[eval]', source, process._breakFirstLine);
diff --git a/lib/internal/main/repl.js b/lib/internal/main/repl.js
index e6b9885351..7656af46a3 100644
--- a/lib/internal/main/repl.js
+++ b/lib/internal/main/repl.js
@@ -11,8 +11,15 @@ const {
   evalScript
 } = require('internal/process/execution');
 
+const { ERR_INVALID_REPL_TYPE } = require('internal/errors').codes;
+
 prepareMainThreadExecution();
 
+// --type flag not supported in REPL
+if (require('internal/process/esm_loader').typeFlag) {
+  throw ERR_INVALID_REPL_TYPE();
+}
+
 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 ee1c814c81..4c3f28349f 100644
--- a/lib/internal/modules/cjs/loader.js
+++ b/lib/internal/modules/cjs/loader.js
@@ -867,15 +867,19 @@ Module.runMain = function() {
   // Load the main module--the command line argument.
   if (experimentalModules) {
     if (asyncESM === undefined) lazyLoadESM();
-    asyncESM.loaderPromise.then((loader) => {
-      return loader.import(pathToFileURL(process.argv[1]).pathname);
-    })
-    .catch((e) => {
-      internalBinding('task_queue').triggerFatalException(e);
-    });
-  } else {
-    Module._load(process.argv[1], null, true);
+    if (asyncESM.typeFlag !== 'commonjs') {
+      asyncESM.loaderPromise.then((loader) => {
+        return loader.import(pathToFileURL(process.argv[1]).pathname);
+      })
+      .catch((e) => {
+        internalBinding('task_queue').triggerFatalException(e);
+      });
+      // 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 33366f0069..5471ee629a 100644
--- a/lib/internal/modules/esm/default_resolve.js
+++ b/lib/internal/modules/esm/default_resolve.js
@@ -1,56 +1,107 @@
 '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 { realpathSync, readFileSync } = 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 { ERR_INVALID_PACKAGE_CONFIG,
+        ERR_TYPE_MISMATCH,
+        ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;
 const { resolve: moduleWrapResolve } = internalBinding('module_wrap');
-const StringStartsWith = Function.call.bind(String.prototype.startsWith);
-const { pathToFileURL, fileURLToPath } = require('internal/url');
+const { pathToFileURL, fileURLToPath, URL } = require('internal/url');
+const asyncESM = require('internal/process/esm_loader');
 
 const realpathCache = new Map();
+// TOOD(@guybedford): Shared cache with C++
+const pjsonCache = new Map();
 
-function search(target, base) {
-  if (base === undefined) {
-    // We cannot search without a base.
-    throw new ERR_MISSING_MODULE(target);
-  }
+const extensionFormatMap = {
+  '__proto__': null,
+  '.cjs': 'commonjs',
+  '.js': 'module',
+  '.mjs': 'module'
+};
+
+const legacyExtensionFormatMap = {
+  '__proto__': null,
+  '.cjs': 'commonjs',
+  '.js': 'commonjs',
+  '.json': 'commonjs',
+  '.mjs': 'module',
+  '.node': 'commonjs'
+};
+
+function readPackageConfig(path, parentURL) {
+  const existing = pjsonCache.get(path);
+  if (existing !== undefined)
+    return existing;
   try {
-    return moduleWrapResolve(target, base);
+    return JSON.parse(readFileSync(path).toString());
   } 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
+    if (e.code === 'ENOENT') {
+      pjsonCache.set(path, null);
+      return null;
+    } else if (e instanceof SyntaxError) {
+      throw new ERR_INVALID_PACKAGE_CONFIG(path, fileURLToPath(parentURL));
     }
-    throw error;
+    throw e;
   }
 }
 
-const extensionFormatMap = {
-  '__proto__': null,
-  '.mjs': 'esm',
-  '.json': 'json',
-  '.node': 'addon',
-  '.js': 'cjs'
-};
+function getPackageBoundaryConfig(url, parentURL) {
+  let pjsonURL = new URL('package.json', url);
+  while (true) {
+    const pcfg = readPackageConfig(fileURLToPath(pjsonURL), parentURL);
+    if (pcfg)
+      return pcfg;
+
+    const lastPjsonURL = pjsonURL;
+    pjsonURL = new URL('../package.json', pjsonURL);
+
+    // Terminates at root where ../package.json equals ../../package.json
+    // (can't just check "/package.json" for Windows support).
+    if (pjsonURL.pathname === lastPjsonURL.pathname)
+      return;
+  }
+}
+
+function getModuleFormat(url, isMain, parentURL) {
+  const pcfg = getPackageBoundaryConfig(url, parentURL);
+
+  const legacy = !pcfg || pcfg.type !== 'module';
+
+  const ext = extname(url.pathname);
+
+  let format = (legacy ? legacyExtensionFormatMap : extensionFormatMap)[ext];
+
+  if (!format) {
+    if (isMain)
+      format = legacy ? 'commonjs' : 'module';
+    else
+      throw new ERR_UNKNOWN_FILE_EXTENSION(fileURLToPath(url),
+                                           fileURLToPath(parentURL));
+  }
+
+  // Check for mismatch between --type and file extension,
+  // and between --type and the "type" field in package.json.
+  if (isMain && format !== 'module' && asyncESM.typeFlag === 'module') {
+    // Conflict between package scope type and --type
+    if (ext === '.js') {
+      if (pcfg && pcfg.type)
+        throw new ERR_TYPE_MISMATCH(
+          fileURLToPath(url), ext, asyncESM.typeFlag, 'scope');
+    // Conflict between explicit extension (.mjs, .cjs) and --type
+    } else {
+      throw new ERR_TYPE_MISMATCH(
+        fileURLToPath(url), ext, asyncESM.typeFlag, 'extension');
+    }
+  }
+
+  return format;
+}
 
 function resolve(specifier, parentURL) {
   if (NativeModule.canBeRequiredByUsers(specifier)) {
@@ -60,21 +111,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 +127,9 @@ function resolve(specifier, parentURL) {
     url.hash = old.hash;
   }
 
-  const ext = extname(url.pathname);
-
-  let format = extensionFormatMap[ext];
-  if (!format) {
-    if (isMain)
-      format = 'cjs';
-    else
-      throw new ERR_UNKNOWN_FILE_EXTENSION(url.pathname);
-  }
+  const format = getModuleFormat(url, isMain, 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 a1a1621909..dced45f7a0 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 016495096c..8e0c7b7be9 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,7 @@ class ModuleJob {
 
   async run() {
     const module = await this.instantiate();
-    module.evaluate(-1, false);
-    return module;
+    return { module, result: module.evaluate(-1, false) };
   }
 }
 Object.setPrototypeOf(ModuleJob.prototype, null);
diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js
index cf1765c7c3..70c68ae0f0 100644
--- a/lib/internal/modules/esm/translators.js
+++ b/lib/internal/modules/esm/translators.js
@@ -3,20 +3,15 @@
 const { NativeModule } = require('internal/bootstrap/loaders');
 const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
 const {
-  stripShebang,
-  stripBOM
+  stripShebang
 } = require('internal/modules/cjs/helpers');
 const CJSModule = require('internal/modules/cjs/loader');
 const internalURLModule = require('internal/url');
 const createDynamicModule = require(
   'internal/modules/esm/create_dynamic_module');
 const fs = require('fs');
-const { _makeLong } = require('path');
 const {
   SafeMap,
-  JSON,
-  FunctionPrototype,
-  StringPrototype
 } = primordials;
 const { URL } = require('url');
 const { debuglog, promisify } = require('util');
@@ -25,14 +20,12 @@ 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 JsonParse = JSON.parse;
+const StringReplace = Function.call.bind(String.prototype.replace);
 
 const debug = debuglog('esm');
 
 const translators = new SafeMap();
-module.exports = translators;
+exports.translators = translators;
 
 function initializeImportMeta(meta, { url }) {
   meta.url = url;
@@ -44,7 +37,7 @@ async function importModuleDynamically(specifier, { url }) {
 }
 
 // Strategy for loading a standard JavaScript module
-translators.set('esm', async (url) => {
+translators.set('module', async function(url) {
   const source = `${await readFileAsync(new URL(url))}`;
   debug(`Translating StandardModule ${url}`);
   const module = new ModuleWrap(stripShebang(source), url);
@@ -61,9 +54,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(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) {
@@ -83,7 +81,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(url) {
   debug(`Translating BuiltinModule ${url}`);
   // slice 'node:' scheme
   const id = url.slice(5);
@@ -101,32 +99,3 @@ translators.set('builtin', async (url) => {
       reflect.exports.default.set(module.exports);
     });
 });
-
-// Strategy for loading a node native module
-translators.set('addon', async (url) => {
-  debug(`Translating NativeModule ${url}`);
-  return createDynamicModule(['default'], url, (reflect) => {
-    debug(`Loading NativeModule ${url}`);
-    const module = { exports: {} };
-    const pathname = internalURLModule.fileURLToPath(new URL(url));
-    process.dlopen(module, _makeLong(pathname));
-    reflect.exports.default.set(module.exports);
-  });
-});
-
-// Strategy for loading a JSON file
-translators.set('json', async (url) => {
-  debug(`Translating JSONModule ${url}`);
-  return createDynamicModule(['default'], url, (reflect) => {
-    debug(`Loading JSONModule ${url}`);
-    const pathname = internalURLModule.fileURLToPath(new URL(url));
-    const content = readFileSync(pathname, 'utf8');
-    try {
-      const exports = JsonParse(stripBOM(content));
-      reflect.exports.default.set(exports);
-    } catch (err) {
-      err.message = pathname + ': ' + err.message;
-      throw err;
-    }
-  });
-});
diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js
index 0b7f1be6ff..803c854d9a 100644
--- a/lib/internal/process/esm_loader.js
+++ b/lib/internal/process/esm_loader.js
@@ -3,15 +3,22 @@
 const {
   callbackMap,
 } = internalBinding('module_wrap');
+const {
+  ERR_INVALID_TYPE_FLAG,
+  ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING,
+} = require('internal/errors').codes;
+const { emitExperimentalWarning } = require('internal/util');
+
+const type = require('internal/options').getOptionValue('--type');
+if (type && type !== 'commonjs' && type !== 'module')
+  throw new ERR_INVALID_TYPE_FLAG(type);
+exports.typeFlag = type;
 
+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 +41,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;
 
@@ -44,6 +49,7 @@ exports.initializeLoader = function(cwd, userLoader) {
   let ESMLoader = new Loader();
   const loaderPromise = (async () => {
     if (userLoader) {
+      emitExperimentalWarning('--loader');
       const hooks = await ESMLoader.import(
         userLoader, pathToFileURL(`${cwd}/`).href);
       ESMLoader = new Loader();
diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js
index 7118dbf3ad..070410ef6f 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 70c566dce9..9426730cb7 100644
--- a/src/env.h
+++ b/src/env.h
@@ -73,14 +73,23 @@ namespace loader {
 class ModuleWrap;
 
 struct PackageConfig {
-  enum class Exists { Yes, No };
-  enum class IsValid { Yes, No };
-  enum class HasMain { Yes, No };
-
-  Exists exists;
-  IsValid is_valid;
-  HasMain has_main;
-  std::string main;
+  struct Exists {
+    enum Bool { No, Yes };
+  };
+  struct IsValid {
+    enum Bool { No, Yes };
+  };
+  struct HasMain {
+    enum Bool { No, Yes };
+  };
+  struct IsESM {
+    enum Bool { No, Yes };
+  };
+  const Exists::Bool exists;
+  const IsValid::Bool is_valid;
+  const HasMain::Bool has_main;
+  const std::string main;
+  const IsESM::Bool esm;
 };
 }  // namespace loader
 
@@ -209,6 +218,7 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2;
   V(kill_signal_string, "killSignal")                                          \
   V(kind_string, "kind")                                                       \
   V(library_string, "library")                                                 \
+  V(legacy_string, "legacy")                                                   \
   V(mac_string, "mac")                                                         \
   V(main_string, "main")                                                       \
   V(max_buffer_string, "maxBuffer")                                            \
diff --git a/src/module_wrap.cc b/src/module_wrap.cc
index ac5d28fb23..8f6f21dcd9 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,8 +45,6 @@ using v8::String;
 using v8::Undefined;
 using v8::Value;
 
-static const char* const EXTENSIONS[] = {".mjs", ".js", ".json", ".node"};
-
 ModuleWrap::ModuleWrap(Environment* env,
                        Local<Object> object,
                        Local<Module> module,
@@ -471,219 +468,330 @@ std::string ReadFile(uv_file file) {
   return contents;
 }
 
-enum CheckFileOptions {
-  LEAVE_OPEN_AFTER_CHECK,
-  CLOSE_AFTER_CHECK
+enum DescriptorType {
+  FILE,
+  DIRECTORY,
+  NONE
 };
 
-Maybe<uv_file> 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<uv_file> OpenDescriptor(const std::string& path) {
   uv_fs_t fs_req;
-  if (path.empty()) {
-    return Nothing<uv_file>();
-  }
-
   uv_file fd = uv_fs_open(nullptr, &fs_req, path.c_str(), O_RDONLY, 0, nullptr);
   uv_fs_req_cleanup(&fs_req);
+  if (fd < 0) return Nothing<uv_file>();
+  return Just(fd);
+}
 
-  if (fd < 0) {
-    return Nothing<uv_file>();
-  }
-
-  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<uv_file>();
+    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<uv_file> fd = OpenDescriptor(path);
+  if (fd.IsNothing()) return NONE;
+  DescriptorType type = CheckDescriptorAtFile(fd.FromJust());
+  CloseDescriptor(fd.FromJust());
+  return type;
+}
 
-  return Just(fd);
+Maybe<std::string> ReadIfFile(const std::string& path) {
+  Maybe<uv_file> fd = OpenDescriptor(path);
+  if (fd.IsNothing()) return Nothing<std::string>();
+  DescriptorType type = CheckDescriptorAtFile(fd.FromJust());
+  if (type != FILE) return Nothing<std::string>();
+  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 IsESM = PackageConfig::IsESM;
 
-const PackageConfig& GetPackageConfig(Environment* env,
-                                      const std::string& path) {
+Maybe<const PackageConfig*> 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;
+    return Just(&existing->second);
   }
-  Maybe<uv_file> check = CheckFile(path, LEAVE_OPEN_AFTER_CHECK);
-  if (check.IsNothing()) {
+
+  Maybe<std::string> 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, "",
+                        IsESM::No });
+    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<String> 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<Value> pkg_json_v;
+  bool parsed = false;
   Local<Object> pkg_json;
+  {
+    Local<String> src;
+    Local<Value> pkg_json_v;
+    if (String::NewFromUtf8(isolate,
+                            pkg_src.c_str(),
+                            v8::NewStringType::kNormal,
+                            pkg_src.length()).ToLocal(&src) &&
+        v8::JSON::Parse(env->context(), src).ToLocal(&pkg_json_v) &&
+        pkg_json_v->ToObject(env->context()).ToLocal(&pkg_json)) {
+      parsed = true;
+    }
+  }
 
-  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;
+  if (!parsed) {
+    env->package_json_cache.emplace(path,
+        PackageConfig { Exists::Yes, IsValid::No, HasMain::No, "",
+                        IsESM::No });
+    std::string msg = "Invalid JSON in '" + path +
+        "' imported from " + base.ToFilePath();
+    node::THROW_ERR_INVALID_PACKAGE_CONFIG(env, msg.c_str());
+    return Nothing<const PackageConfig*>();
   }
 
   Local<Value> pkg_main;
-  HasMain has_main = HasMain::No;
+  HasMain::Bool 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()));
   }
 
-  auto entry = env->package_json_cache.emplace(path,
-      PackageConfig { Exists::Yes, IsValid::Yes, has_main, main_std });
-  return entry.first->second;
-}
+  IsESM::Bool esm = IsESM::No;
+  Local<Value> type_v;
+  if (pkg_json->Get(env->context(), env->type_string()).ToLocal(&type_v)) {
+    if (type_v->StrictEquals(env->module_string())) {
+      esm = IsESM::Yes;
+    }
+  }
 
-enum ResolveExtensionsOptions {
-  TRY_EXACT_NAME,
-  ONLY_VIA_EXTENSIONS
-};
+  Local<Value> exports_v;
+  if (pkg_json->Get(env->context(),
+      env->exports_string()).ToLocal(&exports_v) &&
+      (exports_v->IsObject() || exports_v->IsString() ||
+      exports_v->IsBoolean())) {
+    Persistent<Value> exports;
+    // esm = IsESM::Yes;
+    exports.Reset(env->isolate(), exports_v);
 
-template <ResolveExtensionsOptions options>
-Maybe<URL> ResolveExtensions(const URL& search) {
-  if (options == TRY_EXACT_NAME) {
-    std::string filePath = search.ToFilePath();
-    Maybe<uv_file> check = CheckFile(filePath);
-    if (!check.IsNothing()) {
-      return Just(search);
-    }
+    auto entry = env->package_json_cache.emplace(path,
+        PackageConfig { Exists::Yes, IsValid::Yes, has_main, main_std,
+                        esm });
+    return Just(&entry.first->second);
   }
 
-  for (const char* extension : EXTENSIONS) {
-    URL guess(search.path() + extension, &search);
-    Maybe<uv_file> check = CheckFile(guess.ToFilePath());
-    if (!check.IsNothing()) {
+  auto entry = env->package_json_cache.emplace(path,
+      PackageConfig { Exists::Yes, IsValid::Yes, has_main, main_std,
+                      esm });
+  return Just(&entry.first->second);
+}
+
+/*
+ * 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<URL> 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<URL>();
 }
 
-inline Maybe<URL> ResolveIndex(const URL& search) {
-  return ResolveExtensions<ONLY_VIA_EXTENSIONS>(URL("index", search));
-}
+Maybe<URL> FinalizeResolution(Environment* env,
+                                           const URL& resolved,
+                                           const URL& base,
+                                           bool check_exists) {
+  const std::string& path = resolved.ToFilePath();
+
+  if (check_exists && 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<URL>();
+  }
 
-Maybe<URL> ResolveMain(Environment* env, const URL& search) {
-  URL pkg("package.json", &search);
+  return Just(resolved);
+}
 
-  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<URL> PackageMainResolve(Environment* env,
+                                           const URL& pjson_url,
+                                           const PackageConfig& pcfg,
+                                           const URL& base) {
+  if (pcfg.exists == Exists::No || (
+      pcfg.esm == IsESM::Yes && pcfg.has_main == HasMain::No)) {
+    std::string msg = "Cannot find main entry point for '" +
+        URL(".", pjson_url).ToFilePath() + "' imported from " +
+        base.ToFilePath();
+    node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str());
     return Nothing<URL>();
   }
-  if (!ShouldBeTreatedAsRelativeOrAbsolutePath(pjson.main)) {
-    return Resolve(env, "./" + pjson.main, search, IgnoreMain);
+  if (pcfg.has_main == HasMain::Yes &&
+      pcfg.main.substr(pcfg.main.length() - 4, 4) == ".mjs") {
+    return FinalizeResolution(env, URL(pcfg.main, pjson_url), base, true);
   }
-  return Resolve(env, pjson.main, search, IgnoreMain);
+  if (pcfg.esm == IsESM::Yes &&
+      pcfg.main.substr(pcfg.main.length() - 3, 3) == ".js") {
+    return FinalizeResolution(env, URL(pcfg.main, pjson_url), base, true);
+  }
+
+  Maybe<URL> resolved = LegacyMainResolve(pjson_url, pcfg);
+  // Legacy main resolution error
+  if (resolved.IsNothing()) {
+    return Nothing<URL>();
+  }
+  return resolved;
 }
 
-Maybe<URL> ResolveModule(Environment* env,
-                         const std::string& specifier,
-                         const URL& base) {
-  URL parent(".", base);
-  URL dir("");
+Maybe<URL> 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<URL>();
+  }
+  bool scope = false;
+  if (specifier[0] == '@') {
+    scope = true;
+    sep_index = specifier.find('/', sep_index + 1);
+  }
+  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 {
-    dir = parent;
-    Maybe<URL> 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<URL>();
-      }
-      return check;
+    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<const PackageConfig*> pcfg = GetPackageConfig(env, pjson_path, base);
+    // Invalid package configuration error.
+    if (pcfg.IsNothing()) return Nothing<URL>();
+    if (!pkg_subpath.length()) {
+      return PackageMainResolve(env, pjson_url, *pcfg.FromJust(), base);
     } else {
-      // TODO(bmeck) PREVENT FALLTHROUGH
+      return FinalizeResolution(env, URL(pkg_subpath, pjson_url), base, true);
     }
-    parent = URL("..", &dir);
-  } while (parent.path() != dir.path());
-  return Nothing<URL>();
-}
+    CHECK(false);
+    // Cross-platform root check.
+  } while (pjson_path.length() != last_path.length());
 
-Maybe<URL> ResolveDirectory(Environment* env,
-                            const URL& search,
-                            PackageMainCheck check_pjson_main) {
-  if (check_pjson_main) {
-    Maybe<URL> main = ResolveMain(env, search);
-    if (!main.IsNothing())
-      return main;
-  }
-  return ResolveIndex(search);
+  std::string msg = "Cannot find package '" + pkg_name +
+      "' imported from " + base.ToFilePath();
+  node::THROW_ERR_MODULE_NOT_FOUND(env, msg.c_str());
+  return Nothing<URL>();
 }
 
 }  // anonymous namespace
 
 Maybe<URL> 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<uv_file> check = CheckFile(pure_url.ToFilePath());
-    if (check.IsNothing()) {
-      return Nothing<URL>();
-    }
-    return Just(pure_url);
-  }
-  if (specifier.length() == 0) {
-    return Nothing<URL>();
-  }
+                                const std::string& specifier,
+                                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<URL> file = ResolveExtensions<TRY_EXACT_NAME>(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, true);
 }
 
 void ModuleWrap::Resolve(const FunctionCallbackInfo<Value>& args) {
@@ -705,15 +813,24 @@ void ModuleWrap::Resolve(const FunctionCallbackInfo<Value>& args) {
         env, "second argument is not a URL string");
   }
 
-  Maybe<URL> 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());
+  TryCatchScope try_catch(env);
+  Maybe<URL> result =
+      node::loader::Resolve(env,
+                            specifier_std,
+                            url);
+  if (result.IsNothing()) {
+    CHECK(try_catch.HasCaught());
+    try_catch.ReThrow();
+    return;
   }
+  CHECK(!try_catch.HasCaught());
+
+  URL resolution = result.FromJust();
+  CHECK(!(resolution.flags() & URL_FLAGS_FAILED));
+
+  Local<Value> resolved = resolution.ToObject(env).ToLocalChecked();
 
-  MaybeLocal<Value> obj = result.FromJust().ToObject(env);
-  if (!obj.IsEmpty())
-    args.GetReturnValue().Set(obj.ToLocalChecked());
+  args.GetReturnValue().Set(resolved);
 }
 
 static MaybeLocal<Promise> ImportModuleDynamically(
diff --git a/src/module_wrap.h b/src/module_wrap.h
index dc34685fed..9ddb99d174 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<url::URL> 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[];
diff --git a/src/node_errors.h b/src/node_errors.h
index 835794b178..9d3f2ead71 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 5687fb327b..485ae02ffc 100644
--- a/src/node_options.cc
+++ b/src/node_options.cc
@@ -271,10 +271,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 +303,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
             "show stack traces on process warnings",
             &EnvironmentOptions::trace_warnings,
             kAllowedInEnvironment);
+  AddOption("--type",
+            "top-level module type name",
+            &EnvironmentOptions::module_type,
+            kAllowedInEnvironment);
 
   AddOption("--check",
             "syntax check script without executing",
diff --git a/src/node_options.h b/src/node_options.h
index bcd6d2457d..d8a8440c1c 100644
--- a/src/node_options.h
+++ b/src/node_options.h
@@ -92,6 +92,7 @@ class EnvironmentOptions : public Options {
  public:
   bool abort_on_uncaught_exception = false;
   bool experimental_modules = false;
+  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 02eecec099..0000000000
--- a/test/addons/hello-world-esm/binding.cc
+++ /dev/null
@@ -1,14 +0,0 @@
-#include <node.h>
-#include <v8.h>
-
-void Method(const v8::FunctionCallbackInfo<v8::Value>& args) {
-  v8::Isolate* isolate = args.GetIsolate();
-  args.GetReturnValue().Set(v8::String::NewFromUtf8(
-        isolate, "world", v8::NewStringType::kNormal).ToLocalChecked());
-}
-
-void init(v8::Local<v8::Object> exports) {
-  NODE_SET_METHOD(exports, "hello", Method);
-}
-
-NODE_MODULE(NODE_GYP_MODULE_NAME, init)
diff --git a/test/addons/hello-world-esm/binding.gyp b/test/addons/hello-world-esm/binding.gyp
deleted file mode 100644
index 55fbe7050f..0000000000
--- a/test/addons/hello-world-esm/binding.gyp
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-  'targets': [
-    {
-      'target_name': 'binding',
-      'sources': [ 'binding.cc' ],
-      'includes': ['../common.gypi'],
-    }
-  ]
-}
diff --git a/test/addons/hello-world-esm/test.js b/test/addons/hello-world-esm/test.js
deleted file mode 100644
index d0faf65540..0000000000
--- a/test/addons/hello-world-esm/test.js
+++ /dev/null
@@ -1,20 +0,0 @@
-'use strict';
-const common = require('../../common');
-
-const assert = require('assert');
-const { spawnSync } = require('child_process');
-const { copyFileSync } = require('fs');
-const { join } = require('path');
-
-const buildDir = join(__dirname, 'build');
-
-copyFileSync(join(buildDir, common.buildType, 'binding.node'),
-             join(buildDir, 'binding.node'));
-
-const result = spawnSync(process.execPath,
-                         ['--experimental-modules', `${__dirname}/test.mjs`]);
-
-assert.ifError(result.error);
-// TODO: Uncomment this once ESM is no longer experimental.
-// assert.strictEqual(result.stderr.toString().trim(), '');
-assert.strictEqual(result.stdout.toString().trim(), 'binding.hello() = world');
diff --git a/test/addons/hello-world-esm/test.mjs b/test/addons/hello-world-esm/test.mjs
deleted file mode 100644
index d98de5bf87..0000000000
--- a/test/addons/hello-world-esm/test.mjs
+++ /dev/null
@@ -1,6 +0,0 @@
-/* eslint-disable node-core/required-modules */
-
-import assert from 'assert';
-import binding from './build/binding.node';
-assert.strictEqual(binding.hello(), 'world');
-console.log('binding.hello() =', binding.hello());
diff --git a/test/common/index.mjs b/test/common/index.mjs
index de9119f37e..41592098eb 100644
--- a/test/common/index.mjs
+++ b/test/common/index.mjs
@@ -1,6 +1,15 @@
 // Flags: --experimental-modules
 /* eslint-disable node-core/required-modules */
-import common from './index.js';
+
+import { createRequireFromPath } from 'module';
+import { fileURLToPath as toPath } from 'url';
+
+function createRequire(metaUrl) {
+  return createRequireFromPath(toPath(metaUrl));
+}
+
+const require = createRequire(import.meta.url);
+const common = require('./index.js');
 
 const {
   isMainThread,
@@ -91,5 +100,6 @@ export {
   getBufferSources,
   disableCrashOnUnhandledRejection,
   getTTYfd,
-  runWithInvalidFD
+  runWithInvalidFD,
+  createRequire
 };
diff --git a/test/es-module/test-esm-basic-imports.mjs b/test/es-module/test-esm-basic-imports.mjs
index 78a4106f94..d9bb22be0a 100644
--- a/test/es-module/test-esm-basic-imports.mjs
+++ b/test/es-module/test-esm-basic-imports.mjs
@@ -1,5 +1,6 @@
 // Flags: --experimental-modules
-import '../common';
+/* eslint-disable node-core/required-modules */
+import '../common/index.mjs';
 import assert from 'assert';
 import ok from '../fixtures/es-modules/test-esm-ok.mjs';
 import okShebang from './test-esm-shebang.mjs';
diff --git a/test/es-module/test-esm-cyclic-dynamic-import.mjs b/test/es-module/test-esm-cyclic-dynamic-import.mjs
index c8dfff919c..a207efc73e 100644
--- a/test/es-module/test-esm-cyclic-dynamic-import.mjs
+++ b/test/es-module/test-esm-cyclic-dynamic-import.mjs
@@ -1,3 +1,4 @@
 // Flags: --experimental-modules
-import '../common';
-import('./test-esm-cyclic-dynamic-import');
+/* eslint-disable node-core/required-modules */
+import '../common/index.mjs';
+import('./test-esm-cyclic-dynamic-import.mjs');
diff --git a/test/es-module/test-esm-double-encoding.mjs b/test/es-module/test-esm-double-encoding.mjs
index c81d0530d3..9366d4bd6b 100644
--- a/test/es-module/test-esm-double-encoding.mjs
+++ b/test/es-module/test-esm-double-encoding.mjs
@@ -1,6 +1,7 @@
 // Flags: --experimental-modules
-import '../common';
+/* eslint-disable node-core/required-modules */
+import '../common/index.mjs';
 
 // Assert we can import files with `%` in their pathname.
 
-import '../fixtures/es-modules/test-esm-double-encoding-native%2520.js';
+import '../fixtures/es-modules/test-esm-double-encoding-native%2520.mjs';
diff --git a/test/es-module/test-esm-dynamic-import.js b/test/es-module/test-esm-dynamic-import.js
index b271d43c80..ca9c99007b 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 365a425afa..2cabfdacff 100644
--- a/test/es-module/test-esm-encoded-path.mjs
+++ b/test/es-module/test-esm-encoded-path.mjs
@@ -1,5 +1,6 @@
 // Flags: --experimental-modules
-import '../common';
+/* eslint-disable node-core/required-modules */
+import '../common/index.mjs';
 import assert from 'assert';
 // ./test-esm-ok.mjs
 import ok from '../fixtures/es-modules/test-%65%73%6d-ok.mjs';
diff --git a/test/es-module/test-esm-error-cache.js b/test/es-module/test-esm-error-cache.js
index 98244615ef..79f76357ec 100644
--- a/test/es-module/test-esm-error-cache.js
+++ b/test/es-module/test-esm-error-cache.js
@@ -1,11 +1,11 @@
-'use strict';
-
 // Flags: --experimental-modules
 
+'use strict';
+
 require('../common');
 const assert = require('assert');
 
-const file = '../fixtures/syntax/bad_syntax.js';
+const file = '../fixtures/syntax/bad_syntax.mjs';
 
 let error;
 (async () => {
diff --git a/test/es-module/test-esm-forbidden-globals.mjs b/test/es-module/test-esm-forbidden-globals.mjs
index 4e777412a3..cf110ff290 100644
--- a/test/es-module/test-esm-forbidden-globals.mjs
+++ b/test/es-module/test-esm-forbidden-globals.mjs
@@ -1,5 +1,6 @@
 // Flags: --experimental-modules
-import '../common';
+/* eslint-disable node-core/required-modules */
+import '../common/index.mjs';
 
 // eslint-disable-next-line no-undef
 if (typeof arguments !== 'undefined') {
diff --git a/test/es-module/test-esm-import-meta.mjs b/test/es-module/test-esm-import-meta.mjs
index c17e0e20d4..4c34b337fb 100644
--- a/test/es-module/test-esm-import-meta.mjs
+++ b/test/es-module/test-esm-import-meta.mjs
@@ -1,6 +1,7 @@
 // Flags: --experimental-modules
+/* eslint-disable node-core/required-modules */
 
-import '../common';
+import '../common/index.mjs';
 import assert from 'assert';
 
 assert.strictEqual(Object.getPrototypeOf(import.meta), null);
diff --git a/test/es-module/test-esm-json.mjs b/test/es-module/test-esm-json.mjs
deleted file mode 100644
index a7146d19a9..0000000000
--- a/test/es-module/test-esm-json.mjs
+++ /dev/null
@@ -1,8 +0,0 @@
-// Flags: --experimental-modules
-import '../common';
-import assert from 'assert';
-import ok from '../fixtures/es-modules/test-esm-ok.mjs';
-import json from '../fixtures/es-modules/json.json';
-
-assert(ok);
-assert.strictEqual(json.val, 42);
diff --git a/test/es-module/test-esm-live-binding.mjs b/test/es-module/test-esm-live-binding.mjs
index d151e004df..880a6c389b 100644
--- a/test/es-module/test-esm-live-binding.mjs
+++ b/test/es-module/test-esm-live-binding.mjs
@@ -1,6 +1,7 @@
 // Flags: --experimental-modules
+/* eslint-disable node-core/required-modules */
 
-import '../common';
+import '../common/index.mjs';
 import assert from 'assert';
 
 import fs, { readFile, readFileSync } from 'fs';
diff --git a/test/es-module/test-esm-loader-invalid-format.mjs b/test/es-module/test-esm-loader-invalid-format.mjs
index f8714d4aa1..c3f3a87407 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 43971a2e6e..9cf17b2478 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 f2b37f7e8a..ab2da7adce 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 946d54ffaa..5493c6c47c 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 0ca8990cb7..b5e0d8d656 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 ca313a1d26..19c025beab 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 3aae9230de..e235f598cb 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 da1286d0f4..38b7ef12d5 100644
--- a/test/es-module/test-esm-namespace.mjs
+++ b/test/es-module/test-esm-namespace.mjs
@@ -1,5 +1,7 @@
 // Flags: --experimental-modules
-import '../common';
+/* eslint-disable node-core/required-modules */
+
+import '../common/index.mjs';
 import * as fs from 'fs';
 import assert from 'assert';
 import Module from 'module';
diff --git a/test/es-module/test-esm-package-scope.mjs b/test/es-module/test-esm-package-scope.mjs
new file mode 100644
index 0000000000..6e07a307e0
--- /dev/null
+++ b/test/es-module/test-esm-package-scope.mjs
@@ -0,0 +1,12 @@
+// Flags: --experimental-modules
+/* eslint-disable node-core/required-modules */
+
+import '../common/index.mjs';
+import assert from 'assert';
+
+import legacyLoader from
+  '../fixtures/esm-package-scope/legacy-loader/index.mjs';
+import newLoader from '../fixtures/esm-package-scope/new-loader/index.js';
+
+assert.strictEqual(legacyLoader, 'legacy-loader');
+assert.strictEqual(newLoader, 'new-loader');
diff --git a/test/es-module/test-esm-preserve-symlinks-not-found.mjs b/test/es-module/test-esm-preserve-symlinks-not-found.mjs
index 5119957bae..b5be2d7e63 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 ea9b4b4936..3a23573d33 100644
--- a/test/es-module/test-esm-process.mjs
+++ b/test/es-module/test-esm-process.mjs
@@ -1,5 +1,6 @@
 // Flags: --experimental-modules
-import '../common';
+/* eslint-disable node-core/required-modules */
+import '../common/index.mjs';
 import assert from 'assert';
 import process from 'process';
 
diff --git a/test/es-module/test-esm-require-cache.mjs b/test/es-module/test-esm-require-cache.mjs
index ff32cde36f..09030e0578 100644
--- a/test/es-module/test-esm-require-cache.mjs
+++ b/test/es-module/test-esm-require-cache.mjs
@@ -1,7 +1,12 @@
 // Flags: --experimental-modules
-import '../common';
-import '../fixtures/es-module-require-cache/preload.js';
-import '../fixtures/es-module-require-cache/counter.js';
+/* eslint-disable node-core/required-modules */
+import { createRequire } from '../common/index.mjs';
 import assert from 'assert';
+//
+const require = createRequire(import.meta.url);
+
+require('../fixtures/es-module-require-cache/preload.js');
+require('../fixtures/es-module-require-cache/counter.js');
+
 assert.strictEqual(global.counter, 1);
 delete global.counter;
diff --git a/test/es-module/test-esm-shared-loader-dep.mjs b/test/es-module/test-esm-shared-loader-dep.mjs
index 5c274d835c..b8953ab1ec 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 d5faace479..486e04dade 100644
--- a/test/es-module/test-esm-shebang.mjs
+++ b/test/es-module/test-esm-shebang.mjs
@@ -1,6 +1,7 @@
 #! }]) // isn't js
 // Flags: --experimental-modules
-import '../common';
+/* eslint-disable node-core/required-modules */
+import '../common/index.mjs';
 
 const isJs = true;
 export default isJs;
diff --git a/test/es-module/test-esm-snapshot.mjs b/test/es-module/test-esm-snapshot.mjs
index 3d4b44bbdd..3997e24ed7 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-symlink-main.js b/test/es-module/test-esm-symlink-main.js
index f7631ef2e5..871180f5cc 100644
--- a/test/es-module/test-esm-symlink-main.js
+++ b/test/es-module/test-esm-symlink-main.js
@@ -9,7 +9,7 @@ const fs = require('fs');
 tmpdir.refresh();
 
 const realPath = path.resolve(__dirname, '../fixtures/es-modules/symlink.mjs');
-const symlinkPath = path.resolve(tmpdir.path, 'symlink.js');
+const symlinkPath = path.resolve(tmpdir.path, 'symlink.mjs');
 
 try {
   fs.symlinkSync(realPath, symlinkPath);
diff --git a/test/es-module/test-esm-symlink-type.js b/test/es-module/test-esm-symlink-type.js
new file mode 100644
index 0000000000..6159ebecd1
--- /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 232925a52e..9b9eb98cd9 100644
--- a/test/es-module/test-esm-symlink.js
+++ b/test/es-module/test-esm-symlink.js
@@ -12,8 +12,8 @@ const tmpDir = tmpdir.path;
 
 const entry = path.join(tmpDir, 'entry.mjs');
 const real = path.join(tmpDir, 'index.mjs');
-const link_absolute_path = path.join(tmpDir, 'absolute');
-const link_relative_path = path.join(tmpDir, 'relative');
+const link_absolute_path = path.join(tmpDir, 'absolute.mjs');
+const link_relative_path = path.join(tmpDir, 'relative.mjs');
 const link_ignore_extension = path.join(tmpDir,
                                         'ignore_extension.json');
 const link_directory = path.join(tmpDir, 'directory');
@@ -22,15 +22,13 @@ fs.writeFileSync(real, 'export default [];');
 fs.writeFileSync(entry, `
 import assert from 'assert';
 import real from './index.mjs';
-import absolute from './absolute';
-import relative from './relative';
+import absolute from './absolute.mjs';
+import relative from './relative.mjs';
 import ignoreExtension from './ignore_extension.json';
-import directory from './directory';
 
 assert.strictEqual(absolute, real);
 assert.strictEqual(relative, real);
 assert.strictEqual(ignoreExtension, real);
-assert.strictEqual(directory, real);
 `);
 
 try {
diff --git a/test/es-module/test-esm-throw-undefined.mjs b/test/es-module/test-esm-throw-undefined.mjs
index 541127eee5..97e917da5e 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 0000000000..3c45e81381
--- /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 --type and no package.json "type" works
+expect('--type=commonjs', packageWithoutTypeMain, 'package-without-type');
+expect('--type=module', packageWithoutTypeMain, 'package-without-type');
+
+// Check that running with conflicting --type flags throws errors
+expect('--type=commonjs', mjsFile, 'ERR_REQUIRE_ESM', true);
+expect('--type=module', cjsFile, 'ERR_TYPE_MISMATCH', true);
+expect('--type=commonjs', packageTypeModuleMain,
+       'SyntaxError', true);
+expect('--type=module', packageTypeCommonJsMain,
+       'ERR_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 0000000000..cf91580490
--- /dev/null
+++ b/test/es-module/test-esm-type-flag.mjs
@@ -0,0 +1,11 @@
+// Flags: --experimental-modules --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 a7cf276d4a..d8e0ddcba3 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 2ac959a464..4b8a0fc365 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 cf821afec1..c8154ac5db 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 12efbb5021..f653155899 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 1a19e4c892..3acafcce1e 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 944e6e438c..5afd3b2e21 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 d15f294fe6..d3eebcd47e 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 9cad68c7ce..3a6bc5effc 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 f062276767..0349ae112d 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-modules/cjs-file.cjs b/test/fixtures/es-modules/cjs-file.cjs
new file mode 100644
index 0000000000..3d0637686e
--- /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.json b/test/fixtures/es-modules/json.json
deleted file mode 100644
index 8288d42e2b..0000000000
--- a/test/fixtures/es-modules/json.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-  "val": 42
-}
diff --git a/test/fixtures/es-modules/loop.mjs b/test/fixtures/es-modules/loop.mjs
index 1b5cab10ed..3d2ddd2eb7 100644
--- a/test/fixtures/es-modules/loop.mjs
+++ b/test/fixtures/es-modules/loop.mjs
@@ -1,4 +1,4 @@
-import { message } from './message';
+import { message } from './message.mjs';
 
 var t = 1;
 var k = 1;
diff --git a/test/fixtures/es-modules/mjs-file.mjs b/test/fixtures/es-modules/mjs-file.mjs
new file mode 100644
index 0000000000..489d4ab570
--- /dev/null
+++ b/test/fixtures/es-modules/mjs-file.mjs
@@ -0,0 +1 @@
+console.log('.mjs file');
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 0000000000..009431d851
--- /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 0000000000..4aaa4a2388
--- /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 0000000000..12aba970ef
--- /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 0000000000..07aec65d5a
--- /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 0000000000..a547216cb0
--- /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 0000000000..14ab704d8f
--- /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 dfdd47b877..0000000000
--- a/test/fixtures/es-modules/pjson-main/main.js
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = 'main';
diff --git a/test/fixtures/es-modules/pjson-main/main.mjs b/test/fixtures/es-modules/pjson-main/main.mjs
new file mode 100644
index 0000000000..9eb0aade18
--- /dev/null
+++ b/test/fixtures/es-modules/pjson-main/main.mjs
@@ -0,0 +1 @@
+export const main = 'main'
diff --git a/test/fixtures/es-modules/pjson-main/package.json b/test/fixtures/es-modules/pjson-main/package.json
index c13b8cf6ac..ea9b784692 100644
--- a/test/fixtures/es-modules/pjson-main/package.json
+++ b/test/fixtures/es-modules/pjson-main/package.json
@@ -1,3 +1,3 @@
 {
-  "main": "main.js"
+  "main": "main.mjs"
 }
diff --git a/test/fixtures/es-modules/test-esm-double-encoding-native%20.js b/test/fixtures/es-modules/test-esm-double-encoding-native%20.mjs
similarity index 86%
rename from test/fixtures/es-modules/test-esm-double-encoding-native%20.js
rename to test/fixtures/es-modules/test-esm-double-encoding-native%20.mjs
index ea1caa81be..a3bfe972e5 100644
--- a/test/fixtures/es-modules/test-esm-double-encoding-native%20.js
+++ b/test/fixtures/es-modules/test-esm-double-encoding-native%20.mjs
@@ -3,4 +3,4 @@
 // Trivial test to assert we can load files with `%` in their pathname.
 // Imported by `test-esm-double-encoding.mjs`.
 
-module.exports = 42;
+export default 42;
diff --git a/test/fixtures/esm-package-scope/legacy-loader/a.js b/test/fixtures/esm-package-scope/legacy-loader/a.js
new file mode 100644
index 0000000000..2e7700bc63
--- /dev/null
+++ b/test/fixtures/esm-package-scope/legacy-loader/a.js
@@ -0,0 +1 @@
+module.exports = 'a';
diff --git a/test/fixtures/esm-package-scope/legacy-loader/b.mjs b/test/fixtures/esm-package-scope/legacy-loader/b.mjs
new file mode 100644
index 0000000000..137b8ce642
--- /dev/null
+++ b/test/fixtures/esm-package-scope/legacy-loader/b.mjs
@@ -0,0 +1 @@
+export const b = 'b';
diff --git a/test/fixtures/esm-package-scope/legacy-loader/c.cjs b/test/fixtures/esm-package-scope/legacy-loader/c.cjs
new file mode 100644
index 0000000000..2d5312952f
--- /dev/null
+++ b/test/fixtures/esm-package-scope/legacy-loader/c.cjs
@@ -0,0 +1,5 @@
+module.exports = {
+  one: 1,
+  two: 2,
+  three: 3
+};
diff --git a/test/fixtures/esm-package-scope/legacy-loader/index.mjs b/test/fixtures/esm-package-scope/legacy-loader/index.mjs
new file mode 100644
index 0000000000..1c78c389a2
--- /dev/null
+++ b/test/fixtures/esm-package-scope/legacy-loader/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 '../new-loader/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 'legacy-loader';
diff --git a/test/fixtures/esm-package-scope/legacy-loader/package.json b/test/fixtures/esm-package-scope/legacy-loader/package.json
new file mode 100644
index 0000000000..215a962248
--- /dev/null
+++ b/test/fixtures/esm-package-scope/legacy-loader/package.json
@@ -0,0 +1,13 @@
+{
+  "name": "legacy-loader",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [],
+  "author": "Myles Borins <mylesborins@google.com>",
+  "license": "Apache-2.0",
+  "type": "commonjs"
+}
diff --git a/test/fixtures/esm-package-scope/new-loader/a.js b/test/fixtures/esm-package-scope/new-loader/a.js
new file mode 100644
index 0000000000..90bd54cd7f
--- /dev/null
+++ b/test/fixtures/esm-package-scope/new-loader/a.js
@@ -0,0 +1 @@
+export default 'a'
diff --git a/test/fixtures/esm-package-scope/new-loader/b.mjs b/test/fixtures/esm-package-scope/new-loader/b.mjs
new file mode 100644
index 0000000000..137b8ce642
--- /dev/null
+++ b/test/fixtures/esm-package-scope/new-loader/b.mjs
@@ -0,0 +1 @@
+export const b = 'b';
diff --git a/test/fixtures/esm-package-scope/new-loader/c.cjs b/test/fixtures/esm-package-scope/new-loader/c.cjs
new file mode 100644
index 0000000000..2d5312952f
--- /dev/null
+++ b/test/fixtures/esm-package-scope/new-loader/c.cjs
@@ -0,0 +1,5 @@
+module.exports = {
+  one: 1,
+  two: 2,
+  three: 3
+};
diff --git a/test/fixtures/esm-package-scope/new-loader/index.js b/test/fixtures/esm-package-scope/new-loader/index.js
new file mode 100644
index 0000000000..98c536cc34
--- /dev/null
+++ b/test/fixtures/esm-package-scope/new-loader/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 '../legacy-loader/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 'new-loader';
diff --git a/test/fixtures/esm-package-scope/new-loader/package.json b/test/fixtures/esm-package-scope/new-loader/package.json
new file mode 100644
index 0000000000..1f2c704322
--- /dev/null
+++ b/test/fixtures/esm-package-scope/new-loader/package.json
@@ -0,0 +1,13 @@
+{
+  "name": "new-loader",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [],
+  "author": "Myles Borins <mylesborins@google.com>",
+  "license": "Apache-2.0",
+  "type": "module"
+}
diff --git a/test/fixtures/syntax/bad_syntax.mjs b/test/fixtures/syntax/bad_syntax.mjs
new file mode 100644
index 0000000000..c2cd118b23
--- /dev/null
+++ b/test/fixtures/syntax/bad_syntax.mjs
@@ -0,0 +1 @@
+var foo bar;
diff --git a/test/message/esm_display_syntax_error.out b/test/message/esm_display_syntax_error.out
index 1c68ecb952..32fcf8008a 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.<anonymous> (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 87cedf1d4e..12d10270e9 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 edbbde9f2d..a3601d6cb4 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 32c0edb350..a53bbbcd19 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 0512a9ac77..0daaeff5b9 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 e74b70bec8..5905d2a954 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 4e4cbf2ea3..deb036a369 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.<anonymous> (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 4fb24b24f3..ba93e13038 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 --type=module
+syntaxArgs.forEach(function(arg) {
+  const stdin = 'export var p = 5; var foo bar;';
+  const c = spawnSync(
+    node,
+    ['--experimental-modules', '--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 cdadf449d8..8ee11226d9 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 --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', '--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 db3cfa3582..5f47f191f5 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 3d20316647..9e7eab295e 100644
--- a/test/parallel/test-module-main-extension-lookup.js
+++ b/test/parallel/test-module-main-extension-lookup.js
@@ -6,6 +6,6 @@ const { execFileSync } = require('child_process');
 const node = process.argv[0];
 
 execFileSync(node, ['--experimental-modules',
-                    fixtures.path('es-modules', 'test-esm-ok')]);
+                    fixtures.path('es-modules', 'test-esm-ok.mjs')]);
 execFileSync(node, ['--experimental-modules',
                     fixtures.path('es-modules', 'noext')]);