Skip to content

Commit baba748

Browse files
joyeecheungnodejs-github-bot
authored andcommitted
src: add initial support for ESM in embedder API
This patch extends `LoadEnvironment` to support loading ES modules, and adds the following new types: ```cpp enum class ModuleFormat : uint8_t { kCommonJS, kModule, }; // Data for specifying an entry point script for LoadEnvironment(). // This class uses an opaque layout to allow future additions without // breaking ABI. Use the setter methods to configure the entry point. class ModuleData { void set_source(std::string_view source); void set_format(ModuleFormat format); void set_resource_name(std::string_view name); std::string_view source() const; ModuleFormat format() const; std::string_view resource_name() const; }; class StartExecutionCallbackInfoWithModule { void set_env(Environment* env); void set_process_object(v8::Local<v8::Object> process_object); void set_native_require(v8::Local<v8::Function> native_require); void set_run_module(v8::Local<v8::Function> run_module); void set_data(void* data); Environment* env(); v8::Local<v8::Object> process(); v8::Local<v8::Function> native_require(); v8::Local<v8::Function> run_module(); void* data(); }; ``` And two new `LoadEnvironment()` overloads: ```cpp // Run entry point with ModuleData configuration MaybeLocal<Value> LoadEnvironment( Environment* env, const ModuleData* entry_point, EmbedderPreloadCallback preload = nullptr); // Callback-based with new StartExecutionCallbackInfoWithModule MaybeLocal<Value> LoadEnvironment( Environment* env, StartExecutionCallbackWithModule cb, EmbedderPreloadCallback preload = nullptr, void* callback_data = nullptr); ``` PR-URL: #61548 Refs: #53565 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Aditi Singh <aditisingh1400@gmail.com>
1 parent 5212c07 commit baba748

File tree

10 files changed

+502
-58
lines changed

10 files changed

+502
-58
lines changed

lib/internal/main/embedding.js

Lines changed: 80 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,19 @@ const {
1515
const { isExperimentalSeaWarningNeeded, isSea } = internalBinding('sea');
1616
const { emitExperimentalWarning } = require('internal/util');
1717
const { emitWarningSync } = require('internal/process/warning');
18-
const { BuiltinModule: { normalizeRequirableId } } = require('internal/bootstrap/realm');
18+
const { BuiltinModule } = require('internal/bootstrap/realm');
19+
const { normalizeRequirableId } = BuiltinModule;
1920
const { Module } = require('internal/modules/cjs/loader');
2021
const { compileFunctionForCJSLoader } = internalBinding('contextify');
2122
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
22-
2323
const { codes: {
2424
ERR_UNKNOWN_BUILTIN_MODULE,
2525
} } = require('internal/errors');
26-
26+
const { pathToFileURL } = require('internal/url');
27+
const { loadBuiltinModule } = require('internal/modules/helpers');
28+
const { moduleFormats } = internalBinding('modules');
29+
const assert = require('internal/assert');
30+
const path = require('path');
2731
// Don't expand process.argv[1] because in a single-executable application or an
2832
// embedder application, the user main script isn't necessarily provided via the
2933
// command line (e.g. it could be provided via an API or bundled into the executable).
@@ -44,12 +48,11 @@ if (isExperimentalSeaWarningNeeded()) {
4448
// value of require.main to module.
4549
//
4650
// TODO(RaisinTen): Find a way to deduplicate this.
47-
function embedderRunCjs(content) {
51+
function embedderRunCjs(content, filename) {
4852
// The filename of the module (used for CJS module lookup)
4953
// is always the same as the location of the executable itself
5054
// at the time of the loading (which means it changes depending
5155
// on where the executable is in the file system).
52-
const filename = process.execPath;
5356
const customModule = new Module(filename, null);
5457

5558
const {
@@ -86,33 +89,93 @@ function embedderRunCjs(content) {
8689
customModule.paths = Module._nodeModulePaths(process.execPath);
8790
embedderRequire.main = customModule;
8891

92+
// This currently returns what the wrapper returns i.e. if the code
93+
// happens to have a return statement, it returns that; Otherwise it's
94+
// undefined.
95+
// TODO(joyeecheung): we may want to return the customModule or put it in an
96+
// out parameter.
8997
return compiledWrapper(
9098
customModule.exports, // exports
9199
embedderRequire, // require
92100
customModule, // module
93-
process.execPath, // __filename
101+
filename, // __filename
94102
customModule.path, // __dirname
95103
);
96104
}
97105

98106
let warnedAboutBuiltins = false;
107+
function warnNonBuiltinInSEA() {
108+
if (isBuiltinWarningNeeded && !warnedAboutBuiltins) {
109+
emitWarningSync(
110+
'Currently the require() provided to the main script embedded into ' +
111+
'single-executable applications only supports loading built-in modules.\n' +
112+
'To load a module from disk after the single executable application is ' +
113+
'launched, use require("module").createRequire().\n' +
114+
'Support for bundled module loading or virtual file systems are under ' +
115+
'discussions in https://github.com/nodejs/single-executable');
116+
warnedAboutBuiltins = true;
117+
}
118+
}
99119

100120
function embedderRequire(id) {
101121
const normalizedId = normalizeRequirableId(id);
122+
102123
if (!normalizedId) {
103-
if (isBuiltinWarningNeeded && !warnedAboutBuiltins) {
104-
emitWarningSync(
105-
'Currently the require() provided to the main script embedded into ' +
106-
'single-executable applications only supports loading built-in modules.\n' +
107-
'To load a module from disk after the single executable application is ' +
108-
'launched, use require("module").createRequire().\n' +
109-
'Support for bundled module loading or virtual file systems are under ' +
110-
'discussions in https://github.com/nodejs/single-executable');
111-
warnedAboutBuiltins = true;
112-
}
124+
warnNonBuiltinInSEA();
113125
throw new ERR_UNKNOWN_BUILTIN_MODULE(id);
114126
}
115127
return require(normalizedId);
116128
}
117129

118-
return [process, embedderRequire, embedderRunCjs];
130+
function embedderRunESM(content, filename) {
131+
let resourceName;
132+
if (path.isAbsolute(filename)) {
133+
resourceName = pathToFileURL(filename).href;
134+
} else {
135+
resourceName = filename;
136+
}
137+
const { compileSourceTextModule } = require('internal/modules/esm/utils');
138+
// TODO(joyeecheung): support code cache, dynamic import() and import.meta.
139+
const wrap = compileSourceTextModule(resourceName, content);
140+
// Cache the source map for the module if present.
141+
if (wrap.sourceMapURL) {
142+
maybeCacheSourceMap(resourceName, content, wrap, false, undefined, wrap.sourceMapURL);
143+
}
144+
const requests = wrap.getModuleRequests();
145+
const modules = [];
146+
for (let i = 0; i < requests.length; ++i) {
147+
const { specifier } = requests[i];
148+
const normalizedId = normalizeRequirableId(specifier);
149+
if (!normalizedId) {
150+
warnNonBuiltinInSEA();
151+
throw new ERR_UNKNOWN_BUILTIN_MODULE(specifier);
152+
}
153+
const mod = loadBuiltinModule(normalizedId);
154+
if (!mod) {
155+
throw new ERR_UNKNOWN_BUILTIN_MODULE(specifier);
156+
}
157+
modules.push(mod.getESMFacade());
158+
}
159+
wrap.link(modules);
160+
wrap.instantiate();
161+
wrap.evaluate(-1, false);
162+
163+
// TODO(joyeecheung): we may want to return the v8::Module via a vm.SourceTextModule
164+
// when vm.SourceTextModule stablizes, or put it in an out parameter.
165+
return wrap.getNamespace();
166+
}
167+
168+
function embedderRunEntryPoint(content, format, filename) {
169+
format ||= moduleFormats.kCommonJS;
170+
filename ||= process.execPath;
171+
172+
if (format === moduleFormats.kCommonJS) {
173+
return embedderRunCjs(content, filename);
174+
} else if (format === moduleFormats.kModule) {
175+
return embedderRunESM(content, filename);
176+
}
177+
assert.fail(`Unknown format: ${format}`);
178+
179+
}
180+
181+
return [process, embedderRequire, embedderRunEntryPoint];

src/api/environment.cc

Lines changed: 138 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,7 @@ NODE_EXTERN std::unique_ptr<InspectorParentHandle> GetInspectorParentHandle(
572572
}
573573

574574
MaybeLocal<Value> LoadEnvironment(Environment* env,
575-
StartExecutionCallback cb,
575+
StartExecutionCallbackWithModule cb,
576576
EmbedderPreloadCallback preload) {
577577
env->InitializeLibuv();
578578
env->InitializeDiagnostics();
@@ -584,22 +584,149 @@ MaybeLocal<Value> LoadEnvironment(Environment* env,
584584
return StartExecution(env, cb);
585585
}
586586

587+
struct StartExecutionCallbackInfoWithModule::Impl {
588+
Environment* env = nullptr;
589+
Local<Object> process_object;
590+
Local<Function> native_require;
591+
Local<Function> run_module;
592+
};
593+
594+
StartExecutionCallbackInfoWithModule::StartExecutionCallbackInfoWithModule()
595+
: impl_(std::make_unique<Impl>()) {}
596+
597+
StartExecutionCallbackInfoWithModule::~StartExecutionCallbackInfoWithModule() =
598+
default;
599+
600+
StartExecutionCallbackInfoWithModule::StartExecutionCallbackInfoWithModule(
601+
StartExecutionCallbackInfoWithModule&&) = default;
602+
603+
StartExecutionCallbackInfoWithModule&
604+
StartExecutionCallbackInfoWithModule::operator=(
605+
StartExecutionCallbackInfoWithModule&&) = default;
606+
607+
Environment* StartExecutionCallbackInfoWithModule::env() const {
608+
return impl_->env;
609+
}
610+
611+
Local<Object> StartExecutionCallbackInfoWithModule::process_object() const {
612+
return impl_->process_object;
613+
}
614+
615+
Local<Function> StartExecutionCallbackInfoWithModule::native_require() const {
616+
return impl_->native_require;
617+
}
618+
619+
Local<Function> StartExecutionCallbackInfoWithModule::run_module() const {
620+
return impl_->run_module;
621+
}
622+
623+
void StartExecutionCallbackInfoWithModule::set_env(Environment* env) {
624+
impl_->env = env;
625+
}
626+
627+
void StartExecutionCallbackInfoWithModule::set_process_object(
628+
Local<Object> process_object) {
629+
impl_->process_object = process_object;
630+
}
631+
632+
void StartExecutionCallbackInfoWithModule::set_native_require(
633+
Local<Function> native_require) {
634+
impl_->native_require = native_require;
635+
}
636+
637+
void StartExecutionCallbackInfoWithModule::set_run_module(
638+
Local<Function> run_module) {
639+
impl_->run_module = run_module;
640+
}
641+
642+
struct ModuleData::Impl {
643+
std::string_view source;
644+
ModuleFormat format = ModuleFormat::kCommonJS;
645+
std::string_view resource_name;
646+
};
647+
648+
ModuleData::ModuleData() : impl_(std::make_unique<Impl>()) {}
649+
650+
ModuleData::~ModuleData() = default;
651+
652+
ModuleData::ModuleData(ModuleData&&) = default;
653+
654+
ModuleData& ModuleData::operator=(ModuleData&&) = default;
655+
656+
void ModuleData::set_source(std::string_view source) {
657+
impl_->source = source;
658+
}
659+
660+
void ModuleData::set_format(ModuleFormat format) {
661+
impl_->format = format;
662+
}
663+
664+
void ModuleData::set_resource_name(std::string_view name) {
665+
impl_->resource_name = name;
666+
}
667+
668+
std::string_view ModuleData::source() const {
669+
return impl_->source;
670+
}
671+
672+
ModuleFormat ModuleData::format() const {
673+
return impl_->format;
674+
}
675+
676+
std::string_view ModuleData::resource_name() const {
677+
return impl_->resource_name;
678+
}
679+
680+
MaybeLocal<Value> LoadEnvironment(Environment* env,
681+
StartExecutionCallback cb,
682+
EmbedderPreloadCallback preload) {
683+
if (!cb) {
684+
return LoadEnvironment(
685+
env, StartExecutionCallbackWithModule{}, std::move(preload));
686+
}
687+
688+
return LoadEnvironment(
689+
env,
690+
[cb = std::move(cb)](const StartExecutionCallbackInfoWithModule& info)
691+
-> MaybeLocal<Value> {
692+
StartExecutionCallbackInfo legacy_info{
693+
info.process_object(), info.native_require(), info.run_module()};
694+
return cb(legacy_info);
695+
},
696+
std::move(preload));
697+
}
698+
587699
MaybeLocal<Value> LoadEnvironment(Environment* env,
588700
std::string_view main_script_source_utf8,
589701
EmbedderPreloadCallback preload) {
702+
ModuleData data;
703+
data.set_source(main_script_source_utf8);
704+
data.set_format(ModuleFormat::kCommonJS);
705+
data.set_resource_name(env->exec_path());
706+
return LoadEnvironment(env, &data, std::move(preload));
707+
}
708+
709+
MaybeLocal<Value> LoadEnvironment(Environment* env,
710+
const ModuleData* data,
711+
EmbedderPreloadCallback preload) {
590712
// It could be empty when it's used by SEA to load an empty script.
591-
CHECK_IMPLIES(main_script_source_utf8.size() > 0,
592-
main_script_source_utf8.data());
713+
CHECK_IMPLIES(data->source().size() > 0, data->source().data());
593714
return LoadEnvironment(
594715
env,
595-
[&](const StartExecutionCallbackInfo& info) -> MaybeLocal<Value> {
596-
Local<Value> main_script;
597-
if (!ToV8Value(env->context(), main_script_source_utf8)
598-
.ToLocal(&main_script)) {
599-
return {};
600-
}
601-
return info.run_cjs->Call(
602-
env->context(), Null(env->isolate()), 1, &main_script);
716+
[data](const StartExecutionCallbackInfoWithModule& info)
717+
-> MaybeLocal<Value> {
718+
Environment* env = info.env();
719+
Local<Context> context = env->context();
720+
Isolate* isolate = env->isolate();
721+
Local<Value> main_script =
722+
ToV8Value(context, data->source()).ToLocalChecked();
723+
Local<Value> format =
724+
v8::Integer::New(isolate, static_cast<int>(data->format()));
725+
Local<Value> resource_name =
726+
ToV8Value(context, data->resource_name()).ToLocalChecked();
727+
Local<Value> args[] = {main_script, format, resource_name};
728+
return info.run_module()->Call(
729+
context, Null(isolate), arraysize(args), args);
603730
},
604731
std::move(preload));
605732
}

src/node.cc

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -255,37 +255,33 @@ MaybeLocal<Value> StartExecution(Environment* env, const char* main_script_id) {
255255
}
256256

257257
// Convert the result returned by an intermediate main script into
258-
// StartExecutionCallbackInfo. Currently the result is an array containing
259-
// [process, requireFunction, cjsRunner]
260-
std::optional<StartExecutionCallbackInfo> CallbackInfoFromArray(
261-
Local<Context> context, Local<Value> result) {
258+
// StartExecutionCallbackInfoWithModule. Currently the result is an array
259+
// containing [process, requireFunction, runModule].
260+
std::optional<StartExecutionCallbackInfoWithModule> CallbackInfoFromArray(
261+
Environment* env, Local<Value> result) {
262262
CHECK(result->IsArray());
263263
Local<Array> args = result.As<Array>();
264264
CHECK_EQ(args->Length(), 3);
265-
Local<Value> process_obj, require_fn, runcjs_fn;
265+
Local<Value> process_obj, require_fn, run_module;
266+
Local<Context> context = env->context();
266267
if (!args->Get(context, 0).ToLocal(&process_obj) ||
267268
!args->Get(context, 1).ToLocal(&require_fn) ||
268-
!args->Get(context, 2).ToLocal(&runcjs_fn)) {
269+
!args->Get(context, 2).ToLocal(&run_module)) {
269270
return std::nullopt;
270271
}
271272
CHECK(process_obj->IsObject());
272273
CHECK(require_fn->IsFunction());
273-
CHECK(runcjs_fn->IsFunction());
274-
// TODO(joyeecheung): some support for running ESM as an entrypoint
275-
// is needed. The simplest API would be to add a run_esm to
276-
// StartExecutionCallbackInfo which compiles, links (to builtins)
277-
// and evaluates a SourceTextModule.
278-
// TODO(joyeecheung): the env pointer should be part of
279-
// StartExecutionCallbackInfo, otherwise embedders are forced to use
280-
// lambdas to pass it into the callback, which can make the code
281-
// difficult to read.
282-
node::StartExecutionCallbackInfo info{process_obj.As<Object>(),
283-
require_fn.As<Function>(),
284-
runcjs_fn.As<Function>()};
274+
CHECK(run_module->IsFunction());
275+
StartExecutionCallbackInfoWithModule info;
276+
info.set_env(env);
277+
info.set_process_object(process_obj.As<Object>());
278+
info.set_native_require(require_fn.As<Function>());
279+
info.set_run_module(run_module.As<Function>());
285280
return info;
286281
}
287282

288-
MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
283+
MaybeLocal<Value> StartExecution(Environment* env,
284+
StartExecutionCallbackWithModule cb) {
289285
InternalCallbackScope callback_scope(
290286
env,
291287
Object::New(env->isolate()),
@@ -294,7 +290,7 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
294290

295291
// Only snapshot builder or embedder applications set the
296292
// callback.
297-
if (cb != nullptr) {
293+
if (cb) {
298294
EscapableHandleScope scope(env->isolate());
299295

300296
Local<Value> result;
@@ -308,9 +304,9 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
308304
}
309305
}
310306

311-
auto info = CallbackInfoFromArray(env->context(), result);
307+
auto info = CallbackInfoFromArray(env, result);
312308
if (!info.has_value()) {
313-
MaybeLocal<Value>();
309+
return MaybeLocal<Value>();
314310
}
315311
#if HAVE_INSPECTOR
316312
if (env->options()->debug_options().break_first_line) {

0 commit comments

Comments
 (0)