diff --git a/doc/api/errors.md b/doc/api/errors.md
index b0b0753f400401..0496d5f52d718e 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -1111,6 +1111,14 @@ The JS execution context is not associated with a Node.js environment.
This may occur when Node.js is used as an embedded library and some hooks
for the JS engine are not set up properly.
+
+### `ERR_FAILED_IMPORT_ASSERTION`
+
+
+An import assertion has failed, preventing the specified module to be imported.
+
### `ERR_FALSY_VALUE_REJECTION`
@@ -1662,6 +1670,14 @@ for more information.
An invalid HTTP token was supplied.
+
+### `ERR_INVALID_IMPORT_ASSERTION`
+
+
+An import assertion is not supported by this version of Node.js.
+
### `ERR_INVALID_IP_ADDRESS`
@@ -1913,6 +1929,15 @@ strict compliance with the API specification (which in some cases may accept
`func(undefined)` and `func()` are treated identically, and the
[`ERR_INVALID_ARG_TYPE`][] error code may be used instead.
+
+### `ERR_MISSING_IMPORT_ASSERTION`
+
+
+An attempt was made to import a module without an assertion that requires
+a specific import assertion to be loaded.
+
### `ERR_MISSING_OPTION`
diff --git a/doc/api/esm.md b/doc/api/esm.md
index 9ec67330000907..f4662efd85499d 100644
--- a/doc/api/esm.md
+++ b/doc/api/esm.md
@@ -7,6 +7,9 @@
+
+The [Import Assertions proposal][] adds an inline syntax for module import
+statements to pass on more information alongside the module specifier.
+
+```js
+import json from './foo.json' assert { type: "json" };
+await import('foo.json', { assert: { type: "json" } });
+```
+
+Node.js supports the following `type` values:
+
+| `type` | Resolves to |
+| -------- | ---------------- |
+| `"json"` | [JSON modules][] |
+
## Builtin modules
[Core modules][] provide named exports of their public API. A
@@ -522,9 +544,8 @@ same path.
Assuming an `index.mjs` with
-
```js
-import packageConfig from './package.json';
+import packageConfig from './package.json' assert { type: 'json' };
```
The `--experimental-json-modules` flag is needed for the module
@@ -1355,6 +1376,8 @@ success!
[Dynamic `import()`]: https://wiki.developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Dynamic_Imports
[ECMAScript Top-Level `await` proposal]: https://github.com/tc39/proposal-top-level-await/
[ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration
+[Import Assertions proposal]: https://github.com/tc39/proposal-import-assertions
+[JSON modules]: #json-modules
[Node.js Module Resolution Algorithm]: #resolver-algorithm-specification
[Terminology]: #terminology
[URL]: https://url.spec.whatwg.org/
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 755f1b2b86176d..418b4d747146ad 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -953,6 +953,9 @@ E('ERR_ENCODING_NOT_SUPPORTED', 'The "%s" encoding is not supported',
RangeError);
E('ERR_EVAL_ESM_CANNOT_PRINT', '--print cannot be used with ESM input', Error);
E('ERR_EVENT_RECURSION', 'The event "%s" is already being dispatched', Error);
+E('ERR_FAILED_IMPORT_ASSERTION', (request, key, expectedValue, actualValue) => {
+ return `Failed to load module "${request}", expected ${key} to be ${JSONStringify(expectedValue)}, got ${JSONStringify(actualValue)} instead`;
+}, TypeError);
E('ERR_FALSY_VALUE_REJECTION', function(reason) {
this.reason = reason;
return 'Promise was rejected with falsy value';
@@ -1250,6 +1253,9 @@ E('ERR_INVALID_FILE_URL_HOST',
E('ERR_INVALID_FILE_URL_PATH', 'File URL path %s', TypeError);
E('ERR_INVALID_HANDLE_TYPE', 'This handle type cannot be sent', TypeError);
E('ERR_INVALID_HTTP_TOKEN', '%s must be a valid HTTP token ["%s"]', TypeError);
+E('ERR_INVALID_IMPORT_ASSERTION',
+ (type, value) => `Invalid ${JSONStringify(type)} import assertion: ${JSONStringify(value)}`,
+ TypeError);
E('ERR_INVALID_IP_ADDRESS', 'Invalid IP address: %s', TypeError);
E('ERR_INVALID_MODULE_SPECIFIER', (request, reason, base = undefined) => {
return `Invalid module "${request}" ${reason}${base ?
@@ -1394,6 +1400,9 @@ E('ERR_MISSING_ARGS',
}
return `${msg} must be specified`;
}, TypeError);
+E('ERR_MISSING_IMPORT_ASSERTION',
+ 'Failed to load %s: Node.js requires modules of format "%s" to be loaded ' +
+ 'using an assertion "%s" with value "%s"', TypeError);
E('ERR_MISSING_OPTION', '%s is required', TypeError);
E('ERR_MODULE_NOT_FOUND', (path, base, type = 'package') => {
return `Cannot find ${type} '${path}' imported from ${base}`;
diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js
index 622805ea78fd0c..e0f40ffa2ecf50 100644
--- a/lib/internal/modules/cjs/loader.js
+++ b/lib/internal/modules/cjs/loader.js
@@ -1015,9 +1015,10 @@ function wrapSafe(filename, content, cjsModuleInstance) {
filename,
lineOffset: 0,
displayErrors: true,
- importModuleDynamically: async (specifier) => {
+ importModuleDynamically: async (specifier, _, importAssertions) => {
const loader = asyncESM.esmLoader;
- return loader.import(specifier, normalizeReferrerURL(filename));
+ return loader.import(specifier, normalizeReferrerURL(filename),
+ importAssertions);
},
});
}
@@ -1030,9 +1031,10 @@ function wrapSafe(filename, content, cjsModuleInstance) {
'__dirname',
], {
filename,
- importModuleDynamically(specifier) {
+ importModuleDynamically(specifier, _, importAssertions) {
const loader = asyncESM.esmLoader;
- return loader.import(specifier, normalizeReferrerURL(filename));
+ return loader.import(specifier, normalizeReferrerURL(filename),
+ importAssertions);
},
});
} catch (err) {
diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js
index b12a87a9021242..4c79a0c8b124b4 100644
--- a/lib/internal/modules/esm/loader.js
+++ b/lib/internal/modules/esm/loader.js
@@ -6,11 +6,13 @@ require('internal/modules/cjs/loader');
const {
Array,
ArrayIsArray,
+ ArrayPrototypeIncludes,
ArrayPrototypeJoin,
ArrayPrototypePush,
FunctionPrototypeBind,
FunctionPrototypeCall,
ObjectCreate,
+ ObjectFreeze,
ObjectSetPrototypeOf,
PromiseAll,
RegExpPrototypeExec,
@@ -20,11 +22,14 @@ const {
} = primordials;
const {
+ ERR_FAILED_IMPORT_ASSERTION,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
+ ERR_INVALID_IMPORT_ASSERTION,
ERR_INVALID_MODULE_SPECIFIER,
ERR_INVALID_RETURN_PROPERTY_VALUE,
ERR_INVALID_RETURN_VALUE,
+ ERR_MISSING_IMPORT_ASSERTION,
ERR_UNKNOWN_MODULE_FORMAT
} = require('internal/errors').codes;
const { pathToFileURL, isURLInstance } = require('internal/url');
@@ -44,6 +49,10 @@ const { translators } = require(
'internal/modules/esm/translators');
const { getOptionValue } = require('internal/options');
+const importAssertionTypeCache = new SafeWeakMap();
+const finalFormatCache = new SafeWeakMap();
+const supportedTypes = ObjectFreeze([undefined, 'json']);
+
/**
* An ESMLoader instance is used as the main entry point for loading ES modules.
* Currently, this is a singleton -- there is only one used for loading
@@ -202,8 +211,8 @@ class ESMLoader {
const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
const module = new ModuleWrap(url, undefined, source, 0, 0);
callbackMap.set(module, {
- importModuleDynamically: (specifier, { url }) => {
- return this.import(specifier, url);
+ importModuleDynamically: (specifier, { url }, importAssertions) => {
+ return this.import(specifier, url, importAssertions);
}
});
@@ -211,6 +220,7 @@ class ESMLoader {
};
const job = new ModuleJob(this, url, evalInstance, false, false);
this.moduleMap.set(url, job);
+ finalFormatCache.set(job, 'module');
const { module } = await job.run();
return {
@@ -218,17 +228,57 @@ class ESMLoader {
};
}
- async getModuleJob(specifier, parentURL) {
+ async getModuleJob(specifier, parentURL, importAssertions) {
+ if (!ArrayPrototypeIncludes(supportedTypes, importAssertions.type)) {
+ throw new ERR_INVALID_IMPORT_ASSERTION('type', importAssertions.type);
+ }
+
const { format, url } = await this.resolve(specifier, parentURL);
let job = this.moduleMap.get(url);
// CommonJS will set functions for lazy job evaluation.
if (typeof job === 'function') this.moduleMap.set(url, job = job());
- if (job !== undefined) return job;
+ if (job != null) {
+ const currentImportAssertionType = importAssertionTypeCache.get(job);
+ if (currentImportAssertionType === importAssertions.type) return job;
+
+ try {
+ // To avoid race conditions, wait for previous module to fulfill first.
+ await job.modulePromise;
+ } catch {
+ // If the other job failed with a different `type` assertion, we got
+ // another chance.
+ job = undefined;
+ }
+
+ if (job !== undefined) {
+ const finalFormat = finalFormatCache.get(job);
+ if (importAssertions.type == null && finalFormat === 'json') {
+ throw new ERR_MISSING_IMPORT_ASSERTION(url, finalFormat,
+ 'type', 'json');
+ }
+ if (
+ importAssertions.type == null ||
+ (importAssertions.type === 'json' && finalFormat === 'json')
+ ) return job;
+ throw new ERR_FAILED_IMPORT_ASSERTION(
+ url, 'type', importAssertions.type, finalFormat);
+ }
+ }
const moduleProvider = async (url, isMain) => {
const { format: finalFormat, source } = await this.load(url, { format });
+ if (importAssertions.type === 'json' && finalFormat !== 'json') {
+ throw new ERR_FAILED_IMPORT_ASSERTION(
+ url, 'type', importAssertions.type, finalFormat);
+ }
+ if (importAssertions.type !== 'json' && finalFormat === 'json') {
+ throw new ERR_MISSING_IMPORT_ASSERTION(url, finalFormat,
+ 'type', 'json');
+ }
+ finalFormatCache.set(job, finalFormat);
+
const translator = translators.get(finalFormat);
if (!translator) throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat);
@@ -249,6 +299,7 @@ class ESMLoader {
inspectBrk
);
+ importAssertionTypeCache.set(job, importAssertions.type);
this.moduleMap.set(url, job);
return job;
@@ -262,10 +313,11 @@ class ESMLoader {
* loader module.
*
* @param {string | string[]} specifiers Path(s) to the module
- * @param {string} [parentURL] Path of the parent importing the module
- * @returns {object | object[]} A list of module export(s)
+ * @param {string} parentURL Path of the parent importing the module
+ * @param {Record>} importAssertions
+ * @returns {Promise