From 8475f0e387080e10a0376f25e57adb4ae28c7ae3 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sun, 5 Sep 2021 12:08:37 +0200 Subject: [PATCH 01/13] vm: add support for import assertions in dynamic imports --- doc/api/vm.md | 39 +++++++++++++++++++++++++++++++++++++++ lib/vm.js | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/doc/api/vm.md b/doc/api/vm.md index cff8c9b6e23bf3..2929dffa0d435f 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -54,6 +54,10 @@ executed in specific contexts. * `code` {string} JavaScript Module code to parse * `options` @@ -667,6 +680,8 @@ defined in the ECMAScript specification. `import()` will reject with [`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][]. * `specifier` {string} specifier passed to `import()` * `module` {vm.Module} + * `import_assertions` {Object} The `"assert"` value passed to the + `optionExpression` optional parameter. * Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is recommended in order to take advantage of error tracking, and to avoid issues with namespaces that contain `then` function exports. @@ -852,6 +867,10 @@ const vm = require('vm'); + +An import assertion has failed, preventing the specified module to be imported. + ### `ERR_FALSY_VALUE_REJECTION` diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 84431fe811d9fc..5226485b042a77 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -953,6 +953,9 @@ E('ERR_ENCODING_NOT_SUPPORTED', 'The "%s" encoding is not supported', RangeError); E('ERR_EVAL_ESM_CANNOT_PRINT', '--print cannot be used with ESM input', Error); E('ERR_EVENT_RECURSION', 'The event "%s" is already being dispatched', Error); +E('ERR_FAILED_IMPORT_ASSERTION', (request, key, expectedValue, actualValue) => { + return `Failed to load module "${request}", expected ${key} to be ${JSONStringify(expectedValue)}, got ${JSONStringify(actualValue)} instead`; +}, TypeError); E('ERR_FALSY_VALUE_REJECTION', function(reason) { this.reason = reason; return 'Promise was rejected with falsy value'; diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index b8eff0440624a4..141dc724b96b85 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -1021,9 +1021,10 @@ function wrapSafe(filename, content, cjsModuleInstance) { filename, lineOffset: 0, displayErrors: true, - importModuleDynamically: async (specifier) => { + importModuleDynamically: async (specifier, _, import_assertions) => { const loader = asyncESM.esmLoader; - return loader.import(specifier, normalizeReferrerURL(filename)); + return loader.import(specifier, normalizeReferrerURL(filename), + import_assertions); }, }); } @@ -1036,9 +1037,10 @@ function wrapSafe(filename, content, cjsModuleInstance) { '__dirname', ], { filename, - importModuleDynamically(specifier) { + importModuleDynamically(specifier, _, import_assertions) { const loader = asyncESM.esmLoader; - return loader.import(specifier, normalizeReferrerURL(filename)); + return loader.import(specifier, normalizeReferrerURL(filename), + import_assertions); }, }); } catch (err) { diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index b12a87a9021242..e0d5233155b0ae 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -20,6 +20,7 @@ const { } = primordials; const { + ERR_FAILED_IMPORT_ASSERTION, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_INVALID_MODULE_SPECIFIER, @@ -202,8 +203,8 @@ class ESMLoader { const { ModuleWrap, callbackMap } = internalBinding('module_wrap'); const module = new ModuleWrap(url, undefined, source, 0, 0); callbackMap.set(module, { - importModuleDynamically: (specifier, { url }) => { - return this.import(specifier, url); + importModuleDynamically: (specifier, { url }, import_assertions) => { + return this.import(specifier, url, import_assertions); } }); @@ -218,8 +219,9 @@ class ESMLoader { }; } - async getModuleJob(specifier, parentURL) { + async getModuleJob(specifier, parentURL, import_assertions) { const { format, url } = await this.resolve(specifier, parentURL); + let job = this.moduleMap.get(url); // CommonJS will set functions for lazy job evaluation. if (typeof job === 'function') this.moduleMap.set(url, job = job()); @@ -229,6 +231,11 @@ class ESMLoader { const moduleProvider = async (url, isMain) => { const { format: finalFormat, source } = await this.load(url, { format }); + if (import_assertions.type === 'json' && finalFormat !== 'json') { + throw new ERR_FAILED_IMPORT_ASSERTION( + url, 'type', import_assertions.type, finalFormat); + } + const translator = translators.get(finalFormat); if (!translator) throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat); @@ -262,10 +269,11 @@ class ESMLoader { * loader module. * * @param {string | string[]} specifiers Path(s) to the module - * @param {string} [parentURL] Path of the parent importing the module - * @returns {object | object[]} A list of module export(s) + * @param {string} parentURL Path of the parent importing the module + * @param {Record>} import_assertions + * @returns {Promise} A list of module export(s) */ - async import(specifiers, parentURL) { + async import(specifiers, parentURL, import_assertions) { const wasArr = ArrayIsArray(specifiers); if (!wasArr) specifiers = [specifiers]; @@ -273,7 +281,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, import_assertions) .then((job) => job.run()) .then(({ module }) => module.getNamespace()); } diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index 2f699376d6eaea..c67b816657715a 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -72,8 +72,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; 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/esm_loader.js b/lib/internal/process/esm_loader.js index 0f04ca97751ef6..73385a85b4e106 100644 --- a/lib/internal/process/esm_loader.js +++ b/lib/internal/process/esm_loader.js @@ -1,5 +1,9 @@ 'use strict'; +const { + ObjectCreate, +} = primordials; + const { ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING, } = require('internal/errors').codes; @@ -22,13 +26,14 @@ exports.initializeImportMetaObject = function(wrap, meta) { } }; -exports.importModuleDynamicallyCallback = async function(wrap, specifier) { +exports.importModuleDynamicallyCallback = +async function importModuleDynamicallyCallback(wrap, specifier, assertions) { const { callbackMap } = internalBinding('module_wrap'); if (callbackMap.has(wrap)) { const { importModuleDynamically } = callbackMap.get(wrap); if (importModuleDynamically !== undefined) { return importModuleDynamically( - specifier, getModuleFromWrap(wrap) || wrap); + specifier, getModuleFromWrap(wrap) || wrap, assertions); } } throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING(); @@ -69,6 +74,7 @@ async function initializeLoader() { const exports = await internalEsmLoader.import( customLoaders, pathToFileURL(cwd).href, + ObjectCreate(null), ); // Hooks must then be added to external/public loader diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js index 48c525057f7477..103d7cc7fcfd46 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, _, import_assertions) { + const loader = asyncESM.esmLoader; + return loader.import(specifier, baseUrl, import_assertions); } })); if (print) { diff --git a/lib/repl.js b/lib/repl.js index 4ee8e24d47588c..2e9f0a307588cf 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, _, import_assertions) => { + return asyncESM.esmLoader.import(specifier, parentURL, + import_assertions); } }); } 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, _, import_assertions) => { + return asyncESM.esmLoader.import(specifier, parentURL, + import_assertions); } }); } catch (e) { diff --git a/src/module_wrap.cc b/src/module_wrap.cc index f45ee7627b0a9b..a9defba217626d 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -602,9 +602,20 @@ static MaybeLocal ImportModuleDynamically( UNREACHABLE(); } + Local assertions = + Object::New(isolate, v8::Null(env->isolate()), nullptr, nullptr, 0); + for (int i = 0; i < import_assertions->Length(); i += 2) { + assertions + ->Set(env->context(), + Local::Cast(import_assertions->Get(env->context(), i)), + Local::Cast(import_assertions->Get(env->context(), i + 1))) + .ToChecked(); + } + Local import_args[] = { object, Local(specifier), + assertions, }; Local result; diff --git a/src/node.cc b/src/node.cc index acf4f0fac03c0b..19de2d6e68a411 100644 --- a/src/node.cc +++ b/src/node.cc @@ -803,11 +803,11 @@ int ProcessGlobalArgs(std::vector* args, return 12; } - // TODO(mylesborins): remove this when the harmony-top-level-await flag + // TODO(mylesborins): remove this when the harmony-import-assertions flag // is removed in V8 if (std::find(v8_args.begin(), v8_args.end(), - "--no-harmony-top-level-await") == v8_args.end()) { - v8_args.push_back("--harmony-top-level-await"); + "--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; 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..f3daa4a726da0a --- /dev/null +++ b/test/es-module/test-esm-dynamic-import-assertion.js @@ -0,0 +1,68 @@ +// Flags: --experimental-json-modules +'use strict'; +const common = require('../common'); +const { rejects, strictEqual } = require('assert'); + +const jsModuleDataUrl = 'data:text/javascript,export{}'; + +async function test() { + await rejects( + import(`data:text/javascript,import${JSON.stringify(jsModuleDataUrl)}assert{type:"json"}`), + { code: 'ERR_FAILED_IMPORT_ASSERTION' } + ); + + await rejects( + import(jsModuleDataUrl, { assert: { type: 'json' } }), + { code: 'ERR_FAILED_IMPORT_ASSERTION' } + ); + + { + 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('data:application/json,{"ofLife":42}'), + import( + 'data:application/json,{"ofLife":42}', + { assert: { type: 'json' } } + ), + ]); + + strictEqual(secret0.default.ofLife, 42); + strictEqual(secret1.default.ofLife, 42); + } + + { + 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'); + } +} + +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..b635d8e86ef648 --- /dev/null +++ b/test/es-module/test-esm-dynamic-import-assertion.mjs @@ -0,0 +1,63 @@ +// Flags: --experimental-json-modules +import '../common/index.mjs'; +import { rejects, strictEqual } from 'assert'; + +const jsModuleDataUrl = 'data:text/javascript,export{}'; + +await rejects( + import(`data:text/javascript,import${JSON.stringify(jsModuleDataUrl)}assert{type:"json"}`), + { code: 'ERR_FAILED_IMPORT_ASSERTION' } +); + +await rejects( + import(jsModuleDataUrl, { assert: { type: 'json' } }), + { code: 'ERR_FAILED_IMPORT_ASSERTION' } +); + +{ + 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('data:application/json,{"ofLife":42}'), + import( + 'data:application/json,{"ofLife":42}', + { assert: { type: 'json' } } + ), + ]); + + strictEqual(secret0.default.ofLife, 42); + strictEqual(secret1.default.ofLife, 42); +} + +{ + 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'); +} 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..79d6cf05a1d1e6 --- /dev/null +++ b/test/es-module/test-esm-import-assertion-3.mjs @@ -0,0 +1,10 @@ +// 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'); + +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..b7400bca9d42f9 --- /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'; +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.mjs b/test/es-module/test-esm-import-assertion.mjs new file mode 100644 index 00000000000000..f011c948d8edea --- /dev/null +++ b/test/es-module/test-esm-import-assertion.mjs @@ -0,0 +1,7 @@ +// Flags: --experimental-json-modules +import '../common/index.mjs'; +import { strictEqual } from 'assert'; + +import secret from '../fixtures/experimental.json' assert { type: 'json' }; + +strictEqual(secret.ofLife, 42); diff --git a/test/message/esm_import_assertion_failing.mjs b/test/message/esm_import_assertion_failing.mjs new file mode 100644 index 00000000000000..30ea65c3e34ee3 --- /dev/null +++ b/test/message/esm_import_assertion_failing.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_failing.out b/test/message/esm_import_assertion_failing.out new file mode 100644 index 00000000000000..32024344e45581 --- /dev/null +++ b/test/message/esm_import_assertion_failing.out @@ -0,0 +1,10 @@ +node:internal/errors:* + ErrorCaptureStackTrace(err); + ^ + +TypeError [ERR_FAILED_IMPORT_ASSERTION]: Failed to load module "data:text/javascript,export{}", expected type to be "json", got "module" instead + at new NodeError (node:internal/errors:*:*) + at ESMLoader.moduleProvider (node:internal/modules/esm/loader:*:*) { + code: 'ERR_FAILED_IMPORT_ASSERTION' +} +Node.js * 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 3a1d0ee50e5078..babf8535dbb3e7 100644 --- a/tools/code_cache/mkcodecache.cc +++ b/tools/code_cache/mkcodecache.cc @@ -28,7 +28,7 @@ int main(int argc, char* argv[]) { #endif // _WIN32 v8::V8::SetFlagsFromString("--random_seed=42"); - v8::V8::SetFlagsFromString("--harmony-top-level-await"); + v8::V8::SetFlagsFromString("--harmony-import-assertions"); if (argc < 2) { std::cerr << "Usage: " << argv[0] << " \n"; From 8754a41b68f37c8363a357ede2748212a8813326 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sun, 12 Sep 2021 16:07:27 +0200 Subject: [PATCH 03/13] fixup! module: unflag import assertions --- lib/internal/modules/esm/loader.js | 23 +++++++++++++------ lib/internal/modules/esm/module_map.js | 7 ++++-- .../test-esm-dynamic-import-assertion.js | 2 +- .../test-esm-dynamic-import-assertion.mjs | 2 +- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index e0d5233155b0ae..f94c611ab64ddd 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -211,7 +211,7 @@ class ESMLoader { return module; }; const job = new ModuleJob(this, url, evalInstance, false, false); - this.moduleMap.set(url, job); + this.moduleMap.set(url, job, undefined); const { module } = await job.run(); return { @@ -222,11 +222,20 @@ class ESMLoader { async getModuleJob(specifier, parentURL, import_assertions) { const { format, url } = await this.resolve(specifier, parentURL); - let job = this.moduleMap.get(url); - // CommonJS will set functions for lazy job evaluation. - if (typeof job === 'function') this.moduleMap.set(url, job = job()); - - if (job !== undefined) return job; + let job + const jobMap = this.moduleMap.get(url); + + if (jobMap != null) { + // To avoid race conditions, always wait for non assertion job to fulfill + if(import_assertions.type != null) await jobMap[undefined]; + + let job = jobMap[import_assertions.type]; + + // CommonJS will set functions for lazy job evaluation. + if (typeof job === 'function') this.moduleMap.set(url, job = job(), import_assertions.type); + + if (job !== undefined) return job; + } const moduleProvider = async (url, isMain) => { const { format: finalFormat, source } = await this.load(url, { format }); @@ -256,7 +265,7 @@ class ESMLoader { inspectBrk ); - this.moduleMap.set(url, job); + this.moduleMap.set(url, job, import_assertions.type); return job; } diff --git a/lib/internal/modules/esm/module_map.js b/lib/internal/modules/esm/module_map.js index 9e1116a5647f5f..a8b292d4fa245b 100644 --- a/lib/internal/modules/esm/module_map.js +++ b/lib/internal/modules/esm/module_map.js @@ -2,6 +2,7 @@ const ModuleJob = require('internal/modules/esm/module_job'); const { + ObjectCreate, SafeMap, } = primordials; let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { @@ -17,14 +18,16 @@ class ModuleMap extends SafeMap { validateString(url, 'url'); return super.get(url); } - set(url, job) { + set(url, job, import_assertion_type) { validateString(url, 'url'); 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); + const jobMap = super.get(url) ?? ObjectCreate(null); + jobMap[import_assertion_type] = job + return super.set(url, jobMap); } has(url) { validateString(url, 'url'); diff --git a/test/es-module/test-esm-dynamic-import-assertion.js b/test/es-module/test-esm-dynamic-import-assertion.js index f3daa4a726da0a..abefe8b73e99a2 100644 --- a/test/es-module/test-esm-dynamic-import-assertion.js +++ b/test/es-module/test-esm-dynamic-import-assertion.js @@ -28,7 +28,7 @@ async function test() { strictEqual(secret0.default.ofLife, 42); strictEqual(secret1.default.ofLife, 42); strictEqual(secret0.default, secret1.default); - strictEqual(secret0, secret1); + // strictEqual(secret0, secret1); } { diff --git a/test/es-module/test-esm-dynamic-import-assertion.mjs b/test/es-module/test-esm-dynamic-import-assertion.mjs index b635d8e86ef648..19cc17953b04e5 100644 --- a/test/es-module/test-esm-dynamic-import-assertion.mjs +++ b/test/es-module/test-esm-dynamic-import-assertion.mjs @@ -26,7 +26,7 @@ await rejects( strictEqual(secret0.default.ofLife, 42); strictEqual(secret1.default.ofLife, 42); strictEqual(secret0.default, secret1.default); - strictEqual(secret0, secret1); + // strictEqual(secret0, secret1); } { From debc47f182e3648c736fa4162ceaa76852d2feee Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sun, 12 Sep 2021 16:52:21 +0200 Subject: [PATCH 04/13] fixup! module: unflag import assertions --- lib/internal/modules/esm/loader.js | 33 ++++++++++++++----- lib/internal/modules/esm/module_map.js | 2 +- .../test-esm-dynamic-import-assertion.js | 2 +- .../test-esm-dynamic-import-assertion.mjs | 2 +- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index f94c611ab64ddd..bbc813bd7bcb5c 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -15,6 +15,7 @@ const { PromiseAll, RegExpPrototypeExec, SafeArrayIterator, + SafeMap, SafeWeakMap, globalThis, } = primordials; @@ -45,6 +46,8 @@ const { translators } = require( 'internal/modules/esm/translators'); const { getOptionValue } = require('internal/options'); +const finalFormatCache = new SafeMap(); + /** * An ESMLoader instance is used as the main entry point for loading ES modules. * Currently, this is a singleton -- there is only one used for loading @@ -222,18 +225,30 @@ class ESMLoader { async getModuleJob(specifier, parentURL, import_assertions) { const { format, url } = await this.resolve(specifier, parentURL); - let job const jobMap = this.moduleMap.get(url); + let job; if (jobMap != null) { - // To avoid race conditions, always wait for non assertion job to fulfill - if(import_assertions.type != null) await jobMap[undefined]; - - let job = jobMap[import_assertions.type]; - + if (import_assertions.type != null && undefined in jobMap) { + job = jobMap[undefined]; + // To avoid race conditions, always wait for non typed import to + // fulfill first. + await job.modulePromise; + + const finalFormat = finalFormatCache.get(url); + if (import_assertions.type === 'json' && finalFormat !== 'json') { + throw new ERR_FAILED_IMPORT_ASSERTION( + url, 'type', import_assertions.type, finalFormat); + } + return job; + } + + job = jobMap[import_assertions.type]; + // CommonJS will set functions for lazy job evaluation. - if (typeof job === 'function') this.moduleMap.set(url, job = job(), import_assertions.type); - + if (typeof job === 'function') + this.moduleMap.set(url, job = job(), import_assertions.type); + if (job !== undefined) return job; } @@ -243,6 +258,8 @@ class ESMLoader { if (import_assertions.type === 'json' && finalFormat !== 'json') { throw new ERR_FAILED_IMPORT_ASSERTION( url, 'type', import_assertions.type, finalFormat); + } else if (import_assertions.type == null) { + finalFormatCache.set(url, finalFormat); } const translator = translators.get(finalFormat); diff --git a/lib/internal/modules/esm/module_map.js b/lib/internal/modules/esm/module_map.js index a8b292d4fa245b..89f2fd537c9444 100644 --- a/lib/internal/modules/esm/module_map.js +++ b/lib/internal/modules/esm/module_map.js @@ -26,7 +26,7 @@ class ModuleMap extends SafeMap { } debug(`Storing ${url} in ModuleMap`); const jobMap = super.get(url) ?? ObjectCreate(null); - jobMap[import_assertion_type] = job + jobMap[import_assertion_type] = job; return super.set(url, jobMap); } has(url) { diff --git a/test/es-module/test-esm-dynamic-import-assertion.js b/test/es-module/test-esm-dynamic-import-assertion.js index abefe8b73e99a2..f3daa4a726da0a 100644 --- a/test/es-module/test-esm-dynamic-import-assertion.js +++ b/test/es-module/test-esm-dynamic-import-assertion.js @@ -28,7 +28,7 @@ async function test() { strictEqual(secret0.default.ofLife, 42); strictEqual(secret1.default.ofLife, 42); strictEqual(secret0.default, secret1.default); - // strictEqual(secret0, secret1); + strictEqual(secret0, secret1); } { diff --git a/test/es-module/test-esm-dynamic-import-assertion.mjs b/test/es-module/test-esm-dynamic-import-assertion.mjs index 19cc17953b04e5..b635d8e86ef648 100644 --- a/test/es-module/test-esm-dynamic-import-assertion.mjs +++ b/test/es-module/test-esm-dynamic-import-assertion.mjs @@ -26,7 +26,7 @@ await rejects( strictEqual(secret0.default.ofLife, 42); strictEqual(secret1.default.ofLife, 42); strictEqual(secret0.default, secret1.default); - // strictEqual(secret0, secret1); + strictEqual(secret0, secret1); } { From 733deb87a9b07c5267f30c93e30bb8f76fa6a5ad Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sun, 12 Sep 2021 19:17:55 +0200 Subject: [PATCH 05/13] fixup! fixup! module: unflag import assertions --- lib/internal/modules/esm/loader.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index bbc813bd7bcb5c..47eba194a9dae4 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -15,7 +15,6 @@ const { PromiseAll, RegExpPrototypeExec, SafeArrayIterator, - SafeMap, SafeWeakMap, globalThis, } = primordials; @@ -46,7 +45,7 @@ const { translators } = require( 'internal/modules/esm/translators'); const { getOptionValue } = require('internal/options'); -const finalFormatCache = new SafeMap(); +const finalFormatCache = new SafeWeakMap(); /** * An ESMLoader instance is used as the main entry point for loading ES modules. @@ -235,7 +234,7 @@ class ESMLoader { // fulfill first. await job.modulePromise; - const finalFormat = finalFormatCache.get(url); + const finalFormat = finalFormatCache.get(job); if (import_assertions.type === 'json' && finalFormat !== 'json') { throw new ERR_FAILED_IMPORT_ASSERTION( url, 'type', import_assertions.type, finalFormat); @@ -259,7 +258,7 @@ class ESMLoader { throw new ERR_FAILED_IMPORT_ASSERTION( url, 'type', import_assertions.type, finalFormat); } else if (import_assertions.type == null) { - finalFormatCache.set(url, finalFormat); + finalFormatCache.set(job, finalFormat); } const translator = translators.get(finalFormat); From 4fa6f58bb6a5838c39f54ff4765272a41ba19315 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Mon, 13 Sep 2021 13:51:13 +0200 Subject: [PATCH 06/13] fixup! module: unflag import assertions --- doc/api/errors.md | 8 +++++ doc/api/esm.md | 29 +++++++++++++++++-- lib/internal/errors.js | 3 ++ lib/internal/modules/esm/loader.js | 7 +++++ .../test-esm-dynamic-import-assertion.js | 5 ++++ .../test-esm-dynamic-import-assertion.mjs | 5 ++++ .../esm_import_assertion_unsupported.mjs | 2 ++ .../esm_import_assertion_unsupported.out | 13 +++++++++ 8 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 test/message/esm_import_assertion_unsupported.mjs create mode 100644 test/message/esm_import_assertion_unsupported.out diff --git a/doc/api/errors.md b/doc/api/errors.md index 8f16184e4dd7df..89bf8b044eb573 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -1667,6 +1667,14 @@ for more information. An invalid HTTP token was supplied. + +### `ERR_INVALID_IMPORT_ASSERTION` + + +An import assertion is not supported by this version of Node.js. + ### `ERR_INVALID_IP_ADDRESS` diff --git a/doc/api/esm.md b/doc/api/esm.md index 9d7792441ca6fd..43f2a108100831 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -5,6 +5,9 @@ + +The [Import Assertions proposal][] adds an inline syntax for module import +statements to pass on more information alongside the module specifier. + +```js +import json from './foo.json' assert { type: "json" }; +import('foo.json', { assert: { type: "json" } }); +``` + +Node.js supports the following `type` values: + +| `type` | Resolves to | +| -------- | ---------------- | +| `"json"` | [JSON modules][] | + ## Builtin modules [Core modules][] provide named exports of their public API. A @@ -516,9 +538,10 @@ same path. Assuming an `index.mjs` with - ```js -import packageConfig from './package.json'; +import packageConfig from './package.json' assert { type: 'json' }; +// Or, without using import assertions: +import subPackageConfig from './subPackage/package.json'; ``` The `--experimental-json-modules` flag is needed for the module @@ -1367,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 5226485b042a77..5801de21b50778 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1253,6 +1253,9 @@ E('ERR_INVALID_FILE_URL_HOST', E('ERR_INVALID_FILE_URL_PATH', 'File URL path %s', TypeError); E('ERR_INVALID_HANDLE_TYPE', 'This handle type cannot be sent', TypeError); E('ERR_INVALID_HTTP_TOKEN', '%s must be a valid HTTP token ["%s"]', TypeError); +E('ERR_INVALID_IMPORT_ASSERTION', + (type, value) => `Invalid ${JSONStringify(type)} import assertion: ${JSONStringify(value)}`, + TypeError); E('ERR_INVALID_IP_ADDRESS', 'Invalid IP address: %s', TypeError); E('ERR_INVALID_MODULE_SPECIFIER', (request, reason, base = undefined) => { return `Invalid module "${request}" ${reason}${base ? diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 47eba194a9dae4..f75e47f2a42ae6 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -6,11 +6,13 @@ require('internal/modules/cjs/loader'); const { Array, ArrayIsArray, + ArrayPrototypeIncludes, ArrayPrototypeJoin, ArrayPrototypePush, FunctionPrototypeBind, FunctionPrototypeCall, ObjectCreate, + ObjectFreeze, ObjectSetPrototypeOf, PromiseAll, RegExpPrototypeExec, @@ -23,6 +25,7 @@ const { ERR_FAILED_IMPORT_ASSERTION, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, + ERR_INVALID_IMPORT_ASSERTION, ERR_INVALID_MODULE_SPECIFIER, ERR_INVALID_RETURN_PROPERTY_VALUE, ERR_INVALID_RETURN_VALUE, @@ -46,6 +49,7 @@ const { translators } = require( const { getOptionValue } = require('internal/options'); const finalFormatCache = new SafeWeakMap(); +const supportedTypes = ObjectFreeze([undefined, 'json']); /** * An ESMLoader instance is used as the main entry point for loading ES modules. @@ -222,6 +226,9 @@ class ESMLoader { } async getModuleJob(specifier, parentURL, import_assertions) { + if (!ArrayPrototypeIncludes(supportedTypes, import_assertions.type)) { + throw new ERR_INVALID_IMPORT_ASSERTION('type', import_assertions.type); + } const { format, url } = await this.resolve(specifier, parentURL); const jobMap = this.moduleMap.get(url); diff --git a/test/es-module/test-esm-dynamic-import-assertion.js b/test/es-module/test-esm-dynamic-import-assertion.js index f3daa4a726da0a..5842b7f8383a0a 100644 --- a/test/es-module/test-esm-dynamic-import-assertion.js +++ b/test/es-module/test-esm-dynamic-import-assertion.js @@ -16,6 +16,11 @@ async function test() { { code: 'ERR_FAILED_IMPORT_ASSERTION' } ); + await rejects( + import('specifier', { assert: { type: 'unsupported' } }), + { code: 'ERR_INVALID_IMPORT_ASSERTION' } + ); + { const [secret0, secret1] = await Promise.all([ import('../fixtures/experimental.json'), diff --git a/test/es-module/test-esm-dynamic-import-assertion.mjs b/test/es-module/test-esm-dynamic-import-assertion.mjs index b635d8e86ef648..fa95b17aaa753a 100644 --- a/test/es-module/test-esm-dynamic-import-assertion.mjs +++ b/test/es-module/test-esm-dynamic-import-assertion.mjs @@ -14,6 +14,11 @@ await rejects( { code: 'ERR_FAILED_IMPORT_ASSERTION' } ); +await rejects( + import('specifier', { assert: { type: 'unsupported' } }), + { code: 'ERR_INVALID_IMPORT_ASSERTION' } +); + { const [secret0, secret1] = await Promise.all([ import('../fixtures/experimental.json'), diff --git a/test/message/esm_import_assertion_unsupported.mjs b/test/message/esm_import_assertion_unsupported.mjs new file mode 100644 index 00000000000000..4876aa263d1786 --- /dev/null +++ b/test/message/esm_import_assertion_unsupported.mjs @@ -0,0 +1,2 @@ +import '../common/index.mjs'; +import 'specifier' 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..65579f4dc7cbcd --- /dev/null +++ b/test/message/esm_import_assertion_unsupported.out @@ -0,0 +1,13 @@ +node:internal/errors:* + ErrorCaptureStackTrace(err); + ^ + +TypeError [ERR_INVALID_IMPORT_ASSERTION]: Invalid "type" import assertion: "unsupported" + at new NodeError (node:internal/errors:*:*) + at ESMLoader.getModuleJob (node:internal/modules/esm/loader:*:*) + at ModuleWrap. (node:internal/modules/esm/module_job:*:*) + at link (node:internal/modules/esm/module_job:*:*) { + code: 'ERR_INVALID_IMPORT_ASSERTION' +} + +Node.js * From 1e6330a0fe881cb77c80a7b2166f89945a7daed8 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Mon, 13 Sep 2021 17:41:31 +0200 Subject: [PATCH 07/13] fixup! module: unflag import assertions --- lib/internal/modules/esm/loader.js | 37 ++++++++++--------- lib/internal/modules/esm/module_map.js | 7 +--- .../test-esm-dynamic-import-assertion.js | 5 +++ .../test-esm-dynamic-import-assertion.mjs | 5 +++ 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index f75e47f2a42ae6..63b78108e34d3a 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -48,6 +48,7 @@ const { translators } = require( 'internal/modules/esm/translators'); const { getOptionValue } = require('internal/options'); +const importAssertionTypeCache = new SafeWeakMap(); const finalFormatCache = new SafeWeakMap(); const supportedTypes = ObjectFreeze([undefined, 'json']); @@ -231,26 +232,28 @@ class ESMLoader { } const { format, url } = await this.resolve(specifier, parentURL); - const jobMap = this.moduleMap.get(url); - let job; + let job = this.moduleMap.get(url); - if (jobMap != null) { - if (import_assertions.type != null && undefined in jobMap) { - job = jobMap[undefined]; - // To avoid race conditions, always wait for non typed import to - // fulfill first. + if (job != null) { + const currentImportAssertionType = importAssertionTypeCache.get(job); + if (currentImportAssertionType === import_assertions.type) return job; + + try { + // To avoid race conditions, always wait for previous module to fulfill first. await job.modulePromise; const finalFormat = finalFormatCache.get(job); - if (import_assertions.type === 'json' && finalFormat !== 'json') { - throw new ERR_FAILED_IMPORT_ASSERTION( - url, 'type', import_assertions.type, finalFormat); - } - return job; + if ( + import_assertions.type == null || + (import_assertions.type === 'json' && finalFormat === 'json') + ) return job; + return PromiseReject(new ERR_FAILED_IMPORT_ASSERTION( + url, 'type', import_assertions.type, finalFormat)); + } catch { + // If the other job with a different type assertion failed, we got another chance. + job = undefined } - job = jobMap[import_assertions.type]; - // CommonJS will set functions for lazy job evaluation. if (typeof job === 'function') this.moduleMap.set(url, job = job(), import_assertions.type); @@ -264,9 +267,8 @@ class ESMLoader { if (import_assertions.type === 'json' && finalFormat !== 'json') { throw new ERR_FAILED_IMPORT_ASSERTION( url, 'type', import_assertions.type, finalFormat); - } else if (import_assertions.type == null) { - finalFormatCache.set(job, finalFormat); } + finalFormatCache.set(job, finalFormat); const translator = translators.get(finalFormat); @@ -288,7 +290,8 @@ class ESMLoader { inspectBrk ); - this.moduleMap.set(url, job, import_assertions.type); + importAssertionTypeCache.set(job, import_assertions.type); + this.moduleMap.set(url, job); return job; } diff --git a/lib/internal/modules/esm/module_map.js b/lib/internal/modules/esm/module_map.js index 89f2fd537c9444..9e1116a5647f5f 100644 --- a/lib/internal/modules/esm/module_map.js +++ b/lib/internal/modules/esm/module_map.js @@ -2,7 +2,6 @@ const ModuleJob = require('internal/modules/esm/module_job'); const { - ObjectCreate, SafeMap, } = primordials; let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { @@ -18,16 +17,14 @@ class ModuleMap extends SafeMap { validateString(url, 'url'); return super.get(url); } - set(url, job, import_assertion_type) { + set(url, job) { validateString(url, 'url'); if (job instanceof ModuleJob !== true && typeof job !== 'function') { throw new ERR_INVALID_ARG_TYPE('job', 'ModuleJob', job); } debug(`Storing ${url} in ModuleMap`); - const jobMap = super.get(url) ?? ObjectCreate(null); - jobMap[import_assertion_type] = job; - return super.set(url, jobMap); + return super.set(url, job); } has(url) { validateString(url, 'url'); diff --git a/test/es-module/test-esm-dynamic-import-assertion.js b/test/es-module/test-esm-dynamic-import-assertion.js index 5842b7f8383a0a..c5555336b91413 100644 --- a/test/es-module/test-esm-dynamic-import-assertion.js +++ b/test/es-module/test-esm-dynamic-import-assertion.js @@ -21,6 +21,11 @@ async function test() { { code: 'ERR_INVALID_IMPORT_ASSERTION' } ); + await rejects( + import('specifier', { assert: { type: 'undefined' } }), + { code: 'ERR_INVALID_IMPORT_ASSERTION' } + ); + { const [secret0, secret1] = await Promise.all([ import('../fixtures/experimental.json'), diff --git a/test/es-module/test-esm-dynamic-import-assertion.mjs b/test/es-module/test-esm-dynamic-import-assertion.mjs index fa95b17aaa753a..f509042b8a46f1 100644 --- a/test/es-module/test-esm-dynamic-import-assertion.mjs +++ b/test/es-module/test-esm-dynamic-import-assertion.mjs @@ -19,6 +19,11 @@ await rejects( { code: 'ERR_INVALID_IMPORT_ASSERTION' } ); +await rejects( + import('specifier', { assert: { type: 'undefined' } }), + { code: 'ERR_INVALID_IMPORT_ASSERTION' } +); + { const [secret0, secret1] = await Promise.all([ import('../fixtures/experimental.json'), From 1ecb35a9ca77b62c6f3bf81c6a7872a38707e274 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Mon, 13 Sep 2021 17:46:00 +0200 Subject: [PATCH 08/13] fixup! module: unflag import assertions --- lib/internal/modules/esm/loader.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 63b78108e34d3a..fafb64c8e5d925 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -15,6 +15,7 @@ const { ObjectFreeze, ObjectSetPrototypeOf, PromiseAll, + PromiseReject, RegExpPrototypeExec, SafeArrayIterator, SafeWeakMap, @@ -239,7 +240,7 @@ class ESMLoader { if (currentImportAssertionType === import_assertions.type) return job; try { - // To avoid race conditions, always wait for previous module to fulfill first. + // To avoid race conditions, wait for previous module to fulfill first. await job.modulePromise; const finalFormat = finalFormatCache.get(job); @@ -250,8 +251,9 @@ class ESMLoader { return PromiseReject(new ERR_FAILED_IMPORT_ASSERTION( url, 'type', import_assertions.type, finalFormat)); } catch { - // If the other job with a different type assertion failed, we got another chance. - job = undefined + // If the other job with a different type assertion failed, we got + // another chance. + job = undefined; } // CommonJS will set functions for lazy job evaluation. From 180099e74e6173e2cfc41d4abf3a4f53c68203bb Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Mon, 13 Sep 2021 18:03:51 +0200 Subject: [PATCH 09/13] fixup! module: unflag import assertions --- lib/internal/modules/esm/loader.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index fafb64c8e5d925..a43c9c5f037d24 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -235,6 +235,10 @@ class ESMLoader { let job = this.moduleMap.get(url); + // CommonJS will set functions for lazy job evaluation. + if (typeof job === 'function') + this.moduleMap.set(url, job = job()); + if (job != null) { const currentImportAssertionType = importAssertionTypeCache.get(job); if (currentImportAssertionType === import_assertions.type) return job; @@ -255,12 +259,6 @@ class ESMLoader { // another chance. job = undefined; } - - // CommonJS will set functions for lazy job evaluation. - if (typeof job === 'function') - this.moduleMap.set(url, job = job(), import_assertions.type); - - if (job !== undefined) return job; } const moduleProvider = async (url, isMain) => { From 83ff00e72a7453d82305c1422a175e38eeeba6a3 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Mon, 13 Sep 2021 18:10:20 +0200 Subject: [PATCH 10/13] fixup! module: unflag import assertions --- lib/internal/modules/esm/loader.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index a43c9c5f037d24..882d60dba111b5 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -231,13 +231,11 @@ class ESMLoader { if (!ArrayPrototypeIncludes(supportedTypes, import_assertions.type)) { throw new ERR_INVALID_IMPORT_ASSERTION('type', import_assertions.type); } - const { format, url } = await this.resolve(specifier, parentURL); + const { format, url } = await this.resolve(specifier, parentURL); let job = this.moduleMap.get(url); - // CommonJS will set functions for lazy job evaluation. - if (typeof job === 'function') - this.moduleMap.set(url, job = job()); + if (typeof job === 'function') this.moduleMap.set(url, job = job()); if (job != null) { const currentImportAssertionType = importAssertionTypeCache.get(job); From b5ad8b0bea5f94d9bd97d6c99a4e75ab215df7f6 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Mon, 13 Sep 2021 18:22:56 +0200 Subject: [PATCH 11/13] fixup! module: unflag import assertions --- lib/internal/modules/esm/loader.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 882d60dba111b5..a1fedd0cd05d72 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -15,7 +15,6 @@ const { ObjectFreeze, ObjectSetPrototypeOf, PromiseAll, - PromiseReject, RegExpPrototypeExec, SafeArrayIterator, SafeWeakMap, @@ -244,18 +243,20 @@ class ESMLoader { try { // To avoid race conditions, wait for previous module to fulfill first. await job.modulePromise; + } catch { + // If the other job with a different type assertion failed, we got + // another chance. + job = undefined; + } + if (job !== undefined) { const finalFormat = finalFormatCache.get(job); if ( import_assertions.type == null || (import_assertions.type === 'json' && finalFormat === 'json') ) return job; - return PromiseReject(new ERR_FAILED_IMPORT_ASSERTION( - url, 'type', import_assertions.type, finalFormat)); - } catch { - // If the other job with a different type assertion failed, we got - // another chance. - job = undefined; + throw new ERR_FAILED_IMPORT_ASSERTION( + url, 'type', import_assertions.type, finalFormat); } } From d364844f2ea84a5715533323917c37f3df1b1674 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Tue, 14 Sep 2021 10:30:30 +0200 Subject: [PATCH 12/13] fixup! module: unflag import assertions --- lib/internal/modules/esm/loader.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index a1fedd0cd05d72..dc3fcefb03cf19 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -218,7 +218,8 @@ class ESMLoader { return module; }; const job = new ModuleJob(this, url, evalInstance, false, false); - this.moduleMap.set(url, job, undefined); + this.moduleMap.set(url, job); + finalFormatCache.set(job, 'module'); const { module } = await job.run(); return { @@ -244,7 +245,7 @@ class ESMLoader { // To avoid race conditions, wait for previous module to fulfill first. await job.modulePromise; } catch { - // If the other job with a different type assertion failed, we got + // If the other job failed with a different `type` assertion, we got // another chance. job = undefined; } From d19ecc49952cc51d64a2345017de9e3b5e526fc6 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Tue, 21 Sep 2021 17:07:39 +0200 Subject: [PATCH 13/13] fixup! module: unflag import assertions --- doc/api/esm.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index 43f2a108100831..15158dcaca3031 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -241,7 +241,7 @@ statements to pass on more information alongside the module specifier. ```js import json from './foo.json' assert { type: "json" }; -import('foo.json', { assert: { type: "json" } }); +await import('foo.json', { assert: { type: "json" } }); ``` Node.js supports the following `type` values: @@ -540,8 +540,6 @@ Assuming an `index.mjs` with ```js import packageConfig from './package.json' assert { type: 'json' }; -// Or, without using import assertions: -import subPackageConfig from './subPackage/package.json'; ``` The `--experimental-json-modules` flag is needed for the module