diff --git a/doc/api/errors.md b/doc/api/errors.md
index df81718e7d7fb0..0ebd66edfe6825 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -1036,6 +1036,12 @@ is set for the `Http2Stream`.
`http2.connect()` was passed a URL that uses any protocol other than `http:` or
`https:`.
+
+### ERR_DYNAMIC_IMPORT_CALLBACK_MISSING
+
+There was a dynamic import request but the script or module did not provide
+a callback to handle it.
+
### ERR_INDEX_OUT_OF_RANGE
diff --git a/doc/api/vm.md b/doc/api/vm.md
index 6c7bdeffda23f7..9c10d2ca871f07 100644
--- a/doc/api/vm.md
+++ b/doc/api/vm.md
@@ -437,6 +437,9 @@ changes:
`cachedData` property of the returned `vm.Script` instance.
The `cachedDataProduced` value will be set to either `true` or `false`
depending on whether code cache data is produced successfully.
+ * `resolveDynamicImport` {Function} See [`module.link()`][]
+ * `specifier` {string}
+ * `scriptOrModule` {vm.Script|vm.Module|Module Namespace Object}
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
@@ -894,6 +897,7 @@ associating it with the `sandbox` object is what this document refers to as
[`Error`]: errors.html#errors_class_error
[`URL`]: url.html#url_class_url
[`eval()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
+[`module.link()`]: #vm_module_link_linker
[`script.runInContext()`]: #vm_script_runincontext_contextifiedsandbox_options
[`script.runInThisContext()`]: #vm_script_runinthiscontext_options
[`url.origin`]: url.html#url_url_origin
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 09f58506c44ea0..75ecbd89fad3f1 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -927,6 +927,8 @@ E('ERR_IPC_CHANNEL_CLOSED', 'Channel closed', Error);
E('ERR_IPC_DISCONNECTED', 'IPC channel is already disconnected', Error);
E('ERR_IPC_ONE_PIPE', 'Child process can have only one IPC pipe', Error);
E('ERR_IPC_SYNC_FORK', 'IPC cannot be used with synchronous forks', Error);
+E('ERR_DYNAMIC_IMPORT_CALLBACK_MISSING',
+ 'No callback available for dynamic import request: %s', Error);
E('ERR_METHOD_NOT_IMPLEMENTED', 'The %s method is not implemented', Error);
E('ERR_MISSING_ARGS', missingArgs, TypeError);
E('ERR_MISSING_MODULE', 'Cannot find module %s', Error);
diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js
index 652378ad5782fb..abbec4073c76a8 100644
--- a/lib/internal/modules/cjs/loader.js
+++ b/lib/internal/modules/cjs/loader.js
@@ -646,7 +646,12 @@ Module.prototype._compile = function(content, filename) {
var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
- displayErrors: true
+ displayErrors: true,
+ resolveDynamicImport: experimentalModules ? async (specifier, script) => {
+ const loader = await asyncESM.loaderPromise;
+ const referrer = getURLFromFilePath(script.filename).href;
+ return loader.import(specifier, referrer);
+ } : undefined,
});
var inspectorWrapper = null;
diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js
index d181647c4505e8..9a8bea6f73942f 100644
--- a/lib/internal/modules/esm/translators.js
+++ b/lib/internal/modules/esm/translators.js
@@ -14,6 +14,11 @@ const fs = require('fs');
const { _makeLong } = require('path');
const { SafeMap } = require('internal/safe_globals');
const { URL } = require('url');
+const {
+ initializeImportMetaMap,
+ importModuleDynamicallyMap,
+ loaderPromise,
+} = require('internal/process/esm_loader');
const debug = require('util').debuglog('esm');
const readFileAsync = require('util').promisify(fs.readFile);
const readFileSync = fs.readFileSync;
@@ -27,10 +32,13 @@ module.exports = translators;
translators.set('esm', async (url) => {
const source = `${await readFileAsync(new URL(url))}`;
debug(`Translating StandardModule ${url}`);
- return {
- module: new ModuleWrap(stripShebang(source), url),
- reflect: undefined
- };
+ const module = new ModuleWrap(stripShebang(source), url);
+ initializeImportMetaMap.set(module, (meta) => { meta.url = url; });
+ importModuleDynamicallyMap.set(module, async (specifier) => {
+ const loader = await loaderPromise;
+ return loader.import(specifier, url);
+ });
+ return { module, reflect: undefined };
});
// Strategy for loading a node-style CommonJS module
diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js
index db28ca04b16cdc..888aff01b59d74 100644
--- a/lib/internal/process/esm_loader.js
+++ b/lib/internal/process/esm_loader.js
@@ -5,37 +5,11 @@ const {
setImportModuleDynamicallyCallback,
setInitializeImportMetaObjectCallback
} = internalBinding('module_wrap');
-
+const {
+ ERR_DYNAMIC_IMPORT_CALLBACK_MISSING,
+} = require('internal/errors').codes;
const { getURLFromFilePath } = require('internal/url');
const Loader = require('internal/modules/esm/loader');
-const path = require('path');
-const { URL } = require('url');
-const {
- initImportMetaMap,
- wrapToModuleMap
-} = require('internal/vm/module');
-
-function normalizeReferrerURL(referrer) {
- if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
- return getURLFromFilePath(referrer).href;
- }
- 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.Module, initializer callback was
- // provided.
- initializeImportMeta(meta, vmModule);
- }
- }
-}
let loaderResolve;
exports.loaderPromise = new Promise((resolve, reject) => {
@@ -45,8 +19,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;
@@ -61,10 +33,24 @@ exports.setup = function() {
})();
loaderResolve(loaderPromise);
- setImportModuleDynamicallyCallback(async (referrer, specifier) => {
- const loader = await loaderPromise;
- return loader.import(specifier, normalizeReferrerURL(referrer));
- });
-
exports.ESMLoader = ESMLoader;
};
+
+const initializeImportMetaMap = exports.initializeImportMetaMap = new WeakMap();
+const importModuleDynamicallyMap =
+ exports.importModuleDynamicallyMap = new WeakMap();
+
+const config = process.binding('config');
+if (config.experimentalModules || config.experimentalVMModules) {
+ setInitializeImportMetaObjectCallback((meta, wrap) => {
+ if (initializeImportMetaMap.has(wrap))
+ return initializeImportMetaMap.get(wrap)(meta);
+ });
+
+ setImportModuleDynamicallyCallback(async (referrer, specifier, wrap) => {
+ if (importModuleDynamicallyMap.has(wrap))
+ return importModuleDynamicallyMap.get(wrap)(specifier);
+
+ throw new ERR_DYNAMIC_IMPORT_CALLBACK_MISSING(specifier);
+ });
+}
diff --git a/lib/internal/vm/module.js b/lib/internal/vm/module.js
index 7284c8bd619901..7c3f3470b7fe6e 100644
--- a/lib/internal/vm/module.js
+++ b/lib/internal/vm/module.js
@@ -12,13 +12,14 @@ 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,
customInspectSymbol,
} = require('internal/util');
const { SafePromise } = require('internal/safe_globals');
+const { isModuleNamespaceObject } = require('util').types;
const {
ModuleWrap,
@@ -44,10 +45,7 @@ const perContextModuleId = new WeakMap();
const wrapMap = new WeakMap();
const dependencyCacheMap = new WeakMap();
const linkingStatusMap = new WeakMap();
-// vm.Module -> function
-const initImportMetaMap = new WeakMap();
-// ModuleWrap -> vm.Module
-const wrapToModuleMap = new WeakMap();
+const linkerFnMap = new WeakMap();
class Module {
constructor(src, options = {}) {
@@ -62,7 +60,6 @@ class Module {
context,
lineOffset = 0,
columnOffset = 0,
- initializeImportMeta
} = options;
if (context !== undefined) {
@@ -95,9 +92,11 @@ class Module {
validateInteger(lineOffset, 'options.lineOffset');
validateInteger(columnOffset, 'options.columnOffset');
+ let { initializeImportMeta } = options;
if (initializeImportMeta !== undefined) {
if (typeof initializeImportMeta === 'function') {
- initImportMetaMap.set(this, initializeImportMeta);
+ const fn = initializeImportMeta;
+ initializeImportMeta = (meta) => fn(meta, this);
} else {
throw new ERR_INVALID_ARG_TYPE(
'options.initializeImportMeta', 'function', initializeImportMeta);
@@ -105,9 +104,22 @@ class Module {
}
const wrap = new ModuleWrap(src, url, context, lineOffset, columnOffset);
+
+ const {
+ initializeImportMetaMap,
+ importModuleDynamicallyMap,
+ } = require('internal/process/esm_loader');
+
+ if (initializeImportMeta)
+ initializeImportMetaMap.set(wrap, initializeImportMeta);
+
+ importModuleDynamicallyMap.set(wrap, async (specifier) => {
+ const linker = linkerFnMap.get(this);
+ return callLinkerForNamespace(linker, specifier, this);
+ });
+
wrapMap.set(this, wrap);
linkingStatusMap.set(this, 'unlinked');
- wrapToModuleMap.set(wrap, this);
Object.defineProperties(this, {
url: { value: url, enumerable: true },
@@ -160,20 +172,9 @@ class Module {
throw new ERR_VM_MODULE_STATUS('must be uninstantiated');
linkingStatusMap.set(this, 'linking');
+ linkerFnMap.set(this, linker);
- const promises = wrap.link(async (specifier) => {
- const m = await linker(specifier, this);
- if (!m || !wrapMap.has(m))
- throw new ERR_VM_MODULE_NOT_MODULE();
- if (m.context !== this.context)
- throw new ERR_VM_MODULE_DIFFERENT_CONTEXT();
- const childLinkingStatus = linkingStatusMap.get(m);
- if (childLinkingStatus === 'errored')
- throw new ERR_VM_MODULE_LINKING_ERRORED();
- if (childLinkingStatus === 'unlinked')
- await m.link(linker);
- return wrapMap.get(m);
- });
+ const promises = wrap.link((s) => callLinkerForModuleWrap(linker, s, this));
try {
if (promises !== undefined)
@@ -252,8 +253,43 @@ function validateInteger(prop, propName) {
}
}
+async function getWrapFromModule(m, scriptOrModule, linker) {
+ if (!m || !wrapMap.has(m))
+ throw new ERR_VM_MODULE_NOT_MODULE();
+
+ if (scriptOrModule instanceof Module &&
+ (m.context !== scriptOrModule.context))
+ throw new ERR_VM_MODULE_DIFFERENT_CONTEXT();
+
+ const childLinkingStatus = linkingStatusMap.get(m);
+
+ if (childLinkingStatus === 'errored')
+ throw new ERR_VM_MODULE_LINKING_ERRORED();
+ if (childLinkingStatus === 'unlinked')
+ await m.link(linker);
+
+ return wrapMap.get(m);
+}
+
+async function callLinkerForModuleWrap(linker, specifier, scriptOrModule) {
+ const m = await linker(specifier, scriptOrModule);
+ return getWrapFromModule(m, scriptOrModule, linker);
+}
+
+async function callLinkerForNamespace(linker, specifier, scriptOrModule) {
+ const m = await linker(specifier, scriptOrModule);
+ if (isModuleNamespaceObject(m))
+ return m;
+ const wrap = await getWrapFromModule(m, scriptOrModule, linker);
+ const status = wrap.getStatus();
+ if (status < kInstantiated)
+ wrap.instantiate();
+ if (status < kEvaluated)
+ await wrap.evaluate(-1, false);
+ return wrap.namespace();
+}
+
module.exports = {
Module,
- initImportMetaMap,
- wrapToModuleMap
+ callLinkerForNamespace,
};
diff --git a/lib/repl.js b/lib/repl.js
index a7977dc72fb781..986b22217be6aa 100644
--- a/lib/repl.js
+++ b/lib/repl.js
@@ -49,6 +49,7 @@ const {
} = require('internal/modules/cjs/helpers');
const internalUtil = require('internal/util');
const { isTypedArray } = require('internal/util/types');
+const { getURLFromFilePath } = require('internal/url');
const util = require('util');
const utilBinding = process.binding('util');
const { inherits } = util;
@@ -256,7 +257,17 @@ function REPLServer(prompt,
}
script = vm.createScript(code, {
filename: file,
- displayErrors: true
+ displayErrors: true,
+ async resolveDynamicImport(specifier, scriptOrModule) {
+ const loader = await require('internal/process/esm_loader')
+ .loaderPromise;
+
+ const filename = scriptOrModule.filename;
+ const referrer = scriptOrModule.url ||
+ getURLFromFilePath(filename === 'repl' ?
+ path.join(process.cwd(), filename) : filename).href;
+ return loader.import(specifier, referrer);
+ },
});
} catch (e) {
debug('parse error %j', code, e);
diff --git a/lib/vm.js b/lib/vm.js
index 5a5130d7c9c328..3f1d843c15beaa 100644
--- a/lib/vm.js
+++ b/lib/vm.js
@@ -50,6 +50,7 @@ class Script extends ContextifyScript {
columnOffset = 0,
cachedData,
produceCachedData = false,
+ resolveDynamicImport,
[kParsingContext]: parsingContext
} = options;
@@ -67,6 +68,19 @@ class Script extends ContextifyScript {
produceCachedData);
}
+
+ let importModuleDynamically;
+ if (resolveDynamicImport !== undefined &&
+ typeof resolveDynamicImport !== 'function') {
+ throw new ERR_INVALID_ARG_TYPE(
+ 'options.resolveDynamicImport', 'function', resolveDynamicImport);
+ } else if (resolveDynamicImport !== undefined) {
+ const { callLinkerForNamespace } = require('internal/vm/module');
+ importModuleDynamically = async (specifier) => {
+ return callLinkerForNamespace(resolveDynamicImport, specifier, this);
+ };
+ }
+
// Calling `ReThrow()` on a native TryCatch does not generate a new
// abort-on-uncaught-exception check. A dummy try/catch in JS land
// protects against that.
@@ -81,6 +95,12 @@ class Script extends ContextifyScript {
} catch (e) {
throw e; /* node-do-not-add-exception-line */
}
+
+ if (importModuleDynamically !== undefined) {
+ require('internal/process/esm_loader')
+ .importModuleDynamicallyMap
+ .set(this, importModuleDynamically);
+ }
}
runInThisContext(options) {
diff --git a/src/env-inl.h b/src/env-inl.h
index fa241f9706ec65..d74bf7e4fe6678 100644
--- a/src/env-inl.h
+++ b/src/env-inl.h
@@ -445,6 +445,10 @@ inline double Environment::get_default_trigger_async_id() {
return default_trigger_async_id;
}
+inline int Environment::get_next_module_id() {
+ return module_id_counter_++;
+}
+
inline double* Environment::heap_statistics_buffer() const {
CHECK_NE(heap_statistics_buffer_, nullptr);
return heap_statistics_buffer_;
diff --git a/src/env.h b/src/env.h
index af4470ad8632fe..0467fe6508450f 100644
--- a/src/env.h
+++ b/src/env.h
@@ -163,6 +163,7 @@ struct PackageConfig {
V(fatal_exception_string, "_fatalException") \
V(fd_string, "fd") \
V(file_string, "file") \
+ V(filename_string, "filename") \
V(fingerprint_string, "fingerprint") \
V(fingerprint256_string, "fingerprint256") \
V(flags_string, "flags") \
@@ -628,7 +629,11 @@ 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_map id_to_module_wrap_map;
+ std::unordered_multimap module_to_module_wrap_map;
+ std::unordered_map id_to_script_wrap_map;
+
+ inline int get_next_module_id();
std::unordered_map package_json_cache;
@@ -865,6 +870,8 @@ class Environment {
v8::Local promise,
v8::Local parent);
+ int module_id_counter_ = 0;
+
#define V(PropertyName, TypeName) Persistent PropertyName ## _;
ENVIRONMENT_STRONG_PERSISTENT_PROPERTIES(V)
#undef V
diff --git a/src/module_wrap.cc b/src/module_wrap.cc
index 9bcdb4dce75ff2..af1fff3b1610a8 100644
--- a/src/module_wrap.cc
+++ b/src/module_wrap.cc
@@ -34,6 +34,7 @@ using v8::MaybeLocal;
using v8::Module;
using v8::Nothing;
using v8::Object;
+using v8::PrimitiveArray;
using v8::Promise;
using v8::ScriptCompiler;
using v8::ScriptOrigin;
@@ -50,31 +51,40 @@ ModuleWrap::ModuleWrap(Environment* env,
Local url) : BaseObject(env, object) {
module_.Reset(env->isolate(), module);
url_.Reset(env->isolate(), url);
+ id_ = env->get_next_module_id();
}
ModuleWrap::~ModuleWrap() {
HandleScope scope(env()->isolate());
Local module = module_.Get(env()->isolate());
- auto range = env()->module_map.equal_range(module->GetIdentityHash());
+ env()->id_to_module_wrap_map.erase(id_);
+ auto range = env()->module_to_module_wrap_map.equal_range(
+ module->GetIdentityHash());
for (auto it = range.first; it != range.second; ++it) {
if (it->second == this) {
- env()->module_map.erase(it);
+ env()->module_to_module_wrap_map.erase(it);
break;
}
}
}
+ModuleWrap* ModuleWrap::GetFromID(Environment* env, int id) {
+ auto module_wrap_it = env->id_to_module_wrap_map.find(id);
+ if (module_wrap_it == env->id_to_module_wrap_map.end())
+ return nullptr;
+
+ return module_wrap_it->second;
+}
+
ModuleWrap* ModuleWrap::GetFromModule(Environment* env,
Local module) {
- ModuleWrap* ret = nullptr;
- auto range = env->module_map.equal_range(module->GetIdentityHash());
+ auto range = env->module_to_module_wrap_map.equal_range(
+ module->GetIdentityHash());
for (auto it = range.first; it != range.second; ++it) {
- if (it->second->module_ == module) {
- ret = it->second;
- break;
- }
+ if (it->second->module_ == module)
+ return it->second;
}
- return ret;
+ return nullptr;
}
void ModuleWrap::New(const FunctionCallbackInfo& args) {
@@ -126,6 +136,8 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) {
TryCatch try_catch(isolate);
Local module;
+ Local host_defined_options = PrimitiveArray::New(isolate, 2);
+
// compile
{
ScriptOrigin origin(url,
@@ -136,7 +148,8 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) {
Local(), // source map URL
False(isolate), // is opaque (?)
False(isolate), // is WASM
- True(isolate)); // is ES6 module
+ True(isolate), // is ES6 module
+ host_defined_options);
Context::Scope context_scope(context);
ScriptCompiler::Source source(source_text, origin);
if (!ScriptCompiler::CompileModule(isolate, &source).ToLocal(&module)) {
@@ -157,7 +170,13 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) {
ModuleWrap* obj = new ModuleWrap(env, that, module, url);
obj->context_.Reset(isolate, context);
- env->module_map.emplace(module->GetIdentityHash(), obj);
+ host_defined_options->Set(0,
+ Integer::New(isolate, contextify::SourceType::kModule));
+ host_defined_options->Set(1, Integer::New(isolate, obj->GetID()));
+
+ env->id_to_module_wrap_map[obj->GetID()] = obj;
+ env->module_to_module_wrap_map.emplace(module->GetIdentityHash(), obj);
+
Wrap(that, obj);
that->SetIntegrityLevel(context, IntegrityLevel::kFrozen);
@@ -365,15 +384,10 @@ MaybeLocal ModuleWrap::ResolveCallback(Local context,
Local referrer) {
Environment* env = Environment::GetCurrent(context);
Isolate* isolate = env->isolate();
- if (env->module_map.count(referrer->GetIdentityHash()) == 0) {
- env->ThrowError("linking error, unknown module");
- return MaybeLocal();
- }
ModuleWrap* dependent = ModuleWrap::GetFromModule(env, referrer);
-
if (dependent == nullptr) {
- env->ThrowError("linking error, null dep");
+ env->ThrowError("linking error, unknown module");
return MaybeLocal();
}
@@ -700,29 +714,37 @@ static MaybeLocal ImportModuleDynamically(
Environment* env = Environment::GetCurrent(context);
v8::EscapableHandleScope handle_scope(iso);
- if (env->context() != context) {
- auto maybe_resolver = Promise::Resolver::New(context);
- Local resolver;
- if (maybe_resolver.ToLocal(&resolver)) {
- // TODO(jkrems): Turn into proper error object w/ code
- Local error = v8::Exception::Error(
- OneByteString(iso, "import() called outside of main context"));
- if (resolver->Reject(context, error).IsJust()) {
- return handle_scope.Escape(resolver.As());
- }
- }
- return MaybeLocal();
- }
-
Local import_callback =
env->host_import_module_dynamically_callback();
+
Local import_args[] = {
referrer->GetResourceName(),
- Local(specifier)
+ Local(specifier),
+ Undefined(iso),
};
+
+ Local host_defined_options =
+ referrer->GetHostDefinedOptions();
+
+ if (host_defined_options->Length() == 2) {
+ int type = host_defined_options->Get(0).As()->Value();
+ if (type == contextify::SourceType::kScript) {
+ int id = host_defined_options->Get(1).As()->Value();
+ contextify::ContextifyScript* wrap =
+ contextify::ContextifyScript::GetFromID(env, id);
+ CHECK_NE(wrap, nullptr);
+ import_args[2] = wrap->object();
+ } else if (type == contextify::SourceType::kModule) {
+ int id = host_defined_options->Get(1).As()->Value();
+ ModuleWrap* wrap = ModuleWrap::GetFromID(env, id);
+ CHECK_NE(wrap, nullptr);
+ import_args[2] = wrap->object();
+ }
+ }
+
MaybeLocal maybe_result = import_callback->Call(context,
v8::Undefined(iso),
- 2,
+ 3,
import_args);
Local result;
@@ -752,16 +774,18 @@ void ModuleWrap::HostInitializeImportMetaObjectCallback(
Environment* env = Environment::GetCurrent(context);
ModuleWrap* module_wrap = ModuleWrap::GetFromModule(env, module);
- if (module_wrap == nullptr) {
+ if (module_wrap == nullptr)
return;
- }
- Local