Skip to content

Commit 18593b7

Browse files
joyeecheungmarco-ippolito
authored andcommitted
src: refactor embedded entrypoint loading
This patch: 1. Refactor the routines used to compile and run an embedder entrypoint. In JS land special handling for SEA is done directly in main/embedding.js instead of clobbering the CJS loader. Add warnings to remind users that currently the require() in SEA bundled scripts only supports loading builtins. 2. Don't use the bundled SEA code cache when compiling CJS loaded from disk, since in that case we are certainly not compiling the code bundled into the SEA. Use a is_sea_main flag in CompileFunctionForCJSLoader() (which replaces an unused argument) to pass this into the C++ land - the code cache is still read directly from C++ to avoid the overhead of ArrayBuffer creation. 3. Move SEA loading code into MaybeLoadSingleExecutableApplication() which calls LoadEnvironment() with its own StartExecutionCallback(). This avoids more hidden switches in StartExecution() and make them explicit. Also add some TODOs about how to support ESM in embedded applications. 4. Add more comments PR-URL: #53573 Backport-PR-URL: #56927 Reviewed-By: Geoffrey Booth <[email protected]> Reviewed-By: Chengzhong Wu <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: James M Snell <[email protected]> Refs: #52697
1 parent 533afe8 commit 18593b7

File tree

11 files changed

+208
-128
lines changed

11 files changed

+208
-128
lines changed

lib/internal/main/check_syntax.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,5 @@ async function checkSyntax(source, filename) {
7575
return;
7676
}
7777

78-
wrapSafe(filename, source);
78+
wrapSafe(filename, source, undefined, 'commonjs');
7979
}

lib/internal/main/embedding.js

+103-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,116 @@
11
'use strict';
2+
3+
// This main script is currently only run when LoadEnvironment()
4+
// is run with a non-null StartExecutionCallback or a UTF8
5+
// main script. Effectively there are two cases where this happens:
6+
// 1. It's a single-executable application *loading* a main script
7+
// bundled into the executable. This is currently done from
8+
// NodeMainInstance::Run().
9+
// 2. It's an embedder application and LoadEnvironment() is invoked
10+
// as described above.
11+
212
const {
313
prepareMainThreadExecution,
414
} = require('internal/process/pre_execution');
5-
const { isExperimentalSeaWarningNeeded } = internalBinding('sea');
15+
const { isExperimentalSeaWarningNeeded, isSea } = internalBinding('sea');
616
const { emitExperimentalWarning } = require('internal/util');
7-
const { embedderRequire, embedderRunCjs } = require('internal/util/embedding');
17+
const { emitWarningSync } = require('internal/process/warning');
18+
const { BuiltinModule: { normalizeRequirableId } } = require('internal/bootstrap/realm');
19+
const { Module } = require('internal/modules/cjs/loader');
20+
const { compileFunctionForCJSLoader } = internalBinding('contextify');
21+
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
22+
23+
const { codes: {
24+
ERR_UNKNOWN_BUILTIN_MODULE,
25+
} } = require('internal/errors');
826

27+
// Don't expand process.argv[1] because in a single-executable application or an
28+
// embedder application, the user main script isn't necessarily provided via the
29+
// command line (e.g. it could be provided via an API or bundled into the executable).
930
prepareMainThreadExecution(false, true);
1031

32+
const isLoadingSea = isSea();
1133
if (isExperimentalSeaWarningNeeded()) {
1234
emitExperimentalWarning('Single executable application');
1335
}
1436

37+
// This is roughly the same as:
38+
//
39+
// const mod = new Module(filename);
40+
// mod._compile(content, filename);
41+
//
42+
// but the code has been duplicated because currently there is no way to set the
43+
// value of require.main to module.
44+
//
45+
// TODO(RaisinTen): Find a way to deduplicate this.
46+
function embedderRunCjs(content) {
47+
// The filename of the module (used for CJS module lookup)
48+
// is always the same as the location of the executable itself
49+
// at the time of the loading (which means it changes depending
50+
// on where the executable is in the file system).
51+
const filename = process.execPath;
52+
const customModule = new Module(filename, null);
53+
54+
const {
55+
function: compiledWrapper,
56+
cachedDataRejected,
57+
sourceMapURL,
58+
} = compileFunctionForCJSLoader(
59+
content,
60+
filename,
61+
isLoadingSea, // is_sea_main
62+
false, // should_detect_module, ESM should be supported differently for embedded code
63+
);
64+
// Cache the source map for the module if present.
65+
if (sourceMapURL) {
66+
maybeCacheSourceMap(
67+
filename,
68+
content,
69+
customModule,
70+
false, // isGeneratedSource
71+
undefined, // sourceURL, TODO(joyeecheung): should be extracted by V8
72+
sourceMapURL,
73+
);
74+
}
75+
76+
// cachedDataRejected is only set if cache from SEA is used.
77+
if (cachedDataRejected !== false && isLoadingSea) {
78+
emitWarningSync('Code cache data rejected.');
79+
}
80+
81+
// Patch the module to make it look almost like a regular CJS module
82+
// instance.
83+
customModule.filename = process.execPath;
84+
customModule.paths = Module._nodeModulePaths(process.execPath);
85+
embedderRequire.main = customModule;
86+
87+
return compiledWrapper(
88+
customModule.exports, // exports
89+
embedderRequire, // require
90+
customModule, // module
91+
process.execPath, // __filename
92+
customModule.path, // __dirname
93+
);
94+
}
95+
96+
let warnedAboutBuiltins = false;
97+
98+
function embedderRequire(id) {
99+
const normalizedId = normalizeRequirableId(id);
100+
if (!normalizedId) {
101+
if (isLoadingSea && !warnedAboutBuiltins) {
102+
emitWarningSync(
103+
'Currently the require() provided to the main script embedded into ' +
104+
'single-executable applications only supports loading built-in modules.\n' +
105+
'To load a module from disk after the single executable application is ' +
106+
'launched, use require("module").createRequire().\n' +
107+
'Support for bundled module loading or virtual file systems are under ' +
108+
'discussions in https://github.com/nodejs/single-executable');
109+
warnedAboutBuiltins = true;
110+
}
111+
throw new ERR_UNKNOWN_BUILTIN_MODULE(id);
112+
}
113+
return require(normalizedId);
114+
}
115+
15116
return [process, embedderRequire, embedderRunCjs];

lib/internal/modules/cjs/loader.js

+4-13
Original file line numberDiff line numberDiff line change
@@ -1350,11 +1350,10 @@ function loadESMFromCJS(mod, filename) {
13501350
* Wraps the given content in a script and runs it in a new context.
13511351
* @param {string} filename The name of the file being loaded
13521352
* @param {string} content The content of the file being loaded
1353-
* @param {Module} cjsModuleInstance The CommonJS loader instance
1354-
* @param {object} codeCache The SEA code cache
1353+
* @param {Module|undefined} cjsModuleInstance The CommonJS loader instance
13551354
* @param {'commonjs'|undefined} format Intended format of the module.
13561355
*/
1357-
function wrapSafe(filename, content, cjsModuleInstance, codeCache, format) {
1356+
function wrapSafe(filename, content, cjsModuleInstance, format) {
13581357
assert(format !== 'module'); // ESM should be handled in loadESMFromCJS().
13591358
const hostDefinedOptionId = vm_dynamic_import_default_internal;
13601359
const importModuleDynamically = vm_dynamic_import_default_internal;
@@ -1385,16 +1384,8 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache, format) {
13851384
};
13861385
}
13871386

1388-
const isMain = !!(cjsModuleInstance && cjsModuleInstance[kIsMainSymbol]);
13891387
const shouldDetectModule = (format !== 'commonjs' && getOptionValue('--experimental-detect-module'));
1390-
const result = compileFunctionForCJSLoader(content, filename, isMain, shouldDetectModule);
1391-
1392-
// cachedDataRejected is only set for cache coming from SEA.
1393-
if (codeCache &&
1394-
result.cachedDataRejected !== false &&
1395-
internalBinding('sea').isSea()) {
1396-
process.emitWarning('Code cache data rejected.');
1397-
}
1388+
const result = compileFunctionForCJSLoader(content, filename, false /* is_sea_main */, shouldDetectModule);
13981389

13991390
// Cache the source map for the module if present.
14001391
if (result.sourceMapURL) {
@@ -1423,7 +1414,7 @@ Module.prototype._compile = function(content, filename, format) {
14231414

14241415
let compiledWrapper;
14251416
if (format !== 'module') {
1426-
const result = wrapSafe(filename, content, this, undefined, format);
1417+
const result = wrapSafe(filename, content, this, format);
14271418
compiledWrapper = result.function;
14281419
if (result.canParseAsESM) {
14291420
format = 'module';

lib/internal/modules/esm/translators.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ translators.set('module', function moduleStrategy(url, source, isMain) {
176176
* @param {boolean} isMain - Whether the module is the entrypoint
177177
*/
178178
function loadCJSModule(module, source, url, filename, isMain) {
179-
const compileResult = compileFunctionForCJSLoader(source, filename, isMain, false);
179+
const compileResult = compileFunctionForCJSLoader(source, filename, false /* is_sea_main */, false);
180180

181181
const { function: compiledWrapper, sourceMapURL } = compileResult;
182182
// Cache the source map for the cjs module if present.

lib/internal/util/embedding.js

-51
This file was deleted.

src/node.cc

+8
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,14 @@ std::optional<StartExecutionCallbackInfo> CallbackInfoFromArray(
308308
CHECK(process_obj->IsObject());
309309
CHECK(require_fn->IsFunction());
310310
CHECK(runcjs_fn->IsFunction());
311+
// TODO(joyeecheung): some support for running ESM as an entrypoint
312+
// is needed. The simplest API would be to add a run_esm to
313+
// StartExecutionCallbackInfo which compiles, links (to builtins)
314+
// and evaluates a SourceTextModule.
315+
// TODO(joyeecheung): the env pointer should be part of
316+
// StartExecutionCallbackInfo, otherwise embedders are forced to use
317+
// lambdas to pass it into the callback, which can make the code
318+
// difficult to read.
311319
node::StartExecutionCallbackInfo info{process_obj.As<Object>(),
312320
require_fn.As<Function>(),
313321
runcjs_fn.As<Function>()};

src/node_contextify.cc

+34-21
Original file line numberDiff line numberDiff line change
@@ -1441,12 +1441,17 @@ static std::vector<std::string_view> throws_only_in_cjs_error_messages = {
14411441
"await is only valid in async functions and "
14421442
"the top level bodies of modules"};
14431443

1444-
static MaybeLocal<Function> CompileFunctionForCJSLoader(Environment* env,
1445-
Local<Context> context,
1446-
Local<String> code,
1447-
Local<String> filename,
1448-
bool* cache_rejected,
1449-
bool is_cjs_scope) {
1444+
// If cached_data is provided, it would be used for the compilation and
1445+
// the on-disk compilation cache from NODE_COMPILE_CACHE (if configured)
1446+
// would be ignored.
1447+
static MaybeLocal<Function> CompileFunctionForCJSLoader(
1448+
Environment* env,
1449+
Local<Context> context,
1450+
Local<String> code,
1451+
Local<String> filename,
1452+
bool* cache_rejected,
1453+
bool is_cjs_scope,
1454+
ScriptCompiler::CachedData* cached_data) {
14501455
Isolate* isolate = context->GetIsolate();
14511456
EscapableHandleScope scope(isolate);
14521457

@@ -1464,20 +1469,7 @@ static MaybeLocal<Function> CompileFunctionForCJSLoader(Environment* env,
14641469
false, // is WASM
14651470
false, // is ES Module
14661471
hdo);
1467-
ScriptCompiler::CachedData* cached_data = nullptr;
14681472

1469-
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
1470-
if (sea::IsSingleExecutable()) {
1471-
sea::SeaResource sea = sea::FindSingleExecutableResource();
1472-
if (sea.use_code_cache()) {
1473-
std::string_view data = sea.code_cache.value();
1474-
cached_data = new ScriptCompiler::CachedData(
1475-
reinterpret_cast<const uint8_t*>(data.data()),
1476-
static_cast<int>(data.size()),
1477-
v8::ScriptCompiler::CachedData::BufferNotOwned);
1478-
}
1479-
}
1480-
#endif
14811473
ScriptCompiler::Source source(code, origin, cached_data);
14821474
ScriptCompiler::CompileOptions options;
14831475
if (cached_data == nullptr) {
@@ -1532,6 +1524,7 @@ static void CompileFunctionForCJSLoader(
15321524
CHECK(args[3]->IsBoolean());
15331525
Local<String> code = args[0].As<String>();
15341526
Local<String> filename = args[1].As<String>();
1527+
bool is_sea_main = args[2].As<Boolean>()->Value();
15351528
bool should_detect_module = args[3].As<Boolean>()->Value();
15361529

15371530
Isolate* isolate = args.GetIsolate();
@@ -1544,11 +1537,31 @@ static void CompileFunctionForCJSLoader(
15441537
Local<Value> cjs_exception;
15451538
Local<Message> cjs_message;
15461539

1540+
ScriptCompiler::CachedData* cached_data = nullptr;
1541+
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
1542+
if (is_sea_main) {
1543+
sea::SeaResource sea = sea::FindSingleExecutableResource();
1544+
// Use the "main" field in SEA config for the filename.
1545+
Local<Value> filename_from_sea;
1546+
if (!ToV8Value(context, sea.code_path).ToLocal(&filename_from_sea)) {
1547+
return;
1548+
}
1549+
filename = filename_from_sea.As<String>();
1550+
if (sea.use_code_cache()) {
1551+
std::string_view data = sea.code_cache.value();
1552+
cached_data = new ScriptCompiler::CachedData(
1553+
reinterpret_cast<const uint8_t*>(data.data()),
1554+
static_cast<int>(data.size()),
1555+
v8::ScriptCompiler::CachedData::BufferNotOwned);
1556+
}
1557+
}
1558+
#endif
1559+
15471560
{
15481561
ShouldNotAbortOnUncaughtScope no_abort_scope(realm->env());
15491562
TryCatchScope try_catch(env);
15501563
if (!CompileFunctionForCJSLoader(
1551-
env, context, code, filename, &cache_rejected, true)
1564+
env, context, code, filename, &cache_rejected, true, cached_data)
15521565
.ToLocal(&fn)) {
15531566
CHECK(try_catch.HasCaught());
15541567
CHECK(!try_catch.HasTerminated());
@@ -1703,7 +1716,7 @@ static void ContainsModuleSyntax(const FunctionCallbackInfo<Value>& args) {
17031716
TryCatchScope try_catch(env);
17041717
ShouldNotAbortOnUncaughtScope no_abort_scope(env);
17051718
if (CompileFunctionForCJSLoader(
1706-
env, context, code, filename, &cache_rejected, cjs_var)
1719+
env, context, code, filename, &cache_rejected, cjs_var, nullptr)
17071720
.ToLocal(&fn)) {
17081721
args.GetReturnValue().Set(false);
17091722
return;

src/node_main_instance.cc

+1-14
Original file line numberDiff line numberDiff line change
@@ -103,20 +103,7 @@ ExitCode NodeMainInstance::Run() {
103103

104104
void NodeMainInstance::Run(ExitCode* exit_code, Environment* env) {
105105
if (*exit_code == ExitCode::kNoFailure) {
106-
bool runs_sea_code = false;
107-
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
108-
if (sea::IsSingleExecutable()) {
109-
sea::SeaResource sea = sea::FindSingleExecutableResource();
110-
if (!sea.use_snapshot()) {
111-
runs_sea_code = true;
112-
std::string_view code = sea.main_code_or_snapshot;
113-
LoadEnvironment(env, code);
114-
}
115-
}
116-
#endif
117-
// Either there is already a snapshot main function from SEA, or it's not
118-
// a SEA at all.
119-
if (!runs_sea_code) {
106+
if (!sea::MaybeLoadSingleExecutableApplication(env)) {
120107
LoadEnvironment(env, StartExecutionCallback{});
121108
}
122109

0 commit comments

Comments
 (0)