diff options
author | Gus Caplan <me@gus.host> | 2018-08-17 17:26:34 -0500 |
---|---|---|
committer | Gus Caplan <me@gus.host> | 2018-10-06 17:33:25 -0500 |
commit | 4c37df779cf944b5666fc72e2a27fbf2e745881f (patch) | |
tree | 80d135f6cbd7cd9545bee950c280659985c276b8 | |
parent | 124a8e21238f8452028614625fe491b3049f7244 (diff) | |
download | android-node-v8-4c37df779cf944b5666fc72e2a27fbf2e745881f.tar.gz android-node-v8-4c37df779cf944b5666fc72e2a27fbf2e745881f.tar.bz2 android-node-v8-4c37df779cf944b5666fc72e2a27fbf2e745881f.zip |
vm: add dynamic import support
PR-URL: https://github.com/nodejs/node/pull/22381
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
-rw-r--r-- | doc/api/errors.md | 5 | ||||
-rw-r--r-- | doc/api/vm.md | 22 | ||||
-rw-r--r-- | lib/internal/bootstrap/loaders.js | 2 | ||||
-rw-r--r-- | lib/internal/errors.js | 2 | ||||
-rw-r--r-- | lib/internal/modules/cjs/loader.js | 15 | ||||
-rw-r--r-- | lib/internal/modules/esm/translators.js | 22 | ||||
-rw-r--r-- | lib/internal/process/esm_loader.js | 49 | ||||
-rw-r--r-- | lib/internal/vm/source_text_module.js | 47 | ||||
-rw-r--r-- | lib/vm.js | 34 | ||||
-rw-r--r-- | src/env-inl.h | 7 | ||||
-rw-r--r-- | src/env.h | 15 | ||||
-rw-r--r-- | src/module_wrap.cc | 113 | ||||
-rw-r--r-- | src/module_wrap.h | 16 | ||||
-rw-r--r-- | src/node_contextify.cc | 618 | ||||
-rw-r--r-- | src/node_contextify.h | 32 | ||||
-rw-r--r-- | test/es-module/test-esm-dynamic-import.js | 35 | ||||
-rw-r--r-- | test/parallel/test-bootstrap-modules.js | 2 | ||||
-rw-r--r-- | test/parallel/test-vm-module-dynamic-import.js | 71 |
18 files changed, 674 insertions, 433 deletions
diff --git a/doc/api/errors.md b/doc/api/errors.md index 75146c24c3..7cdee52e79 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. +<a id="ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING"></a> +### ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING + +A dynamic import callback was not specified. + <a id="ERR_VM_MODULE_ALREADY_LINKED"></a> ### ERR_VM_MODULE_ALREADY_LINKED diff --git a/doc/api/vm.md b/doc/api/vm.md index 6259b9b2ff..07923de585 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 8234275700..359812e1e9 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 43124cc66b..4094a40f6b 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 f3f8b0c8e0..6cdafac3de 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 df3c446cab..0c34283b8a 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 23b98c620e..b2415ec171 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 c1c3611d8f..d22db6e914 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, }; @@ -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 e4a635c84d..6ace0bf825 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) { @@ -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<double>* destroy_async_id_list(); - std::unordered_multimap<int, loader::ModuleWrap*> module_map; + std::unordered_multimap<int, loader::ModuleWrap*> hash_to_module_map; + std::unordered_map<uint32_t, loader::ModuleWrap*> id_to_module_map; + std::unordered_map<uint32_t, contextify::ContextifyScript*> + id_to_script_map; + + inline uint32_t get_next_module_id(); + inline uint32_t get_next_script_id(); std::unordered_map<std::string, const loader::PackageConfig> package_json_cache; @@ -924,6 +934,9 @@ class Environment { std::shared_ptr<EnvironmentOptions> options_; + uint32_t module_id_counter_ = 0; + uint32_t script_id_counter_ = 0; + AliasedBuffer<uint32_t, v8::Uint32Array> 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 1ef22b270d..4a7be86af8 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> object, Local<Module> module, - Local<String> url) : BaseObject(env, object) { + Local<String> 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 = 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> 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<Value>& args) { @@ -126,6 +138,11 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) { TryCatch try_catch(isolate); Local<Module> module; + Local<PrimitiveArray> 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<Value>& args) { Local<Value>(), // 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<Value>& 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<Module> ModuleWrap::ResolveCallback(Local<Context> 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<Module>(); - } ModuleWrap* dependent = GetFromModule(env, referrer); - if (dependent == nullptr) { env->ThrowError("linking error, null dep"); return MaybeLocal<Module>(); } - 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<Module> ModuleWrap::ResolveCallback(Local<Context> context, ModuleWrap* module; ASSIGN_OR_RETURN_UNWRAP(&module, module_object, MaybeLocal<Module>()); - return module->module_.Get(env->isolate()); + return module->module_.Get(isolate); } namespace { @@ -704,35 +720,56 @@ static MaybeLocal<Promise> 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<Promise::Resolver> resolver; - if (maybe_resolver.ToLocal(&resolver)) { - // TODO(jkrems): Turn into proper error object w/ code - Local<Value> error = v8::Exception::Error( - OneByteString(iso, "import() called outside of main context")); - if (resolver->Reject(context, error).IsJust()) { - return handle_scope.Escape(resolver.As<Promise>()); - } - } - return MaybeLocal<Promise>(); - } - Local<Function> import_callback = env->host_import_module_dynamically_callback(); + + Local<PrimitiveArray> options = referrer->GetHostDefinedOptions(); + if (options->Length() != HostDefinedOptions::kLength) { + Local<Promise::Resolver> 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<Value> object; + + int type = options->Get(iso, HostDefinedOptions::kType) + .As<Number>() + ->Int32Value(context) + .ToChecked(); + uint32_t id = options->Get(iso, HostDefinedOptions::kID) + .As<Number>() + ->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<Value> import_args[] = { - referrer->GetResourceName(), - Local<Value>(specifier) + object, + Local<Value>(specifier), }; - MaybeLocal<Value> maybe_result = import_callback->Call(context, - v8::Undefined(iso), - 2, - import_args); Local<Value> 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<Promise>()); } + return MaybeLocal<Promise>(); } diff --git a/src/module_wrap.h b/src/module_wrap.h index d6593c4813..0e352c6575 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<url::URL> 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<v8::Module> referrer); static ModuleWrap* GetFromModule(node::Environment*, v8::Local<v8::Module>); - Persistent<v8::Module> module_; Persistent<v8::String> url_; bool linked_ = false; std::unordered_map<std::string, Persistent<v8::Promise>> resolve_cache_; Persistent<v8::Context> context_; + uint32_t id_; }; } // namespace loader diff --git a/src/node_contextify.cc b/src/node_contextify.cc index 4239f07f06..023a659ebb 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -26,6 +26,7 @@ #include "node_contextify.h" #include "node_context_data.h" #include "node_errors.h" +#include "module_wrap.h" namespace node { namespace contextify { @@ -49,8 +50,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; @@ -586,368 +589,381 @@ void ContextifyContext::IndexedPropertyDeleterCallback( args.GetReturnValue().Set(false); } -class ContextifyScript : public BaseObject { - private: - Persistent<UnboundScript> script_; - - public: - SET_NO_MEMORY_INFO() - SET_MEMORY_INFO_NAME(ContextifyScript) - SET_SELF_SIZE(ContextifyScript) - - static void Init(Environment* env, Local<Object> target) { - HandleScope scope(env->isolate()); - Local<String> class_name = - FIXED_ONE_BYTE_STRING(env->isolate(), "ContextifyScript"); - - Local<FunctionTemplate> 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<Value>& args) { - Environment* env = Environment::GetCurrent(args); - Isolate* isolate = env->isolate(); - Local<Context> context = env->context(); - - CHECK(args.IsConstructCall()); - - const int argc = args.Length(); - CHECK_GE(argc, 2); - - CHECK(args[0]->IsString()); - Local<String> code = args[0].As<String>(); - - CHECK(args[1]->IsString()); - Local<String> filename = args[1].As<String>(); - - Local<Integer> line_offset; - Local<Integer> column_offset; - Local<Uint8Array> cached_data_buf; - bool produce_cached_data = false; - Local<Context> 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<Integer>(); - CHECK(args[3]->IsNumber()); - column_offset = args[3].As<Integer>(); - if (!args[4]->IsUndefined()) { - CHECK(args[4]->IsUint8Array()); - cached_data_buf = args[4].As<Uint8Array>(); - } - 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<Object>()); - 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<Object> target) { + HandleScope scope(env->isolate()); + Local<String> class_name = + FIXED_ONE_BYTE_STRING(env->isolate(), "ContextifyScript"); + + Local<FunctionTemplate> 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<uint8_t*>(contents.Data()); - cached_data = new ScriptCompiler::CachedData( - data + cached_data_buf->ByteOffset(), cached_data_buf->ByteLength()); - } +void ContextifyScript::New(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + Local<Context> 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<String> code = args[0].As<String>(); - MaybeLocal<UnboundScript> v8_script = ScriptCompiler::CompileUnboundScript( - isolate, - &source, - compile_options); + CHECK(args[1]->IsString()); + Local<String> filename = args[1].As<String>(); - 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<Integer> line_offset; + Local<Integer> column_offset; + Local<Uint8Array> cached_data_buf; + bool produce_cached_data = false; + Local<Context> 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<Integer>(); + CHECK(args[3]->IsNumber()); + column_offset = args[3].As<Integer>(); + if (!args[4]->IsUndefined()) { + CHECK(args[4]->IsUint8Array()); + cached_data_buf = args[4].As<Uint8Array>(); } - 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<Object> buf = Buffer::Copy( - env, - reinterpret_cast<const char*>(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<Object>()); + 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>& 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<uint8_t*>(contents.Data()); + cached_data = new ScriptCompiler::CachedData( + data + cached_data_buf->ByteOffset(), cached_data_buf->ByteLength()); } + Local<PrimitiveArray> 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<Integer>(), // script id + Local<Value>(), // 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<Value>& args) { - Environment* env = Environment::GetCurrent(args); - ContextifyScript* wrapped_script; - ASSIGN_OR_RETURN_UNWRAP(&wrapped_script, args.Holder()); - Local<UnboundScript> unbound_script = - PersistentToLocal(env->isolate(), wrapped_script->script_); - std::unique_ptr<ScriptCompiler::CachedData> 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<UnboundScript> 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<Object> buf = Buffer::Copy( env, reinterpret_cast<const char*>(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>& value) { + return !value.IsEmpty() && + env->script_context_constructor_template()->HasInstance(value); +} - static void RunInThisContext(const FunctionCallbackInfo<Value>& args) { - Environment* env = Environment::GetCurrent(args); +void ContextifyScript::CreateCachedData( + const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + ContextifyScript* wrapped_script; + ASSIGN_OR_RETURN_UNWRAP(&wrapped_script, args.Holder()); + Local<UnboundScript> unbound_script = + PersistentToLocal(env->isolate(), wrapped_script->script_); + std::unique_ptr<ScriptCompiler::CachedData> cached_data( + ScriptCompiler::CreateCodeCache(unbound_script)); + if (!cached_data) { + args.GetReturnValue().Set(Buffer::New(env, 0).ToLocalChecked()); + } else { + MaybeLocal<Object> buf = Buffer::Copy( + env, + reinterpret_cast<const char*>(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<Value>& 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<Value>& 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<Value>& 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<Object> sandbox = args[0].As<Object>(); - // 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<Object> sandbox = args[0].As<Object>(); + // 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<Value> exception = try_catch.Exception(); - static void DecorateErrorStack(Environment* env, const TryCatch& try_catch) { - Local<Value> exception = try_catch.Exception(); + if (!exception->IsObject()) + return; - if (!exception->IsObject()) - return; + Local<Object> err_obj = exception.As<Object>(); - Local<Object> err_obj = exception.As<Object>(); + if (IsExceptionDecorated(env, err_obj)) + return; - if (IsExceptionDecorated(env, err_obj)) - return; + AppendExceptionLine(env, exception, try_catch.Message(), CONTEXTIFY_ERROR); + Local<Value> stack = err_obj->Get(env->stack_string()); + MaybeLocal<Value> maybe_value = + err_obj->GetPrivate( + env->context(), + env->arrow_message_private_symbol()); - AppendExceptionLine(env, exception, try_catch.Message(), CONTEXTIFY_ERROR); - Local<Value> stack = err_obj->Get(env->stack_string()); - MaybeLocal<Value> maybe_value = - err_obj->GetPrivate( - env->context(), - env->arrow_message_private_symbol()); + Local<Value> arrow; + if (!(maybe_value.ToLocal(&arrow) && arrow->IsString())) { + return; + } - Local<Value> arrow; - if (!(maybe_value.ToLocal(&arrow) && arrow->IsString())) { - return; - } + if (stack.IsEmpty() || !stack->IsString()) { + return; + } - if (stack.IsEmpty() || !stack->IsString()) { - return; - } + Local<String> decorated_stack = String::Concat( + env->isolate(), + String::Concat(env->isolate(), + arrow.As<String>(), + FIXED_ONE_BYTE_STRING(env->isolate(), "\n")), + stack.As<String>()); + err_obj->Set(env->stack_string(), decorated_stack); + err_obj->SetPrivate( + env->context(), + env->decorated_private_symbol(), + True(env->isolate())); +} - Local<String> decorated_stack = String::Concat( - env->isolate(), - String::Concat(env->isolate(), - arrow.As<String>(), - FIXED_ONE_BYTE_STRING(env->isolate(), "\n")), - stack.As<String>()); - 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<Value>& 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<UnboundScript> unbound_script = + PersistentToLocal(env->isolate(), wrapped_script->script_); + Local<Script> script = unbound_script->BindToCurrentContext(); + + MaybeLocal<Value> result; + bool timed_out = false; + bool received_signal = false; + if (break_on_sigint && timeout != -1) { + Watchdog wd(env->isolate(), timeout, &timed_out); + SigintWatchdog swd(env->isolate(), &received_signal); + result = script->Run(env->context()); + } else if (break_on_sigint) { + SigintWatchdog swd(env->isolate(), &received_signal); + result = script->Run(env->context()); + } else if (timeout != -1) { + Watchdog wd(env->isolate(), timeout, &timed_out); + result = script->Run(env->context()); + } else { + result = script->Run(env->context()); } - static bool EvalMachine(Environment* env, - const int64_t timeout, - const bool display_errors, - const bool break_on_sigint, - const FunctionCallbackInfo<Value>& 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<UnboundScript> unbound_script = - PersistentToLocal(env->isolate(), wrapped_script->script_); - Local<Script> script = unbound_script->BindToCurrentContext(); - - MaybeLocal<Value> result; - bool timed_out = false; - bool received_signal = false; - if (break_on_sigint && timeout != -1) { - Watchdog wd(env->isolate(), timeout, &timed_out); - SigintWatchdog swd(env->isolate(), &received_signal); - result = script->Run(env->context()); - } else if (break_on_sigint) { - SigintWatchdog swd(env->isolate(), &received_signal); - result = script->Run(env->context()); - } else if (timeout != -1) { - Watchdog wd(env->isolate(), timeout, &timed_out); - result = script->Run(env->context()); - } else { - result = script->Run(env->context()); + // Convert the termination exception into a regular exception. + if (timed_out || received_signal) { + env->isolate()->CancelTerminateExecution(); + // It is possible that execution was terminated by another timeout in + // which this timeout is nested, so check whether one of the watchdogs + // from this invocation is responsible for termination. + if (timed_out) { + node::THROW_ERR_SCRIPT_EXECUTION_TIMEOUT(env, timeout); + } else if (received_signal) { + node::THROW_ERR_SCRIPT_EXECUTION_INTERRUPTED(env); } + } - // Convert the termination exception into a regular exception. - if (timed_out || received_signal) { - env->isolate()->CancelTerminateExecution(); - // It is possible that execution was terminated by another timeout in - // which this timeout is nested, so check whether one of the watchdogs - // from this invocation is responsible for termination. - if (timed_out) { - node::THROW_ERR_SCRIPT_EXECUTION_TIMEOUT(env, timeout); - } else if (received_signal) { - node::THROW_ERR_SCRIPT_EXECUTION_INTERRUPTED(env); - } + if (try_catch.HasCaught()) { + if (!timed_out && !received_signal && display_errors) { + // We should decorate non-termination exceptions + DecorateErrorStack(env, try_catch); } - if (try_catch.HasCaught()) { - if (!timed_out && !received_signal && display_errors) { - // We should decorate non-termination exceptions - DecorateErrorStack(env, try_catch); - } + // If there was an exception thrown during script execution, re-throw it. + // If one of the above checks threw, re-throw the exception instead of + // letting try_catch catch it. + // If execution has been terminated, but not by one of the watchdogs from + // this invocation, this will re-throw a `null` value. + try_catch.ReThrow(); - // If there was an exception thrown during script execution, re-throw it. - // If one of the above checks threw, re-throw the exception instead of - // letting try_catch catch it. - // If execution has been terminated, but not by one of the watchdogs from - // this invocation, this will re-throw a `null` value. - try_catch.ReThrow(); + return false; + } - return false; - } + args.GetReturnValue().Set(result.ToLocalChecked()); + return true; +} - args.GetReturnValue().Set(result.ToLocalChecked()); - return true; - } + +ContextifyScript::ContextifyScript(Environment* env, Local<Object> object) + : BaseObject(env, object), + id_(env->get_next_script_id()) { + MakeWeak(); + env->id_to_script_map.emplace(id_, this); +} - ContextifyScript(Environment* env, Local<Object> object) - : BaseObject(env, object) { - MakeWeak(); - } -}; +ContextifyScript::~ContextifyScript() { + env()->id_to_script_map.erase(id_); +} void ContextifyContext::CompileFunction( diff --git a/src/node_contextify.h b/src/node_contextify.h index d2b8387f21..6d6dc12b4b 100644 --- a/src/node_contextify.h +++ b/src/node_contextify.h @@ -5,6 +5,7 @@ #include "node_internals.h" #include "node_context_data.h" +#include "base_object-inl.h" namespace node { namespace contextify { @@ -102,6 +103,37 @@ class ContextifyContext { Persistent<v8::Context> context_; }; +class ContextifyScript : public BaseObject { + public: + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(ContextifyScript) + SET_SELF_SIZE(ContextifyScript) + + ContextifyScript(Environment* env, v8::Local<v8::Object> object); + ~ContextifyScript(); + + static void Init(Environment* env, v8::Local<v8::Object> target); + static void New(const v8::FunctionCallbackInfo<v8::Value>& args); + static bool InstanceOf(Environment* env, const v8::Local<v8::Value>& args); + static void CreateCachedData( + const v8::FunctionCallbackInfo<v8::Value>& args); + static void RunInThisContext(const v8::FunctionCallbackInfo<v8::Value>& args); + static void RunInContext(const v8::FunctionCallbackInfo<v8::Value>& args); + static void DecorateErrorStack(Environment* env, + const v8::TryCatch& try_catch); + static bool EvalMachine(Environment* env, + const int64_t timeout, + const bool display_errors, + const bool break_on_sigint, + const v8::FunctionCallbackInfo<v8::Value>& args); + + inline uint32_t id() { return id_; } + + private: + node::Persistent<v8::UnboundScript> script_; + uint32_t id_; +}; + } // namespace contextify } // namespace node diff --git a/test/es-module/test-esm-dynamic-import.js b/test/es-module/test-esm-dynamic-import.js index 6a80da4947..6cbbd0ac67 100644 --- a/test/es-module/test-esm-dynamic-import.js +++ b/test/es-module/test-esm-dynamic-import.js @@ -75,38 +75,3 @@ function expectFsNamespace(result) { expectMissingModuleError(import("node:fs")); expectMissingModuleError(import('http://example.com/foo.js')); })(); - -// vm.runInThisContext: -// * Supports built-ins, always -// * Supports imports if the script has a known defined origin -(function testRunInThisContext() { - // Succeeds because it's got an valid base url - expectFsNamespace(vm.runInThisContext(`import("fs")`, { - filename: __filename, - })); - expectOkNamespace(vm.runInThisContext(`import("${relativePath}")`, { - filename: __filename, - })); - // Rejects because it's got an invalid referrer URL. - // TODO(jkrems): Arguably the first two (built-in + absolute URL) could work - // with some additional effort. - expectInvalidReferrerError(vm.runInThisContext('import("fs")')); - expectInvalidReferrerError(vm.runInThisContext(`import("${targetURL}")`)); - expectInvalidReferrerError(vm.runInThisContext(`import("${relativePath}")`)); -})(); - -// vm.runInNewContext is currently completely unsupported, pending well-defined -// semantics for per-context/realm module maps in node. -(function testRunInNewContext() { - // Rejects because it's running in the wrong context - expectInvalidContextError( - vm.runInNewContext(`import("${targetURL}")`, undefined, { - filename: __filename, - }) - ); - - // Rejects because it's running in the wrong context - expectInvalidContextError(vm.runInNewContext(`import("fs")`, undefined, { - filename: __filename, - })); -})(); diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index e2e5075f6b..885a58ff9f 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -11,4 +11,4 @@ const list = process.moduleLoadList.slice(); const assert = require('assert'); -assert(list.length <= 76, list); +assert(list.length <= 77, list); diff --git a/test/parallel/test-vm-module-dynamic-import.js b/test/parallel/test-vm-module-dynamic-import.js index 1c4b10947f..5c0f6f81fa 100644 --- a/test/parallel/test-vm-module-dynamic-import.js +++ b/test/parallel/test-vm-module-dynamic-import.js @@ -5,11 +5,9 @@ const common = require('../common'); const assert = require('assert'); -const { SourceTextModule, createContext } = require('vm'); +const { Script, SourceTextModule, createContext } = require('vm'); -const finished = common.mustCall(); - -(async function() { +async function testNoCallback() { const m = new SourceTextModule('import("foo")', { context: createContext() }); await m.link(common.mustNotCall()); m.instantiate(); @@ -19,8 +17,67 @@ const finished = common.mustCall(); await result; } catch (err) { threw = true; - assert.strictEqual(err.message, 'import() called outside of main context'); + assert.strictEqual(err.code, 'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING'); } assert(threw); - finished(); -}()); +} + +async function test() { + const foo = new SourceTextModule('export const a = 1;'); + await foo.link(common.mustNotCall()); + foo.instantiate(); + await foo.evaluate(); + + { + const s = new Script('import("foo")', { + importModuleDynamically: common.mustCall((specifier, wrap) => { + assert.strictEqual(specifier, 'foo'); + assert.strictEqual(wrap, s); + return foo; + }), + }); + + const result = s.runInThisContext(); + assert.strictEqual(foo.namespace, await result); + } + + { + const m = new SourceTextModule('import("foo")', { + importModuleDynamically: common.mustCall((specifier, wrap) => { + assert.strictEqual(specifier, 'foo'); + assert.strictEqual(wrap, m); + return foo; + }), + }); + await m.link(common.mustNotCall()); + m.instantiate(); + const { result } = await m.evaluate(); + assert.strictEqual(foo.namespace, await result); + } +} + +async function testInvalid() { + const m = new SourceTextModule('import("foo")', { + importModuleDynamically: common.mustCall((specifier, wrap) => { + return 5; + }), + }); + await m.link(common.mustNotCall()); + m.instantiate(); + const { result } = await m.evaluate(); + await result.catch(common.mustCall((e) => { + assert.strictEqual(e.code, 'ERR_VM_MODULE_NOT_MODULE'); + })); +} + +const done = common.mustCallAtLeast(3); +(async function() { + await testNoCallback(); + done(); + + await test(); + done(); + + await testInvalid(); + done(); +}()).then(common.mustCall()); |