Skip to content

Commit a9db553

Browse files
joyeecheungaduh95
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 Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 5aecbef commit a9db553

File tree

11 files changed

+209
-134
lines changed

11 files changed

+209
-134
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
@@ -1343,11 +1343,10 @@ function loadESMFromCJS(mod, filename) {
13431343
* Wraps the given content in a script and runs it in a new context.
13441344
* @param {string} filename The name of the file being loaded
13451345
* @param {string} content The content of the file being loaded
1346-
* @param {Module} cjsModuleInstance The CommonJS loader instance
1347-
* @param {object} codeCache The SEA code cache
1346+
* @param {Module|undefined} cjsModuleInstance The CommonJS loader instance
13481347
* @param {'commonjs'|undefined} format Intended format of the module.
13491348
*/
1350-
function wrapSafe(filename, content, cjsModuleInstance, codeCache, format) {
1349+
function wrapSafe(filename, content, cjsModuleInstance, format) {
13511350
assert(format !== 'module'); // ESM should be handled in loadESMFromCJS().
13521351
const hostDefinedOptionId = vm_dynamic_import_default_internal;
13531352
const importModuleDynamically = vm_dynamic_import_default_internal;
@@ -1378,16 +1377,8 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache, format) {
13781377
};
13791378
}
13801379

1381-
const isMain = !!(cjsModuleInstance && cjsModuleInstance[kIsMainSymbol]);
13821380
const shouldDetectModule = (format !== 'commonjs' && getOptionValue('--experimental-detect-module'));
1383-
const result = compileFunctionForCJSLoader(content, filename, isMain, shouldDetectModule);
1384-
1385-
// cachedDataRejected is only set for cache coming from SEA.
1386-
if (codeCache &&
1387-
result.cachedDataRejected !== false &&
1388-
internalBinding('sea').isSea()) {
1389-
process.emitWarning('Code cache data rejected.');
1390-
}
1381+
const result = compileFunctionForCJSLoader(content, filename, false /* is_sea_main */, shouldDetectModule);
13911382

13921383
// Cache the source map for the module if present.
13931384
if (result.sourceMapURL) {
@@ -1409,7 +1400,7 @@ Module.prototype._compile = function(content, filename, format) {
14091400

14101401
let compiledWrapper;
14111402
if (format !== 'module') {
1412-
const result = wrapSafe(filename, content, this, undefined, format);
1403+
const result = wrapSafe(filename, content, this, format);
14131404
compiledWrapper = result.function;
14141405
if (result.canParseAsESM) {
14151406
format = 'module';

lib/internal/modules/esm/translators.js

+1-1
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

-53
This file was deleted.

src/node.cc

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

src/node_contextify.cc

+35-25
Original file line numberDiff line numberDiff line change
@@ -1453,12 +1453,17 @@ static std::vector<std::string_view> throws_only_in_cjs_error_messages = {
14531453
"await is only valid in async functions and "
14541454
"the top level bodies of modules"};
14551455

1456-
static MaybeLocal<Function> CompileFunctionForCJSLoader(Environment* env,
1457-
Local<Context> context,
1458-
Local<String> code,
1459-
Local<String> filename,
1460-
bool* cache_rejected,
1461-
bool is_cjs_scope) {
1456+
// If cached_data is provided, it would be used for the compilation and
1457+
// the on-disk compilation cache from NODE_COMPILE_CACHE (if configured)
1458+
// would be ignored.
1459+
static MaybeLocal<Function> CompileFunctionForCJSLoader(
1460+
Environment* env,
1461+
Local<Context> context,
1462+
Local<String> code,
1463+
Local<String> filename,
1464+
bool* cache_rejected,
1465+
bool is_cjs_scope,
1466+
ScriptCompiler::CachedData* cached_data) {
14621467
Isolate* isolate = context->GetIsolate();
14631468
EscapableHandleScope scope(isolate);
14641469

@@ -1475,25 +1480,9 @@ static MaybeLocal<Function> CompileFunctionForCJSLoader(Environment* env,
14751480
false, // is WASM
14761481
false, // is ES Module
14771482
hdo);
1478-
ScriptCompiler::CachedData* cached_data = nullptr;
1479-
1480-
bool used_cache_from_sea = false;
1481-
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
1482-
if (sea::IsSingleExecutable()) {
1483-
sea::SeaResource sea = sea::FindSingleExecutableResource();
1484-
if (sea.use_code_cache()) {
1485-
std::string_view data = sea.code_cache.value();
1486-
cached_data = new ScriptCompiler::CachedData(
1487-
reinterpret_cast<const uint8_t*>(data.data()),
1488-
static_cast<int>(data.size()),
1489-
v8::ScriptCompiler::CachedData::BufferNotOwned);
1490-
used_cache_from_sea = true;
1491-
}
1492-
}
1493-
#endif
14941483

14951484
CompileCacheEntry* cache_entry = nullptr;
1496-
if (!used_cache_from_sea && env->use_compile_cache()) {
1485+
if (cached_data == nullptr && env->use_compile_cache()) {
14971486
cache_entry = env->compile_cache_handler()->GetOrInsert(
14981487
code, filename, CachedCodeType::kCommonJS);
14991488
}
@@ -1559,6 +1548,7 @@ static void CompileFunctionForCJSLoader(
15591548
CHECK(args[3]->IsBoolean());
15601549
Local<String> code = args[0].As<String>();
15611550
Local<String> filename = args[1].As<String>();
1551+
bool is_sea_main = args[2].As<Boolean>()->Value();
15621552
bool should_detect_module = args[3].As<Boolean>()->Value();
15631553

15641554
Isolate* isolate = args.GetIsolate();
@@ -1571,11 +1561,31 @@ static void CompileFunctionForCJSLoader(
15711561
Local<Value> cjs_exception;
15721562
Local<Message> cjs_message;
15731563

1564+
ScriptCompiler::CachedData* cached_data = nullptr;
1565+
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
1566+
if (is_sea_main) {
1567+
sea::SeaResource sea = sea::FindSingleExecutableResource();
1568+
// Use the "main" field in SEA config for the filename.
1569+
Local<Value> filename_from_sea;
1570+
if (!ToV8Value(context, sea.code_path).ToLocal(&filename_from_sea)) {
1571+
return;
1572+
}
1573+
filename = filename_from_sea.As<String>();
1574+
if (sea.use_code_cache()) {
1575+
std::string_view data = sea.code_cache.value();
1576+
cached_data = new ScriptCompiler::CachedData(
1577+
reinterpret_cast<const uint8_t*>(data.data()),
1578+
static_cast<int>(data.size()),
1579+
v8::ScriptCompiler::CachedData::BufferNotOwned);
1580+
}
1581+
}
1582+
#endif
1583+
15741584
{
15751585
ShouldNotAbortOnUncaughtScope no_abort_scope(realm->env());
15761586
TryCatchScope try_catch(env);
15771587
if (!CompileFunctionForCJSLoader(
1578-
env, context, code, filename, &cache_rejected, true)
1588+
env, context, code, filename, &cache_rejected, true, cached_data)
15791589
.ToLocal(&fn)) {
15801590
CHECK(try_catch.HasCaught());
15811591
CHECK(!try_catch.HasTerminated());
@@ -1730,7 +1740,7 @@ static void ContainsModuleSyntax(const FunctionCallbackInfo<Value>& args) {
17301740
TryCatchScope try_catch(env);
17311741
ShouldNotAbortOnUncaughtScope no_abort_scope(env);
17321742
if (CompileFunctionForCJSLoader(
1733-
env, context, code, filename, &cache_rejected, cjs_var)
1743+
env, context, code, filename, &cache_rejected, cjs_var, nullptr)
17341744
.ToLocal(&fn)) {
17351745
args.GetReturnValue().Set(false);
17361746
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)