Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit dce6570

Browse filesBrowse files
joyeecheungaduh95
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 4cf94fa commit dce6570
Copy full SHA for dce6570

10 files changed

+502-58Lines changed: 502 additions & 58 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎lib/internal/main/embedding.js‎

Copy file name to clipboardExpand all lines: lib/internal/main/embedding.js
+80-17Lines 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];
Collapse file

‎src/api/environment.cc‎

Copy file name to clipboardExpand all lines: src/api/environment.cc
+138-11Lines changed: 138 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ NODE_EXTERN std::unique_ptr<InspectorParentHandle> GetInspectorParentHandle(
574574
}
575575

576576
MaybeLocal<Value> LoadEnvironment(Environment* env,
577-
StartExecutionCallback cb,
577+
StartExecutionCallbackWithModule cb,
578578
EmbedderPreloadCallback preload) {
579579
env->InitializeLibuv();
580580
env->InitializeDiagnostics();
@@ -586,22 +586,149 @@ MaybeLocal<Value> LoadEnvironment(Environment* env,
586586
return StartExecution(env, cb);
587587
}
588588

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

‎src/node.cc‎

Copy file name to clipboardExpand all lines: src/node.cc
+18-22Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.