sea: support ESM entry point in SEA · nodejs/node@2d874df · GitHub
Skip to content

Commit 2d874df

Browse files
joyeecheungaduh95
authored andcommitted
sea: support ESM entry point in SEA
This uses the new StartExecutionCallbackWithModule embedder API to support ESM entrypoint in SEA via a new configuration field `"mainFormat"`. The behavior currently aligns with the embedder API and is mostly in sync with the CommonJS entry point behavior, except that support for code caching and snapshot is left for follow-ups. PR-URL: #61813 Reviewed-By: Michaël Zasso <targos@protonmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net>
1 parent 9e40fb9 commit 2d874df

6 files changed

Lines changed: 188 additions & 25 deletions

File tree

doc/api/single-executable-applications.md

Lines changed: 59 additions & 16 deletions

src/node_sea.cc

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ size_t SeaSerializer::Write(const SeaResource& sea) {
8484
static_cast<uint8_t>(sea.exec_argv_extension));
8585
written_total +=
8686
WriteArithmetic<uint8_t>(static_cast<uint8_t>(sea.exec_argv_extension));
87+
88+
Debug("Write SEA main code format %u\n",
89+
static_cast<uint8_t>(sea.main_code_format));
90+
written_total +=
91+
WriteArithmetic<uint8_t>(static_cast<uint8_t>(sea.main_code_format));
8792
DCHECK_EQ(written_total, SeaResource::kHeaderSize);
8893

8994
Debug("Write SEA code path %p, size=%zu\n",
@@ -161,6 +166,11 @@ SeaResource SeaDeserializer::Read() {
161166
SeaExecArgvExtension exec_argv_extension =
162167
static_cast<SeaExecArgvExtension>(extension_value);
163168
Debug("Read SEA resource exec argv extension %u\n", extension_value);
169+
170+
uint8_t format_value = ReadArithmetic<uint8_t>();
171+
CHECK_LE(format_value, static_cast<uint8_t>(ModuleFormat::kModule));
172+
ModuleFormat main_code_format = static_cast<ModuleFormat>(format_value);
173+
Debug("Read SEA main code format %u\n", format_value);
164174
CHECK_EQ(read_total, SeaResource::kHeaderSize);
165175

166176
std::string_view code_path =
@@ -219,6 +229,7 @@ SeaResource SeaDeserializer::Read() {
219229
exec_argv_extension,
220230
code_path,
221231
code,
232+
main_code_format,
222233
code_cache,
223234
assets,
224235
exec_argv};
@@ -501,6 +512,25 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
501512
config_path);
502513
return std::nullopt;
503514
}
515+
} else if (key == "mainFormat") {
516+
std::string_view format_str;
517+
if (field.value().get_string().get(format_str)) {
518+
FPrintF(stderr,
519+
"\"mainFormat\" field of %s is not a string\n",
520+
config_path);
521+
return std::nullopt;
522+
}
523+
if (format_str == "commonjs") {
524+
result.main_format = ModuleFormat::kCommonJS;
525+
} else if (format_str == "module") {
526+
result.main_format = ModuleFormat::kModule;
527+
} else {
528+
FPrintF(stderr,
529+
"\"mainFormat\" field of %s must be one of "
530+
"\"commonjs\" or \"module\"\n",
531+
config_path);
532+
return std::nullopt;
533+
}
504534
}
505535
}
506536

@@ -512,6 +542,23 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
512542
"\"useCodeCache\" is redundant when \"useSnapshot\" is true\n");
513543
}
514544

545+
// TODO(joyeecheung): support ESM with useSnapshot and useCodeCache.
546+
if (result.main_format == ModuleFormat::kModule &&
547+
static_cast<bool>(result.flags & SeaFlags::kUseSnapshot)) {
548+
FPrintF(stderr,
549+
"\"mainFormat\": \"module\" is not supported when "
550+
"\"useSnapshot\" is true\n");
551+
return std::nullopt;
552+
}
553+
554+
if (result.main_format == ModuleFormat::kModule &&
555+
static_cast<bool>(result.flags & SeaFlags::kUseCodeCache)) {
556+
FPrintF(stderr,
557+
"\"mainFormat\": \"module\" is not supported when "
558+
"\"useCodeCache\" is true\n");
559+
return std::nullopt;
560+
}
561+
515562
if (result.main_path.empty()) {
516563
FPrintF(stderr,
517564
"\"main\" field of %s is not a non-empty string\n",
@@ -709,6 +756,7 @@ ExitCode GenerateSingleExecutableBlob(
709756
builds_snapshot_from_main
710757
? std::string_view{snapshot_blob.data(), snapshot_blob.size()}
711758
: std::string_view{main_script.data(), main_script.size()},
759+
config.main_format,
712760
optional_sv_code_cache,
713761
assets_view,
714762
exec_argv_view};
@@ -792,20 +840,25 @@ void GetAssetKeys(const FunctionCallbackInfo<Value>& args) {
792840
}
793841

794842
MaybeLocal<Value> LoadSingleExecutableApplication(
795-
const StartExecutionCallbackInfo& info) {
843+
const StartExecutionCallbackInfoWithModule& info) {
796844
// Here we are currently relying on the fact that in NodeMainInstance::Run(),
797845
// env->context() is entered.
798-
Local<Context> context = Isolate::GetCurrent()->GetCurrentContext();
799-
Environment* env = Environment::GetCurrent(context);
846+
Environment* env = info.env();
847+
Local<Context> context = env->context();
800848
SeaResource sea = FindSingleExecutableResource();
801849

802850
CHECK(!sea.use_snapshot());
803851
// TODO(joyeecheung): this should be an external string. Refactor UnionBytes
804852
// and make it easy to create one based on static content on the fly.
805853
Local<Value> main_script =
806-
ToV8Value(env->context(), sea.main_code_or_snapshot).ToLocalChecked();
807-
return info.run_cjs->Call(
808-
env->context(), Null(env->isolate()), 1, &main_script);
854+
ToV8Value(context, sea.main_code_or_snapshot).ToLocalChecked();
855+
Local<Value> kind =
856+
v8::Integer::New(env->isolate(), static_cast<int>(sea.main_code_format));
857+
Local<Value> resource_name =
858+
ToV8Value(context, env->exec_path()).ToLocalChecked();
859+
Local<Value> args[] = {main_script, kind, resource_name};
860+
return info.run_module()->Call(
861+
env->context(), Null(env->isolate()), arraysize(args), args);
809862
}
810863

811864
bool MaybeLoadSingleExecutableApplication(Environment* env) {
@@ -821,7 +874,7 @@ bool MaybeLoadSingleExecutableApplication(Environment* env) {
821874
// this check is just here to guard against the unlikely case where
822875
// the SEA preparation blob has been manually modified by someone.
823876
CHECK(!env->snapshot_deserialize_main().IsEmpty());
824-
LoadEnvironment(env, StartExecutionCallback{});
877+
LoadEnvironment(env, StartExecutionCallbackWithModule{});
825878
return true;
826879
}
827880

src/node_sea.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <unordered_map>
1212
#include <vector>
1313

14+
#include "node.h"
1415
#include "node_exit_code.h"
1516

1617
namespace node {
@@ -43,6 +44,7 @@ struct SeaConfig {
4344
std::string executable_path;
4445
SeaFlags flags = SeaFlags::kDefault;
4546
SeaExecArgvExtension exec_argv_extension = SeaExecArgvExtension::kEnv;
47+
ModuleFormat main_format = ModuleFormat::kCommonJS;
4648
std::unordered_map<std::string, std::string> assets;
4749
std::vector<std::string> exec_argv;
4850
};
@@ -52,15 +54,17 @@ struct SeaResource {
5254
SeaExecArgvExtension exec_argv_extension = SeaExecArgvExtension::kEnv;
5355
std::string_view code_path;
5456
std::string_view main_code_or_snapshot;
57+
ModuleFormat main_code_format = ModuleFormat::kCommonJS;
5558
std::optional<std::string_view> code_cache;
5659
std::unordered_map<std::string_view, std::string_view> assets;
5760
std::vector<std::string_view> exec_argv;
5861

5962
bool use_snapshot() const;
6063
bool use_code_cache() const;
6164

62-
static constexpr size_t kHeaderSize =
63-
sizeof(kMagic) + sizeof(SeaFlags) + sizeof(SeaExecArgvExtension);
65+
static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags) +
66+
sizeof(SeaExecArgvExtension) +
67+
sizeof(ModuleFormat);
6468
};
6569

6670
bool IsSingleExecutable();
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"main": "sea.mjs",
3+
"output": "sea",
4+
"mainFormat": "module",
5+
"disableExperimentalSEAWarning": true
6+
}

test/fixtures/sea/esm/sea.mjs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import assert from 'node:assert';
2+
import { createRequire } from 'node:module';
3+
import { pathToFileURL } from 'node:url';
4+
import { dirname } from 'node:path';
5+
6+
// Test createRequire with process.execPath.
7+
const assert2 = createRequire(process.execPath)('node:assert');
8+
assert.strictEqual(assert2.strict, assert.strict);
9+
10+
// Test import.meta properties. This should be in sync with the CommonJS entry
11+
// point's corresponding values.
12+
assert.strictEqual(import.meta.url, pathToFileURL(process.execPath).href);
13+
assert.strictEqual(import.meta.filename, process.execPath);
14+
assert.strictEqual(import.meta.dirname, dirname(process.execPath));
15+
assert.strictEqual(import.meta.main, true);
16+
// TODO(joyeecheung): support import.meta.resolve when we also support
17+
// require.resolve in CommonJS entry points, the behavior of the two
18+
// should be in sync.
19+
20+
// Test import() with a built-in module.
21+
const { strict } = await import('node:assert');
22+
assert.strictEqual(strict, assert.strict);
23+
24+
console.log('ESM SEA executed successfully');
Lines changed: 33 additions & 0 deletions

0 commit comments

Comments
 (0)