sea: use JSON configuration and blob content for SEA

PR-URL: https://github.com/nodejs/node/pull/47125
Refs: https://github.com/nodejs/single-executable/discussions/58
Reviewed-By: Darshan Sen <raisinten@gmail.com>
This commit is contained in:
Joyee Cheung 2023-04-09 20:31:15 +02:00 committed by GitHub
parent cfb654cc31
commit 491a5c968f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 639 additions and 45 deletions

View File

@ -594,6 +594,18 @@ added: v16.6.0
Use this flag to disable top-level await in REPL.
### `--experimental-sea-config`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
Use this flag to generate a blob that can be injected into the Node.js
binary to produce a [single executable application][]. See the documentation
about [this configuration][`--experimental-sea-config`] for details.
### `--experimental-shadow-realm`
<!-- YAML
@ -2556,6 +2568,7 @@ done
[`"type"`]: packages.md#type
[`--cpu-prof-dir`]: #--cpu-prof-dir
[`--diagnostic-dir`]: #--diagnostic-dirdirectory
[`--experimental-sea-config`]: single-executable-applications.md#generating-single-executable-preparation-blobs
[`--experimental-wasm-modules`]: #--experimental-wasm-modules
[`--heap-prof-dir`]: #--heap-prof-dir
[`--import`]: #--importmodule
@ -2594,6 +2607,7 @@ done
[scavenge garbage collector]: https://v8.dev/blog/orinoco-parallel-scavenger
[security warning]: #warning-binding-inspector-to-a-public-ipport-combination-is-insecure
[semi-space]: https://www.memorymanagement.org/glossary/s.html#semi.space
[single executable application]: single-executable-applications.md
[test reporters]: test.md#test-reporters
[timezone IDs]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
[tracking issue for user-land snapshots]: https://github.com/nodejs/node/issues/44014

View File

@ -10,15 +10,17 @@ This feature allows the distribution of a Node.js application conveniently to a
system that does not have Node.js installed.
Node.js supports the creation of [single executable applications][] by allowing
the injection of a JavaScript file into the `node` binary. During start up, the
program checks if anything has been injected. If the script is found, it
executes its contents. Otherwise Node.js operates as it normally does.
the injection of a blob prepared by Node.js, which can contain a bundled script,
into the `node` binary. During start up, the program checks if anything has been
injected. If the blob is found, it executes the script in the blob. Otherwise
Node.js operates as it normally does.
The single executable application feature only supports running a single
embedded [CommonJS][] file.
The single executable application feature currently only supports running a
single embedded script using the [CommonJS][] module system.
A bundled JavaScript file can be turned into a single executable application
with any tool which can inject resources into the `node` binary.
Users can create a single executable application from their bundled script
with the `node` binary itself and any tool which can inject resources into the
binary.
Here are the steps for creating a single executable application using one such
tool, [postject][]:
@ -28,12 +30,24 @@ tool, [postject][]:
$ echo 'console.log(`Hello, ${process.argv[2]}!`);' > hello.js
```
2. Create a copy of the `node` executable and name it according to your needs:
2. Create a configuration file building a blob that can be injected into the
single executable application (see
[Generating single executable preparation blobs][] for details):
```console
$ echo '{ "main": "hello.js", "output": "sea-prep.blob" }' > sea-config.json
```
3. Generate the blob to be injected:
```console
$ node --experimental-sea-config sea-config.json
```
4. Create a copy of the `node` executable and name it according to your needs:
```console
$ cp $(command -v node) hello
```
3. Remove the signature of the binary:
5. Remove the signature of the binary:
* On macOS:
@ -50,35 +64,35 @@ tool, [postject][]:
$ signtool remove /s hello
```
4. Inject the JavaScript file into the copied binary by running `postject` with
6. Inject the blob into the copied binary by running `postject` with
the following options:
* `hello` - The name of the copy of the `node` executable created in step 2.
* `NODE_JS_CODE` - The name of the resource / note / section in the binary
where the contents of the JavaScript file will be stored.
* `hello.js` - The name of the JavaScript file created in step 1.
* `--sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2` - The
* `NODE_SEA_BLOB` - The name of the resource / note / section in the binary
where the contents of the blob will be stored.
* `sea-prep.blob` - The name of the blob created in step 1.
* `--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2` - The
[fuse][] used by the Node.js project to detect if a file has been injected.
* `--macho-segment-name NODE_JS` (only needed on macOS) - The name of the
segment in the binary where the contents of the JavaScript file will be
* `--macho-segment-name NODE_SEA` (only needed on macOS) - The name of the
segment in the binary where the contents of the blob will be
stored.
To summarize, here is the required command for each platform:
* On systems other than macOS:
```console
$ npx postject hello NODE_JS_CODE hello.js \
--sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2
$ npx postject hello NODE_SEA_BLOB sea-prep.blob \
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
```
* On macOS:
```console
$ npx postject hello NODE_JS_CODE hello.js \
--sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
--macho-segment-name NODE_JS
$ npx postject hello NODE_SEA_BLOB sea-prep.blob \
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
--macho-segment-name NODE_SEA
```
5. Sign the binary:
7. Sign the binary:
* On macOS:
@ -95,12 +109,33 @@ tool, [postject][]:
$ signtool sign /fd SHA256 hello
```
6. Run the binary:
8. Run the binary:
```console
$ ./hello world
Hello, world!
```
## Generating single executable preparation blobs
Single executable preparation blobs that are injected into the application can
be generated using the `--experimental-sea-config` flag of the Node.js binary
that will be used to build the single executable. It takes a path to a
configuration file in JSON format. If the path passed to it isn't absolute,
Node.js will use the path relative to the current working directory.
The configuration currently reads the following top-level fields:
```json
{
"main": "/path/to/bundled/script.js",
"output": "/path/to/write/the/generated/blob.blob"
}
```
If the paths are not absolute, Node.js will use the path relative to the
current working directory. The version of the Node.js binary used to produce
the blob must be the same as the one to which the blob will be injected.
## Notes
### `require(id)` in the injected module is not file based
@ -135,15 +170,16 @@ of [`process.execPath`][].
### Single executable application creation process
A tool aiming to create a single executable Node.js application must
inject the contents of a JavaScript file into:
inject the contents of the blob prepared with `--experimental-sea-config"`
into:
* a resource named `NODE_JS_CODE` if the `node` binary is a [PE][] file
* a section named `NODE_JS_CODE` in the `NODE_JS` segment if the `node` binary
* a resource named `NODE_SEA_BLOB` if the `node` binary is a [PE][] file
* a section named `NODE_SEA_BLOB` in the `NODE_SEA` segment if the `node` binary
is a [Mach-O][] file
* a note named `NODE_JS_CODE` if the `node` binary is an [ELF][] file
* a note named `NODE_SEA_BLOB` if the `node` binary is an [ELF][] file
Search the binary for the
`NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0` [fuse][] string and flip the
`NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2:0` [fuse][] string and flip the
last character to `1` to indicate that a resource has been injected.
### Platform support
@ -165,6 +201,7 @@ to help us document them.
[CommonJS]: modules.md#modules-commonjs-modules
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
[Generating single executable preparation blobs]: #generating-single-executable-preparation-blobs
[Mach-O]: https://en.wikipedia.org/wiki/Mach-O
[PE]: https://en.wikipedia.org/wiki/Portable_Executable
[Windows SDK]: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/

View File

@ -66,7 +66,7 @@ To disable single executable application support, build Node.js with the
## Implementation
When built with single executable application support, the Node.js process uses
[`postject-api.h`][] to check if the `NODE_JS_CODE` section exists in the
[`postject-api.h`][] to check if the `NODE_SEA_BLOB` section exists in the
binary. If it is found, it passes the buffer to
[`single_executable_application.js`][], which executes the contents of the
embedded script.

View File

@ -82,6 +82,8 @@
'src/js_stream.cc',
'src/json_utils.cc',
'src/js_udp_wrap.cc',
'src/json_parser.h',
'src/json_parser.cc',
'src/module_wrap.cc',
'src/node.cc',
'src/node_api.cc',

80
src/json_parser.cc Normal file
View File

@ -0,0 +1,80 @@
#include "json_parser.h"
#include "node_errors.h"
#include "node_v8_platform-inl.h"
#include "util-inl.h"
namespace node {
using v8::ArrayBuffer;
using v8::Context;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;
static Isolate* NewIsolate(v8::ArrayBuffer::Allocator* allocator) {
Isolate* isolate = Isolate::Allocate();
CHECK_NOT_NULL(isolate);
per_process::v8_platform.Platform()->RegisterIsolate(isolate,
uv_default_loop());
Isolate::CreateParams params;
params.array_buffer_allocator = allocator;
Isolate::Initialize(isolate, params);
return isolate;
}
void JSONParser::FreeIsolate(Isolate* isolate) {
per_process::v8_platform.Platform()->UnregisterIsolate(isolate);
isolate->Dispose();
}
JSONParser::JSONParser()
: allocator_(ArrayBuffer::Allocator::NewDefaultAllocator()),
isolate_(NewIsolate(allocator_.get())),
handle_scope_(isolate_.get()),
context_(isolate_.get(), Context::New(isolate_.get())),
context_scope_(context_.Get(isolate_.get())) {}
bool JSONParser::Parse(const std::string& content) {
DCHECK(!parsed_);
Isolate* isolate = isolate_.get();
Local<Context> context = context_.Get(isolate);
// It's not a real script, so don't print the source line.
errors::PrinterTryCatch bootstrapCatch(
isolate, errors::PrinterTryCatch::kDontPrintSourceLine);
Local<Value> json_string_value;
Local<Value> result_value;
if (!ToV8Value(context, content).ToLocal(&json_string_value) ||
!json_string_value->IsString() ||
!v8::JSON::Parse(context, json_string_value.As<String>())
.ToLocal(&result_value) ||
!result_value->IsObject()) {
return false;
}
content_.Reset(isolate, result_value.As<Object>());
parsed_ = true;
return true;
}
std::optional<std::string> JSONParser::GetTopLevelField(
const std::string& field) {
Isolate* isolate = isolate_.get();
Local<Context> context = context_.Get(isolate);
Local<Object> content_object = content_.Get(isolate);
Local<Value> value;
// It's not a real script, so don't print the source line.
errors::PrinterTryCatch bootstrapCatch(
isolate, errors::PrinterTryCatch::kDontPrintSourceLine);
if (!content_object
->Get(context, OneByteString(isolate, field.c_str(), field.length()))
.ToLocal(&value) ||
!value->IsString()) {
return {};
}
Utf8Value utf8_value(isolate, value);
return utf8_value.ToString();
}
} // namespace node

39
src/json_parser.h Normal file
View File

@ -0,0 +1,39 @@
#ifndef SRC_JSON_PARSER_H_
#define SRC_JSON_PARSER_H_
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include <memory>
#include <optional>
#include <string>
#include "util.h"
#include "v8.h"
namespace node {
// This is intended to be used to get some top-level fields out of a JSON
// without having to spin up a full Node.js environment that unnecessarily
// complicates things.
class JSONParser {
public:
JSONParser();
~JSONParser() {}
bool Parse(const std::string& content);
std::optional<std::string> GetTopLevelField(const std::string& field);
private:
// We might want a lighter-weight JSON parser for this use case. But for now
// using V8 is good enough.
static void FreeIsolate(v8::Isolate* isolate);
std::unique_ptr<v8::ArrayBuffer::Allocator> allocator_;
DeleteFnPtr<v8::Isolate, FreeIsolate> isolate_;
v8::HandleScope handle_scope_;
v8::Global<v8::Context> context_;
v8::Context::Scope context_scope_;
v8::Global<v8::Object> content_;
bool parsed_ = false;
};
} // namespace node
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#endif // SRC_JSON_PARSER_H_

View File

@ -1239,6 +1239,11 @@ static ExitCode StartInternal(int argc, char** argv) {
uv_loop_configure(uv_default_loop(), UV_METRICS_IDLE_TIME);
std::string sea_config = per_process::cli_options->experimental_sea_config;
if (!sea_config.empty()) {
return sea::BuildSingleExecutableBlob(sea_config);
}
// --build-snapshot indicates that we are in snapshot building mode.
if (per_process::cli_options->per_isolate->build_snapshot) {
if (result->args().size() < 2) {

View File

@ -246,14 +246,19 @@ void PrintStackTrace(Isolate* isolate, Local<StackTrace> stack) {
std::string FormatCaughtException(Isolate* isolate,
Local<Context> context,
Local<Value> err,
Local<Message> message) {
Local<Message> message,
bool add_source_line = true) {
std::string result;
node::Utf8Value reason(isolate,
err->ToDetailString(context)
.FromMaybe(Local<String>()));
bool added_exception_line = false;
std::string source =
GetErrorSource(isolate, context, message, &added_exception_line);
std::string result = source + '\n' + reason.ToString() + '\n';
if (add_source_line) {
bool added_exception_line = false;
std::string source =
GetErrorSource(isolate, context, message, &added_exception_line);
result = source + '\n';
}
result += reason.ToString() + '\n';
Local<v8::StackTrace> stack = message->GetStackTrace();
if (!stack.IsEmpty()) result += FormatStackTrace(isolate, stack);
@ -1209,6 +1214,19 @@ void TriggerUncaughtException(Isolate* isolate, const v8::TryCatch& try_catch) {
false /* from_promise */);
}
PrinterTryCatch::~PrinterTryCatch() {
if (!HasCaught()) {
return;
}
std::string str =
FormatCaughtException(isolate_,
isolate_->GetCurrentContext(),
Exception(),
Message(),
print_source_line_ == kPrintSourceLine);
PrintToStderrAndFlush(str);
}
} // namespace errors
} // namespace node

View File

@ -275,6 +275,22 @@ void PerIsolateMessageListener(v8::Local<v8::Message> message,
void DecorateErrorStack(Environment* env,
const errors::TryCatchScope& try_catch);
class PrinterTryCatch : public v8::TryCatch {
public:
enum PrintSourceLine { kPrintSourceLine, kDontPrintSourceLine };
explicit PrinterTryCatch(v8::Isolate* isolate,
PrintSourceLine print_source_line)
: v8::TryCatch(isolate),
isolate_(isolate),
print_source_line_(print_source_line) {}
~PrinterTryCatch();
private:
v8::Isolate* isolate_;
PrintSourceLine print_source_line_;
};
} // namespace errors
v8::ModifyCodeGenerationFromStringsResult ModifyCodeGenerationFromStrings(

View File

@ -991,6 +991,11 @@ PerProcessOptionsParser::PerProcessOptionsParser(
kAllowedInEnvvar);
Implies("--node-memory-debug", "--debug-arraybuffer-allocations");
Implies("--node-memory-debug", "--verify-base-objects");
AddOption("--experimental-sea-config",
"Generate a blob that can be embedded into the single executable "
"application",
&PerProcessOptions::experimental_sea_config);
}
inline std::string RemoveBrackets(const std::string& host) {

View File

@ -264,6 +264,7 @@ class PerProcessOptions : public Options {
bool print_help = false;
bool print_v8_help = false;
bool print_version = false;
std::string experimental_sea_config;
#ifdef NODE_HAVE_I18N_SUPPORT
std::string icu_data_dir;

View File

@ -1,6 +1,8 @@
#include "node_sea.h"
#include "debug_utils-inl.h"
#include "env-inl.h"
#include "json_parser.h"
#include "node_external_reference.h"
#include "node_internals.h"
#include "node_union_bytes.h"
@ -10,7 +12,7 @@
// used by the postject_has_resource() function to efficiently detect if a
// resource has been injected. See
// https://github.com/nodejs/postject/blob/35343439cac8c488f2596d7c4c1dddfec1fddcae/postject-api.h#L42-L45.
#define POSTJECT_SENTINEL_FUSE "NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2"
#define POSTJECT_SENTINEL_FUSE "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2"
#include "postject-api.h"
#undef POSTJECT_SENTINEL_FUSE
@ -21,6 +23,7 @@
#if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION)
using node::ExitCode;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Local;
@ -30,6 +33,12 @@ using v8::Value;
namespace node {
namespace sea {
// A special number that will appear at the beginning of the single executable
// preparation blobs ready to be injected into the binary. We use this to check
// that the data given to us are intended for building single executable
// applications.
static const uint32_t kMagic = 0x143da20;
std::string_view FindSingleExecutableCode() {
CHECK(IsSingleExecutable());
static const std::string_view sea_code = []() -> std::string_view {
@ -37,14 +46,17 @@ std::string_view FindSingleExecutableCode() {
#ifdef __APPLE__
postject_options options;
postject_options_init(&options);
options.macho_segment_name = "NODE_JS";
options.macho_segment_name = "NODE_SEA";
const char* code = static_cast<const char*>(
postject_find_resource("NODE_JS_CODE", &size, &options));
postject_find_resource("NODE_SEA_BLOB", &size, &options));
#else
const char* code = static_cast<const char*>(
postject_find_resource("NODE_JS_CODE", &size, nullptr));
postject_find_resource("NODE_SEA_BLOB", &size, nullptr));
#endif
return {code, size};
uint32_t first_word = reinterpret_cast<const uint32_t*>(code)[0];
CHECK_EQ(first_word, kMagic);
// TODO(joyeecheung): do more checks here e.g. matching the versions.
return {code + sizeof(first_word), size - sizeof(first_word)};
}();
return sea_code;
}
@ -73,6 +85,98 @@ std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
return {argc, argv};
}
namespace {
struct SeaConfig {
std::string main_path;
std::string output_path;
};
std::optional<SeaConfig> ParseSingleExecutableConfig(
const std::string& config_path) {
std::string config;
int r = ReadFileSync(&config, config_path.c_str());
if (r != 0) {
const char* err = uv_strerror(r);
FPrintF(stderr,
"Cannot read single executable configuration from %s: %s\n",
config_path,
err);
return std::nullopt;
}
SeaConfig result;
JSONParser parser;
if (!parser.Parse(config)) {
FPrintF(stderr, "Cannot parse JSON from %s\n", config_path);
return std::nullopt;
}
result.main_path = parser.GetTopLevelField("main").value_or(std::string());
if (result.main_path.empty()) {
FPrintF(stderr,
"\"main\" field of %s is not a non-empty string\n",
config_path);
return std::nullopt;
}
result.output_path =
parser.GetTopLevelField("output").value_or(std::string());
if (result.output_path.empty()) {
FPrintF(stderr,
"\"output\" field of %s is not a non-empty string\n",
config_path);
return std::nullopt;
}
return result;
}
bool GenerateSingleExecutableBlob(const SeaConfig& config) {
std::string main_script;
// TODO(joyeecheung): unify the file utils.
int r = ReadFileSync(&main_script, config.main_path.c_str());
if (r != 0) {
const char* err = uv_strerror(r);
FPrintF(stderr, "Cannot read main script %s:%s\n", config.main_path, err);
return false;
}
std::vector<char> sink;
// TODO(joyeecheung): reuse the SnapshotSerializerDeserializer for this.
sink.reserve(sizeof(kMagic) + main_script.size());
const char* pos = reinterpret_cast<const char*>(&kMagic);
sink.insert(sink.end(), pos, pos + sizeof(kMagic));
sink.insert(
sink.end(), main_script.data(), main_script.data() + main_script.size());
uv_buf_t buf = uv_buf_init(sink.data(), sink.size());
r = WriteFileSync(config.output_path.c_str(), buf);
if (r != 0) {
const char* err = uv_strerror(r);
FPrintF(stderr, "Cannot write output to %s:%s\n", config.output_path, err);
return false;
}
FPrintF(stderr,
"Wrote single executable preparation blob to %s\n",
config.output_path);
return true;
}
} // anonymous namespace
ExitCode BuildSingleExecutableBlob(const std::string& config_path) {
std::optional<SeaConfig> config_opt =
ParseSingleExecutableConfig(config_path);
if (!config_opt.has_value() ||
!GenerateSingleExecutableBlob(config_opt.value())) {
return ExitCode::kGenericUserError;
}
return ExitCode::kNoFailure;
}
void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,

View File

@ -7,6 +7,7 @@
#include <string_view>
#include <tuple>
#include "node_exit_code.h"
namespace node {
namespace sea {
@ -14,7 +15,7 @@ namespace sea {
bool IsSingleExecutable();
std::string_view FindSingleExecutableCode();
std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv);
node::ExitCode BuildSingleExecutableBlob(const std::string& config_path);
} // namespace sea
} // namespace node

View File

@ -5,10 +5,11 @@ const common = require('../common');
const fixtures = require('../common/fixtures');
const tmpdir = require('../common/tmpdir');
const { copyFileSync, readFileSync, writeFileSync } = require('fs');
const { copyFileSync, readFileSync, writeFileSync, existsSync } = require('fs');
const { execFileSync } = require('child_process');
const { join } = require('path');
const { strictEqual } = require('assert');
const assert = require('assert');
if (!process.config.variables.single_executable_application)
common.skip('Single Executable Application support has been disabled.');
@ -51,6 +52,8 @@ if (process.platform === 'linux') {
const inputFile = fixtures.path('sea.js');
const requirableFile = join(tmpdir.path, 'requirable.js');
const configFile = join(tmpdir.path, 'sea-config.json');
const seaPrepBlob = join(tmpdir.path, 'sea-prep.blob');
const outputFile = join(tmpdir.path, process.platform === 'win32' ? 'sea.exe' : 'sea');
tmpdir.refresh();
@ -61,15 +64,30 @@ module.exports = {
};
`);
writeFileSync(configFile, `
{
"main": "sea.js",
"output": "sea-prep.blob"
}
`);
// Copy input to working directory
copyFileSync(inputFile, join(tmpdir.path, 'sea.js'));
execFileSync(process.execPath, ['--experimental-sea-config', 'sea-config.json'], {
cwd: tmpdir.path
});
assert(existsSync(seaPrepBlob));
copyFileSync(process.execPath, outputFile);
const postjectFile = fixtures.path('postject-copy', 'node_modules', 'postject', 'dist', 'cli.js');
execFileSync(process.execPath, [
postjectFile,
outputFile,
'NODE_JS_CODE',
inputFile,
'--sentinel-fuse', 'NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2',
...process.platform === 'darwin' ? [ '--macho-segment-name', 'NODE_JS' ] : [],
'NODE_SEA_BLOB',
seaPrepBlob,
'--sentinel-fuse', 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2',
...process.platform === 'darwin' ? [ '--macho-segment-name', 'NODE_SEA' ] : [],
]);
if (process.platform === 'darwin') {

View File

@ -0,0 +1,204 @@
// This tests invalid options for --experimental-sea-config.
'use strict';
require('../common');
const tmpdir = require('../common/tmpdir');
const { writeFileSync, mkdirSync } = require('fs');
const { spawnSync } = require('child_process');
const assert = require('assert');
const { join } = require('path');
{
tmpdir.refresh();
const config = 'non-existent-relative.json';
const child = spawnSync(
process.execPath,
['--experimental-sea-config', config], {
cwd: tmpdir.path,
});
const stderr = child.stderr.toString();
assert.strictEqual(child.status, 1);
assert.match(
stderr,
/Cannot read single executable configuration from non-existent-relative\.json/
);
}
{
tmpdir.refresh();
const config = join(tmpdir.path, 'non-existent-absolute.json');
const child = spawnSync(
process.execPath,
['--experimental-sea-config', config], {
cwd: tmpdir.path,
});
const stderr = child.stderr.toString();
assert.strictEqual(child.status, 1);
assert(
stderr.includes(
`Cannot read single executable configuration from ${config}`
)
);
}
{
tmpdir.refresh();
const config = join(tmpdir.path, 'invalid.json');
writeFileSync(config, '\n{\n"main"', 'utf8');
const child = spawnSync(
process.execPath,
['--experimental-sea-config', config], {
cwd: tmpdir.path,
});
const stderr = child.stderr.toString();
assert.strictEqual(child.status, 1);
assert.match(stderr, /SyntaxError: Expected ':' after property name/);
assert(
stderr.includes(
`Cannot parse JSON from ${config}`
)
);
}
{
tmpdir.refresh();
const config = join(tmpdir.path, 'empty.json');
writeFileSync(config, '{}', 'utf8');
const child = spawnSync(
process.execPath,
['--experimental-sea-config', config], {
cwd: tmpdir.path,
});
const stderr = child.stderr.toString();
assert.strictEqual(child.status, 1);
assert(
stderr.includes(
`"main" field of ${config} is not a non-empty string`
)
);
}
{
tmpdir.refresh();
const config = join(tmpdir.path, 'no-main.json');
writeFileSync(config, '{"output": "test.blob"}', 'utf8');
const child = spawnSync(
process.execPath,
['--experimental-sea-config', config], {
cwd: tmpdir.path,
});
const stderr = child.stderr.toString();
assert.strictEqual(child.status, 1);
assert(
stderr.includes(
`"main" field of ${config} is not a non-empty string`
)
);
}
{
tmpdir.refresh();
const config = join(tmpdir.path, 'no-output.json');
writeFileSync(config, '{"main": "bundle.js"}', 'utf8');
const child = spawnSync(
process.execPath,
['--experimental-sea-config', config], {
cwd: tmpdir.path,
});
const stderr = child.stderr.toString();
assert.strictEqual(child.status, 1);
assert(
stderr.includes(
`"output" field of ${config} is not a non-empty string`
)
);
}
{
tmpdir.refresh();
const config = join(tmpdir.path, 'nonexistent-main-relative.json');
writeFileSync(config, '{"main": "bundle.js", "output": "sea.blob"}', 'utf8');
const child = spawnSync(
process.execPath,
['--experimental-sea-config', config], {
cwd: tmpdir.path,
});
const stderr = child.stderr.toString();
assert.strictEqual(child.status, 1);
assert.match(stderr, /Cannot read main script bundle\.js/);
}
{
tmpdir.refresh();
const config = join(tmpdir.path, 'nonexistent-main-absolute.json');
const main = join(tmpdir.path, 'bundle.js');
const configJson = JSON.stringify({
main,
output: 'sea.blob'
});
writeFileSync(config, configJson, 'utf8');
const child = spawnSync(
process.execPath,
['--experimental-sea-config', config], {
cwd: tmpdir.path,
});
const stderr = child.stderr.toString();
assert.strictEqual(child.status, 1);
assert(
stderr.includes(
`Cannot read main script ${main}`
)
);
}
{
tmpdir.refresh();
const config = join(tmpdir.path, 'output-is-dir-absolute.json');
const main = join(tmpdir.path, 'bundle.js');
const output = join(tmpdir.path, 'output-dir');
mkdirSync(output);
writeFileSync(main, 'console.log("hello")', 'utf-8');
const configJson = JSON.stringify({
main,
output,
});
writeFileSync(config, configJson, 'utf8');
const child = spawnSync(
process.execPath,
['--experimental-sea-config', config], {
cwd: tmpdir.path,
});
const stderr = child.stderr.toString();
assert.strictEqual(child.status, 1);
assert(
stderr.includes(
`Cannot write output to ${output}`
)
);
}
{
tmpdir.refresh();
const config = join(tmpdir.path, 'output-is-dir-relative.json');
const main = join(tmpdir.path, 'bundle.js');
const output = join(tmpdir.path, 'output-dir');
mkdirSync(output);
writeFileSync(main, 'console.log("hello")', 'utf-8');
const configJson = JSON.stringify({
main,
output: 'output-dir'
});
writeFileSync(config, configJson, 'utf8');
const child = spawnSync(
process.execPath,
['--experimental-sea-config', config], {
cwd: tmpdir.path,
});
const stderr = child.stderr.toString();
assert.strictEqual(child.status, 1);
assert.match(stderr, /Cannot write output to output-dir/);
}

View File

@ -0,0 +1,50 @@
// This tests valid options for --experimental-sea-config.
'use strict';
require('../common');
const tmpdir = require('../common/tmpdir');
const { writeFileSync, existsSync } = require('fs');
const { spawnSync } = require('child_process');
const assert = require('assert');
const { join } = require('path');
{
tmpdir.refresh();
const config = join(tmpdir.path, 'absolute.json');
const main = join(tmpdir.path, 'bundle.js');
const output = join(tmpdir.path, 'output.blob');
writeFileSync(main, 'console.log("hello")', 'utf-8');
const configJson = JSON.stringify({
main,
output,
});
writeFileSync(config, configJson, 'utf8');
const child = spawnSync(
process.execPath,
['--experimental-sea-config', config], {
cwd: tmpdir.path,
});
assert.strictEqual(child.status, 0);
assert(existsSync(output));
}
{
tmpdir.refresh();
const config = join(tmpdir.path, 'relative.json');
const main = join(tmpdir.path, 'bundle.js');
const output = join(tmpdir.path, 'output.blob');
writeFileSync(main, 'console.log("hello")', 'utf-8');
const configJson = JSON.stringify({
main: 'bundle.js',
output: 'output.blob'
});
writeFileSync(config, configJson, 'utf8');
const child = spawnSync(
process.execPath,
['--experimental-sea-config', config], {
cwd: tmpdir.path,
});
assert.strictEqual(child.status, 0);
assert(existsSync(output));
}