Skip to content

Commit 00d5422

Browse files
Jan KremsMylesBorins
Jan Krems
authored andcommitted
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. Backport-PR-URL: #17823 PR-URL: #15713 Reviewed-By: Timothy Gu <[email protected]> Reviewed-By: Bradley Farias <[email protected]>
1 parent 11566fe commit 00d5422

File tree

8 files changed

+201
-3
lines changed

8 files changed

+201
-3
lines changed

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
lib/internal/v8_prof_polyfill.js
22
lib/punycode.js
33
test/addons/??_*
4+
test/es-module/test-esm-dynamic-import.js
45
test/fixtures
56
test/message/esm_display_syntax_error.mjs
67
tools/node_modules

lib/internal/loader/Loader.js

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
'use strict';
22

3-
const { getURLFromFilePath } = require('internal/url');
3+
const path = require('path');
4+
const { getURLFromFilePath, URL } = require('internal/url');
45

5-
const { createDynamicModule } = require('internal/loader/ModuleWrap');
6+
const {
7+
createDynamicModule,
8+
setImportModuleDynamicallyCallback
9+
} = require('internal/loader/ModuleWrap');
610

711
const ModuleMap = require('internal/loader/ModuleMap');
812
const ModuleJob = require('internal/loader/ModuleJob');
@@ -24,6 +28,13 @@ function getURLStringForCwd() {
2428
}
2529
}
2630

31+
function normalizeReferrerURL(referrer) {
32+
if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
33+
return getURLFromFilePath(referrer).href;
34+
}
35+
return new URL(referrer).href;
36+
}
37+
2738
/* A Loader instance is used as the main entry point for loading ES modules.
2839
* Currently, this is a singleton -- there is only one used for loading
2940
* the main module and everything in its dependency graph. */
@@ -129,6 +140,12 @@ class Loader {
129140
const module = await job.run();
130141
return module.namespace();
131142
}
143+
144+
static registerImportDynamicallyCallback(loader) {
145+
setImportModuleDynamicallyCallback(async (referrer, specifier) => {
146+
return loader.import(specifier, normalizeReferrerURL(referrer));
147+
});
148+
}
132149
}
133150
Loader.validFormats = ['esm', 'cjs', 'builtin', 'addon', 'json', 'dynamic'];
134151
Object.setPrototypeOf(Loader.prototype, null);

lib/internal/loader/ModuleWrap.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
'use strict';
22

3-
const { ModuleWrap } = internalBinding('module_wrap');
3+
const {
4+
ModuleWrap,
5+
setImportModuleDynamicallyCallback
6+
} = internalBinding('module_wrap');
47
const debug = require('util').debuglog('esm');
58
const ArrayJoin = Function.call.bind(Array.prototype.join);
69
const ArrayMap = Function.call.bind(Array.prototype.map);
@@ -59,5 +62,6 @@ const createDynamicModule = (exports, url = '', evaluate) => {
5962

6063
module.exports = {
6164
createDynamicModule,
65+
setImportModuleDynamicallyCallback,
6266
ModuleWrap
6367
};

lib/module.js

+1
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,7 @@ Module._load = function(request, parent, isMain) {
469469
ESMLoader.hook(hooks);
470470
}
471471
}
472+
Loader.registerImportDynamicallyCallback(ESMLoader);
472473
await ESMLoader.import(getURLFromFilePath(request).pathname);
473474
})()
474475
.catch((e) => {

src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ class ModuleWrap;
283283
V(async_hooks_binding, v8::Object) \
284284
V(buffer_prototype_object, v8::Object) \
285285
V(context, v8::Context) \
286+
V(host_import_module_dynamically_callback, v8::Function) \
286287
V(http2ping_constructor_template, v8::ObjectTemplate) \
287288
V(http2stream_constructor_template, v8::ObjectTemplate) \
288289
V(http2settings_constructor_template, v8::ObjectTemplate) \

src/module_wrap.cc

+59
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,62 @@ void ModuleWrap::Resolve(const FunctionCallbackInfo<Value>& args) {
566566
args.GetReturnValue().Set(result.FromJust().ToObject(env));
567567
}
568568

569+
static MaybeLocal<Promise> ImportModuleDynamically(
570+
Local<Context> context,
571+
Local<v8::ScriptOrModule> referrer,
572+
Local<String> specifier) {
573+
Isolate* iso = context->GetIsolate();
574+
Environment* env = Environment::GetCurrent(context);
575+
v8::EscapableHandleScope handle_scope(iso);
576+
577+
if (env->context() != context) {
578+
auto maybe_resolver = Promise::Resolver::New(context);
579+
Local<Promise::Resolver> resolver;
580+
if (maybe_resolver.ToLocal(&resolver)) {
581+
// TODO(jkrems): Turn into proper error object w/ code
582+
Local<Value> error = v8::Exception::Error(
583+
OneByteString(iso, "import() called outside of main context"));
584+
if (resolver->Reject(context, error).IsJust()) {
585+
return handle_scope.Escape(resolver.As<Promise>());
586+
}
587+
}
588+
return MaybeLocal<Promise>();
589+
}
590+
591+
Local<Function> import_callback =
592+
env->host_import_module_dynamically_callback();
593+
Local<Value> import_args[] = {
594+
referrer->GetResourceName(),
595+
Local<Value>(specifier)
596+
};
597+
MaybeLocal<Value> maybe_result = import_callback->Call(context,
598+
v8::Undefined(iso),
599+
2,
600+
import_args);
601+
602+
Local<Value> result;
603+
if (maybe_result.ToLocal(&result)) {
604+
return handle_scope.Escape(result.As<Promise>());
605+
}
606+
return MaybeLocal<Promise>();
607+
}
608+
609+
void ModuleWrap::SetImportModuleDynamicallyCallback(
610+
const FunctionCallbackInfo<Value>& args) {
611+
Isolate* iso = args.GetIsolate();
612+
Environment* env = Environment::GetCurrent(args);
613+
HandleScope handle_scope(iso);
614+
if (!args[0]->IsFunction()) {
615+
env->ThrowError("first argument is not a function");
616+
return;
617+
}
618+
619+
Local<Function> import_callback = args[0].As<Function>();
620+
env->set_host_import_module_dynamically_callback(import_callback);
621+
622+
iso->SetHostImportModuleDynamicallyCallback(ImportModuleDynamically);
623+
}
624+
569625
void ModuleWrap::Initialize(Local<Object> target,
570626
Local<Value> unused,
571627
Local<Context> context) {
@@ -583,6 +639,9 @@ void ModuleWrap::Initialize(Local<Object> target,
583639

584640
target->Set(FIXED_ONE_BYTE_STRING(isolate, "ModuleWrap"), tpl->GetFunction());
585641
env->SetMethod(target, "resolve", node::loader::ModuleWrap::Resolve);
642+
env->SetMethod(target,
643+
"setImportModuleDynamicallyCallback",
644+
node::loader::ModuleWrap::SetImportModuleDynamicallyCallback);
586645
}
587646

588647
} // namespace loader

src/module_wrap.h

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ class ModuleWrap : public BaseObject {
3939
static void GetUrl(v8::Local<v8::String> property,
4040
const v8::PropertyCallbackInfo<v8::Value>& info);
4141
static void Resolve(const v8::FunctionCallbackInfo<v8::Value>& args);
42+
static void SetImportModuleDynamicallyCallback(
43+
const v8::FunctionCallbackInfo<v8::Value>& args);
4244
static v8::MaybeLocal<v8::Module> ResolveCallback(
4345
v8::Local<v8::Context> context,
4446
v8::Local<v8::String> specifier,
+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Flags: --experimental-modules --harmony-dynamic-import
2+
'use strict';
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const { URL } = require('url');
6+
const vm = require('vm');
7+
8+
common.crashOnUnhandledRejection();
9+
10+
const relativePath = './test-esm-ok.mjs';
11+
const absolutePath = require.resolve('./test-esm-ok.mjs');
12+
const targetURL = new URL('file:///');
13+
targetURL.pathname = absolutePath;
14+
15+
function expectErrorProperty(result, propertyKey, value) {
16+
Promise.resolve(result)
17+
.catch(common.mustCall(error => {
18+
assert.equal(error[propertyKey], value);
19+
}));
20+
}
21+
22+
function expectMissingModuleError(result) {
23+
expectErrorProperty(result, 'code', 'MODULE_NOT_FOUND');
24+
}
25+
26+
function expectInvalidUrlError(result) {
27+
expectErrorProperty(result, 'code', 'ERR_INVALID_URL');
28+
}
29+
30+
function expectInvalidReferrerError(result) {
31+
expectErrorProperty(result, 'code', 'ERR_INVALID_URL');
32+
}
33+
34+
function expectInvalidProtocolError(result) {
35+
expectErrorProperty(result, 'code', 'ERR_INVALID_PROTOCOL');
36+
}
37+
38+
function expectInvalidContextError(result) {
39+
expectErrorProperty(result,
40+
'message', 'import() called outside of main context');
41+
}
42+
43+
function expectOkNamespace(result) {
44+
Promise.resolve(result)
45+
.then(common.mustCall(ns => {
46+
// Can't deepStrictEqual because ns isn't a normal object
47+
assert.deepEqual(ns, { default: true });
48+
}));
49+
}
50+
51+
function expectFsNamespace(result) {
52+
Promise.resolve(result)
53+
.then(common.mustCall(ns => {
54+
assert.equal(typeof ns.default.writeFile, 'function');
55+
}));
56+
}
57+
58+
// For direct use of import expressions inside of CJS or ES modules, including
59+
// via eval, all kinds of specifiers should work without issue.
60+
(function testScriptOrModuleImport() {
61+
// Importing another file, both direct & via eval
62+
// expectOkNamespace(import(relativePath));
63+
expectOkNamespace(eval.call(null, `import("${relativePath}")`));
64+
expectOkNamespace(eval(`import("${relativePath}")`));
65+
expectOkNamespace(eval.call(null, `import("${targetURL}")`));
66+
67+
// Importing a built-in, both direct & via eval
68+
expectFsNamespace(import("fs"));
69+
expectFsNamespace(eval('import("fs")'));
70+
expectFsNamespace(eval.call(null, 'import("fs")'));
71+
72+
expectMissingModuleError(import("./not-an-existing-module.mjs"));
73+
// TODO(jkrems): Right now this doesn't hit a protocol error because the
74+
// module resolution step already rejects it. These arguably should be
75+
// protocol errors.
76+
expectMissingModuleError(import("node:fs"));
77+
expectMissingModuleError(import('http://example.com/foo.js'));
78+
})();
79+
80+
// vm.runInThisContext:
81+
// * Supports built-ins, always
82+
// * Supports imports if the script has a known defined origin
83+
(function testRunInThisContext() {
84+
// Succeeds because it's got an valid base url
85+
expectFsNamespace(vm.runInThisContext(`import("fs")`, {
86+
filename: __filename,
87+
}));
88+
expectOkNamespace(vm.runInThisContext(`import("${relativePath}")`, {
89+
filename: __filename,
90+
}));
91+
// Rejects because it's got an invalid referrer URL.
92+
// TODO(jkrems): Arguably the first two (built-in + absolute URL) could work
93+
// with some additional effort.
94+
expectInvalidReferrerError(vm.runInThisContext('import("fs")'));
95+
expectInvalidReferrerError(vm.runInThisContext(`import("${targetURL}")`));
96+
expectInvalidReferrerError(vm.runInThisContext(`import("${relativePath}")`));
97+
})();
98+
99+
// vm.runInNewContext is currently completely unsupported, pending well-defined
100+
// semantics for per-context/realm module maps in node.
101+
(function testRunInNewContext() {
102+
// Rejects because it's running in the wrong context
103+
expectInvalidContextError(
104+
vm.runInNewContext(`import("${targetURL}")`, undefined, {
105+
filename: __filename,
106+
})
107+
);
108+
109+
// Rejects because it's running in the wrong context
110+
expectInvalidContextError(vm.runInNewContext(`import("fs")`, undefined, {
111+
filename: __filename,
112+
}));
113+
})();

0 commit comments

Comments
 (0)