module: Set dynamic import callback

This is an initial implementation to support dynamic import in
both scripts and modules. It's off by default since support for
dynamic import is still flagged in V8. Without setting the V8 flag,
this code won't be executed.

This initial version does not support importing into vm contexts.

PR-URL: https://github.com/nodejs/node/pull/15713
Reviewed-By: Timothy Gu <timothygu99@gmail.com>
Reviewed-By: Bradley Farias <bradley.meck@gmail.com>
This commit is contained in:
Jan Krems 2017-10-01 10:31:04 -07:00
parent b28af4dc26
commit 59e48329d0
8 changed files with 201 additions and 3 deletions

View File

@ -2,6 +2,7 @@ lib/internal/v8.js
lib/internal/v8_prof_polyfill.js
lib/punycode.js
test/addons/??_*
test/es-module/test-esm-dynamic-import.js
test/fixtures
test/message/esm_display_syntax_error.mjs
tools/eslint

View File

@ -1,8 +1,12 @@
'use strict';
const { getURLFromFilePath } = require('internal/url');
const path = require('path');
const { getURLFromFilePath, URL } = require('internal/url');
const { createDynamicModule } = require('internal/loader/ModuleWrap');
const {
createDynamicModule,
setImportModuleDynamicallyCallback
} = require('internal/loader/ModuleWrap');
const ModuleMap = require('internal/loader/ModuleMap');
const ModuleJob = require('internal/loader/ModuleJob');
@ -24,6 +28,13 @@ function getURLStringForCwd() {
}
}
function normalizeReferrerURL(referrer) {
if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
return getURLFromFilePath(referrer).href;
}
return new URL(referrer).href;
}
/* A Loader instance is used as the main entry point for loading ES modules.
* Currently, this is a singleton -- there is only one used for loading
* the main module and everything in its dependency graph. */
@ -129,6 +140,12 @@ class Loader {
const module = await job.run();
return module.namespace();
}
static registerImportDynamicallyCallback(loader) {
setImportModuleDynamicallyCallback(async (referrer, specifier) => {
return loader.import(specifier, normalizeReferrerURL(referrer));
});
}
}
Loader.validFormats = ['esm', 'cjs', 'builtin', 'addon', 'json', 'dynamic'];
Object.setPrototypeOf(Loader.prototype, null);

View File

@ -1,6 +1,9 @@
'use strict';
const { ModuleWrap } = internalBinding('module_wrap');
const {
ModuleWrap,
setImportModuleDynamicallyCallback
} = internalBinding('module_wrap');
const debug = require('util').debuglog('esm');
const ArrayJoin = Function.call.bind(Array.prototype.join);
const ArrayMap = Function.call.bind(Array.prototype.map);
@ -59,5 +62,6 @@ const createDynamicModule = (exports, url = '', evaluate) => {
module.exports = {
createDynamicModule,
setImportModuleDynamicallyCallback,
ModuleWrap
};

View File

@ -472,6 +472,7 @@ Module._load = function(request, parent, isMain) {
ESMLoader.hook(hooks);
}
}
Loader.registerImportDynamicallyCallback(ESMLoader);
await ESMLoader.import(getURLFromFilePath(request).pathname);
})()
.catch((e) => {

View File

@ -308,6 +308,7 @@ class ModuleWrap;
V(internal_binding_cache_object, v8::Object) \
V(buffer_prototype_object, v8::Object) \
V(context, v8::Context) \
V(host_import_module_dynamically_callback, v8::Function) \
V(http2ping_constructor_template, v8::ObjectTemplate) \
V(http2stream_constructor_template, v8::ObjectTemplate) \
V(inspector_console_api_object, v8::Object) \

View File

@ -558,6 +558,62 @@ void ModuleWrap::Resolve(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(result.FromJust().ToObject(env));
}
static MaybeLocal<Promise> ImportModuleDynamically(
Local<Context> context,
Local<v8::ScriptOrModule> referrer,
Local<String> specifier) {
Isolate* iso = context->GetIsolate();
Environment* env = Environment::GetCurrent(context);
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<Value> import_args[] = {
referrer->GetResourceName(),
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)) {
return handle_scope.Escape(result.As<Promise>());
}
return MaybeLocal<Promise>();
}
void ModuleWrap::SetImportModuleDynamicallyCallback(
const FunctionCallbackInfo<Value>& args) {
Isolate* iso = args.GetIsolate();
Environment* env = Environment::GetCurrent(args);
HandleScope handle_scope(iso);
if (!args[0]->IsFunction()) {
env->ThrowError("first argument is not a function");
return;
}
Local<Function> import_callback = args[0].As<Function>();
env->set_host_import_module_dynamically_callback(import_callback);
iso->SetHostImportModuleDynamicallyCallback(ImportModuleDynamically);
}
void ModuleWrap::Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context) {
@ -575,6 +631,9 @@ void ModuleWrap::Initialize(Local<Object> target,
target->Set(FIXED_ONE_BYTE_STRING(isolate, "ModuleWrap"), tpl->GetFunction());
env->SetMethod(target, "resolve", node::loader::ModuleWrap::Resolve);
env->SetMethod(target,
"setImportModuleDynamicallyCallback",
node::loader::ModuleWrap::SetImportModuleDynamicallyCallback);
}
} // namespace loader

View File

@ -39,6 +39,8 @@ class ModuleWrap : public BaseObject {
static void GetUrl(v8::Local<v8::String> property,
const v8::PropertyCallbackInfo<v8::Value>& info);
static void Resolve(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetImportModuleDynamicallyCallback(
const v8::FunctionCallbackInfo<v8::Value>& args);
static v8::MaybeLocal<v8::Module> ResolveCallback(
v8::Local<v8::Context> context,
v8::Local<v8::String> specifier,

View File

@ -0,0 +1,113 @@
// Flags: --experimental-modules --harmony-dynamic-import
'use strict';
const common = require('../common');
const assert = require('assert');
const { URL } = require('url');
const vm = require('vm');
common.crashOnUnhandledRejection();
const relativePath = './test-esm-ok.mjs';
const absolutePath = require.resolve('./test-esm-ok.mjs');
const targetURL = new URL('file:///');
targetURL.pathname = absolutePath;
function expectErrorProperty(result, propertyKey, value) {
Promise.resolve(result)
.catch(common.mustCall(error => {
assert.equal(error[propertyKey], value);
}));
}
function expectMissingModuleError(result) {
expectErrorProperty(result, 'code', 'MODULE_NOT_FOUND');
}
function expectInvalidUrlError(result) {
expectErrorProperty(result, 'code', 'ERR_INVALID_URL');
}
function expectInvalidReferrerError(result) {
expectErrorProperty(result, 'code', 'ERR_INVALID_URL');
}
function expectInvalidProtocolError(result) {
expectErrorProperty(result, 'code', 'ERR_INVALID_PROTOCOL');
}
function expectInvalidContextError(result) {
expectErrorProperty(result,
'message', 'import() called outside of main context');
}
function expectOkNamespace(result) {
Promise.resolve(result)
.then(common.mustCall(ns => {
// Can't deepStrictEqual because ns isn't a normal object
assert.deepEqual(ns, { default: true });
}));
}
function expectFsNamespace(result) {
Promise.resolve(result)
.then(common.mustCall(ns => {
assert.equal(typeof ns.default.writeFile, 'function');
}));
}
// For direct use of import expressions inside of CJS or ES modules, including
// via eval, all kinds of specifiers should work without issue.
(function testScriptOrModuleImport() {
// Importing another file, both direct & via eval
// expectOkNamespace(import(relativePath));
expectOkNamespace(eval.call(null, `import("${relativePath}")`));
expectOkNamespace(eval(`import("${relativePath}")`));
expectOkNamespace(eval.call(null, `import("${targetURL}")`));
// Importing a built-in, both direct & via eval
expectFsNamespace(import("fs"));
expectFsNamespace(eval('import("fs")'));
expectFsNamespace(eval.call(null, 'import("fs")'));
expectMissingModuleError(import("./not-an-existing-module.mjs"));
// TODO(jkrems): Right now this doesn't hit a protocol error because the
// module resolution step already rejects it. These arguably should be
// protocol errors.
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,
}));
})();