Skip to content

Commit 5af75d4

Browse files
committed
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: nodejs#53573 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]>
1 parent 3b5f6d3 commit 5af75d4

File tree

11 files changed

+208
-128
lines changed

11 files changed

+208
-128
lines changed

lib/internal/main/check_syntax.js

Lines changed: 1 addition & 1 deletion
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

Lines changed: 103 additions & 2 deletions
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

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,11 +1376,10 @@ function loadESMFromCJS(mod, filename) {
13761376
* Wraps the given content in a script and runs it in a new context.
13771377
* @param {string} filename The name of the file being loaded
13781378
* @param {string} content The content of the file being loaded
1379-
* @param {Module} cjsModuleInstance The CommonJS loader instance
1380-
* @param {object} codeCache The SEA code cache
1379+
* @param {Module|undefined} cjsModuleInstance The CommonJS loader instance
13811380
* @param {'commonjs'|undefined} format Intended format of the module.
13821381
*/
1383-
function wrapSafe(filename, content, cjsModuleInstance, codeCache, format) {
1382+
function wrapSafe(filename, content, cjsModuleInstance, format) {
13841383
assert(format !== 'module'); // ESM should be handled in loadESMFromCJS().
13851384
const hostDefinedOptionId = vm_dynamic_import_default_internal;
13861385
const importModuleDynamically = vm_dynamic_import_default_internal;
@@ -1411,16 +1410,8 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache, format) {
14111410
};
14121411
}
14131412

1414-
const isMain = !!(cjsModuleInstance && cjsModuleInstance[kIsMainSymbol]);
14151413
const shouldDetectModule = (format !== 'commonjs' && getOptionValue('--experimental-detect-module'));
1416-
const result = compileFunctionForCJSLoader(content, filename, isMain, shouldDetectModule);
1417-
1418-
// cachedDataRejected is only set for cache coming from SEA.
1419-
if (codeCache &&
1420-
result.cachedDataRejected !== false &&
1421-
internalBinding('sea').isSea()) {
1422-
process.emitWarning('Code cache data rejected.');
1423-
}
1414+
const result = compileFunctionForCJSLoader(content, filename, false /* is_sea_main */, shouldDetectModule);
14241415

14251416
// Cache the source map for the module if present.
14261417
if (result.sourceMapURL) {
@@ -1449,7 +1440,7 @@ Module.prototype._compile = function(content, filename, format) {
14491440

14501441
let compiledWrapper;
14511442
if (format !== 'module') {
1452-
const result = wrapSafe(filename, content, this, undefined, format);
1443+
const result = wrapSafe(filename, content, this, format);
14531444
compiledWrapper = result.function;
14541445
if (result.canParseAsESM) {
14551446
format = 'module';

lib/internal/modules/esm/translators.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ translators.set('module', function moduleStrategy(url, source, isMain) {
177177
* @param {boolean} isMain - Whether the module is the entrypoint
178178
*/
179179
function loadCJSModule(module, source, url, filename, isMain) {
180-
const compileResult = compileFunctionForCJSLoader(source, filename, isMain, false);
180+
const compileResult = compileFunctionForCJSLoader(source, filename, false /* is_sea_main */, false);
181181

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

lib/internal/util/embedding.js

Lines changed: 0 additions & 51 deletions
This file was deleted.

src/node.cc

Lines changed: 8 additions & 0 deletions
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

Lines changed: 34 additions & 21 deletions
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

Lines changed: 1 addition & 14 deletions
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)