Skip to content

Commit

Permalink
sea: support embedding assets
Browse files Browse the repository at this point in the history
With this patch:

Users can now include assets by adding a key-path dictionary
to the configuration as the `assets` field. At build time, Node.js
would read the assets from the specified paths and bundle them into
the preparation blob. In the generated executable, users can retrieve
the assets using the `sea.getAsset()` and `sea.getAssetAsBlob()` API.

```json
{
  "main": "/path/to/bundled/script.js",
  "output": "/path/to/write/the/generated/blob.blob",
  "assets": {
    "a.jpg": "/path/to/a.jpg",
    "b.txt": "/path/to/b.txt"
  }
}
```

The single-executable application can access the assets as follows:

```cjs
const { getAsset } = require('node:sea');
// Returns a copy of the data in an ArrayBuffer
const image = getAsset('a.jpg');
// Returns a string decoded from the asset as UTF8.
const text = getAsset('b.txt', 'utf8');
// Returns a Blob containing the asset.
const blob = getAssetAsBlob('a.jpg');
```

Drive-by: update the  documentation to include a section dedicated
to the injected main script and refer to it as "injected main
script" instead of "injected module" because it's a script, not
a module.

PR-URL: #50960
Refs: nodejs/single-executable#68
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
joyeecheung authored and richardlau committed Mar 25, 2024
1 parent a58c98e commit db0efa3
Showing 12 changed files with 578 additions and 12 deletions.
22 changes: 22 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
@@ -2401,6 +2401,17 @@ error indicates that the idle loop has failed to stop.
An attempt was made to use operations that can only be used when building
V8 startup snapshot even though Node.js isn't building one.

<a id="ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION"></a>

### `ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION`

<!-- YAML
added: REPLACEME
-->

The operation cannot be performed when it's not in a single-executable
application.

<a id="ERR_NOT_SUPPORTED_IN_SNAPSHOT"></a>

### `ERR_NOT_SUPPORTED_IN_SNAPSHOT`
@@ -2547,6 +2558,17 @@ The [`server.close()`][] method was called when a `net.Server` was not
running. This applies to all instances of `net.Server`, including HTTP, HTTPS,
and HTTP/2 `Server` instances.

<a id="ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND"></a>

### `ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND`

<!-- YAML
added: REPLACEME
-->

A key was passed to single executable application APIs to identify an asset,
but no match could be found.

<a id="ERR_SOCKET_ALREADY_BOUND"></a>

### `ERR_SOCKET_ALREADY_BOUND`
110 changes: 100 additions & 10 deletions doc/api/single-executable-applications.md
Original file line number Diff line number Diff line change
@@ -178,14 +178,52 @@ The configuration currently reads the following top-level fields:
"output": "/path/to/write/the/generated/blob.blob",
"disableExperimentalSEAWarning": true, // Default: false
"useSnapshot": false, // Default: false
"useCodeCache": true // Default: false
"useCodeCache": true, // Default: false
"assets": { // Optional
"a.dat": "/path/to/a.dat",
"b.txt": "/path/to/b.txt"
}
}
```
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.
### Assets
Users can include assets by adding a key-path dictionary to the configuration
as the `assets` field. At build time, Node.js would read the assets from the
specified paths and bundle them into the preparation blob. In the generated
executable, users can retrieve the assets using the [`sea.getAsset()`][] and
[`sea.getAssetAsBlob()`][] APIs.
```json
{
"main": "/path/to/bundled/script.js",
"output": "/path/to/write/the/generated/blob.blob",
"assets": {
"a.jpg": "/path/to/a.jpg",
"b.txt": "/path/to/b.txt"
}
}
```
The single-executable application can access the assets as follows:
```cjs
const { getAsset } = require('node:sea');
// Returns a copy of the data in an ArrayBuffer.
const image = getAsset('a.jpg');
// Returns a string decoded from the asset as UTF8.
const text = getAsset('b.txt', 'utf8');
// Returns a Blob containing the asset.
const blob = getAssetAsBlob('a.jpg');
```
See documentation of the [`sea.getAsset()`][] and [`sea.getAssetAsBlob()`][]
APIs for more information.
### Startup snapshot support
The `useSnapshot` field can be used to enable startup snapshot support. In this
@@ -229,11 +267,58 @@ execute the script, which would improve the startup performance.
**Note:** `import()` does not work when `useCodeCache` is `true`.
## Notes
## In the injected main script
### `require(id)` in the injected module is not file based
### Single-executable application API
`require()` in the injected module is not the same as the [`require()`][]
The `node:sea` builtin allows interaction with the single-executable application
from the JavaScript main script embedded into the executable.
#### `sea.isSea()`
<!-- YAML
added: REPLACEME
-->
* Returns: {boolean} Whether this script is running inside a single-executable
application.
### `sea.getAsset(key[, encoding])`
<!-- YAML
added: REPLACEME
-->
This method can be used to retrieve the assets configured to be bundled into the
single-executable application at build time.
An error is thrown when no matching asset can be found.
* `key` {string} the key for the asset in the dictionary specified by the
`assets` field in the single-executable application configuration.
* `encoding` {string} If specified, the asset will be decoded as
a string. Any encoding supported by the `TextDecoder` is accepted.
If unspecified, an `ArrayBuffer` containing a copy of the asset would be
returned instead.
* Returns: {string|ArrayBuffer}
### `sea.getAssetAsBlob(key[, options])`
<!-- YAML
added: REPLACEME
-->
Similar to [`sea.getAsset()`][], but returns the result in a [`Blob`][].
An error is thrown when no matching asset can be found.
* `key` {string} the key for the asset in the dictionary specified by the
`assets` field in the single-executable application configuration.
* `options` {Object}
* `type` {string} An optional mime type for the blob.
* Returns: {Blob}
### `require(id)` in the injected main script is not file based
`require()` in the injected main script is not the same as the [`require()`][]
available to modules that are not injected. It also does not have any of the
properties that non-injected [`require()`][] has except [`require.main`][]. It
can only be used to load built-in modules. Attempting to load a module that can
@@ -250,15 +335,17 @@ const { createRequire } = require('node:module');
require = createRequire(__filename);
```
### `__filename` and `module.filename` in the injected module
### `__filename` and `module.filename` in the injected main script
The values of `__filename` and `module.filename` in the injected module are
equal to [`process.execPath`][].
The values of `__filename` and `module.filename` in the injected main script
are equal to [`process.execPath`][].
### `__dirname` in the injected module
### `__dirname` in the injected main script
The value of `__dirname` in the injected module is equal to the directory name
of [`process.execPath`][].
The value of `__dirname` in the injected main script is equal to the directory
name of [`process.execPath`][].
## Notes
### Single executable application creation process
@@ -298,9 +385,12 @@ to help us document them.
[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/
[`Blob`]: https://developer.mozilla.org/en-US/docs/Web/API/Blob
[`process.execPath`]: process.md#processexecpath
[`require()`]: modules.md#requireid
[`require.main`]: modules.md#accessing-the-main-module
[`sea.getAsset()`]: #seagetassetkey-encoding
[`sea.getAssetAsBlob()`]: #seagetassetasblobkey-options
[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data
[`v8.startupSnapshot` API]: v8.md#startup-snapshot-api
[documentation about startup snapshot support in Node.js]: cli.md#--build-snapshot
1 change: 1 addition & 0 deletions lib/internal/bootstrap/realm.js
Original file line number Diff line number Diff line change
@@ -128,6 +128,7 @@ const legacyWrapperList = new SafeSet([
// beginning with "internal/".
// Modules that can only be imported via the node: scheme.
const schemelessBlockList = new SafeSet([
'sea',
'test',
'test/reporters',
]);
4 changes: 4 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
@@ -1637,6 +1637,8 @@ E('ERR_NETWORK_IMPORT_DISALLOWED',
"import of '%s' by %s is not supported: %s", Error);
E('ERR_NOT_BUILDING_SNAPSHOT',
'Operation cannot be invoked when not building startup snapshot', Error);
E('ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION',
'Operation cannot be invoked when not in a single-executable application', Error);
E('ERR_NOT_SUPPORTED_IN_SNAPSHOT', '%s is not supported in startup snapshot', Error);
E('ERR_NO_CRYPTO',
'Node.js is not compiled with OpenSSL crypto support', Error);
@@ -1720,6 +1722,8 @@ E('ERR_SCRIPT_EXECUTION_INTERRUPTED',
E('ERR_SERVER_ALREADY_LISTEN',
'Listen method has been called more than once without closing.', Error);
E('ERR_SERVER_NOT_RUNNING', 'Server is not running.', Error);
E('ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND',
'Cannot find asset %s for the single executable application', Error);
E('ERR_SOCKET_ALREADY_BOUND', 'Socket is already bound', Error);
E('ERR_SOCKET_BAD_BUFFER_SIZE',
'Buffer size must be a positive integer', TypeError);
75 changes: 75 additions & 0 deletions lib/sea.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use strict';
const {
ArrayBufferPrototypeSlice,
} = primordials;

const { isSea, getAsset: getAssetInternal } = internalBinding('sea');
const { TextDecoder } = require('internal/encoding');
const { validateString } = require('internal/validators');
const {
ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION,
ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND,
} = require('internal/errors').codes;
const { Blob } = require('internal/blob');

/**
* Look for the asset in the injected SEA blob using the key. If
* no matching asset is found an error is thrown. The returned
* ArrayBuffer should not be mutated or otherwise the process
* can crash due to access violation.
* @param {string} key
* @returns {ArrayBuffer}
*/
function getRawAsset(key) {
validateString(key, 'key');

if (!isSea()) {
throw new ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION();
}

const asset = getAssetInternal(key);
if (asset === undefined) {
throw new ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND(key);
}
return asset;
}

/**
* Look for the asset in the injected SEA blob using the key. If the
* encoding is specified, return a string decoded from it by TextDecoder,
* otherwise return *a copy* of the original data in an ArrayBuffer. If
* no matching asset is found an error is thrown.
* @param {string} key
* @param {string|undefined} encoding
* @returns {string|ArrayBuffer}
*/
function getAsset(key, encoding) {
if (encoding !== undefined) {
validateString(encoding, 'encoding');
}
const asset = getRawAsset(key);
if (encoding === undefined) {
return ArrayBufferPrototypeSlice(asset);
}
const decoder = new TextDecoder(encoding);
return decoder.decode(asset);
}

/**
* Look for the asset in the injected SEA blob using the key. If
* no matching asset is found an error is thrown. The data is returned
* in a Blob. If no matching asset is found an error is thrown.
* @param {string} key
* @param {ConstructorParameters<Blob>[1]} [options]
* @returns {Blob}
*/
function getAssetAsBlob(key, options) {
const asset = getRawAsset(key);
return new Blob([asset], options);
}

module.exports = {
isSea,
getAsset,
getAssetAsBlob,
};
48 changes: 48 additions & 0 deletions src/json_parser.cc
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
#include "util-inl.h"

namespace node {
using v8::Array;
using v8::Context;
using v8::Isolate;
using v8::Local;
@@ -101,4 +102,51 @@ std::optional<bool> JSONParser::GetTopLevelBoolField(std::string_view field) {
return value->BooleanValue(isolate);
}

std::optional<JSONParser::StringDict> JSONParser::GetTopLevelStringDict(
std::string_view field) {
Isolate* isolate = isolate_.get();
v8::HandleScope handle_scope(isolate);
Local<Context> context = context_.Get(isolate);
Local<Object> content_object = content_.Get(isolate);
Local<Value> value;
bool has_field;
// It's not a real script, so don't print the source line.
errors::PrinterTryCatch bootstrapCatch(
isolate, errors::PrinterTryCatch::kDontPrintSourceLine);
Local<Value> field_local;
if (!ToV8Value(context, field, isolate).ToLocal(&field_local)) {
return std::nullopt;
}
if (!content_object->Has(context, field_local).To(&has_field)) {
return std::nullopt;
}
if (!has_field) {
return StringDict();
}
if (!content_object->Get(context, field_local).ToLocal(&value) ||
!value->IsObject()) {
return std::nullopt;
}
Local<Object> dict = value.As<Object>();
Local<Array> keys;
if (!dict->GetOwnPropertyNames(context).ToLocal(&keys)) {
return std::nullopt;
}
std::unordered_map<std::string, std::string> result;
uint32_t length = keys->Length();
for (uint32_t i = 0; i < length; ++i) {
Local<Value> key;
Local<Value> value;
if (!keys->Get(context, i).ToLocal(&key) || !key->IsString())
return StringDict();
if (!dict->Get(context, key).ToLocal(&value) || !value->IsString())
return StringDict();

Utf8Value key_utf8(isolate, key);
Utf8Value value_utf8(isolate, value);
result.emplace(*key_utf8, *value_utf8);
}
return result;
}

} // namespace node
3 changes: 3 additions & 0 deletions src/json_parser.h
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include "util.h"
#include "v8.h"

@@ -15,11 +16,13 @@ namespace node {
// complicates things.
class JSONParser {
public:
using StringDict = std::unordered_map<std::string, std::string>;
JSONParser();
~JSONParser() = default;
bool Parse(const std::string& content);
std::optional<std::string> GetTopLevelStringField(std::string_view field);
std::optional<bool> GetTopLevelBoolField(std::string_view field);
std::optional<StringDict> GetTopLevelStringDict(std::string_view field);

private:
// We might want a lighter-weight JSON parser for this use case. But for now
93 changes: 91 additions & 2 deletions src/node_sea.cc
Original file line number Diff line number Diff line change
@@ -110,6 +110,19 @@ size_t SeaSerializer::Write(const SeaResource& sea) {
written_total +=
WriteStringView(sea.code_cache.value(), StringLogMode::kAddressOnly);
}

if (!sea.assets.empty()) {
Debug("Write SEA resource assets size %zu\n", sea.assets.size());
written_total += WriteArithmetic<size_t>(sea.assets.size());
for (auto const& [key, content] : sea.assets) {
Debug("Write SEA resource asset %s at %p, size=%zu\n",
key,
content.data(),
content.size());
written_total += WriteStringView(key, StringLogMode::kAddressAndContent);
written_total += WriteStringView(content, StringLogMode::kAddressOnly);
}
}
return written_total;
}

@@ -157,7 +170,22 @@ SeaResource SeaDeserializer::Read() {
code_cache.data(),
code_cache.size());
}
return {flags, code_path, code, code_cache};

std::unordered_map<std::string_view, std::string_view> assets;
if (static_cast<bool>(flags & SeaFlags::kIncludeAssets)) {
size_t assets_size = ReadArithmetic<size_t>();
Debug("Read SEA resource assets size %zu\n", assets_size);
for (size_t i = 0; i < assets_size; ++i) {
std::string_view key = ReadStringView(StringLogMode::kAddressAndContent);
std::string_view content = ReadStringView(StringLogMode::kAddressOnly);
Debug("Read SEA resource asset %s at %p, size=%zu\n",
key,
content.data(),
content.size());
assets.emplace(key, content);
}
}
return {flags, code_path, code, code_cache, assets};
}

std::string_view FindSingleExecutableBlob() {
@@ -298,6 +326,7 @@ struct SeaConfig {
std::string main_path;
std::string output_path;
SeaFlags flags = SeaFlags::kDefault;
std::unordered_map<std::string, std::string> assets;
};

std::optional<SeaConfig> ParseSingleExecutableConfig(
@@ -371,6 +400,17 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
result.flags |= SeaFlags::kUseCodeCache;
}

auto assets_opt = parser.GetTopLevelStringDict("assets");
if (!assets_opt.has_value()) {
FPrintF(stderr,
"\"assets\" field of %s is not a map of strings\n",
config_path);
return std::nullopt;
} else if (!assets_opt.value().empty()) {
result.flags |= SeaFlags::kIncludeAssets;
result.assets = std::move(assets_opt.value());
}

return result;
}

@@ -468,6 +508,21 @@ std::optional<std::string> GenerateCodeCache(std::string_view main_path,
return code_cache;
}

int BuildAssets(const std::unordered_map<std::string, std::string>& config,
std::unordered_map<std::string, std::string>* assets) {
for (auto const& [key, path] : config) {
std::string blob;
int r = ReadFileSync(&blob, path.c_str());
if (r != 0) {
const char* err = uv_strerror(r);
FPrintF(stderr, "Cannot read asset %s: %s\n", path.c_str(), err);
return r;
}
assets->emplace(key, std::move(blob));
}
return 0;
}

ExitCode GenerateSingleExecutableBlob(
const SeaConfig& config,
const std::vector<std::string>& args,
@@ -513,13 +568,22 @@ ExitCode GenerateSingleExecutableBlob(
}
}

std::unordered_map<std::string, std::string> assets;
if (!config.assets.empty() && BuildAssets(config.assets, &assets) != 0) {
return ExitCode::kGenericUserError;
}
std::unordered_map<std::string_view, std::string_view> assets_view;
for (auto const& [key, content] : assets) {
assets_view.emplace(key, content);
}
SeaResource sea{
config.flags,
config.main_path,
builds_snapshot_from_main
? std::string_view{snapshot_blob.data(), snapshot_blob.size()}
: std::string_view{main_script.data(), main_script.size()},
optional_sv_code_cache};
optional_sv_code_cache,
assets_view};

SeaSerializer serializer;
serializer.Write(sea);
@@ -554,6 +618,29 @@ ExitCode BuildSingleExecutableBlob(const std::string& config_path,
return ExitCode::kGenericUserError;
}

void GetAsset(const FunctionCallbackInfo<Value>& args) {
CHECK_EQ(args.Length(), 1);
CHECK(args[0]->IsString());
Utf8Value key(args.GetIsolate(), args[0]);
SeaResource sea_resource = FindSingleExecutableResource();
if (sea_resource.assets.empty()) {
return;
}
auto it = sea_resource.assets.find(*key);
if (it == sea_resource.assets.end()) {
return;
}
// We cast away the constness here, the JS land should ensure that
// the data is not mutated.
std::unique_ptr<v8::BackingStore> store = ArrayBuffer::NewBackingStore(
const_cast<char*>(it->second.data()),
it->second.size(),
[](void*, size_t, void*) {},
nullptr);
Local<ArrayBuffer> ab = ArrayBuffer::New(args.GetIsolate(), std::move(store));
args.GetReturnValue().Set(ab);
}

void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
@@ -565,13 +652,15 @@ void Initialize(Local<Object> target,
IsExperimentalSeaWarningNeeded);
SetMethod(context, target, "getCodePath", GetCodePath);
SetMethod(context, target, "getCodeCache", GetCodeCache);
SetMethod(context, target, "getAsset", GetAsset);
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(IsSea);
registry->Register(IsExperimentalSeaWarningNeeded);
registry->Register(GetCodePath);
registry->Register(GetCodeCache);
registry->Register(GetAsset);
}

} // namespace sea
3 changes: 3 additions & 0 deletions src/node_sea.h
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@
#include <string>
#include <string_view>
#include <tuple>
#include <unordered_map>
#include <vector>

#include "node_exit_code.h"
@@ -27,13 +28,15 @@ enum class SeaFlags : uint32_t {
kDisableExperimentalSeaWarning = 1 << 0,
kUseSnapshot = 1 << 1,
kUseCodeCache = 1 << 2,
kIncludeAssets = 1 << 3,
};

struct SeaResource {
SeaFlags flags = SeaFlags::kDefault;
std::string_view code_path;
std::string_view main_code_or_snapshot;
std::optional<std::string_view> code_cache;
std::unordered_map<std::string_view, std::string_view> assets;

bool use_snapshot() const;
static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags);
100 changes: 100 additions & 0 deletions test/fixtures/sea/get-asset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
'use strict';

const { isSea, getAsset, getAssetAsBlob } = require('node:sea');
const { readFileSync } = require('node:fs');
const assert = require('node:assert');

assert(isSea());

// Test invalid getAsset() calls.
{
assert.throws(() => getAsset('utf8_test_text.txt', 'invalid'), {
code: 'ERR_ENCODING_NOT_SUPPORTED'
});

[
1,
1n,
Symbol(),
false,
() => {},
{},
[],
null,
undefined,
].forEach(arg => assert.throws(() => getAsset(arg), {
code: 'ERR_INVALID_ARG_TYPE'
}));

assert.throws(() => getAsset('nonexistent'), {
code: 'ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND'
});
}

// Test invalid getAssetAsBlob() calls.
{
// Invalid options argument.
[
123,
123n,
Symbol(),
'',
true,
].forEach(arg => assert.throws(() => {
getAssetAsBlob('utf8_test_text.txt', arg)
}, {
code: 'ERR_INVALID_ARG_TYPE'
}));

assert.throws(() => getAssetAsBlob('nonexistent'), {
code: 'ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND'
});
}

const textAssetOnDisk = readFileSync(process.env.__TEST_UTF8_TEXT_PATH, 'utf8');
const binaryAssetOnDisk = readFileSync(process.env.__TEST_PERSON_JPG);

// Check getAsset() buffer copies.
{
// Check that the asset embedded is the same as the original.
const assetCopy1 = getAsset('person.jpg')
const assetCopyBuffer1 = Buffer.from(assetCopy1);
assert.deepStrictEqual(assetCopyBuffer1, binaryAssetOnDisk);

const assetCopy2 = getAsset('person.jpg');
const assetCopyBuffer2 = Buffer.from(assetCopy2);
assert.deepStrictEqual(assetCopyBuffer2, binaryAssetOnDisk);

// Zero-fill copy1.
assetCopyBuffer1.fill(0);

// Test that getAsset() returns an immutable copy.
assert.deepStrictEqual(assetCopyBuffer2, binaryAssetOnDisk);
assert.notDeepStrictEqual(assetCopyBuffer1, binaryAssetOnDisk);
}

// Check getAsset() with encoding.
{
const actualAsset = getAsset('utf8_test_text.txt', 'utf8')
assert.strictEqual(actualAsset, textAssetOnDisk);
console.log(actualAsset);
}

// Check getAssetAsBlob().
{
let called = false;
async function test() {
const blob = getAssetAsBlob('person.jpg');
const buffer = await blob.arrayBuffer();
assert.deepStrictEqual(Buffer.from(buffer), binaryAssetOnDisk);
const blob2 = getAssetAsBlob('utf8_test_text.txt');
const text = await blob2.text();
assert.strictEqual(text, textAssetOnDisk);
}
test().then(() => {
called = true;
});
process.on('exit', () => {
assert(called);
});
}
1 change: 1 addition & 0 deletions test/sequential/sequential.status
Original file line number Diff line number Diff line change
@@ -51,6 +51,7 @@ test-performance-eventloopdelay: PASS, FLAKY

[$system==linux && $arch==ppc64]
# https://github.com/nodejs/node/issues/50740
test-single-executable-application-assets: PASS, FLAKY
test-single-executable-application-disable-experimental-sea-warning: PASS, FLAKY
test-single-executable-application-empty: PASS, FLAKY
test-single-executable-application-snapshot-and-code-cache: PASS, FLAKY
130 changes: 130 additions & 0 deletions test/sequential/test-single-executable-application-assets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use strict';

const common = require('../common');

const {
injectAndCodeSign,
skipIfSingleExecutableIsNotSupported,
} = require('../common/sea');

skipIfSingleExecutableIsNotSupported();

// This tests the snapshot support in single executable applications.
const tmpdir = require('../common/tmpdir');

const { copyFileSync, writeFileSync, existsSync } = require('fs');
const {
spawnSyncAndExit,
spawnSyncAndExitWithoutError,
} = require('../common/child_process');
const assert = require('assert');
const fixtures = require('../common/fixtures');

tmpdir.refresh();
if (!tmpdir.hasEnoughSpace(120 * 1024 * 1024)) {
common.skip('Not enough disk space');
}

const configFile = tmpdir.resolve('sea-config.json');
const seaPrepBlob = tmpdir.resolve('sea-prep.blob');
const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea');

{
tmpdir.refresh();
copyFileSync(fixtures.path('sea', 'get-asset.js'), tmpdir.resolve('sea.js'));
writeFileSync(configFile, `
{
"main": "sea.js",
"output": "sea-prep.blob",
"assets": "invalid"
}
`);

spawnSyncAndExit(
process.execPath,
['--experimental-sea-config', 'sea-config.json'],
{
cwd: tmpdir.path
},
{
status: 1,
signal: null,
stderr: /"assets" field of sea-config\.json is not a map of strings/
});
}

{
tmpdir.refresh();
copyFileSync(fixtures.path('sea', 'get-asset.js'), tmpdir.resolve('sea.js'));
writeFileSync(configFile, `
{
"main": "sea.js",
"output": "sea-prep.blob",
"assets": {
"nonexistent": "nonexistent.txt"
}
}
`);

spawnSyncAndExit(
process.execPath,
['--experimental-sea-config', 'sea-config.json'],
{
cwd: tmpdir.path
},
{
status: 1,
signal: null,
stderr: /Cannot read asset nonexistent\.txt: no such file or directory/
});
}

{
tmpdir.refresh();
copyFileSync(fixtures.path('sea', 'get-asset.js'), tmpdir.resolve('sea.js'));
copyFileSync(fixtures.utf8TestTextPath, tmpdir.resolve('utf8_test_text.txt'));
copyFileSync(fixtures.path('person.jpg'), tmpdir.resolve('person.jpg'));
writeFileSync(configFile, `
{
"main": "sea.js",
"output": "sea-prep.blob",
"assets": {
"utf8_test_text.txt": "utf8_test_text.txt",
"person.jpg": "person.jpg"
}
}
`, 'utf8');

spawnSyncAndExitWithoutError(
process.execPath,
['--experimental-sea-config', 'sea-config.json'],
{
env: {
NODE_DEBUG_NATIVE: 'SEA',
...process.env,
},
cwd: tmpdir.path
},
{});

assert(existsSync(seaPrepBlob));

copyFileSync(process.execPath, outputFile);
injectAndCodeSign(outputFile, seaPrepBlob);

spawnSyncAndExitWithoutError(
outputFile,
{
env: {
...process.env,
NODE_DEBUG_NATIVE: 'SEA',
__TEST_PERSON_JPG: fixtures.path('person.jpg'),
__TEST_UTF8_TEXT_PATH: fixtures.path('utf8_test_text.txt'),
}
},
{
trim: true,
stdout: fixtures.utf8TestText,
}
);
}

0 comments on commit db0efa3

Please sign in to comment.