diff --git a/doc/api/errors.md b/doc/api/errors.md
index 78106c938019da..d2812b47a38dd5 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -1689,6 +1689,36 @@ is set for the `Http2Stream`.
An attempt was made to construct an object using a non-public constructor.
+
+
+### `ERR_IMPORT_ASSERTION_TYPE_FAILED`
+
+
+
+An import assertion has failed, preventing the specified module to be imported.
+
+
+
+### `ERR_IMPORT_ASSERTION_TYPE_MISSING`
+
+
+
+An import assertion is missing, preventing the specified module to be imported.
+
+
+
+### `ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED`
+
+
+
+An import assertion is not supported by this version of Node.js.
+
### `ERR_INCOMPATIBLE_OPTION_PAIR`
diff --git a/doc/api/esm.md b/doc/api/esm.md
index 0013f882a4435b..8f0b2e732077c2 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 fooData from './foo.json' assert { type: 'json' };
+
+const { default: barData } =
+ await import('./bar.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
@@ -516,10 +541,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
@@ -607,12 +630,20 @@ CommonJS modules loaded.
#### `resolve(specifier, context, defaultResolve)`
+
+
> Note: The loaders API is being redesigned. This hook may disappear or its
> signature may change. Do not rely on the API described below.
* `specifier` {string}
* `context` {Object}
* `conditions` {string\[]}
+ * `importAssertions` {Object}
* `parentURL` {string|undefined}
* `defaultResolve` {Function} The Node.js default resolver.
* Returns: {Object}
@@ -689,13 +720,15 @@ export async function resolve(specifier, context, defaultResolve) {
* `context` {Object}
* `format` {string|null|undefined} The format optionally supplied by the
`resolve` hook.
+ * `importAssertions` {Object}
* `defaultLoad` {Function}
* Returns: {Object}
* `format` {string}
* `source` {string|ArrayBuffer|TypedArray}
The `load` hook provides a way to define a custom method of determining how
-a URL should be interpreted, retrieved, and parsed.
+a URL should be interpreted, retrieved, and parsed. It is also in charge of
+validating the import assertion.
The final value of `format` must be one of the following:
@@ -1357,6 +1390,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..8d7a369a62299c 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -1083,6 +1083,12 @@ E('ERR_HTTP_SOCKET_ENCODING',
E('ERR_HTTP_TRAILER_INVALID',
'Trailers are invalid with this transfer encoding', Error);
E('ERR_ILLEGAL_CONSTRUCTOR', 'Illegal constructor', TypeError);
+E('ERR_IMPORT_ASSERTION_TYPE_FAILED',
+ 'Module "%s" is not of type "%s"', TypeError);
+E('ERR_IMPORT_ASSERTION_TYPE_MISSING',
+ 'Module "%s" needs an import assertion of type "%s"', TypeError);
+E('ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED',
+ 'Import assertion type "%s" is unsupported', TypeError);
E('ERR_INCOMPATIBLE_OPTION_PAIR',
'Option "%s" cannot be used in combination with option "%s"', TypeError);
E('ERR_INPUT_TYPE_NOT_ALLOWED', '--input-type can only be used with string ' +
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/assert.js b/lib/internal/modules/esm/assert.js
new file mode 100644
index 00000000000000..e7d8cbb519fb79
--- /dev/null
+++ b/lib/internal/modules/esm/assert.js
@@ -0,0 +1,102 @@
+'use strict';
+
+const {
+ ArrayPrototypeIncludes,
+ ObjectCreate,
+ ObjectValues,
+ ObjectPrototypeHasOwnProperty,
+ Symbol,
+} = primordials;
+const { validateString } = require('internal/validators');
+
+const {
+ ERR_IMPORT_ASSERTION_TYPE_FAILED,
+ ERR_IMPORT_ASSERTION_TYPE_MISSING,
+ ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED,
+} = require('internal/errors').codes;
+
+const kImplicitAssertType = Symbol('implicit assert type');
+
+/**
+ * Define a map of module formats to import assertion types (the value of `type`
+ * in `assert { type: 'json' }`).
+ * @type {Map} importAssertions Validations for the
+ * module import.
+ * @returns {true}
+ * @throws {TypeError} If the format and assertion type are incompatible.
+ */
+function validateAssertions(url, format,
+ importAssertions = ObjectCreate(null)) {
+ const validType = formatTypeMap[format];
+
+ switch (validType) {
+ case undefined:
+ // Ignore assertions for module types we don't recognize, to allow new
+ // formats in the future.
+ return true;
+
+ case importAssertions.type:
+ // The asserted type is the valid type for this format.
+ return true;
+
+ case kImplicitAssertType:
+ // This format doesn't allow an import assertion type, so the property
+ // must not be set on the import assertions object.
+ if (!ObjectPrototypeHasOwnProperty(importAssertions, 'type')) {
+ return true;
+ }
+ return handleInvalidType(url, importAssertions.type);
+
+ default:
+ // There is an expected type for this format, but the value of
+ // `importAssertions.type` was not it.
+ if (!ObjectPrototypeHasOwnProperty(importAssertions, 'type')) {
+ // `type` wasn't specified at all.
+ throw new ERR_IMPORT_ASSERTION_TYPE_MISSING(url, validType);
+ }
+ handleInvalidType(url, importAssertions.type);
+ }
+}
+
+/**
+ * Throw the correct error depending on what's wrong with the type assertion.
+ * @param {string} url The resolved URL for the module to be imported
+ * @param {string} type The value of the import assertion `type` property
+ */
+function handleInvalidType(url, type) {
+ // `type` might have not been a string.
+ validateString(type, 'type');
+
+ // `type` was not one of the types we understand.
+ if (!ArrayPrototypeIncludes(supportedAssertionTypes, type)) {
+ throw new ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED(type);
+ }
+
+ // `type` was the wrong value for this format.
+ throw new ERR_IMPORT_ASSERTION_TYPE_FAILED(url, type);
+}
+
+
+module.exports = {
+ kImplicitAssertType,
+ validateAssertions,
+};
diff --git a/lib/internal/modules/esm/load.js b/lib/internal/modules/esm/load.js
index 38785e78f338ce..67123792e8903a 100644
--- a/lib/internal/modules/esm/load.js
+++ b/lib/internal/modules/esm/load.js
@@ -3,14 +3,26 @@
const { defaultGetFormat } = require('internal/modules/esm/get_format');
const { defaultGetSource } = require('internal/modules/esm/get_source');
const { translators } = require('internal/modules/esm/translators');
+const { validateAssertions } = require('internal/modules/esm/assert');
+/**
+ * Node.js default load hook.
+ * @param {string} url
+ * @param {object} context
+ * @returns {object}
+ */
async function defaultLoad(url, context) {
let {
format,
source,
} = context;
+ const { importAssertions } = context;
- if (!translators.has(format)) format = defaultGetFormat(url);
+ if (!format || !translators.has(format)) {
+ format = defaultGetFormat(url);
+ }
+
+ validateAssertions(url, format, importAssertions);
if (
format === 'builtin' ||
diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js
index b12a87a9021242..3b8d2ae158f930 100644
--- a/lib/internal/modules/esm/loader.js
+++ b/lib/internal/modules/esm/loader.js
@@ -10,6 +10,7 @@ const {
ArrayPrototypePush,
FunctionPrototypeBind,
FunctionPrototypeCall,
+ ObjectAssign,
ObjectCreate,
ObjectSetPrototypeOf,
PromiseAll,
@@ -202,15 +203,16 @@ 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);
}
});
return module;
};
- const job = new ModuleJob(this, url, evalInstance, false, false);
- this.moduleMap.set(url, job);
+ const job = new ModuleJob(
+ this, url, undefined, evalInstance, false, false);
+ this.moduleMap.set(url, undefined, job);
const { module } = await job.run();
return {
@@ -218,20 +220,65 @@ class ESMLoader {
};
}
- async getModuleJob(specifier, parentURL) {
- const { format, url } = await this.resolve(specifier, parentURL);
- let job = this.moduleMap.get(url);
+ /**
+ * Get a (possibly still pending) module job from the cache,
+ * or create one and return its Promise.
+ * @param {string} specifier The string after `from` in an `import` statement,
+ * or the first parameter of an `import()`
+ * expression
+ * @param {string | undefined} parentURL The URL of the module importing this
+ * one, unless this is the Node.js entry
+ * point.
+ * @param {Record} importAssertions Validations for the
+ * module import.
+ * @returns {Promise} The (possibly pending) module job
+ */
+ async getModuleJob(specifier, parentURL, importAssertions) {
+ let importAssertionsForResolve;
+ if (this.#loaders.length !== 1) {
+ // We can skip cloning if there are no user provided loaders because
+ // the Node.js default resolve hook does not use import assertions.
+ importAssertionsForResolve =
+ ObjectAssign(ObjectCreate(null), importAssertions);
+ }
+ const { format, url } =
+ await this.resolve(specifier, parentURL, importAssertionsForResolve);
+
+ let job = this.moduleMap.get(url, importAssertions.type);
+
// CommonJS will set functions for lazy job evaluation.
- if (typeof job === 'function') this.moduleMap.set(url, job = job());
+ if (typeof job === 'function') {
+ this.moduleMap.set(url, undefined, job = job());
+ }
+
+ if (job === undefined) {
+ job = this.#createModuleJob(url, importAssertions, parentURL, format);
+ }
- if (job !== undefined) return job;
+ return job;
+ }
+ /**
+ * Create and cache an object representing a loaded module.
+ * @param {string} url The absolute URL that was resolved for this module
+ * @param {Record} importAssertions Validations for the
+ * module import.
+ * @param {string} [parentURL] The absolute URL of the module importing this
+ * one, unless this is the Node.js entry point
+ * @param {string} [format] The format hint possibly returned by the
+ * `resolve` hook
+ * @returns {Promise} The (possibly pending) module job
+ */
+ #createModuleJob(url, importAssertions, parentURL, format) {
const moduleProvider = async (url, isMain) => {
- const { format: finalFormat, source } = await this.load(url, { format });
+ const { format: finalFormat, source } = await this.load(
+ url, { format, importAssertions });
const translator = translators.get(finalFormat);
- if (!translator) throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat);
+ if (!translator) {
+ throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat);
+ }
return FunctionPrototypeCall(translator, this, url, source, isMain);
};
@@ -241,15 +288,16 @@ class ESMLoader {
getOptionValue('--inspect-brk')
);
- job = new ModuleJob(
+ const job = new ModuleJob(
this,
url,
+ importAssertions,
moduleProvider,
parentURL === undefined,
inspectBrk
);
- this.moduleMap.set(url, job);
+ this.moduleMap.set(url, importAssertions.type, job);
return job;
}
@@ -261,11 +309,13 @@ class ESMLoader {
* This method must NOT be renamed: it functions as a dynamic import on a
* 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 | string[]} specifiers Path(s) to the module.
+ * @param {string} parentURL Path of the parent importing the module.
+ * @param {Record} importAssertions Validations for the
+ * module import.
+ * @returns {Promise} A list of module export(s).
*/
- async import(specifiers, parentURL) {
+ async import(specifiers, parentURL, importAssertions) {
const wasArr = ArrayIsArray(specifiers);
if (!wasArr) specifiers = [specifiers];
@@ -273,7 +323,7 @@ class ESMLoader {
const jobs = new Array(count);
for (let i = 0; i < count; i++) {
- jobs[i] = this.getModuleJob(specifiers[i], parentURL)
+ jobs[i] = this.getModuleJob(specifiers[i], parentURL, importAssertions)
.then((job) => job.run())
.then(({ module }) => module.getNamespace());
}
@@ -393,13 +443,16 @@ class ESMLoader {
* Resolve the location of the module.
*
* The internals of this WILL change when chaining is implemented,
- * depending on the resolution/consensus from #36954
+ * depending on the resolution/consensus from #36954.
* @param {string} originalSpecifier The specified URL path of the module to
- * be resolved
- * @param {String} parentURL The URL path of the module's parent
- * @returns {{ url: String }}
+ * be resolved.
+ * @param {string} [parentURL] The URL path of the module's parent.
+ * @param {ImportAssertions} [importAssertions] Assertions from the import
+ * statement or expression.
+ * @returns {{ url: string }}
*/
- async resolve(originalSpecifier, parentURL) {
+ async resolve(originalSpecifier, parentURL,
+ importAssertions = ObjectCreate(null)) {
const isMain = parentURL === undefined;
if (
@@ -423,6 +476,7 @@ class ESMLoader {
originalSpecifier,
{
conditions,
+ importAssertions,
parentURL,
},
defaultResolver,
diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js
index 2f699376d6eaea..018d598796f153 100644
--- a/lib/internal/modules/esm/module_job.js
+++ b/lib/internal/modules/esm/module_job.js
@@ -6,6 +6,7 @@ const {
ArrayPrototypePush,
ArrayPrototypeSome,
FunctionPrototype,
+ ObjectCreate,
ObjectSetPrototypeOf,
PromiseAll,
PromiseResolve,
@@ -52,8 +53,10 @@ const isCommonJSGlobalLikeNotDefinedError = (errorMessage) =>
class ModuleJob {
// `loader` is the Loader instance used for loading dependencies.
// `moduleProvider` is a function
- constructor(loader, url, moduleProvider, isMain, inspectBrk) {
+ constructor(loader, url, importAssertions = ObjectCreate(null),
+ moduleProvider, isMain, inspectBrk) {
this.loader = loader;
+ this.importAssertions = importAssertions;
this.isMain = isMain;
this.inspectBrk = inspectBrk;
@@ -72,8 +75,8 @@ class ModuleJob {
// so that circular dependencies can't cause a deadlock by two of
// these `link` callbacks depending on each other.
const dependencyJobs = [];
- const promises = this.module.link(async (specifier) => {
- const jobPromise = this.loader.getModuleJob(specifier, url);
+ const promises = this.module.link(async (specifier, assertions) => {
+ const jobPromise = this.loader.getModuleJob(specifier, url, assertions);
ArrayPrototypePush(dependencyJobs, jobPromise);
const job = await jobPromise;
return job.modulePromise;
@@ -144,7 +147,14 @@ class ModuleJob {
const { url: childFileURL } = await this.loader.resolve(
childSpecifier, parentFileUrl,
);
- const { format } = await this.loader.load(childFileURL);
+ let format;
+ try {
+ // This might throw for non-CommonJS modules because we aren't passing
+ // in the import assertions and some formats require them; but we only
+ // care about CommonJS for the purposes of this error message.
+ ({ format } =
+ await this.loader.load(childFileURL));
+ } catch {}
if (format === 'commonjs') {
const importStatement = splitStack[1];
diff --git a/lib/internal/modules/esm/module_map.js b/lib/internal/modules/esm/module_map.js
index 9e1116a5647f5f..d51986dd700c85 100644
--- a/lib/internal/modules/esm/module_map.js
+++ b/lib/internal/modules/esm/module_map.js
@@ -1,7 +1,9 @@
'use strict';
const ModuleJob = require('internal/modules/esm/module_job');
+const { kImplicitAssertType } = require('internal/modules/esm/assert');
const {
+ ObjectCreate,
SafeMap,
} = primordials;
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
@@ -10,25 +12,35 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes;
const { validateString } = require('internal/validators');
+const validateAssertType = (type) =>
+ type === kImplicitAssertType || validateString(type, 'type');
+
// Tracks the state of the loader-level module cache
class ModuleMap extends SafeMap {
constructor(i) { super(i); } // eslint-disable-line no-useless-constructor
- get(url) {
+ get(url, type = kImplicitAssertType) {
validateString(url, 'url');
- return super.get(url);
+ validateAssertType(type);
+ return super.get(url)?.[type];
}
- set(url, job) {
+ set(url, type = kImplicitAssertType, job) {
validateString(url, 'url');
+ validateAssertType(type);
if (job instanceof ModuleJob !== true &&
typeof job !== 'function') {
throw new ERR_INVALID_ARG_TYPE('job', 'ModuleJob', job);
}
- debug(`Storing ${url} in ModuleMap`);
- return super.set(url, job);
+ debug(`Storing ${url} (${
+ type === kImplicitAssertType ? 'implicit type' : type
+ }) in ModuleMap`);
+ const cachedJobsForUrl = super.get(url) ?? ObjectCreate(null);
+ cachedJobsForUrl[type] = job;
+ return super.set(url, cachedJobsForUrl);
}
- has(url) {
+ has(url, type = kImplicitAssertType) {
validateString(url, 'url');
- return super.has(url);
+ validateAssertType(type);
+ return super.get(url)?.[type] !== undefined;
}
}
module.exports = ModuleMap;
diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js
index ba00041c417706..157e23044b07fb 100644
--- a/lib/internal/modules/esm/translators.js
+++ b/lib/internal/modules/esm/translators.js
@@ -107,8 +107,8 @@ function errPath(url) {
return url;
}
-async function importModuleDynamically(specifier, { url }) {
- return asyncESM.esmLoader.import(specifier, url);
+async function importModuleDynamically(specifier, { url }, assertions) {
+ return asyncESM.esmLoader.import(specifier, url, assertions);
}
function createImportMetaResolve(defaultParentUrl) {
diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js
index d0c08b75e7a524..9a0263024144fb 100644
--- a/lib/internal/modules/run_main.js
+++ b/lib/internal/modules/run_main.js
@@ -1,6 +1,7 @@
'use strict';
const {
+ ObjectCreate,
StringPrototypeEndsWith,
} = primordials;
const CJSLoader = require('internal/modules/cjs/loader');
@@ -46,9 +47,8 @@ function runMainESM(mainPath) {
handleMainPromise(loadESM((esmLoader) => {
const main = path.isAbsolute(mainPath) ?
- pathToFileURL(mainPath).href :
- mainPath;
- return esmLoader.import(main);
+ pathToFileURL(mainPath).href : mainPath;
+ return esmLoader.import(main, undefined, ObjectCreate(null));
}));
}
diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js
index 48c525057f7477..f28baeb538a528 100644
--- a/lib/internal/process/execution.js
+++ b/lib/internal/process/execution.js
@@ -82,9 +82,9 @@ function evalScript(name, body, breakFirstLine, print) {
filename: name,
displayErrors: true,
[kVmBreakFirstLineSymbol]: !!breakFirstLine,
- async importModuleDynamically(specifier) {
- const loader = await asyncESM.esmLoader;
- return loader.import(specifier, baseUrl);
+ importModuleDynamically(specifier, _, importAssertions) {
+ const loader = asyncESM.esmLoader;
+ return loader.import(specifier, baseUrl, importAssertions);
}
}));
if (print) {
diff --git a/lib/repl.js b/lib/repl.js
index 4ee8e24d47588c..c85ccbde5a44ac 100644
--- a/lib/repl.js
+++ b/lib/repl.js
@@ -454,8 +454,9 @@ function REPLServer(prompt,
vm.createScript(fallbackCode, {
filename: file,
displayErrors: true,
- importModuleDynamically: async (specifier) => {
- return asyncESM.esmLoader.import(specifier, parentURL);
+ importModuleDynamically: (specifier, _, importAssertions) => {
+ return asyncESM.esmLoader.import(specifier, parentURL,
+ importAssertions);
}
});
} catch (fallbackError) {
@@ -496,8 +497,9 @@ function REPLServer(prompt,
script = vm.createScript(code, {
filename: file,
displayErrors: true,
- importModuleDynamically: async (specifier) => {
- return asyncESM.esmLoader.import(specifier, parentURL);
+ importModuleDynamically: (specifier, _, importAssertions) => {
+ return asyncESM.esmLoader.import(specifier, parentURL,
+ importAssertions);
}
});
} catch (e) {
diff --git a/src/node.cc b/src/node.cc
index 0fa51b269764f4..254181b6fbdb25 100644
--- a/src/node.cc
+++ b/src/node.cc
@@ -803,6 +803,13 @@ int ProcessGlobalArgs(std::vector* args,
return 12;
}
+ // TODO(aduh95): remove this when the harmony-import-assertions flag
+ // is removed in V8.
+ if (std::find(v8_args.begin(), v8_args.end(),
+ "--no-harmony-import-assertions") == v8_args.end()) {
+ v8_args.push_back("--harmony-import-assertions");
+ }
+
auto env_opts = per_process::cli_options->per_isolate->per_env;
if (std::find(v8_args.begin(), v8_args.end(),
"--abort-on-uncaught-exception") != v8_args.end() ||
diff --git a/test/es-module/test-esm-assertionless-json-import.js b/test/es-module/test-esm-assertionless-json-import.js
new file mode 100644
index 00000000000000..2f06508dd2e509
--- /dev/null
+++ b/test/es-module/test-esm-assertionless-json-import.js
@@ -0,0 +1,81 @@
+// Flags: --experimental-json-modules --experimental-loader ./test/fixtures/es-module-loaders/assertionless-json-import.mjs
+'use strict';
+const common = require('../common');
+const { strictEqual } = require('assert');
+
+async function test() {
+ {
+ const [secret0, secret1] = await Promise.all([
+ import('../fixtures/experimental.json'),
+ import(
+ '../fixtures/experimental.json',
+ { assert: { type: 'json' } }
+ ),
+ ]);
+
+ strictEqual(secret0.default.ofLife, 42);
+ strictEqual(secret1.default.ofLife, 42);
+ strictEqual(secret0.default, secret1.default);
+ strictEqual(secret0, secret1);
+ }
+
+ {
+ const [secret0, secret1] = await Promise.all([
+ import('../fixtures/experimental.json?test'),
+ import(
+ '../fixtures/experimental.json?test',
+ { assert: { type: 'json' } }
+ ),
+ ]);
+
+ strictEqual(secret0.default.ofLife, 42);
+ strictEqual(secret1.default.ofLife, 42);
+ strictEqual(secret0.default, secret1.default);
+ strictEqual(secret0, secret1);
+ }
+
+ {
+ const [secret0, secret1] = await Promise.all([
+ import('../fixtures/experimental.json#test'),
+ import(
+ '../fixtures/experimental.json#test',
+ { assert: { type: 'json' } }
+ ),
+ ]);
+
+ strictEqual(secret0.default.ofLife, 42);
+ strictEqual(secret1.default.ofLife, 42);
+ strictEqual(secret0.default, secret1.default);
+ strictEqual(secret0, secret1);
+ }
+
+ {
+ const [secret0, secret1] = await Promise.all([
+ import('../fixtures/experimental.json?test2#test'),
+ import(
+ '../fixtures/experimental.json?test2#test',
+ { assert: { type: 'json' } }
+ ),
+ ]);
+
+ strictEqual(secret0.default.ofLife, 42);
+ strictEqual(secret1.default.ofLife, 42);
+ strictEqual(secret0.default, secret1.default);
+ strictEqual(secret0, secret1);
+ }
+
+ {
+ const [secret0, secret1] = await Promise.all([
+ import('data:application/json,{"ofLife":42}'),
+ import(
+ 'data:application/json,{"ofLife":42}',
+ { assert: { type: 'json' } }
+ ),
+ ]);
+
+ strictEqual(secret0.default.ofLife, 42);
+ strictEqual(secret1.default.ofLife, 42);
+ }
+}
+
+test().then(common.mustCall());
diff --git a/test/es-module/test-esm-data-urls.js b/test/es-module/test-esm-data-urls.js
index 886a2f45379973..85a693b54221a7 100644
--- a/test/es-module/test-esm-data-urls.js
+++ b/test/es-module/test-esm-data-urls.js
@@ -59,21 +59,22 @@ function createBase64URL(mime, body) {
assert.strictEqual(ns.default, plainESMURL);
}
{
- const ns = await import('data:application/json;foo="test,"this"');
+ const ns = await import('data:application/json;foo="test,"this"',
+ { assert: { type: 'json' } });
assert.deepStrictEqual(Object.keys(ns), ['default']);
assert.strictEqual(ns.default, 'this');
}
{
const ns = await import(`data:application/json;foo=${
encodeURIComponent('test,')
- },0`);
+ },0`, { assert: { type: 'json' } });
assert.deepStrictEqual(Object.keys(ns), ['default']);
assert.strictEqual(ns.default, 0);
}
{
- await assert.rejects(async () => {
- return import('data:application/json;foo="test,",0');
- }, {
+ await assert.rejects(async () =>
+ import('data:application/json;foo="test,",0',
+ { assert: { type: 'json' } }), {
name: 'SyntaxError',
message: /Unexpected end of JSON input/
});
@@ -81,14 +82,14 @@ function createBase64URL(mime, body) {
{
const body = '{"x": 1}';
const plainESMURL = createURL('application/json', body);
- const ns = await import(plainESMURL);
+ const ns = await import(plainESMURL, { assert: { type: 'json' } });
assert.deepStrictEqual(Object.keys(ns), ['default']);
assert.strictEqual(ns.default.x, 1);
}
{
const body = '{"default": 2}';
const plainESMURL = createURL('application/json', body);
- const ns = await import(plainESMURL);
+ const ns = await import(plainESMURL, { assert: { type: 'json' } });
assert.deepStrictEqual(Object.keys(ns), ['default']);
assert.strictEqual(ns.default.default, 2);
}
diff --git a/test/es-module/test-esm-dynamic-import-assertion.js b/test/es-module/test-esm-dynamic-import-assertion.js
new file mode 100644
index 00000000000000..c6ff97d790a44c
--- /dev/null
+++ b/test/es-module/test-esm-dynamic-import-assertion.js
@@ -0,0 +1,48 @@
+// Flags: --experimental-json-modules
+'use strict';
+const common = require('../common');
+const { strictEqual } = require('assert');
+
+async function test() {
+ {
+ const results = await Promise.allSettled([
+ import('../fixtures/empty.js', { assert: { type: 'json' } }),
+ import('../fixtures/empty.js'),
+ ]);
+
+ strictEqual(results[0].status, 'rejected');
+ strictEqual(results[1].status, 'fulfilled');
+ }
+
+ {
+ const results = await Promise.allSettled([
+ import('../fixtures/empty.js'),
+ import('../fixtures/empty.js', { assert: { type: 'json' } }),
+ ]);
+
+ strictEqual(results[0].status, 'fulfilled');
+ strictEqual(results[1].status, 'rejected');
+ }
+
+ {
+ const results = await Promise.allSettled([
+ import('../fixtures/empty.json', { assert: { type: 'json' } }),
+ import('../fixtures/empty.json'),
+ ]);
+
+ strictEqual(results[0].status, 'fulfilled');
+ strictEqual(results[1].status, 'rejected');
+ }
+
+ {
+ const results = await Promise.allSettled([
+ import('../fixtures/empty.json'),
+ import('../fixtures/empty.json', { assert: { type: 'json' } }),
+ ]);
+
+ strictEqual(results[0].status, 'rejected');
+ strictEqual(results[1].status, 'fulfilled');
+ }
+}
+
+test().then(common.mustCall());
diff --git a/test/es-module/test-esm-dynamic-import-assertion.mjs b/test/es-module/test-esm-dynamic-import-assertion.mjs
new file mode 100644
index 00000000000000..a53ea145479eb5
--- /dev/null
+++ b/test/es-module/test-esm-dynamic-import-assertion.mjs
@@ -0,0 +1,43 @@
+// Flags: --experimental-json-modules
+import '../common/index.mjs';
+import { strictEqual } from 'assert';
+
+{
+ const results = await Promise.allSettled([
+ import('../fixtures/empty.js', { assert: { type: 'json' } }),
+ import('../fixtures/empty.js'),
+ ]);
+
+ strictEqual(results[0].status, 'rejected');
+ strictEqual(results[1].status, 'fulfilled');
+}
+
+{
+ const results = await Promise.allSettled([
+ import('../fixtures/empty.js'),
+ import('../fixtures/empty.js', { assert: { type: 'json' } }),
+ ]);
+
+ strictEqual(results[0].status, 'fulfilled');
+ strictEqual(results[1].status, 'rejected');
+}
+
+{
+ const results = await Promise.allSettled([
+ import('../fixtures/empty.json', { assert: { type: 'json' } }),
+ import('../fixtures/empty.json'),
+ ]);
+
+ strictEqual(results[0].status, 'fulfilled');
+ strictEqual(results[1].status, 'rejected');
+}
+
+{
+ const results = await Promise.allSettled([
+ import('../fixtures/empty.json'),
+ import('../fixtures/empty.json', { assert: { type: 'json' } }),
+ ]);
+
+ strictEqual(results[0].status, 'rejected');
+ strictEqual(results[1].status, 'fulfilled');
+}
diff --git a/test/es-module/test-esm-import-assertion-1.mjs b/test/es-module/test-esm-import-assertion-1.mjs
index 90ccadf5334f7f..f011c948d8edea 100644
--- a/test/es-module/test-esm-import-assertion-1.mjs
+++ b/test/es-module/test-esm-import-assertion-1.mjs
@@ -1,4 +1,4 @@
-// Flags: --experimental-json-modules --harmony-import-assertions
+// Flags: --experimental-json-modules
import '../common/index.mjs';
import { strictEqual } from 'assert';
diff --git a/test/es-module/test-esm-import-assertion-2.mjs b/test/es-module/test-esm-import-assertion-2.mjs
new file mode 100644
index 00000000000000..3598f353a3f9d5
--- /dev/null
+++ b/test/es-module/test-esm-import-assertion-2.mjs
@@ -0,0 +1,8 @@
+// Flags: --experimental-json-modules
+import '../common/index.mjs';
+import { strictEqual } from 'assert';
+
+// eslint-disable-next-line max-len
+import secret from '../fixtures/experimental.json' assert { type: 'json', unsupportedAssertion: 'should ignore' };
+
+strictEqual(secret.ofLife, 42);
diff --git a/test/es-module/test-esm-import-assertion-3.mjs b/test/es-module/test-esm-import-assertion-3.mjs
new file mode 100644
index 00000000000000..0409095aec5d97
--- /dev/null
+++ b/test/es-module/test-esm-import-assertion-3.mjs
@@ -0,0 +1,11 @@
+// Flags: --experimental-json-modules
+import '../common/index.mjs';
+import { strictEqual } from 'assert';
+
+import secret0 from '../fixtures/experimental.json' assert { type: 'json' };
+const secret1 = await import('../fixtures/experimental.json',
+ { assert: { type: 'json' } });
+
+strictEqual(secret0.ofLife, 42);
+strictEqual(secret1.default.ofLife, 42);
+strictEqual(secret1.default, secret0);
diff --git a/test/es-module/test-esm-import-assertion-4.mjs b/test/es-module/test-esm-import-assertion-4.mjs
new file mode 100644
index 00000000000000..4f3e33a6eefe2d
--- /dev/null
+++ b/test/es-module/test-esm-import-assertion-4.mjs
@@ -0,0 +1,12 @@
+// Flags: --experimental-json-modules
+import '../common/index.mjs';
+import { strictEqual } from 'assert';
+
+import secret0 from '../fixtures/experimental.json' assert { type: 'json' };
+const secret1 = await import('../fixtures/experimental.json', {
+ assert: { type: 'json' },
+ });
+
+strictEqual(secret0.ofLife, 42);
+strictEqual(secret1.default.ofLife, 42);
+strictEqual(secret1.default, secret0);
diff --git a/test/es-module/test-esm-import-assertion-errors.js b/test/es-module/test-esm-import-assertion-errors.js
new file mode 100644
index 00000000000000..3a55c23fbfbdf2
--- /dev/null
+++ b/test/es-module/test-esm-import-assertion-errors.js
@@ -0,0 +1,53 @@
+// Flags: --experimental-json-modules
+'use strict';
+const common = require('../common');
+const { rejects } = require('assert');
+
+const jsModuleDataUrl = 'data:text/javascript,export{}';
+const jsonModuleDataUrl = 'data:application/json,""';
+
+async function test() {
+ await rejects(
+ // This rejects because of the unsupported MIME type, not because of the
+ // unsupported assertion.
+ import('data:text/css,', { assert: { type: 'css' } }),
+ { code: 'ERR_INVALID_MODULE_SPECIFIER' }
+ );
+
+ await rejects(
+ import(`data:text/javascript,import${JSON.stringify(jsModuleDataUrl)}assert{type:"json"}`),
+ { code: 'ERR_IMPORT_ASSERTION_TYPE_FAILED' }
+ );
+
+ await rejects(
+ import(jsModuleDataUrl, { assert: { type: 'json' } }),
+ { code: 'ERR_IMPORT_ASSERTION_TYPE_FAILED' }
+ );
+
+ await rejects(
+ import('data:text/javascript,', { assert: { type: 'unsupported' } }),
+ { code: 'ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED' }
+ );
+
+ await rejects(
+ import(jsonModuleDataUrl),
+ { code: 'ERR_IMPORT_ASSERTION_TYPE_MISSING' }
+ );
+
+ await rejects(
+ import(jsonModuleDataUrl, { assert: {} }),
+ { code: 'ERR_IMPORT_ASSERTION_TYPE_MISSING' }
+ );
+
+ await rejects(
+ import(jsonModuleDataUrl, { assert: { foo: 'bar' } }),
+ { code: 'ERR_IMPORT_ASSERTION_TYPE_MISSING' }
+ );
+
+ await rejects(
+ import(jsonModuleDataUrl, { assert: { type: 'unsupported' }}),
+ { code: 'ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED' }
+ );
+}
+
+test().then(common.mustCall());
diff --git a/test/es-module/test-esm-import-assertion-errors.mjs b/test/es-module/test-esm-import-assertion-errors.mjs
new file mode 100644
index 00000000000000..c96e8f3dd046b7
--- /dev/null
+++ b/test/es-module/test-esm-import-assertion-errors.mjs
@@ -0,0 +1,48 @@
+// Flags: --experimental-json-modules
+import '../common/index.mjs';
+import { rejects } from 'assert';
+
+const jsModuleDataUrl = 'data:text/javascript,export{}';
+const jsonModuleDataUrl = 'data:application/json,""';
+
+await rejects(
+ // This rejects because of the unsupported MIME type, not because of the
+ // unsupported assertion.
+ import('data:text/css,', { assert: { type: 'css' } }),
+ { code: 'ERR_INVALID_MODULE_SPECIFIER' }
+);
+
+await rejects(
+ import(`data:text/javascript,import${JSON.stringify(jsModuleDataUrl)}assert{type:"json"}`),
+ { code: 'ERR_IMPORT_ASSERTION_TYPE_FAILED' }
+);
+
+await rejects(
+ import(jsModuleDataUrl, { assert: { type: 'json' } }),
+ { code: 'ERR_IMPORT_ASSERTION_TYPE_FAILED' }
+);
+
+await rejects(
+ import(import.meta.url, { assert: { type: 'unsupported' } }),
+ { code: 'ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED' }
+);
+
+await rejects(
+ import(jsonModuleDataUrl),
+ { code: 'ERR_IMPORT_ASSERTION_TYPE_MISSING' }
+);
+
+await rejects(
+ import(jsonModuleDataUrl, { assert: {} }),
+ { code: 'ERR_IMPORT_ASSERTION_TYPE_MISSING' }
+);
+
+await rejects(
+ import(jsonModuleDataUrl, { assert: { foo: 'bar' } }),
+ { code: 'ERR_IMPORT_ASSERTION_TYPE_MISSING' }
+);
+
+await rejects(
+ import(jsonModuleDataUrl, { assert: { type: 'unsupported' }}),
+ { code: 'ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED' }
+);
diff --git a/test/es-module/test-esm-import-assertion-validation.js b/test/es-module/test-esm-import-assertion-validation.js
new file mode 100644
index 00000000000000..7e64bd47392ab0
--- /dev/null
+++ b/test/es-module/test-esm-import-assertion-validation.js
@@ -0,0 +1,37 @@
+// Flags: --expose-internals
+'use strict';
+require('../common');
+
+const assert = require('assert');
+
+const { validateAssertions } = require('internal/modules/esm/assert');
+
+const url = 'test://';
+
+assert.ok(validateAssertions(url, 'builtin', {}));
+assert.ok(validateAssertions(url, 'commonjs', {}));
+assert.ok(validateAssertions(url, 'json', { type: 'json' }));
+assert.ok(validateAssertions(url, 'module', {}));
+assert.ok(validateAssertions(url, 'wasm', {}));
+
+assert.throws(() => validateAssertions(url, 'json', {}), {
+ code: 'ERR_IMPORT_ASSERTION_TYPE_MISSING',
+});
+
+assert.throws(() => validateAssertions(url, 'module', { type: 'json' }), {
+ code: 'ERR_IMPORT_ASSERTION_TYPE_FAILED',
+});
+
+// This should be allowed according to HTML spec. Let's keep it disabled
+// until WASM module import is sorted out.
+assert.throws(() => validateAssertions(url, 'module', { type: 'javascript' }), {
+ code: 'ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED',
+});
+
+assert.throws(() => validateAssertions(url, 'module', { type: 'css' }), {
+ code: 'ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED',
+});
+
+assert.throws(() => validateAssertions(url, 'module', { type: false }), {
+ code: 'ERR_INVALID_ARG_TYPE',
+});
diff --git a/test/es-module/test-esm-json-cache.mjs b/test/es-module/test-esm-json-cache.mjs
index 68ea832ab69585..90694748c39e5f 100644
--- a/test/es-module/test-esm-json-cache.mjs
+++ b/test/es-module/test-esm-json-cache.mjs
@@ -7,7 +7,8 @@ import { createRequire } from 'module';
import mod from '../fixtures/es-modules/json-cache/mod.cjs';
import another from '../fixtures/es-modules/json-cache/another.cjs';
-import test from '../fixtures/es-modules/json-cache/test.json';
+import test from '../fixtures/es-modules/json-cache/test.json' assert
+ { type: 'json' };
const require = createRequire(import.meta.url);
diff --git a/test/es-module/test-esm-json.mjs b/test/es-module/test-esm-json.mjs
index df4f75fbd6e067..f33b4f9937ddb1 100644
--- a/test/es-module/test-esm-json.mjs
+++ b/test/es-module/test-esm-json.mjs
@@ -4,7 +4,7 @@ import { path } from '../common/fixtures.mjs';
import { strictEqual, ok } from 'assert';
import { spawn } from 'child_process';
-import secret from '../fixtures/experimental.json';
+import secret from '../fixtures/experimental.json' assert { type: 'json' };
strictEqual(secret.ofLife, 42);
diff --git a/test/es-module/test-esm-loader-modulemap.js b/test/es-module/test-esm-loader-modulemap.js
index 48443de4c270c6..dbfda754924372 100644
--- a/test/es-module/test-esm-loader-modulemap.js
+++ b/test/es-module/test-esm-loader-modulemap.js
@@ -1,61 +1,101 @@
'use strict';
// Flags: --expose-internals
-// This test ensures that the type checking of ModuleMap throws
-// errors appropriately
-
require('../common');
-const assert = require('assert');
+const { strictEqual, throws } = require('assert');
const { ESMLoader } = require('internal/modules/esm/loader');
const ModuleMap = require('internal/modules/esm/module_map');
const ModuleJob = require('internal/modules/esm/module_job');
+const { kImplicitAssertType } = require('internal/modules/esm/assert');
const createDynamicModule = require(
'internal/modules/esm/create_dynamic_module');
-const stubModuleUrl = new URL('file://tmp/test');
-const stubModule = createDynamicModule(['default'], stubModuleUrl);
+const jsModuleDataUrl = 'data:text/javascript,export{}';
+const jsonModuleDataUrl = 'data:application/json,""';
+
+const stubJsModule = createDynamicModule([], ['default'], jsModuleDataUrl);
+const stubJsonModule = createDynamicModule([], ['default'], jsonModuleDataUrl);
+
const loader = new ESMLoader();
-const moduleMap = new ModuleMap();
-const moduleJob = new ModuleJob(loader, stubModule.module,
- () => new Promise(() => {}));
+const jsModuleJob = new ModuleJob(loader, stubJsModule.module, undefined,
+ () => new Promise(() => {}));
+const jsonModuleJob = new ModuleJob(loader, stubJsonModule.module,
+ { type: 'json' },
+ () => new Promise(() => {}));
-assert.throws(
- () => moduleMap.get(1),
- {
- code: 'ERR_INVALID_ARG_TYPE',
- name: 'TypeError',
- message: 'The "url" argument must be of type string. Received type number' +
- ' (1)'
- }
-);
-
-assert.throws(
- () => moduleMap.set(1, moduleJob),
- {
- code: 'ERR_INVALID_ARG_TYPE',
- name: 'TypeError',
- message: 'The "url" argument must be of type string. Received type number' +
- ' (1)'
- }
-);
-
-assert.throws(
- () => moduleMap.set('somestring', 'notamodulejob'),
- {
+
+// ModuleMap.set and ModuleMap.get store and retrieve module jobs for a
+// specified url/type tuple; ModuleMap.has correctly reports whether such jobs
+// are stored in the map.
+{
+ const moduleMap = new ModuleMap();
+
+ moduleMap.set(jsModuleDataUrl, undefined, jsModuleJob);
+ moduleMap.set(jsonModuleDataUrl, 'json', jsonModuleJob);
+
+ strictEqual(moduleMap.get(jsModuleDataUrl), jsModuleJob);
+ strictEqual(moduleMap.get(jsonModuleDataUrl, 'json'), jsonModuleJob);
+
+ strictEqual(moduleMap.has(jsModuleDataUrl), true);
+ strictEqual(moduleMap.has(jsModuleDataUrl, kImplicitAssertType), true);
+ strictEqual(moduleMap.has(jsonModuleDataUrl, 'json'), true);
+
+ strictEqual(moduleMap.has('unknown'), false);
+
+ // The types must match
+ strictEqual(moduleMap.has(jsModuleDataUrl, 'json'), false);
+ strictEqual(moduleMap.has(jsonModuleDataUrl, kImplicitAssertType), false);
+ strictEqual(moduleMap.has(jsonModuleDataUrl), false);
+ strictEqual(moduleMap.has(jsModuleDataUrl, 'unknown'), false);
+ strictEqual(moduleMap.has(jsonModuleDataUrl, 'unknown'), false);
+}
+
+// ModuleMap.get, ModuleMap.has and ModuleMap.set should only accept string
+// values as url argument.
+{
+ const moduleMap = new ModuleMap();
+
+ const errorObj = {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
- message: 'The "job" argument must be an instance of ModuleJob. ' +
- "Received type string ('notamodulejob')"
- }
-);
-
-assert.throws(
- () => moduleMap.has(1),
- {
+ message: /^The "url" argument must be of type string/
+ };
+
+ [{}, [], true, 1].forEach((value) => {
+ throws(() => moduleMap.get(value), errorObj);
+ throws(() => moduleMap.has(value), errorObj);
+ throws(() => moduleMap.set(value, undefined, jsModuleJob), errorObj);
+ });
+}
+
+// ModuleMap.get, ModuleMap.has and ModuleMap.set should only accept string
+// values (or the kAssertType symbol) as type argument.
+{
+ const moduleMap = new ModuleMap();
+
+ const errorObj = {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
- message: 'The "url" argument must be of type string. Received type number' +
- ' (1)'
- }
-);
+ message: /^The "type" argument must be of type string/
+ };
+
+ [{}, [], true, 1].forEach((value) => {
+ throws(() => moduleMap.get(jsModuleDataUrl, value), errorObj);
+ throws(() => moduleMap.has(jsModuleDataUrl, value), errorObj);
+ throws(() => moduleMap.set(jsModuleDataUrl, value, jsModuleJob), errorObj);
+ });
+}
+
+// ModuleMap.set should only accept ModuleJob values as job argument.
+{
+ const moduleMap = new ModuleMap();
+
+ [{}, [], true, 1].forEach((value) => {
+ throws(() => moduleMap.set('', undefined, value), {
+ code: 'ERR_INVALID_ARG_TYPE',
+ name: 'TypeError',
+ message: /^The "job" argument must be an instance of ModuleJob/
+ });
+ });
+}
diff --git a/test/fixtures/empty.json b/test/fixtures/empty.json
new file mode 100644
index 00000000000000..0967ef424bce67
--- /dev/null
+++ b/test/fixtures/empty.json
@@ -0,0 +1 @@
+{}
diff --git a/test/fixtures/es-module-loaders/assertionless-json-import.mjs b/test/fixtures/es-module-loaders/assertionless-json-import.mjs
new file mode 100644
index 00000000000000..c5c2fadf28fb58
--- /dev/null
+++ b/test/fixtures/es-module-loaders/assertionless-json-import.mjs
@@ -0,0 +1,17 @@
+const DATA_URL_PATTERN = /^data:application\/json(?:[^,]*?)(;base64)?,([\s\S]*)$/;
+const JSON_URL_PATTERN = /\.json(\?[^#]*)?(#.*)?$/;
+
+export function resolve(url, context, next) {
+ // Mutation from resolve hook should be discarded.
+ context.importAssertions.type = 'whatever';
+ return next(url, context);
+}
+
+export function load(url, context, next) {
+ if (context.importAssertions.type == null &&
+ (DATA_URL_PATTERN.test(url) || JSON_URL_PATTERN.test(url))) {
+ const { importAssertions } = context;
+ importAssertions.type = 'json';
+ }
+ return next(url, context);
+}
diff --git a/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs b/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs
index f206d7635b3f63..82e64567494842 100644
--- a/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs
+++ b/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs
@@ -19,6 +19,7 @@ export function resolve(specifier, context, next) {
if (def.url.startsWith('node:')) {
return {
url: `custom-${def.url}`,
+ importAssertions: context.importAssertions,
};
}
return def;
diff --git a/test/fixtures/es-module-loaders/hooks-custom.mjs b/test/fixtures/es-module-loaders/hooks-custom.mjs
index 59f49ff9e60c13..cd9d5020ad3234 100644
--- a/test/fixtures/es-module-loaders/hooks-custom.mjs
+++ b/test/fixtures/es-module-loaders/hooks-custom.mjs
@@ -63,6 +63,7 @@ export function resolve(specifier, context, next) {
if (specifier.startsWith('esmHook')) return {
format,
url: specifier,
+ importAssertions: context.importAssertions,
};
return next(specifier, context, next);
diff --git a/test/fixtures/es-module-loaders/loader-invalid-format.mjs b/test/fixtures/es-module-loaders/loader-invalid-format.mjs
index fc1b84658b76de..0210f73b554382 100644
--- a/test/fixtures/es-module-loaders/loader-invalid-format.mjs
+++ b/test/fixtures/es-module-loaders/loader-invalid-format.mjs
@@ -1,10 +1,10 @@
-export async function resolve(specifier, { parentURL }, defaultResolve) {
+export async function resolve(specifier, { parentURL, importAssertions }, defaultResolve) {
if (parentURL && specifier === '../fixtures/es-modules/test-esm-ok.mjs') {
return {
url: 'file:///asdf'
};
}
- return defaultResolve(specifier, {parentURL}, defaultResolve);
+ return defaultResolve(specifier, {parentURL, importAssertions}, defaultResolve);
}
export async function load(url, context, next) {
diff --git a/test/fixtures/es-module-loaders/loader-invalid-url.mjs b/test/fixtures/es-module-loaders/loader-invalid-url.mjs
index e7de0d4ed92378..ad69faff26d40f 100644
--- a/test/fixtures/es-module-loaders/loader-invalid-url.mjs
+++ b/test/fixtures/es-module-loaders/loader-invalid-url.mjs
@@ -1,9 +1,10 @@
/* eslint-disable node-core/required-modules */
-export async function resolve(specifier, { parentURL }, defaultResolve) {
+export async function resolve(specifier, { parentURL, importAssertions }, defaultResolve) {
if (parentURL && specifier === '../fixtures/es-modules/test-esm-ok.mjs') {
return {
- url: specifier
+ url: specifier,
+ importAssertions,
};
}
- return defaultResolve(specifier, {parentURL}, defaultResolve);
+ return defaultResolve(specifier, {parentURL, importAssertions}, defaultResolve);
}
diff --git a/test/fixtures/es-module-loaders/loader-shared-dep.mjs b/test/fixtures/es-module-loaders/loader-shared-dep.mjs
index 3576c074d52cec..387575794c00dc 100644
--- a/test/fixtures/es-module-loaders/loader-shared-dep.mjs
+++ b/test/fixtures/es-module-loaders/loader-shared-dep.mjs
@@ -1,11 +1,11 @@
import assert from 'assert';
-import {createRequire} from '../../common/index.mjs';
+import { createRequire } from '../../common/index.mjs';
const require = createRequire(import.meta.url);
const dep = require('./loader-dep.js');
-export function resolve(specifier, { parentURL }, defaultResolve) {
+export function resolve(specifier, { parentURL, importAssertions }, defaultResolve) {
assert.strictEqual(dep.format, 'module');
- return defaultResolve(specifier, {parentURL}, defaultResolve);
+ return defaultResolve(specifier, { parentURL, importAssertions }, defaultResolve);
}
diff --git a/test/fixtures/es-module-loaders/loader-with-dep.mjs b/test/fixtures/es-module-loaders/loader-with-dep.mjs
index da7d44ae793e22..78a72cca6d9009 100644
--- a/test/fixtures/es-module-loaders/loader-with-dep.mjs
+++ b/test/fixtures/es-module-loaders/loader-with-dep.mjs
@@ -3,9 +3,9 @@ import {createRequire} from '../../common/index.mjs';
const require = createRequire(import.meta.url);
const dep = require('./loader-dep.js');
-export function resolve (specifier, { parentURL }, defaultResolve) {
+export function resolve (specifier, { parentURL, importAssertions }, defaultResolve) {
return {
- url: defaultResolve(specifier, {parentURL}, defaultResolve).url,
+ url: defaultResolve(specifier, {parentURL, importAssertions}, defaultResolve).url,
format: dep.format
};
}
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 9a2cd735a2fd66..5213ddedb34e8d 100644
--- a/test/fixtures/es-module-loaders/not-found-assert-loader.mjs
+++ b/test/fixtures/es-module-loaders/not-found-assert-loader.mjs
@@ -3,18 +3,19 @@ import assert from 'assert';
// a loader that asserts that the defaultResolve will throw "not found"
// (skipping the top-level main of course)
let mainLoad = true;
-export async function resolve(specifier, { parentURL }, defaultResolve) {
+export async function resolve(specifier, { parentURL, importAssertions }, defaultResolve) {
if (mainLoad) {
mainLoad = false;
- return defaultResolve(specifier, {parentURL}, defaultResolve);
+ return defaultResolve(specifier, {parentURL, importAssertions}, defaultResolve);
}
try {
- await defaultResolve(specifier, {parentURL}, defaultResolve);
+ await defaultResolve(specifier, {parentURL, importAssertions}, defaultResolve);
}
catch (e) {
assert.strictEqual(e.code, 'ERR_MODULE_NOT_FOUND');
return {
- url: 'node:fs'
+ url: 'node:fs',
+ importAssertions,
};
}
assert.fail(`Module resolution for ${specifier} should be throw ERR_MODULE_NOT_FOUND`);
diff --git a/test/fixtures/es-module-loaders/string-sources.mjs b/test/fixtures/es-module-loaders/string-sources.mjs
index 180a356bc81478..384098d6d9e822 100644
--- a/test/fixtures/es-module-loaders/string-sources.mjs
+++ b/test/fixtures/es-module-loaders/string-sources.mjs
@@ -22,7 +22,7 @@ const SOURCES = {
}
export function resolve(specifier, context, next) {
if (specifier.startsWith('test:')) {
- return { url: specifier };
+ return { url: specifier, importAssertions: context.importAssertions };
}
return next(specifier, context);
}
diff --git a/test/fixtures/es-modules/json-modules.mjs b/test/fixtures/es-modules/json-modules.mjs
index fa3f936bac921e..607c09e51cda2b 100644
--- a/test/fixtures/es-modules/json-modules.mjs
+++ b/test/fixtures/es-modules/json-modules.mjs
@@ -1 +1 @@
-import secret from '../experimental.json';
+import secret from '../experimental.json' assert { type: 'json' };
diff --git a/test/message/esm_display_syntax_error_import_json_named_export.mjs b/test/message/esm_display_syntax_error_import_json_named_export.mjs
new file mode 100644
index 00000000000000..3b7c721daf1601
--- /dev/null
+++ b/test/message/esm_display_syntax_error_import_json_named_export.mjs
@@ -0,0 +1,4 @@
+// Flags: --experimental-json-modules
+/* eslint-disable no-unused-vars */
+import '../common/index.mjs';
+import { ofLife } from '../fixtures/experimental.json' assert { type: 'json' };
diff --git a/test/message/esm_display_syntax_error_import_json_named_export.out b/test/message/esm_display_syntax_error_import_json_named_export.out
new file mode 100644
index 00000000000000..d4636489f27c3a
--- /dev/null
+++ b/test/message/esm_display_syntax_error_import_json_named_export.out
@@ -0,0 +1,12 @@
+file:///*/test/message/esm_display_syntax_error_import_json_named_export.mjs:*
+import { ofLife } from '../fixtures/experimental.json' assert { type: 'json' };
+ ^^^^^^
+SyntaxError: The requested module '../fixtures/experimental.json' does not provide an export named 'ofLife'
+ at ModuleJob._instantiate (node:internal/modules/esm/module_job:*:*)
+ at async ModuleJob.run (node:internal/modules/esm/module_job:*:*)
+ at async Promise.all (index 0)
+ at async ESMLoader.import (node:internal/modules/esm/loader:*:*)
+ at async loadESM (node:internal/process/esm_loader:*:*)
+ at async handleMainPromise (node:internal/modules/run_main:*:*)
+
+Node.js *
diff --git a/test/message/esm_import_assertion_failed.mjs b/test/message/esm_import_assertion_failed.mjs
new file mode 100644
index 00000000000000..30ea65c3e34ee3
--- /dev/null
+++ b/test/message/esm_import_assertion_failed.mjs
@@ -0,0 +1,2 @@
+import '../common/index.mjs';
+import 'data:text/javascript,export{}' assert {type:'json'};
diff --git a/test/message/esm_import_assertion_failed.out b/test/message/esm_import_assertion_failed.out
new file mode 100644
index 00000000000000..eed42565166467
--- /dev/null
+++ b/test/message/esm_import_assertion_failed.out
@@ -0,0 +1,18 @@
+node:internal/errors:*
+ ErrorCaptureStackTrace(err);
+ ^
+
+TypeError [ERR_IMPORT_ASSERTION_TYPE_FAILED]: Module "data:text/javascript,export{}" is not of type "json"
+ at new NodeError (node:internal/errors:*:*)
+ at handleInvalidType (node:internal/modules/esm/assert:*:*)
+ at validateAssertions (node:internal/modules/esm/assert:*:*)
+ at defaultLoad (node:internal/modules/esm/load:*:*)
+ at ESMLoader.load (node:internal/modules/esm/loader:*:*)
+ at ESMLoader.moduleProvider (node:internal/modules/esm/loader:*:*)
+ at new ModuleJob (node:internal/modules/esm/module_job:*:*)
+ at ESMLoader.#createModuleJob (node:internal/modules/esm/loader:*:*)
+ at ESMLoader.getModuleJob (node:internal/modules/esm/loader:*:*)
+ at async ModuleWrap. (node:internal/modules/esm/module_job:*:*) {
+ code: 'ERR_IMPORT_ASSERTION_TYPE_FAILED'
+}
+Node.js *
diff --git a/test/message/esm_import_assertion_missing.mjs b/test/message/esm_import_assertion_missing.mjs
new file mode 100644
index 00000000000000..0b402d9e7ff90a
--- /dev/null
+++ b/test/message/esm_import_assertion_missing.mjs
@@ -0,0 +1,3 @@
+// Flags: --experimental-json-modules
+import '../common/index.mjs';
+import 'data:application/json,{}';
diff --git a/test/message/esm_import_assertion_missing.out b/test/message/esm_import_assertion_missing.out
new file mode 100644
index 00000000000000..a56ec12aeeefb3
--- /dev/null
+++ b/test/message/esm_import_assertion_missing.out
@@ -0,0 +1,19 @@
+node:internal/errors:*
+ ErrorCaptureStackTrace(err);
+ ^
+
+TypeError [ERR_IMPORT_ASSERTION_TYPE_MISSING]: Module "data:application/json,{}" needs an import assertion of type "json"
+ at new NodeError (node:internal/errors:*:*)
+ at validateAssertions (node:internal/modules/esm/assert:*:*)
+ at defaultLoad (node:internal/modules/esm/load:*:*)
+ at ESMLoader.load (node:internal/modules/esm/loader:*:*)
+ at ESMLoader.moduleProvider (node:internal/modules/esm/loader:*:*)
+ at new ModuleJob (node:internal/modules/esm/module_job:*:*)
+ at ESMLoader.#createModuleJob (node:internal/modules/esm/loader:*:*)
+ at ESMLoader.getModuleJob (node:internal/modules/esm/loader:*:*)
+ at async ModuleWrap. (node:internal/modules/esm/module_job:*:*)
+ at async Promise.all (index *) {
+ code: 'ERR_IMPORT_ASSERTION_TYPE_MISSING'
+}
+
+Node.js *
diff --git a/test/message/esm_import_assertion_unsupported.mjs b/test/message/esm_import_assertion_unsupported.mjs
new file mode 100644
index 00000000000000..86e594ce02ae5d
--- /dev/null
+++ b/test/message/esm_import_assertion_unsupported.mjs
@@ -0,0 +1,2 @@
+import '../common/index.mjs';
+import '../fixtures/empty.js' assert { type: 'unsupported' };
diff --git a/test/message/esm_import_assertion_unsupported.out b/test/message/esm_import_assertion_unsupported.out
new file mode 100644
index 00000000000000..0dc3657e43dadb
--- /dev/null
+++ b/test/message/esm_import_assertion_unsupported.out
@@ -0,0 +1,19 @@
+node:internal/errors:*
+ ErrorCaptureStackTrace(err);
+ ^
+
+TypeError [ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED]: Import assertion type "unsupported" is unsupported
+ at new NodeError (node:internal/errors:*:*)
+ at handleInvalidType (node:internal/modules/esm/assert:*:*)
+ at validateAssertions (node:internal/modules/esm/assert:*:*)
+ at defaultLoad (node:internal/modules/esm/load:*:*)
+ at ESMLoader.load (node:internal/modules/esm/loader:*:*)
+ at ESMLoader.moduleProvider (node:internal/modules/esm/loader:*:*)
+ at new ModuleJob (node:internal/modules/esm/module_job:*:*)
+ at ESMLoader.#createModuleJob (node:internal/modules/esm/loader:*:*)
+ at ESMLoader.getModuleJob (node:internal/modules/esm/loader:*:*)
+ at async ModuleWrap. (node:internal/modules/esm/module_job:*:*) {
+ code: 'ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED'
+}
+
+Node.js *
diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js
index 581ca701462fa1..b8101388a9c64d 100644
--- a/test/parallel/test-bootstrap-modules.js
+++ b/test/parallel/test-bootstrap-modules.js
@@ -68,6 +68,7 @@ const expectedModules = new Set([
'NativeModule internal/modules/package_json_reader',
'NativeModule internal/modules/cjs/helpers',
'NativeModule internal/modules/cjs/loader',
+ 'NativeModule internal/modules/esm/assert',
'NativeModule internal/modules/esm/create_dynamic_module',
'NativeModule internal/modules/esm/get_format',
'NativeModule internal/modules/esm/get_source',
diff --git a/test/parallel/test-internal-module-map-asserts.js b/test/parallel/test-internal-module-map-asserts.js
deleted file mode 100644
index 6f985faccd92bb..00000000000000
--- a/test/parallel/test-internal-module-map-asserts.js
+++ /dev/null
@@ -1,42 +0,0 @@
-// Flags: --expose-internals
-'use strict';
-
-require('../common');
-const assert = require('assert');
-const ModuleMap = require('internal/modules/esm/module_map');
-
-// ModuleMap.get, ModuleMap.has and ModuleMap.set should only accept string
-// values as url argument.
-{
- const errorObj = {
- code: 'ERR_INVALID_ARG_TYPE',
- name: 'TypeError',
- message: /^The "url" argument must be of type string/
- };
-
- const moduleMap = new ModuleMap();
-
- // As long as the assertion of "job" argument is done after the assertion of
- // "url" argument this test suite is ok. Tried to mock the "job" parameter,
- // but I think it's useless, and was not simple to mock...
- const job = undefined;
-
- [{}, [], true, 1].forEach((value) => {
- assert.throws(() => moduleMap.get(value), errorObj);
- assert.throws(() => moduleMap.has(value), errorObj);
- assert.throws(() => moduleMap.set(value, job), errorObj);
- });
-}
-
-// ModuleMap.set, job argument should only accept ModuleJob values.
-{
- const moduleMap = new ModuleMap();
-
- [{}, [], true, 1].forEach((value) => {
- assert.throws(() => moduleMap.set('', value), {
- code: 'ERR_INVALID_ARG_TYPE',
- name: 'TypeError',
- message: /^The "job" argument must be an instance of ModuleJob/
- });
- });
-}
diff --git a/test/parallel/test-vm-module-dynamic-import.js b/test/parallel/test-vm-module-dynamic-import.js
index 2273497d27677c..cd318511401412 100644
--- a/test/parallel/test-vm-module-dynamic-import.js
+++ b/test/parallel/test-vm-module-dynamic-import.js
@@ -1,6 +1,6 @@
'use strict';
-// Flags: --experimental-vm-modules --harmony-import-assertions
+// Flags: --experimental-vm-modules
const common = require('../common');
diff --git a/test/parallel/test-vm-module-link.js b/test/parallel/test-vm-module-link.js
index 9805d8fe3eee9c..16694d5d846075 100644
--- a/test/parallel/test-vm-module-link.js
+++ b/test/parallel/test-vm-module-link.js
@@ -1,6 +1,6 @@
'use strict';
-// Flags: --experimental-vm-modules --harmony-import-assertions
+// Flags: --experimental-vm-modules
const common = require('../common');
diff --git a/tools/code_cache/mkcodecache.cc b/tools/code_cache/mkcodecache.cc
index 9a0127184372bc..babf8535dbb3e7 100644
--- a/tools/code_cache/mkcodecache.cc
+++ b/tools/code_cache/mkcodecache.cc
@@ -28,6 +28,7 @@ int main(int argc, char* argv[]) {
#endif // _WIN32
v8::V8::SetFlagsFromString("--random_seed=42");
+ v8::V8::SetFlagsFromString("--harmony-import-assertions");
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " \n";