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";