Skip to content

Commit 73a2d91

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 0af02ab commit 73a2d91

File tree

8 files changed

+202
-4
lines changed

8 files changed

+202
-4
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
tools/eslint
67
tools/icu

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

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
'use strict';
22

3-
const { ModuleWrap } =
4-
require('internal/process').internalBinding('module_wrap');
3+
const {
4+
ModuleWrap,
5+
setImportModuleDynamicallyCallback
6+
} = require('internal/process').internalBinding('module_wrap');
7+
58
const debug = require('util').debuglog('esm');
69
const ArrayJoin = Function.call.bind(Array.prototype.join);
710
const ArrayMap = Function.call.bind(Array.prototype.map);
@@ -60,5 +63,6 @@ const createDynamicModule = (exports, url = '', evaluate) => {
6063

6164
module.exports = {
6265
createDynamicModule,
66+
setImportModuleDynamicallyCallback,
6367
ModuleWrap
6468
};

lib/module.js

+1
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ Module._load = function(request, parent, isMain) {
462462
ESMLoader.hook(hooks);
463463
}
464464
}
465+
Loader.registerImportDynamicallyCallback(ESMLoader);
465466
await ESMLoader.import(getURLFromFilePath(request).pathname);
466467
})()
467468
.catch((e) => {

src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ class ModuleWrap;
316316
V(context, v8::Context) \
317317
V(domain_array, v8::Array) \
318318
V(domains_stack_array, v8::Array) \
319+
V(host_import_module_dynamically_callback, v8::Function) \
319320
V(http2ping_constructor_template, v8::ObjectTemplate) \
320321
V(http2stream_constructor_template, v8::ObjectTemplate) \
321322
V(http2settings_constructor_template, v8::ObjectTemplate) \

src/module_wrap.cc

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

553+
static MaybeLocal<Promise> ImportModuleDynamically(
554+
Local<Context> context,
555+
Local<v8::ScriptOrModule> referrer,
556+
Local<String> specifier) {
557+
Isolate* iso = context->GetIsolate();
558+
Environment* env = Environment::GetCurrent(context);
559+
v8::EscapableHandleScope handle_scope(iso);
560+
561+
if (env->context() != context) {
562+
auto maybe_resolver = Promise::Resolver::New(context);
563+
Local<Promise::Resolver> resolver;
564+
if (maybe_resolver.ToLocal(&resolver)) {
565+
// TODO(jkrems): Turn into proper error object w/ code
566+
Local<Value> error = v8::Exception::Error(
567+
OneByteString(iso, "import() called outside of main context"));
568+
if (resolver->Reject(context, error).IsJust()) {
569+
return handle_scope.Escape(resolver.As<Promise>());
570+
}
571+
}
572+
return MaybeLocal<Promise>();
573+
}
574+
575+
Local<Function> import_callback =
576+
env->host_import_module_dynamically_callback();
577+
Local<Value> import_args[] = {
578+
referrer->GetResourceName(),
579+
Local<Value>(specifier)
580+
};
581+
MaybeLocal<Value> maybe_result = import_callback->Call(context,
582+
v8::Undefined(iso),
583+
2,
584+
import_args);
585+
586+
Local<Value> result;
587+
if (maybe_result.ToLocal(&result)) {
588+
return handle_scope.Escape(result.As<Promise>());
589+
}
590+
return MaybeLocal<Promise>();
591+
}
592+
593+
void ModuleWrap::SetImportModuleDynamicallyCallback(
594+
const FunctionCallbackInfo<Value>& args) {
595+
Isolate* iso = args.GetIsolate();
596+
Environment* env = Environment::GetCurrent(args);
597+
HandleScope handle_scope(iso);
598+
if (!args[0]->IsFunction()) {
599+
env->ThrowError("first argument is not a function");
600+
return;
601+
}
602+
603+
Local<Function> import_callback = args[0].As<Function>();
604+
env->set_host_import_module_dynamically_callback(import_callback);
605+
606+
iso->SetHostImportModuleDynamicallyCallback(ImportModuleDynamically);
607+
}
608+
553609
void ModuleWrap::Initialize(Local<Object> target,
554610
Local<Value> unused,
555611
Local<Context> context) {
@@ -567,6 +623,9 @@ void ModuleWrap::Initialize(Local<Object> target,
567623

568624
target->Set(FIXED_ONE_BYTE_STRING(isolate, "ModuleWrap"), tpl->GetFunction());
569625
env->SetMethod(target, "resolve", node::loader::ModuleWrap::Resolve);
626+
env->SetMethod(target,
627+
"setImportModuleDynamicallyCallback",
628+
node::loader::ModuleWrap::SetImportModuleDynamicallyCallback);
570629
}
571630

572631
} // 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)