diff --git a/doc/api/errors.md b/doc/api/errors.md
index 75146c24c359f1..7cdee52e792660 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -1779,6 +1779,11 @@ The V8 `BreakIterator` API was used but the full ICU data set is not installed.
While using the Performance Timing API (`perf_hooks`), no valid performance
entry types were found.
+
+### ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING
+
+A dynamic import callback was not specified.
+
### ERR_VM_MODULE_ALREADY_LINKED
diff --git a/doc/api/vm.md b/doc/api/vm.md
index 6259b9b2ffd3f5..07923de5856e7d 100644
--- a/doc/api/vm.md
+++ b/doc/api/vm.md
@@ -167,10 +167,19 @@ const contextifiedSandbox = vm.createContext({ secret: 42 });
in stack traces produced by this `Module`.
* `columnOffset` {integer} Specifies the column number offset that is
displayed in stack traces produced by this `Module`.
- * `initalizeImportMeta` {Function} Called during evaluation of this `Module`
+ * `initializeImportMeta` {Function} Called during evaluation of this `Module`
to initialize the `import.meta`. This function has the signature `(meta,
module)`, where `meta` is the `import.meta` object in the `Module`, and
`module` is this `vm.SourceTextModule` object.
+ * `importModuleDynamically` {Function} Called during evaluation of this
+ module when `import()` is called. This function has the signature
+ `(specifier, module)` where `specifier` is the specifier passed to
+ `import()` and `module` is this `vm.SourceTextModule`. If this option is
+ not specified, calls to `import()` will reject with
+ [`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][]. This method can return a
+ [Module Namespace Object][], but returning a `vm.SourceTextModule` is
+ recommended in order to take advantage of error tracking, and to avoid
+ issues with namespaces that contain `then` function exports.
Creates a new ES `Module` object.
@@ -436,6 +445,15 @@ changes:
The `cachedDataProduced` value will be set to either `true` or `false`
depending on whether code cache data is produced successfully.
This option is deprecated in favor of `script.createCachedData()`.
+ * `importModuleDynamically` {Function} Called during evaluation of this
+ module when `import()` is called. This function has the signature
+ `(specifier, module)` where `specifier` is the specifier passed to
+ `import()` and `module` is this `vm.SourceTextModule`. If this option is
+ not specified, calls to `import()` will reject with
+ [`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][]. This method can return a
+ [Module Namespace Object][], but returning a `vm.SourceTextModule` is
+ recommended in order to take advantage of error tracking, and to avoid
+ issues with namespaces that contain `then` function exports.
Creating a new `vm.Script` object compiles `code` but does not run it. The
compiled `vm.Script` can be run later multiple times. The `code` is not bound to
@@ -945,6 +963,7 @@ associating it with the `sandbox` object is what this document refers to as
"contextifying" the `sandbox`.
[`Error`]: errors.html#errors_class_error
+[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`]: errors.html#ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING
[`URL`]: url.html#url_class_url
[`eval()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
[`script.runInContext()`]: #vm_script_runincontext_contextifiedsandbox_options
@@ -954,6 +973,7 @@ associating it with the `sandbox` object is what this document refers to as
[`vm.runInContext()`]: #vm_vm_runincontext_code_contextifiedsandbox_options
[`vm.runInThisContext()`]: #vm_vm_runinthiscontext_code_options
[GetModuleNamespace]: https://tc39.github.io/ecma262/#sec-getmodulenamespace
+[Module Namespace Object]: https://tc39.github.io/ecma262/#sec-module-namespace-exotic-objects
[ECMAScript Module Loader]: esm.html#esm_ecmascript_modules
[Evaluate() concrete method]: https://tc39.github.io/ecma262/#sec-moduleevaluation
[HostResolveImportedModule]: https://tc39.github.io/ecma262/#sec-hostresolveimportedmodule
diff --git a/lib/internal/bootstrap/loaders.js b/lib/internal/bootstrap/loaders.js
index 8234275700c535..359812e1e9e342 100644
--- a/lib/internal/bootstrap/loaders.js
+++ b/lib/internal/bootstrap/loaders.js
@@ -107,6 +107,8 @@
};
}
+ // Create this WeakMap in js-land because V8 has no C++ API for WeakMap
+ internalBinding('module_wrap').callbackMap = new WeakMap();
const { ContextifyScript } = internalBinding('contextify');
// Set up NativeModule
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 43124cc66b452c..4094a40f6b5631 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -873,6 +873,8 @@ E('ERR_V8BREAKITERATOR',
// This should probably be a `TypeError`.
E('ERR_VALID_PERFORMANCE_ENTRY_TYPE',
'At least one valid performance entry type is required', Error);
+E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING',
+ 'A dynamic import callback was not specified.', TypeError);
E('ERR_VM_MODULE_ALREADY_LINKED', 'Module has already been linked', Error);
E('ERR_VM_MODULE_DIFFERENT_CONTEXT',
'Linked modules must use the same context', Error);
diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js
index f3f8b0c8e0afe7..6cdafac3de5c17 100644
--- a/lib/internal/modules/cjs/loader.js
+++ b/lib/internal/modules/cjs/loader.js
@@ -29,6 +29,7 @@ const assert = require('assert').ok;
const fs = require('fs');
const internalFS = require('internal/fs/utils');
const path = require('path');
+const { URL } = require('url');
const {
internalModuleReadJSON,
internalModuleStat
@@ -656,6 +657,13 @@ Module.prototype.require = function(id) {
// (needed for setting breakpoint when called with --inspect-brk)
var resolvedArgv;
+function normalizeReferrerURL(referrer) {
+ if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
+ return pathToFileURL(referrer).href;
+ }
+ return new URL(referrer).href;
+}
+
// Run the file contents in the correct scope or sandbox. Expose
// the correct helper variables (require, module, exports) to
@@ -671,7 +679,12 @@ Module.prototype._compile = function(content, filename) {
var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
- displayErrors: true
+ displayErrors: true,
+ importModuleDynamically: experimentalModules ? async (specifier) => {
+ if (asyncESM === undefined) lazyLoadESM();
+ const loader = await asyncESM.loaderPromise;
+ return loader.import(specifier, normalizeReferrerURL(filename));
+ } : undefined,
});
var inspectorWrapper = null;
diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js
index df3c446cab7ce7..0c34283b8af9e0 100644
--- a/lib/internal/modules/esm/translators.js
+++ b/lib/internal/modules/esm/translators.js
@@ -1,7 +1,7 @@
'use strict';
const { NativeModule } = require('internal/bootstrap/loaders');
-const { ModuleWrap } = internalBinding('module_wrap');
+const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
const {
stripShebang,
stripBOM
@@ -15,6 +15,8 @@ const { _makeLong } = require('path');
const { SafeMap } = require('internal/safe_globals');
const { URL } = require('url');
const { debuglog, promisify } = require('util');
+const esmLoader = require('internal/process/esm_loader');
+
const readFileAsync = promisify(fs.readFile);
const readFileSync = fs.readFileSync;
const StringReplace = Function.call.bind(String.prototype.replace);
@@ -25,13 +27,27 @@ const debug = debuglog('esm');
const translators = new SafeMap();
module.exports = translators;
+function initializeImportMeta(meta, { url }) {
+ meta.url = url;
+}
+
+async function importModuleDynamically(specifier, { url }) {
+ const loader = await esmLoader.loaderPromise;
+ return loader.import(specifier, url);
+}
+
// Strategy for loading a standard JavaScript module
translators.set('esm', async (url) => {
const source = `${await readFileAsync(new URL(url))}`;
debug(`Translating StandardModule ${url}`);
+ const module = new ModuleWrap(stripShebang(source), url);
+ callbackMap.set(module, {
+ initializeImportMeta,
+ importModuleDynamically,
+ });
return {
- module: new ModuleWrap(stripShebang(source), url),
- reflect: undefined
+ module,
+ reflect: undefined,
};
});
diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js
index 23b98c620e64e5..b2415ec171f985 100644
--- a/lib/internal/process/esm_loader.js
+++ b/lib/internal/process/esm_loader.js
@@ -2,40 +2,42 @@
const {
setImportModuleDynamicallyCallback,
- setInitializeImportMetaObjectCallback
+ setInitializeImportMetaObjectCallback,
+ callbackMap,
} = internalBinding('module_wrap');
const { pathToFileURL } = require('internal/url');
const Loader = require('internal/modules/esm/loader');
-const path = require('path');
-const { URL } = require('url');
const {
- initImportMetaMap,
- wrapToModuleMap
+ wrapToModuleMap,
} = require('internal/vm/source_text_module');
+const {
+ ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING,
+} = require('internal/errors').codes;
-function normalizeReferrerURL(referrer) {
- if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
- return pathToFileURL(referrer).href;
+function initializeImportMetaObject(wrap, meta) {
+ if (callbackMap.has(wrap)) {
+ const { initializeImportMeta } = callbackMap.get(wrap);
+ if (initializeImportMeta !== undefined) {
+ initializeImportMeta(meta, wrapToModuleMap.get(wrap) || wrap);
+ }
}
- return new URL(referrer).href;
}
-function initializeImportMetaObject(wrap, meta) {
- const vmModule = wrapToModuleMap.get(wrap);
- if (vmModule === undefined) {
- // This ModuleWrap belongs to the Loader.
- meta.url = wrap.url;
- } else {
- const initializeImportMeta = initImportMetaMap.get(vmModule);
- if (initializeImportMeta !== undefined) {
- // This ModuleWrap belongs to vm.SourceTextModule,
- // initializer callback was provided.
- initializeImportMeta(meta, vmModule);
+async function importModuleDynamicallyCallback(wrap, specifier) {
+ if (callbackMap.has(wrap)) {
+ const { importModuleDynamically } = callbackMap.get(wrap);
+ if (importModuleDynamically !== undefined) {
+ return importModuleDynamically(
+ specifier, wrapToModuleMap.get(wrap) || wrap);
}
}
+ throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING();
}
+setInitializeImportMetaObjectCallback(initializeImportMetaObject);
+setImportModuleDynamicallyCallback(importModuleDynamicallyCallback);
+
let loaderResolve;
exports.loaderPromise = new Promise((resolve, reject) => {
loaderResolve = resolve;
@@ -44,8 +46,6 @@ exports.loaderPromise = new Promise((resolve, reject) => {
exports.ESMLoader = undefined;
exports.setup = function() {
- setInitializeImportMetaObjectCallback(initializeImportMetaObject);
-
let ESMLoader = new Loader();
const loaderPromise = (async () => {
const userLoader = process.binding('config').userLoader;
@@ -60,10 +60,5 @@ exports.setup = function() {
})();
loaderResolve(loaderPromise);
- setImportModuleDynamicallyCallback(async (referrer, specifier) => {
- const loader = await loaderPromise;
- return loader.import(specifier, normalizeReferrerURL(referrer));
- });
-
exports.ESMLoader = ESMLoader;
};
diff --git a/lib/internal/vm/source_text_module.js b/lib/internal/vm/source_text_module.js
index c1c3611d8fd38a..d22db6e914f50d 100644
--- a/lib/internal/vm/source_text_module.js
+++ b/lib/internal/vm/source_text_module.js
@@ -1,5 +1,6 @@
'use strict';
+const { isModuleNamespaceObject } = require('util').types;
const { URL } = require('internal/url');
const { isContext } = internalBinding('contextify');
const {
@@ -9,7 +10,7 @@ const {
ERR_VM_MODULE_LINKING_ERRORED,
ERR_VM_MODULE_NOT_LINKED,
ERR_VM_MODULE_NOT_MODULE,
- ERR_VM_MODULE_STATUS
+ ERR_VM_MODULE_STATUS,
} = require('internal/errors').codes;
const {
getConstructorOf,
@@ -21,6 +22,7 @@ const { validateInt32, validateUint32 } = require('internal/validators');
const {
ModuleWrap,
+ callbackMap,
kUninstantiated,
kInstantiating,
kInstantiated,
@@ -43,8 +45,6 @@ const perContextModuleId = new WeakMap();
const wrapMap = new WeakMap();
const dependencyCacheMap = new WeakMap();
const linkingStatusMap = new WeakMap();
-// vm.SourceTextModule -> function
-const initImportMetaMap = new WeakMap();
// ModuleWrap -> vm.SourceTextModule
const wrapToModuleMap = new WeakMap();
const defaultModuleName = 'vm:module';
@@ -63,7 +63,8 @@ class SourceTextModule {
context,
lineOffset = 0,
columnOffset = 0,
- initializeImportMeta
+ initializeImportMeta,
+ importModuleDynamically,
} = options;
if (context !== undefined) {
@@ -96,13 +97,16 @@ class SourceTextModule {
validateInt32(lineOffset, 'options.lineOffset');
validateInt32(columnOffset, 'options.columnOffset');
- if (initializeImportMeta !== undefined) {
- if (typeof initializeImportMeta === 'function') {
- initImportMetaMap.set(this, initializeImportMeta);
- } else {
- throw new ERR_INVALID_ARG_TYPE(
- 'options.initializeImportMeta', 'function', initializeImportMeta);
- }
+ if (initializeImportMeta !== undefined &&
+ typeof initializeImportMeta !== 'function') {
+ throw new ERR_INVALID_ARG_TYPE(
+ 'options.initializeImportMeta', 'function', initializeImportMeta);
+ }
+
+ if (importModuleDynamically !== undefined &&
+ typeof importModuleDynamically !== 'function') {
+ throw new ERR_INVALID_ARG_TYPE(
+ 'options.importModuleDynamically', 'function', importModuleDynamically);
}
const wrap = new ModuleWrap(src, url, context, lineOffset, columnOffset);
@@ -110,6 +114,22 @@ class SourceTextModule {
linkingStatusMap.set(this, 'unlinked');
wrapToModuleMap.set(wrap, this);
+ callbackMap.set(wrap, {
+ initializeImportMeta,
+ importModuleDynamically: importModuleDynamically ? async (...args) => {
+ const m = await importModuleDynamically(...args);
+ if (isModuleNamespaceObject(m)) {
+ return m;
+ }
+ if (!m || !wrapMap.has(m))
+ throw new ERR_VM_MODULE_NOT_MODULE();
+ const childLinkingStatus = linkingStatusMap.get(m);
+ if (childLinkingStatus === 'errored')
+ throw m.error;
+ return m.namespace;
+ } : undefined,
+ });
+
Object.defineProperties(this, {
url: { value: url, enumerable: true },
context: { value: context, enumerable: true },
@@ -245,6 +265,7 @@ class SourceTextModule {
module.exports = {
SourceTextModule,
- initImportMetaMap,
- wrapToModuleMap
+ wrapToModuleMap,
+ wrapMap,
+ linkingStatusMap,
};
diff --git a/lib/vm.js b/lib/vm.js
index 373fb4029dd605..869b4aa65485cb 100644
--- a/lib/vm.js
+++ b/lib/vm.js
@@ -27,9 +27,12 @@ const {
isContext: _isContext,
compileFunction: _compileFunction
} = internalBinding('contextify');
-
-const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes;
-const { isUint8Array } = require('internal/util/types');
+const { callbackMap } = internalBinding('module_wrap');
+const {
+ ERR_INVALID_ARG_TYPE,
+ ERR_VM_MODULE_NOT_MODULE,
+} = require('internal/errors').codes;
+const { isModuleNamespaceObject, isUint8Array } = require('util').types;
const { validateInt32, validateUint32 } = require('internal/validators');
const kParsingContext = Symbol('script parsing context');
@@ -52,7 +55,8 @@ class Script extends ContextifyScript {
columnOffset = 0,
cachedData,
produceCachedData = false,
- [kParsingContext]: parsingContext
+ importModuleDynamically,
+ [kParsingContext]: parsingContext,
} = options;
if (typeof filename !== 'string') {
@@ -83,6 +87,28 @@ class Script extends ContextifyScript {
} catch (e) {
throw e; /* node-do-not-add-exception-line */
}
+
+ if (importModuleDynamically !== undefined) {
+ if (typeof importModuleDynamically !== 'function') {
+ throw new ERR_INVALID_ARG_TYPE('options.importModuleDynamically',
+ 'function',
+ importModuleDynamically);
+ }
+ const { wrapMap, linkingStatusMap } =
+ require('internal/vm/source_text_module');
+ callbackMap.set(this, { importModuleDynamically: async (...args) => {
+ const m = await importModuleDynamically(...args);
+ if (isModuleNamespaceObject(m)) {
+ return m;
+ }
+ if (!m || !wrapMap.has(m))
+ throw new ERR_VM_MODULE_NOT_MODULE();
+ const childLinkingStatus = linkingStatusMap.get(m);
+ if (childLinkingStatus === 'errored')
+ throw m.error;
+ return m.namespace;
+ } });
+ }
}
runInThisContext(options) {
diff --git a/src/env-inl.h b/src/env-inl.h
index e4a635c84d3a18..6ace0bf82533d5 100644
--- a/src/env-inl.h
+++ b/src/env-inl.h
@@ -446,6 +446,13 @@ Environment::trace_category_state() {
return trace_category_state_;
}
+inline uint32_t Environment::get_next_module_id() {
+ return module_id_counter_++;
+}
+inline uint32_t Environment::get_next_script_id() {
+ return script_id_counter_++;
+}
+
Environment::ShouldNotAbortOnUncaughtScope::ShouldNotAbortOnUncaughtScope(
Environment* env)
: env_(env) {
diff --git a/src/env.h b/src/env.h
index a210252643c8a4..1eb333b0ef31d9 100644
--- a/src/env.h
+++ b/src/env.h
@@ -47,6 +47,10 @@ struct nghttp2_rcbuf;
namespace node {
+namespace contextify {
+class ContextifyScript;
+}
+
namespace fs {
class FileHandleReadWrap;
}
@@ -674,7 +678,13 @@ class Environment {
// List of id's that have been destroyed and need the destroy() cb called.
inline std::vector* destroy_async_id_list();
- std::unordered_multimap module_map;
+ std::unordered_multimap hash_to_module_map;
+ std::unordered_map id_to_module_map;
+ std::unordered_map
+ id_to_script_map;
+
+ inline uint32_t get_next_module_id();
+ inline uint32_t get_next_script_id();
std::unordered_map
package_json_cache;
@@ -924,6 +934,9 @@ class Environment {
std::shared_ptr options_;
+ uint32_t module_id_counter_ = 0;
+ uint32_t script_id_counter_ = 0;
+
AliasedBuffer should_abort_on_uncaught_toggle_;
int should_not_abort_scope_counter_ = 0;
diff --git a/src/module_wrap.cc b/src/module_wrap.cc
index 1ef22b270d1230..4a7be86af80250 100644
--- a/src/module_wrap.cc
+++ b/src/module_wrap.cc
@@ -33,7 +33,9 @@ using v8::Maybe;
using v8::MaybeLocal;
using v8::Module;
using v8::Nothing;
+using v8::Number;
using v8::Object;
+using v8::PrimitiveArray;
using v8::Promise;
using v8::ScriptCompiler;
using v8::ScriptOrigin;
@@ -47,18 +49,22 @@ static const char* const EXTENSIONS[] = {".mjs", ".js", ".json", ".node"};
ModuleWrap::ModuleWrap(Environment* env,
Local