| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 9e40fb9 commit 2d874df
6 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -31,8 +31,8 @@ into the `node` binary. During start up, the program checks if anything has been | |||
| 31 | 31 | injected. If the blob is found, it executes the script in the blob. Otherwise | |
| 32 | 32 | Node.js operates as it normally does. | |
| 33 | 33 | ||
| 34 | - The single executable application feature currently only supports running a | ||
| 35 | - single embedded script using the [CommonJS][] module system. | ||
| 34 | + The single executable application feature supports running a | ||
| 35 | + single embedded script using the [CommonJS][] or the [ECMAScript Modules][] module system. | ||
| 36 | 36 | ||
| 37 | 37 | Users can create a single executable application from their bundled script | |
| 38 | 38 | with the `node` binary itself and any tool which can inject resources into the | |
@@ -110,6 +110,7 @@ The configuration currently reads the following top-level fields: | |||
| 110 | 110 | ```json | |
| 111 | 111 | { | |
| 112 | 112 | "main": "/path/to/bundled/script.js", | |
| 113 | + "mainFormat": "commonjs", // Default: "commonjs", options: "commonjs", "module" | ||
| 113 | 114 | "executable": "/path/to/node/binary", // Optional, if not specified, uses the current Node.js binary | |
| 114 | 115 | "output": "/path/to/write/the/generated/executable", | |
| 115 | 116 | "disableExperimentalSEAWarning": true, // Default: false | |
@@ -290,14 +291,12 @@ This would be equivalent to running: | |||
| 290 | 291 | node --no-warnings --trace-exit /path/to/bundled/script.js user-arg1 user-arg2 | |
| 291 | 292 | ``` | |
| 292 | 293 | ||
| 293 | - ## In the injected main script | ||
| 294 | - | ||
| 295 | - ### Single-executable application API | ||
| 294 | + ## Single-executable application API | ||
| 296 | 295 | ||
| 297 | 296 | The `node:sea` builtin allows interaction with the single-executable application | |
| 298 | 297 | from the JavaScript main script embedded into the executable. | |
| 299 | 298 | ||
| 300 | - #### `sea.isSea()` | ||
| 299 | + ### `sea.isSea()` | ||
| 301 | 300 | ||
| 302 | 301 | <!-- YAML | |
| 303 | 302 | added: | |
@@ -383,25 +382,48 @@ This method can be used to retrieve an array of all the keys of assets | |||
| 383 | 382 | embedded into the single-executable application. | |
| 384 | 383 | An error is thrown when not running inside a single-executable application. | |
| 385 | 384 | ||
| 386 | - ### `require(id)` in the injected main script is not file based | ||
| 385 | + ## In the injected main script | ||
| 387 | 386 | ||
| 388 | - `require()` in the injected main script is not the same as the [`require()`][] | ||
| 389 | - available to modules that are not injected. It also does not have any of the | ||
| 390 | - properties that non-injected [`require()`][] has except [`require.main`][]. It | ||
| 391 | - can only be used to load built-in modules. Attempting to load a module that can | ||
| 392 | - only be found in the file system will throw an error. | ||
| 387 | + ### Module format of the injected main script | ||
| 388 | + | ||
| 389 | + To specify how Node.js should interpret the injected main script, use the | ||
| 390 | + `mainFormat` field in the single-executable application configuration. | ||
| 391 | + The accepted values are: | ||
| 392 | + | ||
| 393 | + * `"commonjs"`: The injected main script is treated as a CommonJS module. | ||
| 394 | + * `"module"`: The injected main script is treated as an ECMAScript module. | ||
| 395 | + | ||
| 396 | + If the `mainFormat` field is not specified, it defaults to `"commonjs"`. | ||
| 397 | + | ||
| 398 | + Currently, `"mainFormat": "module"` cannot be used together with `"useSnapshot"` | ||
| 399 | + or `"useCodeCache"`. | ||
| 400 | + | ||
| 401 | + ### Module loading in the injected main script | ||
| 402 | + | ||
| 403 | + In the injected main script, module loading does not read from the file system. | ||
| 404 | + By default, both `require()` and `import` statements would only be able to load | ||
| 405 | + the built-in modules. Attempting to load a module that can only be found in the | ||
| 406 | + file system will throw an error. | ||
| 393 | 407 | ||
| 394 | - Instead of relying on a file based `require()`, users can bundle their | ||
| 395 | - application into a standalone JavaScript file to inject into the executable. | ||
| 396 | - This also ensures a more deterministic dependency graph. | ||
| 408 | + Users can bundle their application into a standalone JavaScript file to inject | ||
| 409 | + into the executable. This also ensures a more deterministic dependency graph. | ||
| 397 | 410 | ||
| 398 | - However, if a file based `require()` is still needed, that can also be achieved: | ||
| 411 | + To load modules from the file system in the injected main script, users can | ||
| 412 | + create a `require` function that can load from the file system using | ||
| 413 | + `module.createRequire()`. For example, in a CommonJS entry point: | ||
| 399 | 414 | ||
| 400 | 415 | ```js | |
| 401 | 416 | const { createRequire } = require('node:module'); | |
| 402 | 417 | require = createRequire(__filename); | |
| 403 | 418 | ``` | |
| 404 | 419 | ||
| 420 | + ### `require()` in the injected main script | ||
| 421 | + | ||
| 422 | + `require()` in the injected main script is not the same as the [`require()`][] | ||
| 423 | + available to modules that are not injected. | ||
| 424 | + Currently, it does not have any of the properties that non-injected | ||
| 425 | + [`require()`][] has except [`require.main`][]. | ||
| 426 | + | ||
| 405 | 427 | ### `__filename` and `module.filename` in the injected main script | |
| 406 | 428 | ||
| 407 | 429 | The values of `__filename` and `module.filename` in the injected main script | |
@@ -412,6 +434,26 @@ are equal to [`process.execPath`][]. | |||
| 412 | 434 | The value of `__dirname` in the injected main script is equal to the directory | |
| 413 | 435 | name of [`process.execPath`][]. | |
| 414 | 436 | ||
| 437 | + ### `import.meta` in the injected main script | ||
| 438 | + | ||
| 439 | + When using `"mainFormat": "module"`, `import.meta` is available in the | ||
| 440 | + injected main script with the following properties: | ||
| 441 | + | ||
| 442 | + * `import.meta.url`: A `file:` URL corresponding to [`process.execPath`][]. | ||
| 443 | + * `import.meta.filename`: Equal to [`process.execPath`][]. | ||
| 444 | + * `import.meta.dirname`: The directory name of [`process.execPath`][]. | ||
| 445 | + * `import.meta.main`: `true`. | ||
| 446 | + | ||
| 447 | + `import.meta.resolve` is currently not supported. | ||
| 448 | + | ||
| 449 | + ### `import()` in the injected main script | ||
| 450 | + | ||
| 451 | + <!-- TODO(joyeecheung): support and document module.registerHooks --> | ||
| 452 | + | ||
| 453 | + When using `"mainFormat": "module"`, `import()` can be used to dynamically | ||
| 454 | + load built-in modules. Attempting to use `import()` to load modules from | ||
| 455 | + the file system will throw an error. | ||
| 456 | + | ||
| 415 | 457 | ### Using native addons in the injected main script | |
| 416 | 458 | ||
| 417 | 459 | Native addons can be bundled as assets into the single-executable application | |
@@ -599,6 +641,7 @@ start a discussion at <https://github.com/nodejs/single-executable/discussions> | |||
| 599 | 641 | to help us document them. | |
| 600 | 642 | ||
| 601 | 643 | [CommonJS]: modules.md#modules-commonjs-modules | |
| 644 | + [ECMAScript Modules]: esm.md#modules-ecmascript-modules | ||
| 602 | 645 | [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format | |
| 603 | 646 | [Generating single executable preparation blobs]: #1-generating-single-executable-preparation-blobs | |
| 604 | 647 | [Mach-O]: https://en.wikipedia.org/wiki/Mach-O | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -84,6 +84,11 @@ size_t SeaSerializer::Write(const SeaResource& sea) { | |||
| 84 | 84 | static_cast<uint8_t>(sea.exec_argv_extension)); | |
| 85 | 85 | written_total += | |
| 86 | 86 | 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)); | ||
| 87 | 92 | DCHECK_EQ(written_total, SeaResource::kHeaderSize); | |
| 88 | 93 | ||
| 89 | 94 | Debug("Write SEA code path %p, size=%zu\n", | |
@@ -161,6 +166,11 @@ SeaResource SeaDeserializer::Read() { | |||
| 161 | 166 | SeaExecArgvExtension exec_argv_extension = | |
| 162 | 167 | static_cast<SeaExecArgvExtension>(extension_value); | |
| 163 | 168 | 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); | ||
| 164 | 174 | CHECK_EQ(read_total, SeaResource::kHeaderSize); | |
| 165 | 175 | ||
| 166 | 176 | std::string_view code_path = | |
@@ -219,6 +229,7 @@ SeaResource SeaDeserializer::Read() { | |||
| 219 | 229 | exec_argv_extension, | |
| 220 | 230 | code_path, | |
| 221 | 231 | code, | |
| 232 | + main_code_format, | ||
| 222 | 233 | code_cache, | |
| 223 | 234 | assets, | |
| 224 | 235 | exec_argv}; | |
@@ -501,6 +512,25 @@ std::optional<SeaConfig> ParseSingleExecutableConfig( | |||
| 501 | 512 | config_path); | |
| 502 | 513 | return std::nullopt; | |
| 503 | 514 | } | |
| 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 | + } | ||
| 504 | 534 | } | |
| 505 | 535 | } | |
| 506 | 536 | ||
@@ -512,6 +542,23 @@ std::optional<SeaConfig> ParseSingleExecutableConfig( | |||
| 512 | 542 | "\"useCodeCache\" is redundant when \"useSnapshot\" is true\n"); | |
| 513 | 543 | } | |
| 514 | 544 | ||
| 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 | + | ||
| 515 | 562 | if (result.main_path.empty()) { | |
| 516 | 563 | FPrintF(stderr, | |
| 517 | 564 | "\"main\" field of %s is not a non-empty string\n", | |
@@ -709,6 +756,7 @@ ExitCode GenerateSingleExecutableBlob( | |||
| 709 | 756 | builds_snapshot_from_main | |
| 710 | 757 | ? std::string_view{snapshot_blob.data(), snapshot_blob.size()} | |
| 711 | 758 | : std::string_view{main_script.data(), main_script.size()}, | |
| 759 | + config.main_format, | ||
| 712 | 760 | optional_sv_code_cache, | |
| 713 | 761 | assets_view, | |
| 714 | 762 | exec_argv_view}; | |
@@ -792,20 +840,25 @@ void GetAssetKeys(const FunctionCallbackInfo<Value>& args) { | |||
| 792 | 840 | } | |
| 793 | 841 | ||
| 794 | 842 | MaybeLocal<Value> LoadSingleExecutableApplication( | |
| 795 | - const StartExecutionCallbackInfo& info) { | ||
| 843 | + const StartExecutionCallbackInfoWithModule& info) { | ||
| 796 | 844 | // Here we are currently relying on the fact that in NodeMainInstance::Run(), | |
| 797 | 845 | // 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(); | ||
| 800 | 848 | SeaResource sea = FindSingleExecutableResource(); | |
| 801 | 849 | ||
| 802 | 850 | CHECK(!sea.use_snapshot()); | |
| 803 | 851 | // TODO(joyeecheung): this should be an external string. Refactor UnionBytes | |
| 804 | 852 | // and make it easy to create one based on static content on the fly. | |
| 805 | 853 | 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); | ||
| 809 | 862 | } | |
| 810 | 863 | ||
| 811 | 864 | bool MaybeLoadSingleExecutableApplication(Environment* env) { | |
@@ -821,7 +874,7 @@ bool MaybeLoadSingleExecutableApplication(Environment* env) { | |||
| 821 | 874 | // this check is just here to guard against the unlikely case where | |
| 822 | 875 | // the SEA preparation blob has been manually modified by someone. | |
| 823 | 876 | CHECK(!env->snapshot_deserialize_main().IsEmpty()); | |
| 824 | - LoadEnvironment(env, StartExecutionCallback{}); | ||
| 877 | + LoadEnvironment(env, StartExecutionCallbackWithModule{}); | ||
| 825 | 878 | return true; | |
| 826 | 879 | } | |
| 827 | 880 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -11,6 +11,7 @@ | |||
| 11 | 11 | #include <unordered_map> | |
| 12 | 12 | #include <vector> | |
| 13 | 13 | ||
| 14 | + #include "node.h" | ||
| 14 | 15 | #include "node_exit_code.h" | |
| 15 | 16 | ||
| 16 | 17 | namespace node { | |
@@ -43,6 +44,7 @@ struct SeaConfig { | |||
| 43 | 44 | std::string executable_path; | |
| 44 | 45 | SeaFlags flags = SeaFlags::kDefault; | |
| 45 | 46 | SeaExecArgvExtension exec_argv_extension = SeaExecArgvExtension::kEnv; | |
| 47 | + ModuleFormat main_format = ModuleFormat::kCommonJS; | ||
| 46 | 48 | std::unordered_map<std::string, std::string> assets; | |
| 47 | 49 | std::vector<std::string> exec_argv; | |
| 48 | 50 | }; | |
@@ -52,15 +54,17 @@ struct SeaResource { | |||
| 52 | 54 | SeaExecArgvExtension exec_argv_extension = SeaExecArgvExtension::kEnv; | |
| 53 | 55 | std::string_view code_path; | |
| 54 | 56 | std::string_view main_code_or_snapshot; | |
| 57 | + ModuleFormat main_code_format = ModuleFormat::kCommonJS; | ||
| 55 | 58 | std::optional<std::string_view> code_cache; | |
| 56 | 59 | std::unordered_map<std::string_view, std::string_view> assets; | |
| 57 | 60 | std::vector<std::string_view> exec_argv; | |
| 58 | 61 | ||
| 59 | 62 | bool use_snapshot() const; | |
| 60 | 63 | bool use_code_cache() const; | |
| 61 | 64 | ||
| 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); | ||
| 64 | 68 | }; | |
| 65 | 69 | ||
| 66 | 70 | bool IsSingleExecutable(); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,6 @@ | |||
| 1 | + { | ||
| 2 | + "main": "sea.mjs", | ||
| 3 | + "output": "sea", | ||
| 4 | + "mainFormat": "module", | ||
| 5 | + "disableExperimentalSEAWarning": true | ||
| 6 | + } | ||
| Original file line number | Diff line number | Diff 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'); | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,33 @@ | |||
| 1 | + 'use strict'; | ||
| 2 | + | ||
| 3 | + require('../common'); | ||
| 4 | + | ||
| 5 | + const { | ||
| 6 | + buildSEA, | ||
| 7 | + skipIfBuildSEAIsNotSupported, | ||
| 8 | + } = require('../common/sea'); | ||
| 9 | + | ||
| 10 | + skipIfBuildSEAIsNotSupported(); | ||
| 11 | + | ||
| 12 | + // This tests the creation of a single executable application with an ESM | ||
| 13 | + // entry point using the "mainFormat": "module" configuration. | ||
| 14 | + | ||
| 15 | + const tmpdir = require('../common/tmpdir'); | ||
| 16 | + const fixtures = require('../common/fixtures'); | ||
| 17 | + const { spawnSyncAndExitWithoutError } = require('../common/child_process'); | ||
| 18 | + | ||
| 19 | + tmpdir.refresh(); | ||
| 20 | + | ||
| 21 | + const outputFile = buildSEA(fixtures.path('sea', 'esm')); | ||
| 22 | + | ||
| 23 | + spawnSyncAndExitWithoutError( | ||
| 24 | + outputFile, | ||
| 25 | + { | ||
| 26 | + env: { | ||
| 27 | + NODE_DEBUG_NATIVE: 'SEA', | ||
| 28 | + ...process.env, | ||
| 29 | + }, | ||
| 30 | + }, | ||
| 31 | + { | ||
| 32 | + stdout: /ESM SEA executed successfully/, | ||
| 33 | + }); | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments