Skip to content

Commit 00d5422

Browse files
Jan KremsMylesBorins
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‎

Lines changed: 1 addition & 0 deletions
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‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
'use strict';
22

3-
const{ getURLFromFilePath }=require('internal/url');
3+
constpath=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
constModuleMap=require('internal/loader/ModuleMap');
812
constModuleJob=require('internal/loader/ModuleJob');
@@ -24,6 +28,13 @@ function getURLStringForCwd(){
2428
}
2529
}
2630

31+
functionnormalizeReferrerURL(referrer){
32+
if(typeofreferrer==='string'&&path.isAbsolute(referrer)){
33+
returngetURLFromFilePath(referrer).href;
34+
}
35+
returnnewURL(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
constmodule=awaitjob.run();
130141
returnmodule.namespace();
131142
}
143+
144+
staticregisterImportDynamicallyCallback(loader){
145+
setImportModuleDynamicallyCallback(async(referrer,specifier)=>{
146+
returnloader.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‎

Lines changed: 5 additions & 1 deletion
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
constdebug=require('util').debuglog('esm');
58
constArrayJoin=Function.call.bind(Array.prototype.join);
69
constArrayMap=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‎

Lines changed: 1 addition & 0 deletions
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
awaitESMLoader.import(getURLFromFilePath(request).pathname);
473474
})()
474475
.catch((e)=>{

‎src/env.h‎

Lines changed: 1 addition & 0 deletions
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‎

Lines changed: 59 additions & 0 deletions
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+
voidModuleWrap::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
voidModuleWrap::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‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ class ModuleWrap : public BaseObject{
3939
staticvoidGetUrl(v8::Local<v8::String> property,
4040
const v8::PropertyCallbackInfo<v8::Value>& info);
4141
staticvoidResolve(const v8::FunctionCallbackInfo<v8::Value>& args);
42+
staticvoidSetImportModuleDynamicallyCallback(
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,
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Flags: --experimental-modules --harmony-dynamic-import
2+
'use strict';
3+
constcommon=require('../common');
4+
constassert=require('assert');
5+
const{URL}=require('url');
6+
constvm=require('vm');
7+
8+
common.crashOnUnhandledRejection();
9+
10+
constrelativePath='./test-esm-ok.mjs';
11+
constabsolutePath=require.resolve('./test-esm-ok.mjs');
12+
consttargetURL=newURL('file:///');
13+
targetURL.pathname=absolutePath;
14+
15+
functionexpectErrorProperty(result,propertyKey,value){
16+
Promise.resolve(result)
17+
.catch(common.mustCall(error=>{
18+
assert.equal(error[propertyKey],value);
19+
}));
20+
}
21+
22+
functionexpectMissingModuleError(result){
23+
expectErrorProperty(result,'code','MODULE_NOT_FOUND');
24+
}
25+
26+
functionexpectInvalidUrlError(result){
27+
expectErrorProperty(result,'code','ERR_INVALID_URL');
28+
}
29+
30+
functionexpectInvalidReferrerError(result){
31+
expectErrorProperty(result,'code','ERR_INVALID_URL');
32+
}
33+
34+
functionexpectInvalidProtocolError(result){
35+
expectErrorProperty(result,'code','ERR_INVALID_PROTOCOL');
36+
}
37+
38+
functionexpectInvalidContextError(result){
39+
expectErrorProperty(result,
40+
'message','import() called outside of main context');
41+
}
42+
43+
functionexpectOkNamespace(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+
functionexpectFsNamespace(result){
52+
Promise.resolve(result)
53+
.then(common.mustCall(ns=>{
54+
assert.equal(typeofns.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+
(functiontestScriptOrModuleImport(){
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+
(functiontestRunInThisContext(){
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+
(functiontestRunInNewContext(){
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)