diff --git a/doc/api/errors.md b/doc/api/errors.md index 72df6b27cb7b08..e050392fc65163 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -1795,6 +1795,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 d023d9c216903a..e8c4d1a6cbf2f2 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 @@ -977,6 +995,7 @@ This issue occurs because all contexts share the same microtask and nextTick queues. [`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 @@ -985,6 +1004,7 @@ queues. [`vm.createContext()`]: #vm_vm_createcontext_sandbox_options [`vm.runInContext()`]: #vm_vm_runincontext_code_contextifiedsandbox_options [`vm.runInThisContext()`]: #vm_vm_runinthiscontext_code_options +[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 [GetModuleNamespace]: https://tc39.github.io/ecma262/#sec-getmodulenamespace diff --git a/lib/internal/bootstrap/loaders.js b/lib/internal/bootstrap/loaders.js index e2cfdbf83d38ef..06aee591496440 100644 --- a/lib/internal/bootstrap/loaders.js +++ b/lib/internal/bootstrap/loaders.js @@ -115,6 +115,8 @@ }; } + // Create this WeakMap in js-land because V8 has no C++ API for WeakMap + internalBinding('module_wrap').callbackMap = new WeakMap(); const { ContextifyScript } = process.binding('contextify'); // Set up NativeModule diff --git a/lib/internal/errors.js b/lib/internal/errors.js index b418fa295d2dcc..71b4a87ca0d86f 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -920,6 +920,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 fb3770b7299d4e..8e768bf5d07850 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 @@ -642,6 +643,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 @@ -657,7 +665,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 be08a6364781ed..f81053a1c3c3ad 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 = require('internal/options').getOptionValue('--loader'); @@ -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 915289a0591373..1898831bf6f238 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 } = process.binding('contextify'); const { @@ -10,7 +11,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 { SafePromise } = require('internal/safe_globals'); 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 { validateInteger(lineOffset, 'options.lineOffset'); validateInteger(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 }, @@ -255,6 +275,7 @@ function validateInteger(prop, propName) { module.exports = { SourceTextModule, - initImportMetaMap, - wrapToModuleMap + wrapToModuleMap, + wrapMap, + linkingStatusMap, }; diff --git a/lib/vm.js b/lib/vm.js index dbe48b7b15c194..c8c6f0f7e9473d 100644 --- a/lib/vm.js +++ b/lib/vm.js @@ -27,12 +27,13 @@ const { isContext: _isContext, compileFunction: _compileFunction } = process.binding('contextify'); - +const { callbackMap } = internalBinding('module_wrap'); const { ERR_INVALID_ARG_TYPE, - ERR_OUT_OF_RANGE + ERR_OUT_OF_RANGE, + ERR_VM_MODULE_NOT_MODULE, } = require('internal/errors').codes; -const { isUint8Array } = require('internal/util/types'); +const { isModuleNamespaceObject, isUint8Array } = require('util').types; const { validateUint32 } = require('internal/validators'); const kParsingContext = Symbol('script parsing context'); @@ -55,7 +56,8 @@ class Script extends ContextifyScript { columnOffset = 0, cachedData, produceCachedData = false, - [kParsingContext]: parsingContext + importModuleDynamically, + [kParsingContext]: parsingContext, } = options; if (typeof filename !== 'string') { @@ -86,6 +88,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 b512412c0ff4f0..4af5add4c8f52c 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -441,6 +441,13 @@ Environment::should_abort_on_uncaught_toggle() { return should_abort_on_uncaught_toggle_; } +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 1d2baf58b007ea..cd222d70fdf138 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; } @@ -671,7 +675,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; @@ -899,6 +909,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 816c68d55d16ce..fe6a8e2639c7a7 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 object, Local module, - Local url) : BaseObject(env, object) { + Local url) : + BaseObject(env, object), + id_(env->get_next_module_id()) { module_.Reset(env->isolate(), module); url_.Reset(env->isolate(), url); + env->id_to_module_map.emplace(id_, this); } 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_map.erase(id_); + auto range = env()->hash_to_module_map.equal_range(module->GetIdentityHash()); for (auto it = range.first; it != range.second; ++it) { if (it->second == this) { - env()->module_map.erase(it); + env()->hash_to_module_map.erase(it); break; } } @@ -66,15 +72,21 @@ ModuleWrap::~ModuleWrap() { ModuleWrap* ModuleWrap::GetFromModule(Environment* env, Local module) { - ModuleWrap* ret = nullptr; - auto range = env->module_map.equal_range(module->GetIdentityHash()); + auto range = env->hash_to_module_map.equal_range(module->GetIdentityHash()); for (auto it = range.first; it != range.second; ++it) { if (it->second->module_ == module) { - ret = it->second; - break; + return it->second; } } - return ret; + return nullptr; +} + +ModuleWrap* ModuleWrap::GetFromID(Environment* env, uint32_t id) { + auto module_wrap_it = env->id_to_module_map.find(id); + if (module_wrap_it == env->id_to_module_map.end()) { + return nullptr; + } + return module_wrap_it->second; } void ModuleWrap::New(const FunctionCallbackInfo& args) { @@ -126,6 +138,11 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) { TryCatch try_catch(isolate); Local module; + Local host_defined_options = + PrimitiveArray::New(isolate, HostDefinedOptions::kLength); + host_defined_options->Set(isolate, HostDefinedOptions::kType, + Number::New(isolate, ScriptType::kModule)); + // compile { ScriptOrigin origin(url, @@ -136,7 +153,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 ES Module + host_defined_options); Context::Scope context_scope(context); ScriptCompiler::Source source(source_text, origin); if (!ScriptCompiler::CompileModule(isolate, &source).ToLocal(&module)) { @@ -157,7 +175,10 @@ 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); + env->hash_to_module_map.emplace(module->GetIdentityHash(), obj); + + host_defined_options->Set(isolate, HostDefinedOptions::kID, + Number::New(isolate, obj->id())); that->SetIntegrityLevel(context, IntegrityLevel::kFrozen); args.GetReturnValue().Set(that); @@ -364,19 +385,14 @@ MaybeLocal ModuleWrap::ResolveCallback(Local context, Environment* env = Environment::GetCurrent(context); CHECK_NOT_NULL(env); // TODO(addaleax): Handle nullptr here. Isolate* isolate = env->isolate(); - if (env->module_map.count(referrer->GetIdentityHash()) == 0) { - env->ThrowError("linking error, unknown module"); - return MaybeLocal(); - } ModuleWrap* dependent = GetFromModule(env, referrer); - if (dependent == nullptr) { env->ThrowError("linking error, null dep"); return MaybeLocal(); } - Utf8Value specifier_utf8(env->isolate(), specifier); + Utf8Value specifier_utf8(isolate, specifier); std::string specifier_std(*specifier_utf8, specifier_utf8.length()); if (dependent->resolve_cache_.count(specifier_std) != 1) { @@ -402,7 +418,7 @@ MaybeLocal ModuleWrap::ResolveCallback(Local context, ModuleWrap* module; ASSIGN_OR_RETURN_UNWRAP(&module, module_object, MaybeLocal()); - return module->module_.Get(env->isolate()); + return module->module_.Get(isolate); } namespace { @@ -703,35 +719,56 @@ static MaybeLocal ImportModuleDynamically( CHECK_NOT_NULL(env); // TODO(addaleax): Handle nullptr here. 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 options = referrer->GetHostDefinedOptions(); + if (options->Length() != HostDefinedOptions::kLength) { + Local resolver = + Promise::Resolver::New(context).ToLocalChecked(); + resolver + ->Reject(context, + v8::Exception::TypeError(FIXED_ONE_BYTE_STRING( + context->GetIsolate(), "Invalid host defined options"))) + .ToChecked(); + return handle_scope.Escape(resolver->GetPromise()); + } + + Local object; + + int type = options->Get(iso, HostDefinedOptions::kType) + .As() + ->Int32Value(context) + .ToChecked(); + uint32_t id = options->Get(iso, HostDefinedOptions::kID) + .As() + ->Uint32Value(context) + .ToChecked(); + if (type == ScriptType::kScript) { + contextify::ContextifyScript* wrap = env->id_to_script_map.find(id)->second; + object = wrap->object(); + } else if (type == ScriptType::kModule) { + ModuleWrap* wrap = ModuleWrap::GetFromID(env, id); + object = wrap->object(); + } else { + UNREACHABLE(); + } + Local import_args[] = { - referrer->GetResourceName(), - Local(specifier) + object, + Local(specifier), }; - MaybeLocal maybe_result = import_callback->Call(context, - v8::Undefined(iso), - 2, - import_args); Local result; - if (maybe_result.ToLocal(&result)) { + if (import_callback->Call( + context, + v8::Undefined(iso), + arraysize(import_args), + import_args).ToLocal(&result)) { + CHECK(result->IsPromise()); return handle_scope.Escape(result.As()); } + return MaybeLocal(); } diff --git a/src/module_wrap.h b/src/module_wrap.h index d6593c48135d18..0e352c657580e4 100644 --- a/src/module_wrap.h +++ b/src/module_wrap.h @@ -17,6 +17,17 @@ enum PackageMainCheck : bool { IgnoreMain = false }; +enum ScriptType : int { + kScript, + kModule, +}; + +enum HostDefinedOptions : int { + kType = 8, + kID = 9, + kLength = 10, +}; + v8::Maybe Resolve(Environment* env, const std::string& specifier, const url::URL& base, @@ -38,6 +49,9 @@ class ModuleWrap : public BaseObject { tracker->TrackField("resolve_cache", resolve_cache_); } + inline uint32_t id() { return id_; } + static ModuleWrap* GetFromID(node::Environment*, uint32_t id); + SET_MEMORY_INFO_NAME(ModuleWrap) SET_SELF_SIZE(ModuleWrap) @@ -69,12 +83,12 @@ class ModuleWrap : public BaseObject { v8::Local referrer); static ModuleWrap* GetFromModule(node::Environment*, v8::Local); - Persistent module_; Persistent url_; bool linked_ = false; std::unordered_map> resolve_cache_; Persistent context_; + uint32_t id_; }; } // namespace loader diff --git a/src/node_contextify.cc b/src/node_contextify.cc index c99f6c7ea5aef5..a1903d9bd9b7e0 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -25,6 +25,7 @@ #include "node_contextify.h" #include "node_context_data.h" #include "node_errors.h" +#include "module_wrap.h" namespace node { namespace contextify { @@ -48,8 +49,10 @@ using v8::Maybe; using v8::MaybeLocal; using v8::Name; using v8::NamedPropertyHandlerConfiguration; +using v8::Number; using v8::Object; using v8::ObjectTemplate; +using v8::PrimitiveArray; using v8::PropertyAttribute; using v8::PropertyCallbackInfo; using v8::PropertyDescriptor; @@ -590,368 +593,381 @@ void ContextifyContext::IndexedPropertyDeleterCallback( args.GetReturnValue().Set(false); } -class ContextifyScript : public BaseObject { - private: - Persistent script_; - - public: - SET_NO_MEMORY_INFO() - SET_MEMORY_INFO_NAME(ContextifyScript) - SET_SELF_SIZE(ContextifyScript) - - static void Init(Environment* env, Local target) { - HandleScope scope(env->isolate()); - Local class_name = - FIXED_ONE_BYTE_STRING(env->isolate(), "ContextifyScript"); - - Local script_tmpl = env->NewFunctionTemplate(New); - script_tmpl->InstanceTemplate()->SetInternalFieldCount(1); - script_tmpl->SetClassName(class_name); - env->SetProtoMethod(script_tmpl, "createCachedData", CreateCachedData); - env->SetProtoMethod(script_tmpl, "runInContext", RunInContext); - env->SetProtoMethod(script_tmpl, "runInThisContext", RunInThisContext); - - target->Set(class_name, - script_tmpl->GetFunction(env->context()).ToLocalChecked()); - env->set_script_context_constructor_template(script_tmpl); - } - - - static void New(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - Isolate* isolate = env->isolate(); - Local context = env->context(); - - CHECK(args.IsConstructCall()); - - const int argc = args.Length(); - CHECK_GE(argc, 2); - - CHECK(args[0]->IsString()); - Local code = args[0].As(); - - CHECK(args[1]->IsString()); - Local filename = args[1].As(); - - Local line_offset; - Local column_offset; - Local cached_data_buf; - bool produce_cached_data = false; - Local parsing_context = context; - - if (argc > 2) { - // new ContextifyScript(code, filename, lineOffset, columnOffset, - // cachedData, produceCachedData, parsingContext) - CHECK_EQ(argc, 7); - CHECK(args[2]->IsNumber()); - line_offset = args[2].As(); - CHECK(args[3]->IsNumber()); - column_offset = args[3].As(); - if (!args[4]->IsUndefined()) { - CHECK(args[4]->IsUint8Array()); - cached_data_buf = args[4].As(); - } - CHECK(args[5]->IsBoolean()); - produce_cached_data = args[5]->IsTrue(); - if (!args[6]->IsUndefined()) { - CHECK(args[6]->IsObject()); - ContextifyContext* sandbox = - ContextifyContext::ContextFromContextifiedSandbox( - env, args[6].As()); - CHECK_NOT_NULL(sandbox); - parsing_context = sandbox->context(); - } - } else { - line_offset = Integer::New(isolate, 0); - column_offset = Integer::New(isolate, 0); - } - - ContextifyScript* contextify_script = - new ContextifyScript(env, args.This()); - - if (*TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED( - TRACING_CATEGORY_NODE2(vm, script)) != 0) { - Utf8Value fn(isolate, filename); - TRACE_EVENT_NESTABLE_ASYNC_BEGIN1( - TRACING_CATEGORY_NODE2(vm, script), - "ContextifyScript::New", - contextify_script, - "filename", TRACE_STR_COPY(*fn)); - } +void ContextifyScript::Init(Environment* env, Local target) { + HandleScope scope(env->isolate()); + Local class_name = + FIXED_ONE_BYTE_STRING(env->isolate(), "ContextifyScript"); + + Local script_tmpl = env->NewFunctionTemplate(New); + script_tmpl->InstanceTemplate()->SetInternalFieldCount(1); + script_tmpl->SetClassName(class_name); + env->SetProtoMethod(script_tmpl, "createCachedData", CreateCachedData); + env->SetProtoMethod(script_tmpl, "runInContext", RunInContext); + env->SetProtoMethod(script_tmpl, "runInThisContext", RunInThisContext); + + target->Set(class_name, + script_tmpl->GetFunction(env->context()).ToLocalChecked()); + env->set_script_context_constructor_template(script_tmpl); +} - ScriptCompiler::CachedData* cached_data = nullptr; - if (!cached_data_buf.IsEmpty()) { - ArrayBuffer::Contents contents = cached_data_buf->Buffer()->GetContents(); - uint8_t* data = static_cast(contents.Data()); - cached_data = new ScriptCompiler::CachedData( - data + cached_data_buf->ByteOffset(), cached_data_buf->ByteLength()); - } +void ContextifyScript::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + Local context = env->context(); - ScriptOrigin origin(filename, line_offset, column_offset); - ScriptCompiler::Source source(code, origin, cached_data); - ScriptCompiler::CompileOptions compile_options = - ScriptCompiler::kNoCompileOptions; + CHECK(args.IsConstructCall()); - if (source.GetCachedData() != nullptr) - compile_options = ScriptCompiler::kConsumeCodeCache; + const int argc = args.Length(); + CHECK_GE(argc, 2); - TryCatch try_catch(isolate); - Environment::ShouldNotAbortOnUncaughtScope no_abort_scope(env); - Context::Scope scope(parsing_context); + CHECK(args[0]->IsString()); + Local code = args[0].As(); - MaybeLocal v8_script = ScriptCompiler::CompileUnboundScript( - isolate, - &source, - compile_options); + CHECK(args[1]->IsString()); + Local filename = args[1].As(); - if (v8_script.IsEmpty()) { - DecorateErrorStack(env, try_catch); - no_abort_scope.Close(); - try_catch.ReThrow(); - TRACE_EVENT_NESTABLE_ASYNC_END0( - TRACING_CATEGORY_NODE2(vm, script), - "ContextifyScript::New", - contextify_script); - return; + Local line_offset; + Local column_offset; + Local cached_data_buf; + bool produce_cached_data = false; + Local parsing_context = context; + + if (argc > 2) { + // new ContextifyScript(code, filename, lineOffset, columnOffset, + // cachedData, produceCachedData, parsingContext) + CHECK_EQ(argc, 7); + CHECK(args[2]->IsNumber()); + line_offset = args[2].As(); + CHECK(args[3]->IsNumber()); + column_offset = args[3].As(); + if (!args[4]->IsUndefined()) { + CHECK(args[4]->IsUint8Array()); + cached_data_buf = args[4].As(); } - contextify_script->script_.Reset(isolate, v8_script.ToLocalChecked()); - - if (compile_options == ScriptCompiler::kConsumeCodeCache) { - args.This()->Set( - env->cached_data_rejected_string(), - Boolean::New(isolate, source.GetCachedData()->rejected)); - } else if (produce_cached_data) { - const ScriptCompiler::CachedData* cached_data = - ScriptCompiler::CreateCodeCache(v8_script.ToLocalChecked()); - bool cached_data_produced = cached_data != nullptr; - if (cached_data_produced) { - MaybeLocal buf = Buffer::Copy( - env, - reinterpret_cast(cached_data->data), - cached_data->length); - args.This()->Set(env->cached_data_string(), buf.ToLocalChecked()); - } - args.This()->Set( - env->cached_data_produced_string(), - Boolean::New(isolate, cached_data_produced)); + CHECK(args[5]->IsBoolean()); + produce_cached_data = args[5]->IsTrue(); + if (!args[6]->IsUndefined()) { + CHECK(args[6]->IsObject()); + ContextifyContext* sandbox = + ContextifyContext::ContextFromContextifiedSandbox( + env, args[6].As()); + CHECK_NOT_NULL(sandbox); + parsing_context = sandbox->context(); } - TRACE_EVENT_NESTABLE_ASYNC_END0( + } else { + line_offset = Integer::New(isolate, 0); + column_offset = Integer::New(isolate, 0); + } + + ContextifyScript* contextify_script = + new ContextifyScript(env, args.This()); + + if (*TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED( + TRACING_CATEGORY_NODE2(vm, script)) != 0) { + Utf8Value fn(isolate, filename); + TRACE_EVENT_NESTABLE_ASYNC_BEGIN1( TRACING_CATEGORY_NODE2(vm, script), "ContextifyScript::New", - contextify_script); + contextify_script, + "filename", TRACE_STR_COPY(*fn)); } - - static bool InstanceOf(Environment* env, const Local& value) { - return !value.IsEmpty() && - env->script_context_constructor_template()->HasInstance(value); + ScriptCompiler::CachedData* cached_data = nullptr; + if (!cached_data_buf.IsEmpty()) { + ArrayBuffer::Contents contents = cached_data_buf->Buffer()->GetContents(); + uint8_t* data = static_cast(contents.Data()); + cached_data = new ScriptCompiler::CachedData( + data + cached_data_buf->ByteOffset(), cached_data_buf->ByteLength()); } + Local host_defined_options = + PrimitiveArray::New(isolate, loader::HostDefinedOptions::kLength); + host_defined_options->Set(isolate, loader::HostDefinedOptions::kType, + Number::New(isolate, loader::ScriptType::kScript)); + host_defined_options->Set(isolate, loader::HostDefinedOptions::kID, + Number::New(isolate, contextify_script->id())); + + ScriptOrigin origin(filename, + line_offset, // line offset + column_offset, // column offset + False(isolate), // is cross origin + Local(), // script id + Local(), // source map URL + False(isolate), // is opaque (?) + False(isolate), // is WASM + False(isolate), // is ES Module + host_defined_options); + ScriptCompiler::Source source(code, origin, cached_data); + ScriptCompiler::CompileOptions compile_options = + ScriptCompiler::kNoCompileOptions; - static void CreateCachedData(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - ContextifyScript* wrapped_script; - ASSIGN_OR_RETURN_UNWRAP(&wrapped_script, args.Holder()); - Local unbound_script = - PersistentToLocal(env->isolate(), wrapped_script->script_); - std::unique_ptr cached_data( - ScriptCompiler::CreateCodeCache(unbound_script)); - if (!cached_data) { - args.GetReturnValue().Set(Buffer::New(env, 0).ToLocalChecked()); - } else { + if (source.GetCachedData() != nullptr) + compile_options = ScriptCompiler::kConsumeCodeCache; + + TryCatch try_catch(isolate); + Environment::ShouldNotAbortOnUncaughtScope no_abort_scope(env); + Context::Scope scope(parsing_context); + + MaybeLocal v8_script = ScriptCompiler::CompileUnboundScript( + isolate, + &source, + compile_options); + + if (v8_script.IsEmpty()) { + DecorateErrorStack(env, try_catch); + no_abort_scope.Close(); + try_catch.ReThrow(); + TRACE_EVENT_NESTABLE_ASYNC_END0( + TRACING_CATEGORY_NODE2(vm, script), + "ContextifyScript::New", + contextify_script); + return; + } + contextify_script->script_.Reset(isolate, v8_script.ToLocalChecked()); + + if (compile_options == ScriptCompiler::kConsumeCodeCache) { + args.This()->Set( + env->cached_data_rejected_string(), + Boolean::New(isolate, source.GetCachedData()->rejected)); + } else if (produce_cached_data) { + const ScriptCompiler::CachedData* cached_data = + ScriptCompiler::CreateCodeCache(v8_script.ToLocalChecked()); + bool cached_data_produced = cached_data != nullptr; + if (cached_data_produced) { MaybeLocal buf = Buffer::Copy( env, reinterpret_cast(cached_data->data), cached_data->length); - args.GetReturnValue().Set(buf.ToLocalChecked()); + args.This()->Set(env->cached_data_string(), buf.ToLocalChecked()); } + args.This()->Set( + env->cached_data_produced_string(), + Boolean::New(isolate, cached_data_produced)); } + TRACE_EVENT_NESTABLE_ASYNC_END0( + TRACING_CATEGORY_NODE2(vm, script), + "ContextifyScript::New", + contextify_script); +} +bool ContextifyScript::InstanceOf(Environment* env, + const Local& value) { + return !value.IsEmpty() && + env->script_context_constructor_template()->HasInstance(value); +} - static void RunInThisContext(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); +void ContextifyScript::CreateCachedData( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + ContextifyScript* wrapped_script; + ASSIGN_OR_RETURN_UNWRAP(&wrapped_script, args.Holder()); + Local unbound_script = + PersistentToLocal(env->isolate(), wrapped_script->script_); + std::unique_ptr cached_data( + ScriptCompiler::CreateCodeCache(unbound_script)); + if (!cached_data) { + args.GetReturnValue().Set(Buffer::New(env, 0).ToLocalChecked()); + } else { + MaybeLocal buf = Buffer::Copy( + env, + reinterpret_cast(cached_data->data), + cached_data->length); + args.GetReturnValue().Set(buf.ToLocalChecked()); + } +} - ContextifyScript* wrapped_script; - ASSIGN_OR_RETURN_UNWRAP(&wrapped_script, args.Holder()); +void ContextifyScript::RunInThisContext( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); - TRACE_EVENT_NESTABLE_ASYNC_BEGIN0( - TRACING_CATEGORY_NODE2(vm, script), "RunInThisContext", wrapped_script); + ContextifyScript* wrapped_script; + ASSIGN_OR_RETURN_UNWRAP(&wrapped_script, args.Holder()); - CHECK_EQ(args.Length(), 3); + TRACE_EVENT_NESTABLE_ASYNC_BEGIN0( + TRACING_CATEGORY_NODE2(vm, script), "RunInThisContext", wrapped_script); - CHECK(args[0]->IsNumber()); - int64_t timeout = args[0]->IntegerValue(env->context()).FromJust(); + CHECK_EQ(args.Length(), 3); - CHECK(args[1]->IsBoolean()); - bool display_errors = args[1]->IsTrue(); + CHECK(args[0]->IsNumber()); + int64_t timeout = args[0]->IntegerValue(env->context()).FromJust(); - CHECK(args[2]->IsBoolean()); - bool break_on_sigint = args[2]->IsTrue(); + CHECK(args[1]->IsBoolean()); + bool display_errors = args[1]->IsTrue(); - // Do the eval within this context - EvalMachine(env, timeout, display_errors, break_on_sigint, args); + CHECK(args[2]->IsBoolean()); + bool break_on_sigint = args[2]->IsTrue(); - TRACE_EVENT_NESTABLE_ASYNC_END0( - TRACING_CATEGORY_NODE2(vm, script), "RunInThisContext", wrapped_script); - } + // Do the eval within this context + EvalMachine(env, timeout, display_errors, break_on_sigint, args); - static void RunInContext(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); + TRACE_EVENT_NESTABLE_ASYNC_END0( + TRACING_CATEGORY_NODE2(vm, script), "RunInThisContext", wrapped_script); +} - ContextifyScript* wrapped_script; - ASSIGN_OR_RETURN_UNWRAP(&wrapped_script, args.Holder()); +void ContextifyScript::RunInContext(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); - CHECK_EQ(args.Length(), 4); + ContextifyScript* wrapped_script; + ASSIGN_OR_RETURN_UNWRAP(&wrapped_script, args.Holder()); - CHECK(args[0]->IsObject()); - Local sandbox = args[0].As(); - // Get the context from the sandbox - ContextifyContext* contextify_context = - ContextifyContext::ContextFromContextifiedSandbox(env, sandbox); - CHECK_NOT_NULL(contextify_context); + CHECK_EQ(args.Length(), 4); - if (contextify_context->context().IsEmpty()) - return; + CHECK(args[0]->IsObject()); + Local sandbox = args[0].As(); + // Get the context from the sandbox + ContextifyContext* contextify_context = + ContextifyContext::ContextFromContextifiedSandbox(env, sandbox); + CHECK_NOT_NULL(contextify_context); - TRACE_EVENT_NESTABLE_ASYNC_BEGIN0( - TRACING_CATEGORY_NODE2(vm, script), "RunInContext", wrapped_script); + if (contextify_context->context().IsEmpty()) + return; - CHECK(args[1]->IsNumber()); - int64_t timeout = args[1]->IntegerValue(env->context()).FromJust(); + TRACE_EVENT_NESTABLE_ASYNC_BEGIN0( + TRACING_CATEGORY_NODE2(vm, script), "RunInContext", wrapped_script); - CHECK(args[2]->IsBoolean()); - bool display_errors = args[2]->IsTrue(); + CHECK(args[1]->IsNumber()); + int64_t timeout = args[1]->IntegerValue(env->context()).FromJust(); - CHECK(args[3]->IsBoolean()); - bool break_on_sigint = args[3]->IsTrue(); + CHECK(args[2]->IsBoolean()); + bool display_errors = args[2]->IsTrue(); - // Do the eval within the context - Context::Scope context_scope(contextify_context->context()); - EvalMachine(contextify_context->env(), - timeout, - display_errors, - break_on_sigint, - args); + CHECK(args[3]->IsBoolean()); + bool break_on_sigint = args[3]->IsTrue(); + + // Do the eval within the context + Context::Scope context_scope(contextify_context->context()); + EvalMachine(contextify_context->env(), + timeout, + display_errors, + break_on_sigint, + args); + + TRACE_EVENT_NESTABLE_ASYNC_END0( + TRACING_CATEGORY_NODE2(vm, script), "RunInContext", wrapped_script); +} - TRACE_EVENT_NESTABLE_ASYNC_END0( - TRACING_CATEGORY_NODE2(vm, script), "RunInContext", wrapped_script); - } +void ContextifyScript::DecorateErrorStack( + Environment* env, const TryCatch& try_catch) { + Local exception = try_catch.Exception(); - static void DecorateErrorStack(Environment* env, const TryCatch& try_catch) { - Local exception = try_catch.Exception(); + if (!exception->IsObject()) + return; - if (!exception->IsObject()) - return; + Local err_obj = exception.As(); - Local err_obj = exception.As(); + if (IsExceptionDecorated(env, err_obj)) + return; - if (IsExceptionDecorated(env, err_obj)) - return; + AppendExceptionLine(env, exception, try_catch.Message(), CONTEXTIFY_ERROR); + Local stack = err_obj->Get(env->stack_string()); + MaybeLocal maybe_value = + err_obj->GetPrivate( + env->context(), + env->arrow_message_private_symbol()); - AppendExceptionLine(env, exception, try_catch.Message(), CONTEXTIFY_ERROR); - Local stack = err_obj->Get(env->stack_string()); - MaybeLocal maybe_value = - err_obj->GetPrivate( - env->context(), - env->arrow_message_private_symbol()); + Local arrow; + if (!(maybe_value.ToLocal(&arrow) && arrow->IsString())) { + return; + } - Local arrow; - if (!(maybe_value.ToLocal(&arrow) && arrow->IsString())) { - return; - } + if (stack.IsEmpty() || !stack->IsString()) { + return; + } - if (stack.IsEmpty() || !stack->IsString()) { - return; - } + Local decorated_stack = String::Concat( + env->isolate(), + String::Concat(env->isolate(), + arrow.As(), + FIXED_ONE_BYTE_STRING(env->isolate(), "\n")), + stack.As()); + err_obj->Set(env->stack_string(), decorated_stack); + err_obj->SetPrivate( + env->context(), + env->decorated_private_symbol(), + True(env->isolate())); +} - Local decorated_stack = String::Concat( - env->isolate(), - String::Concat(env->isolate(), - arrow.As(), - FIXED_ONE_BYTE_STRING(env->isolate(), "\n")), - stack.As()); - err_obj->Set(env->stack_string(), decorated_stack); - err_obj->SetPrivate( - env->context(), - env->decorated_private_symbol(), - True(env->isolate())); +bool ContextifyScript::EvalMachine(Environment* env, + const int64_t timeout, + const bool display_errors, + const bool break_on_sigint, + const FunctionCallbackInfo& args) { + if (!env->can_call_into_js()) + return false; + if (!ContextifyScript::InstanceOf(env, args.Holder())) { + env->ThrowTypeError( + "Script methods can only be called on script instances."); + return false; + } + TryCatch try_catch(env->isolate()); + ContextifyScript* wrapped_script; + ASSIGN_OR_RETURN_UNWRAP(&wrapped_script, args.Holder(), false); + Local unbound_script = + PersistentToLocal(env->isolate(), wrapped_script->script_); + Local