Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions doc/api/single-executable-applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,8 +395,7 @@ The accepted values are:

If the `mainFormat` field is not specified, it defaults to `"commonjs"`.

Currently, `"mainFormat": "module"` cannot be used together with `"useSnapshot"`
or `"useCodeCache"`.
Currently, `"mainFormat": "module"` cannot be used together with `"useSnapshot"`.

### Module loading in the injected main script

Expand Down
42 changes: 36 additions & 6 deletions src/module_wrap.cc
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
#include "module_wrap.h"

#include "debug_utils-inl.h"
#include "env.h"
#include "memory_tracker-inl.h"
#include "node_contextify.h"
#include "node_errors.h"
#include "node_external_reference.h"
#include "node_internals.h"
#include "node_process-inl.h"
#include "node_sea.h"
#include "node_url.h"
#include "node_watchdog.h"
#include "util-inl.h"
Expand Down Expand Up @@ -365,6 +367,20 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
new ScriptCompiler::CachedData(data + cached_data_buf->ByteOffset(),
cached_data_buf->ByteLength());
}
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
// For embedder ESM in a SEA, use the bundled code cache if available.
if (id_symbol == realm->isolate_data()->embedder_module_hdo() &&
sea::IsSingleExecutable()) {
sea::SeaResource sea = sea::FindSingleExecutableResource();
if (sea.use_code_cache()) {
std::string_view data = sea.code_cache.value();
user_cached_data = new ScriptCompiler::CachedData(
reinterpret_cast<const uint8_t*>(data.data()),
static_cast<int>(data.size()),
ScriptCompiler::CachedData::BufferNotOwned);
}
}
#endif // !DISABLE_SINGLE_EXECUTABLE_APPLICATION
Local<String> source_text = args[2].As<String>();

bool cache_rejected = false;
Expand All @@ -389,12 +405,26 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
return;
}

if (user_cached_data.has_value() && user_cached_data.value() != nullptr &&
cache_rejected) {
THROW_ERR_VM_MODULE_CACHED_DATA_REJECTED(
realm, "cachedData buffer was rejected");
try_catch.ReThrow();
return;
if (user_cached_data.has_value() && user_cached_data.value() != nullptr) {
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
if (id_symbol == realm->isolate_data()->embedder_module_hdo() &&
sea::IsSingleExecutable()) {
if (cache_rejected) {
per_process::Debug(DebugCategory::SEA,
"SEA module code cache rejected\n");
ProcessEmitWarningSync(realm->env(), "Code cache data rejected.");
} else {
per_process::Debug(DebugCategory::SEA,
"SEA module code cache accepted\n");
}
} else // NOLINT(readability/braces)
#endif // !DISABLE_SINGLE_EXECUTABLE_APPLICATION
if (cache_rejected) {
THROW_ERR_VM_MODULE_CACHED_DATA_REJECTED(
realm, "cachedData buffer was rejected");
try_catch.ReThrow();
return;
}
}

if (that->Set(context,
Expand Down
98 changes: 61 additions & 37 deletions src/node_sea.cc
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,21 @@ using v8::Array;
using v8::ArrayBuffer;
using v8::BackingStore;
using v8::Context;
using v8::Data;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::LocalVector;
using v8::MaybeLocal;
using v8::Module;
using v8::NewStringType;
using v8::Object;
using v8::ScriptCompiler;
using v8::ScriptOrigin;
using v8::String;
using v8::UnboundModuleScript;
using v8::Value;

namespace node {
Expand Down Expand Up @@ -542,7 +545,7 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
"\"useCodeCache\" is redundant when \"useSnapshot\" is true\n");
}

// TODO(joyeecheung): support ESM with useSnapshot and useCodeCache.
// TODO(joyeecheung): support ESM with useSnapshot.
if (result.main_format == ModuleFormat::kModule &&
static_cast<bool>(result.flags & SeaFlags::kUseSnapshot)) {
FPrintF(stderr,
Expand All @@ -551,14 +554,6 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
return std::nullopt;
}

if (result.main_format == ModuleFormat::kModule &&
static_cast<bool>(result.flags & SeaFlags::kUseCodeCache)) {
FPrintF(stderr,
"\"mainFormat\": \"module\" is not supported when "
"\"useCodeCache\" is true\n");
return std::nullopt;
}

if (result.main_path.empty()) {
FPrintF(stderr,
"\"main\" field of %s is not a non-empty string\n",
Expand Down Expand Up @@ -616,7 +611,8 @@ ExitCode GenerateSnapshotForSEA(const SeaConfig& config,
}

std::optional<std::string> GenerateCodeCache(std::string_view main_path,
std::string_view main_script) {
std::string_view main_script,
ModuleFormat format) {
RAIIIsolate raii_isolate(SnapshotBuilder::GetEmbeddedSnapshotData());
Isolate* isolate = raii_isolate.get();

Expand Down Expand Up @@ -647,34 +643,62 @@ std::optional<std::string> GenerateCodeCache(std::string_view main_path,
return std::nullopt;
}

LocalVector<String> parameters(
isolate,
{
FIXED_ONE_BYTE_STRING(isolate, "exports"),
FIXED_ONE_BYTE_STRING(isolate, "require"),
FIXED_ONE_BYTE_STRING(isolate, "module"),
FIXED_ONE_BYTE_STRING(isolate, "__filename"),
FIXED_ONE_BYTE_STRING(isolate, "__dirname"),
});
ScriptOrigin script_origin(filename, 0, 0, true);
ScriptCompiler::Source script_source(content, script_origin);
MaybeLocal<Function> maybe_fn =
ScriptCompiler::CompileFunction(context,
&script_source,
parameters.size(),
parameters.data(),
0,
nullptr);
Local<Function> fn;
if (!maybe_fn.ToLocal(&fn)) {
return std::nullopt;
std::unique_ptr<ScriptCompiler::CachedData> cache;

if (format == ModuleFormat::kModule) {
// Using empty host defined options is fine as it is not part of the cache
// key and will be reset after deserialization.
ScriptOrigin origin(filename,
0, // line offset
0, // column offset
true, // is cross origin
-1, // script id
Local<Value>(), // source map URL
false, // is opaque
false, // is WASM
true, // is ES Module
Local<Data>()); // host defined options
ScriptCompiler::Source source(content, origin);
Local<Module> module;
if (!ScriptCompiler::CompileModule(isolate, &source).ToLocal(&module)) {
return std::nullopt;
}
Local<UnboundModuleScript> unbound = module->GetUnboundModuleScript();
cache.reset(ScriptCompiler::CreateCodeCache(unbound));
} else {
// TODO(RaisinTen): Using the V8 code cache prevents us from using
// `import()` in the SEA code. Support it. Refs:
// https://github.com/nodejs/node/pull/48191#discussion_r1213271430
// TODO(joyeecheung): this likely has been fixed by
// https://chromium-review.googlesource.com/c/v8/v8/+/5401780 - add a test
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that this should already be fixed - and locally it works. But since it was talking about the CommonJS case I think it's out of scope of this PR, so I only left a TODO.

// and update docs.
LocalVector<String> parameters(
isolate,
{
FIXED_ONE_BYTE_STRING(isolate, "exports"),
FIXED_ONE_BYTE_STRING(isolate, "require"),
FIXED_ONE_BYTE_STRING(isolate, "module"),
FIXED_ONE_BYTE_STRING(isolate, "__filename"),
FIXED_ONE_BYTE_STRING(isolate, "__dirname"),
});
ScriptOrigin script_origin(filename, 0, 0, true);
ScriptCompiler::Source script_source(content, script_origin);
Local<Function> fn;
if (!ScriptCompiler::CompileFunction(context,
&script_source,
parameters.size(),
parameters.data(),
0,
nullptr)
.ToLocal(&fn)) {
return std::nullopt;
}
cache.reset(ScriptCompiler::CreateCodeCacheForFunction(fn));
}

// TODO(RaisinTen): Using the V8 code cache prevents us from using `import()`
// in the SEA code. Support it.
// Refs: https://github.com/nodejs/node/pull/48191#discussion_r1213271430
std::unique_ptr<ScriptCompiler::CachedData> cache{
ScriptCompiler::CreateCodeCacheForFunction(fn)};
if (!cache) {
return std::nullopt;
}
std::string code_cache(cache->data, cache->data + cache->length);
return code_cache;
}
Expand Down Expand Up @@ -728,7 +752,7 @@ ExitCode GenerateSingleExecutableBlob(
std::string code_cache;
if (static_cast<bool>(config.flags & SeaFlags::kUseCodeCache)) {
std::optional<std::string> optional_code_cache =
GenerateCodeCache(config.main_path, main_script);
GenerateCodeCache(config.main_path, main_script, config.main_format);
if (!optional_code_cache.has_value()) {
FPrintF(stderr, "Cannot generate V8 code cache\n");
return ExitCode::kGenericUserError;
Expand Down
7 changes: 7 additions & 0 deletions test/fixtures/sea/esm-code-cache/sea-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"main": "sea.mjs",
"output": "sea",
"mainFormat": "module",
"useCodeCache": true,
"disableExperimentalSEAWarning": true
}
20 changes: 20 additions & 0 deletions test/fixtures/sea/esm-code-cache/sea.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import assert from 'node:assert';
import { createRequire } from 'node:module';
import { pathToFileURL } from 'node:url';
import { dirname } from 'node:path';

// Test createRequire with process.execPath.
const assert2 = createRequire(process.execPath)('node:assert');
assert.strictEqual(assert2.strict, assert.strict);

// Test import.meta properties.
assert.strictEqual(import.meta.url, pathToFileURL(process.execPath).href);
assert.strictEqual(import.meta.filename, process.execPath);
assert.strictEqual(import.meta.dirname, dirname(process.execPath));
assert.strictEqual(import.meta.main, true);

// Test import() with a built-in module.
const { strict } = await import('node:assert');
assert.strictEqual(strict, assert.strict);

console.log('ESM SEA with code cache executed successfully');
34 changes: 34 additions & 0 deletions test/sea/test-single-executable-application-esm-code-cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict';

// This tests the creation of a single executable application with an ESM
// entry point using "mainFormat": "module" and "useCodeCache": true.

require('../common');

const {
buildSEA,
skipIfBuildSEAIsNotSupported,
} = require('../common/sea');

skipIfBuildSEAIsNotSupported();

const tmpdir = require('../common/tmpdir');
const fixtures = require('../common/fixtures');
const { spawnSyncAndExitWithoutError } = require('../common/child_process');

tmpdir.refresh();

const outputFile = buildSEA(fixtures.path('sea', 'esm-code-cache'));

spawnSyncAndExitWithoutError(
outputFile,
{
env: {
NODE_DEBUG_NATIVE: 'SEA',
...process.env,
},
},
{
stdout: /ESM SEA with code cache executed successfully/,
stderr: /SEA module code cache accepted/,
});
Loading