Skip to content

Commit 121c8e0

Browse files
committed
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); ```
1 parent 83893bb commit 121c8e0

File tree

10 files changed

+538
-62
lines changed

10 files changed

+538
-62
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: 166 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -572,36 +572,186 @@ NODE_EXTERN std::unique_ptr<InspectorParentHandle> GetInspectorParentHandle(
572572
}
573573

574574
MaybeLocal<Value> LoadEnvironment(Environment* env,
575-
StartExecutionCallback cb,
576-
EmbedderPreloadCallback preload) {
575+
StartExecutionCallbackWithModule cb,
576+
EmbedderPreloadCallback preload,
577+
void* callback_data) {
577578
env->InitializeLibuv();
578579
env->InitializeDiagnostics();
579580
if (preload) {
580581
env->set_embedder_preload(std::move(preload));
581582
}
582583
env->InitializeCompileCache();
583584

584-
return StartExecution(env, cb);
585+
return StartExecution(env, cb, callback_data);
586+
}
587+
588+
struct StartExecutionCallbackInfoWithModule::Impl {
589+
Environment* env = nullptr;
590+
Local<Object> process_object;
591+
Local<Function> native_require;
592+
Local<Function> run_module;
593+
void* data = nullptr;
594+
};
595+
596+
StartExecutionCallbackInfoWithModule::StartExecutionCallbackInfoWithModule()
597+
: impl_(std::make_unique<Impl>()) {}
598+
599+
StartExecutionCallbackInfoWithModule::~StartExecutionCallbackInfoWithModule() =
600+
default;
601+
602+
StartExecutionCallbackInfoWithModule::StartExecutionCallbackInfoWithModule(
603+
StartExecutionCallbackInfoWithModule&&) = default;
604+
605+
StartExecutionCallbackInfoWithModule&
606+
StartExecutionCallbackInfoWithModule::operator=(
607+
StartExecutionCallbackInfoWithModule&&) = default;
608+
609+
void StartExecutionCallbackInfoWithModule::set_env(Environment* env) {
610+
impl_->env = env;
611+
}
612+
613+
void StartExecutionCallbackInfoWithModule::set_process_object(
614+
Local<Object> process_object) {
615+
impl_->process_object = process_object;
616+
}
617+
618+
void StartExecutionCallbackInfoWithModule::set_native_require(
619+
Local<Function> native_require) {
620+
impl_->native_require = native_require;
621+
}
622+
623+
void StartExecutionCallbackInfoWithModule::set_run_module(
624+
Local<Function> run_module) {
625+
impl_->run_module = run_module;
626+
}
627+
628+
void StartExecutionCallbackInfoWithModule::set_data(void* data) {
629+
impl_->data = data;
630+
}
631+
632+
Environment* StartExecutionCallbackInfoWithModule::env() const {
633+
return impl_->env;
634+
}
635+
636+
Local<Object> StartExecutionCallbackInfoWithModule::process_object() const {
637+
return impl_->process_object;
638+
}
639+
640+
Local<Function> StartExecutionCallbackInfoWithModule::native_require() const {
641+
return impl_->native_require;
642+
}
643+
644+
Local<Function> StartExecutionCallbackInfoWithModule::run_module() const {
645+
return impl_->run_module;
646+
}
647+
648+
void* StartExecutionCallbackInfoWithModule::data() const {
649+
return impl_->data;
650+
}
651+
652+
struct ModuleData::Impl {
653+
std::string_view source;
654+
ModuleFormat format = ModuleFormat::kCommonJS;
655+
std::string_view resource_name;
656+
};
657+
658+
ModuleData::ModuleData() : impl_(std::make_unique<Impl>()) {}
659+
660+
ModuleData::~ModuleData() = default;
661+
662+
ModuleData::ModuleData(ModuleData&&) = default;
663+
664+
ModuleData& ModuleData::operator=(ModuleData&&) = default;
665+
666+
void ModuleData::set_source(std::string_view source) {
667+
impl_->source = source;
668+
}
669+
670+
void ModuleData::set_format(ModuleFormat format) {
671+
impl_->format = format;
672+
}
673+
674+
void ModuleData::set_resource_name(std::string_view name) {
675+
impl_->resource_name = name;
676+
}
677+
678+
std::string_view ModuleData::source() const {
679+
return impl_->source;
680+
}
681+
682+
ModuleFormat ModuleData::format() const {
683+
return impl_->format;
684+
}
685+
686+
std::string_view ModuleData::resource_name() const {
687+
return impl_->resource_name;
688+
}
689+
690+
struct LegacyModuleData {
691+
StartExecutionCallback cb;
692+
void* callback_data;
693+
};
694+
695+
MaybeLocal<Value> LoadEnvironment(Environment* env,
696+
StartExecutionCallback cb,
697+
EmbedderPreloadCallback preload,
698+
void* callback_data) {
699+
if (cb == nullptr) {
700+
return LoadEnvironment(
701+
env, StartExecutionCallbackWithModule{}, std::move(preload));
702+
}
703+
704+
LegacyModuleData data{cb, callback_data};
705+
return LoadEnvironment(
706+
env,
707+
[](const StartExecutionCallbackInfoWithModule& info)
708+
-> MaybeLocal<Value> {
709+
const LegacyModuleData* data =
710+
static_cast<const LegacyModuleData*>(info.data());
711+
StartExecutionCallbackInfo legacy_info{
712+
info.process_object(),
713+
info.native_require(),
714+
info.run_module(),
715+
data->callback_data,
716+
};
717+
return data->cb(legacy_info);
718+
},
719+
nullptr,
720+
&data);
721+
}
722+
723+
MaybeLocal<Value> RunModule(const StartExecutionCallbackInfoWithModule& info) {
724+
const ModuleData* data = static_cast<const ModuleData*>(info.data());
725+
Environment* env = info.env();
726+
Local<Context> context = env->context();
727+
Isolate* isolate = env->isolate();
728+
Local<Value> main_script =
729+
ToV8Value(context, data->source()).ToLocalChecked();
730+
Local<Value> format =
731+
v8::Integer::New(isolate, static_cast<int>(data->format()));
732+
Local<Value> resource_name =
733+
ToV8Value(context, data->resource_name()).ToLocalChecked();
734+
Local<Value> args[] = {main_script, format, resource_name};
735+
return info.run_module()->Call(context, Null(isolate), arraysize(args), args);
585736
}
586737

587738
MaybeLocal<Value> LoadEnvironment(Environment* env,
588739
std::string_view main_script_source_utf8,
589740
EmbedderPreloadCallback preload) {
741+
ModuleData data;
742+
data.set_source(main_script_source_utf8);
743+
data.set_format(ModuleFormat::kCommonJS);
744+
data.set_resource_name(env->exec_path());
745+
return LoadEnvironment(env, &data, std::move(preload));
746+
}
747+
748+
MaybeLocal<Value> LoadEnvironment(Environment* env,
749+
const ModuleData* data,
750+
EmbedderPreloadCallback preload) {
590751
// 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());
752+
CHECK_IMPLIES(data->source().size() > 0, data->source().data());
593753
return LoadEnvironment(
594-
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);
603-
},
604-
std::move(preload));
754+
env, RunModule, std::move(preload), const_cast<ModuleData*>(data));
605755
}
606756

607757
Environment* GetCurrentEnvironment(Local<Context> context) {

0 commit comments

Comments
 (0)