Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit cf7d0a1

Browse files
committedNov 29, 2023
sea: support embedding assets
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.
1 parent 500ff24 commit cf7d0a1

12 files changed

+527
-12
lines changed
 

‎doc/api/errors.md

+14
Original file line numberDiff line numberDiff line change
@@ -2360,6 +2360,13 @@ error indicates that the idle loop has failed to stop.
23602360
An attempt was made to use operations that can only be used when building
23612361
V8 startup snapshot even though Node.js isn't building one.
23622362

2363+
<a id="ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION"></a>
2364+
2365+
### `ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION`
2366+
2367+
The operation cannot be performed when it's not in a single-executable
2368+
application.
2369+
23632370
<a id="ERR_NOT_SUPPORTED_IN_SNAPSHOT"></a>
23642371

23652372
### `ERR_NOT_SUPPORTED_IN_SNAPSHOT`
@@ -2506,6 +2513,13 @@ The [`server.close()`][] method was called when a `net.Server` was not
25062513
running. This applies to all instances of `net.Server`, including HTTP, HTTPS,
25072514
and HTTP/2 `Server` instances.
25082515

2516+
<a id="ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND"></a>
2517+
2518+
### `ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND`
2519+
2520+
A key was passed to single exectuable application APIs to identify an asset,
2521+
but no match could be found.
2522+
25092523
<a id="ERR_SOCKET_ALREADY_BOUND"></a>
25102524

25112525
### `ERR_SOCKET_ALREADY_BOUND`

‎doc/api/single-executable-applications.md

+98-10
Original file line numberDiff line numberDiff line change
@@ -178,14 +178,52 @@ The configuration currently reads the following top-level fields:
178178
"output": "/path/to/write/the/generated/blob.blob",
179179
"disableExperimentalSEAWarning": true, // Default: false
180180
"useSnapshot": false, // Default: false
181-
"useCodeCache": true // Default: false
181+
"useCodeCache": true, // Default: false
182+
"assets": { // Optional
183+
"a.dat": "/path/to/a.dat",
184+
"b.txt": "/path/to/b.txt"
185+
}
182186
}
183187
```
184188
185189
If the paths are not absolute, Node.js will use the path relative to the
186190
current working directory. The version of the Node.js binary used to produce
187191
the blob must be the same as the one to which the blob will be injected.
188192
193+
### Assets
194+
195+
Users can include assets by adding a key-path dictionary to the configuration
196+
as the `assets` field. At build time, Node.js would read the assets from the
197+
specified paths and bundle them into the preparation blob. In the generated
198+
executable, users can retrieve the assets using the [`sea.getAsset()`][] and
199+
[`sea.getAssetAsBlob()`][] APIs.
200+
201+
```json
202+
{
203+
"main": "/path/to/bundled/script.js",
204+
"output": "/path/to/write/the/generated/blob.blob",
205+
"assets": {
206+
"a.jpg": "/path/to/a.jpg",
207+
"b.txt": "/path/to/b.txt"
208+
}
209+
}
210+
```
211+
212+
The single-executable application can access the assets as follows:
213+
214+
```cjs
215+
const { getAsset } = require('node:sea');
216+
// Returns a copy of the data in an ArrayBuffer.
217+
const image = getAsset('a.jpg');
218+
// Returns a string decoded from the asset as UTF8.
219+
const text = getAsset('b.txt', 'utf8');
220+
// Returns a Blob containing the asset.
221+
const blob = getAssetAsBlob('a.jpg');
222+
```
223+
224+
See documentation of the [`sea.getAsset()`][] and [`sea.getAssetAsBlob()`][]
225+
APIs for more information.
226+
189227
### Startup snapshot support
190228
191229
The `useSnapshot` field can be used to enable startup snapshot support. In this
@@ -229,11 +267,56 @@ execute the script, which would improve the startup performance.
229267
230268
**Note:** `import()` does not work when `useCodeCache` is `true`.
231269
232-
## Notes
270+
## In the injected main script
233271
234-
### `require(id)` in the injected module is not file based
272+
### Single-executable application API
235273
236-
`require()` in the injected module is not the same as the [`require()`][]
274+
The `node:sea` builtin allows interaction with the single-executable application
275+
from the JavaScript main script embedded into the executable.
276+
277+
#### `sea.isSea()`
278+
279+
<!-- YAML
280+
added: REPLACEME
281+
-->
282+
283+
* Returns: {boolean} Whether this script is running inside a single-executable
284+
application.
285+
286+
### `sea.getAsset(key[, encoding])`
287+
288+
<!-- YAML
289+
added: REPLACEME
290+
-->
291+
292+
This method can be used to retrieve the assets configured to be bundled into the
293+
single-executable application at build time.
294+
295+
* `key` {string} the key for the asset in the dictionary specified by the
296+
`assets` field in the single-executable application configuration.
297+
* `encoding` {undefined|string} If specified, the asset will be decoded as
298+
a string. Any encoding supported by the `TextDecoder` is accepted.
299+
If unspecified, an `ArrayBuffer` containing a copy of the asset would be
300+
returned instead.
301+
* Returns: {string|ArrayBuffer}
302+
303+
### `sea.getAssetAsBlob(key[, options])`
304+
305+
<!-- YAML
306+
added: REPLACEME
307+
-->
308+
309+
Similar to [`sea.getAsset()`][], but returns the result in a [`Blob`].
310+
311+
* `key` {string} the key for the asset in the dictionary specified by the
312+
`assets` field in the single-executable application configuration.
313+
* `options` {Object}
314+
* `type` {string} An optional mime type for the blob.
315+
* Returns: {Blob}
316+
317+
### `require(id)` in the injected main script is not file based
318+
319+
`require()` in the injected main script is not the same as the [`require()`][]
237320
available to modules that are not injected. It also does not have any of the
238321
properties that non-injected [`require()`][] has except [`require.main`][]. It
239322
can only be used to load built-in modules. Attempting to load a module that can
@@ -250,15 +333,17 @@ const { createRequire } = require('node:module');
250333
require = createRequire(__filename);
251334
```
252335
253-
### `__filename` and `module.filename` in the injected module
336+
### `__filename` and `module.filename` in the injected main script
254337
255-
The values of `__filename` and `module.filename` in the injected module are
256-
equal to [`process.execPath`][].
338+
The values of `__filename` and `module.filename` in the injected main script
339+
are equal to [`process.execPath`][].
257340
258-
### `__dirname` in the injected module
341+
### `__dirname` in the injected main script
259342
260-
The value of `__dirname` in the injected module is equal to the directory name
261-
of [`process.execPath`][].
343+
The value of `__dirname` in the injected main script is equal to the directory
344+
name of [`process.execPath`][].
345+
346+
## Notes
262347
263348
### Single executable application creation process
264349
@@ -298,9 +383,12 @@ to help us document them.
298383
[Mach-O]: https://en.wikipedia.org/wiki/Mach-O
299384
[PE]: https://en.wikipedia.org/wiki/Portable_Executable
300385
[Windows SDK]: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/
386+
[`Blob`]: https://developer.mozilla.org/en-US/docs/Web/API/Blob
301387
[`process.execPath`]: process.md#processexecpath
302388
[`require()`]: modules.md#requireid
303389
[`require.main`]: modules.md#accessing-the-main-module
390+
[`sea.getAsset()`]: #seagetassetkey-encoding
391+
[`sea.getAssetAsBlob()`]: #seagetassetasblobkey-options
304392
[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data
305393
[`v8.startupSnapshot` API]: v8.md#startup-snapshot-api
306394
[documentation about startup snapshot support in Node.js]: cli.md#--build-snapshot

‎lib/internal/bootstrap/realm.js

+1
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ const legacyWrapperList = new SafeSet([
128128
// beginning with "internal/".
129129
// Modules that can only be imported via the node: scheme.
130130
const schemelessBlockList = new SafeSet([
131+
'sea',
131132
'test',
132133
'test/reporters',
133134
]);

‎lib/internal/errors.js

+4
Original file line numberDiff line numberDiff line change
@@ -1601,6 +1601,8 @@ E('ERR_NETWORK_IMPORT_DISALLOWED',
16011601
"import of '%s' by %s is not supported: %s", Error);
16021602
E('ERR_NOT_BUILDING_SNAPSHOT',
16031603
'Operation cannot be invoked when not building startup snapshot', Error);
1604+
E('ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION',
1605+
'Operation cannot be invoked when not in a single-executable application', Error);
16041606
E('ERR_NOT_SUPPORTED_IN_SNAPSHOT', '%s is not supported in startup snapshot', Error);
16051607
E('ERR_NO_CRYPTO',
16061608
'Node.js is not compiled with OpenSSL crypto support', Error);
@@ -1684,6 +1686,8 @@ E('ERR_SCRIPT_EXECUTION_INTERRUPTED',
16841686
E('ERR_SERVER_ALREADY_LISTEN',
16851687
'Listen method has been called more than once without closing.', Error);
16861688
E('ERR_SERVER_NOT_RUNNING', 'Server is not running.', Error);
1689+
E('ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND',
1690+
'Cannot find asset %s for the single executable application', Error);
16871691
E('ERR_SOCKET_ALREADY_BOUND', 'Socket is already bound', Error);
16881692
E('ERR_SOCKET_BAD_BUFFER_SIZE',
16891693
'Buffer size must be a positive integer', TypeError);

‎lib/sea.js

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict';
2+
3+
const { isSea, getAsset: getAssetInternal } = internalBinding('sea');
4+
const { TextDecoder } = require('internal/encoding');
5+
const { validateString } = require('internal/validators');
6+
const {
7+
ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION,
8+
ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND,
9+
} = require('internal/errors').codes;
10+
const { Blob } = require('internal/blob');
11+
12+
function getRawAsset(key) {
13+
validateString(key, 'key');
14+
15+
if (!isSea()) {
16+
throw new ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION();
17+
}
18+
19+
const asset = getAssetInternal(key);
20+
if (asset === undefined) {
21+
throw new ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND(key);
22+
}
23+
return asset;
24+
}
25+
26+
function getAsset(key, encoding) {
27+
if (encoding !== undefined) {
28+
validateString(encoding, 'encoding');
29+
}
30+
const asset = getRawAsset(key);
31+
if (encoding === undefined) {
32+
return asset.slice();
33+
}
34+
const decoder = new TextDecoder(encoding);
35+
return decoder.decode(asset);
36+
}
37+
38+
function getAssetAsBlob(key, options) {
39+
const asset = getRawAsset(key);
40+
return new Blob([asset], options);
41+
}
42+
43+
module.exports = {
44+
isSea,
45+
getAsset,
46+
getAssetAsBlob,
47+
};

‎src/json_parser.cc

+47
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "util-inl.h"
55

66
namespace node {
7+
using v8::Array;
78
using v8::Context;
89
using v8::Isolate;
910
using v8::Local;
@@ -101,4 +102,50 @@ std::optional<bool> JSONParser::GetTopLevelBoolField(std::string_view field) {
101102
return value->BooleanValue(isolate);
102103
}
103104

105+
std::optional<std::map<std::string, std::string>>
106+
JSONParser::GetTopLevelDictOfStrings(std::string_view field) {
107+
Isolate* isolate = isolate_.get();
108+
v8::HandleScope handle_scope(isolate);
109+
Local<Context> context = context_.Get(isolate);
110+
Local<Object> content_object = content_.Get(isolate);
111+
Local<Value> value;
112+
bool has_field;
113+
// It's not a real script, so don't print the source line.
114+
errors::PrinterTryCatch bootstrapCatch(
115+
isolate, errors::PrinterTryCatch::kDontPrintSourceLine);
116+
Local<Value> field_local;
117+
if (!ToV8Value(context, field, isolate).ToLocal(&field_local)) {
118+
return std::nullopt;
119+
}
120+
if (!content_object->Has(context, field_local).To(&has_field)) {
121+
return std::nullopt;
122+
}
123+
if (!has_field) {
124+
return std::map<std::string, std::string>{};
125+
}
126+
if (!content_object->Get(context, field_local).ToLocal(&value) ||
127+
!value->IsObject()) {
128+
return std::nullopt;
129+
}
130+
Local<Object> dict = value.As<Object>();
131+
Local<Array> keys;
132+
if (!dict->GetOwnPropertyNames(context).ToLocal(&keys)) {
133+
return std::nullopt;
134+
}
135+
std::map<std::string, std::string> result;
136+
uint32_t length = keys->Length();
137+
for (uint32_t i = 0; i < length; ++i) {
138+
Local<Value> key;
139+
Local<Value> value;
140+
if (!keys->Get(context, i).ToLocal(&key) || !key->IsString()) return {};
141+
if (!dict->Get(context, key).ToLocal(&value) || !value->IsString())
142+
return {};
143+
144+
Utf8Value key_utf8(isolate, key);
145+
Utf8Value value_utf8(isolate, value);
146+
result.emplace(*key_utf8, *value_utf8);
147+
}
148+
return result;
149+
}
150+
104151
} // namespace node

‎src/json_parser.h

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
55

6+
#include <map>
67
#include <memory>
78
#include <optional>
89
#include <string>
@@ -20,6 +21,8 @@ class JSONParser {
2021
bool Parse(const std::string& content);
2122
std::optional<std::string> GetTopLevelStringField(std::string_view field);
2223
std::optional<bool> GetTopLevelBoolField(std::string_view field);
24+
std::optional<std::map<std::string, std::string>> GetTopLevelDictOfStrings(
25+
std::string_view field);
2326

2427
private:
2528
// We might want a lighter-weight JSON parser for this use case. But for now

‎src/node_sea.cc

+92-2
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,19 @@ size_t SeaSerializer::Write(const SeaResource& sea) {
110110
written_total +=
111111
WriteStringView(sea.code_cache.value(), StringLogMode::kAddressOnly);
112112
}
113+
114+
if (!sea.assets.empty()) {
115+
Debug("Write SEA resource assets size %zu\n", sea.assets.size());
116+
written_total += WriteArithmetic<size_t>(sea.assets.size());
117+
for (auto const& [key, content] : sea.assets) {
118+
Debug("Write SEA resource asset %s at %p, size=%zu\n",
119+
key,
120+
content.data(),
121+
content.size());
122+
written_total += WriteStringView(key, StringLogMode::kAddressAndContent);
123+
written_total += WriteStringView(content, StringLogMode::kAddressOnly);
124+
}
125+
}
113126
return written_total;
114127
}
115128

@@ -157,7 +170,22 @@ SeaResource SeaDeserializer::Read() {
157170
code_cache.data(),
158171
code_cache.size());
159172
}
160-
return {flags, code_path, code, code_cache};
173+
174+
std::map<std::string_view, std::string_view> assets;
175+
if (static_cast<bool>(flags & SeaFlags::kIncludeAssets)) {
176+
size_t assets_size = ReadArithmetic<size_t>();
177+
Debug("Read SEA resource assets size %zu\n", assets_size);
178+
for (size_t i = 0; i < assets_size; ++i) {
179+
std::string_view key = ReadStringView(StringLogMode::kAddressAndContent);
180+
std::string_view content = ReadStringView(StringLogMode::kAddressOnly);
181+
Debug("Read SEA resource asset %s at %p, size=%zu\n",
182+
key,
183+
content.data(),
184+
content.size());
185+
assets.emplace(key, content);
186+
}
187+
}
188+
return {flags, code_path, code, code_cache, assets};
161189
}
162190

163191
std::string_view FindSingleExecutableBlob() {
@@ -298,6 +326,7 @@ struct SeaConfig {
298326
std::string main_path;
299327
std::string output_path;
300328
SeaFlags flags = SeaFlags::kDefault;
329+
std::map<std::string, std::string> assets;
301330
};
302331

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

403+
std::optional<std::map<std::string, std::string>> assets_opt =
404+
parser.GetTopLevelDictOfStrings("assets");
405+
if (!assets_opt.has_value()) {
406+
FPrintF(stderr,
407+
"\"assets\" field of %s is not a map of strings\n",
408+
config_path);
409+
return std::nullopt;
410+
} else if (!assets_opt.value().empty()) {
411+
result.flags |= SeaFlags::kIncludeAssets;
412+
result.assets = std::move(assets_opt.value());
413+
}
414+
374415
return result;
375416
}
376417

@@ -464,6 +505,21 @@ std::optional<std::string> GenerateCodeCache(std::string_view main_path,
464505
return code_cache;
465506
}
466507

508+
int BuildAssets(const std::map<std::string, std::string>& config,
509+
std::map<std::string, std::string>* assets) {
510+
for (auto const& [key, path] : config) {
511+
std::string blob;
512+
int r = ReadFileSync(&blob, path.c_str());
513+
if (r != 0) {
514+
const char* err = uv_strerror(r);
515+
FPrintF(stderr, "Cannot read asset %s: %s\n", path.c_str(), err);
516+
return r;
517+
}
518+
assets->emplace(key, std::move(blob));
519+
}
520+
return 0;
521+
}
522+
467523
ExitCode GenerateSingleExecutableBlob(
468524
const SeaConfig& config,
469525
const std::vector<std::string>& args,
@@ -506,13 +562,22 @@ ExitCode GenerateSingleExecutableBlob(
506562
}
507563
}
508564

565+
std::map<std::string, std::string> assets;
566+
if (!config.assets.empty() && BuildAssets(config.assets, &assets) != 0) {
567+
return ExitCode::kGenericUserError;
568+
}
569+
std::map<std::string_view, std::string_view> assets_view;
570+
for (auto const& [key, content] : assets) {
571+
assets_view.emplace(key, content);
572+
}
509573
SeaResource sea{
510574
config.flags,
511575
config.main_path,
512576
builds_snapshot_from_main
513577
? std::string_view{snapshot_blob.data(), snapshot_blob.size()}
514578
: std::string_view{main_script.data(), main_script.size()},
515-
optional_sv_code_cache};
579+
optional_sv_code_cache,
580+
assets_view};
516581

517582
SeaSerializer serializer;
518583
serializer.Write(sea);
@@ -547,6 +612,29 @@ ExitCode BuildSingleExecutableBlob(const std::string& config_path,
547612
return ExitCode::kGenericUserError;
548613
}
549614

615+
void GetAsset(const FunctionCallbackInfo<Value>& args) {
616+
CHECK_EQ(args.Length(), 1);
617+
CHECK(args[0]->IsString());
618+
Utf8Value key(args.GetIsolate(), args[0]);
619+
SeaResource sea_resource = FindSingleExecutableResource();
620+
if (sea_resource.assets.empty()) {
621+
return;
622+
}
623+
auto it = sea_resource.assets.find(*key);
624+
if (it == sea_resource.assets.end()) {
625+
return;
626+
}
627+
// We cast away the constness here, the JS land should ensure that
628+
// the data is not mutated.
629+
std::unique_ptr<v8::BackingStore> store = ArrayBuffer::NewBackingStore(
630+
const_cast<char*>(it->second.data()),
631+
it->second.size(),
632+
[](void*, size_t, void*) {},
633+
nullptr);
634+
Local<ArrayBuffer> ab = ArrayBuffer::New(args.GetIsolate(), std::move(store));
635+
args.GetReturnValue().Set(ab);
636+
}
637+
550638
void Initialize(Local<Object> target,
551639
Local<Value> unused,
552640
Local<Context> context,
@@ -558,13 +646,15 @@ void Initialize(Local<Object> target,
558646
IsExperimentalSeaWarningNeeded);
559647
SetMethod(context, target, "getCodePath", GetCodePath);
560648
SetMethod(context, target, "getCodeCache", GetCodeCache);
649+
SetMethod(context, target, "getAsset", GetAsset);
561650
}
562651

563652
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
564653
registry->Register(IsSea);
565654
registry->Register(IsExperimentalSeaWarningNeeded);
566655
registry->Register(GetCodePath);
567656
registry->Register(GetCodeCache);
657+
registry->Register(GetAsset);
568658
}
569659

570660
} // namespace sea

‎src/node_sea.h

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION)
77

88
#include <cinttypes>
9+
#include <map>
910
#include <optional>
1011
#include <string>
1112
#include <string_view>
@@ -27,13 +28,15 @@ enum class SeaFlags : uint32_t {
2728
kDisableExperimentalSeaWarning = 1 << 0,
2829
kUseSnapshot = 1 << 1,
2930
kUseCodeCache = 1 << 2,
31+
kIncludeAssets = 1 << 3,
3032
};
3133

3234
struct SeaResource {
3335
SeaFlags flags = SeaFlags::kDefault;
3436
std::string_view code_path;
3537
std::string_view main_code_or_snapshot;
3638
std::optional<std::string_view> code_cache;
39+
std::map<std::string_view, std::string_view> assets;
3740

3841
bool use_snapshot() const;
3942
static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags);

‎test/fixtures/sea/get-asset.js

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
'use strict';
2+
3+
const { isSea, getAsset, getAssetAsBlob } = require('node:sea');
4+
const { readFileSync } = require('fs');
5+
const assert = require('assert');
6+
7+
assert(isSea());
8+
9+
// Test invalid getAsset() calls.
10+
{
11+
assert.throws(() => getAsset('utf8_test_text.txt', 'invalid'), {
12+
code: 'ERR_ENCODING_NOT_SUPPORTED'
13+
});
14+
15+
assert.throws(() => getAsset(null), {
16+
code: 'ERR_INVALID_ARG_TYPE'
17+
});
18+
assert.throws(() => getAsset(1), {
19+
code: 'ERR_INVALID_ARG_TYPE'
20+
});
21+
22+
assert.throws(() => getAsset('nonexistent'), {
23+
code: 'ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND'
24+
});
25+
}
26+
27+
// Test invalid getAssetAsBlob() calls.
28+
{
29+
// Invalid options argument.
30+
assert.throws(() => {
31+
getAssetAsBlob('utf8_test_text.txt', 123)
32+
}, {
33+
code: 'ERR_INVALID_ARG_TYPE'
34+
});
35+
36+
assert.throws(() => getAssetAsBlob('nonexistent'), {
37+
code: 'ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND'
38+
});
39+
}
40+
41+
const textAssetOnDisk = readFileSync(process.env.__TEST_UTF8_TEXT_PATH, 'utf8');
42+
const binaryAssetOnDisk = readFileSync(process.env.__TEST_PERSON_JPG);
43+
44+
// Check getAsset() buffer copies.
45+
{
46+
// Check that the asset embedded is the same as the original.
47+
const assetCopy1 = getAsset('person.jpg')
48+
const assetCopyBuffer1 = Buffer.from(assetCopy1);
49+
assert.deepStrictEqual(assetCopyBuffer1, binaryAssetOnDisk);
50+
51+
const assetCopy2 = getAsset('person.jpg');
52+
const assetCopyBuffer2 = Buffer.from(assetCopy2);
53+
assert.deepStrictEqual(assetCopyBuffer2, binaryAssetOnDisk);
54+
55+
// Zero-fill copy1.
56+
assetCopyBuffer1.fill(0);
57+
58+
// Test that getAsset() returns an immutable copy.
59+
assert.deepStrictEqual(assetCopyBuffer2, binaryAssetOnDisk);
60+
assert.notDeepStrictEqual(assetCopyBuffer1, binaryAssetOnDisk);
61+
}
62+
63+
// Check getAsset() with encoding.
64+
{
65+
const actualAsset = getAsset('utf8_test_text.txt', 'utf8')
66+
assert.strictEqual(actualAsset, textAssetOnDisk);
67+
console.log(actualAsset);
68+
}
69+
70+
// Check getAssetAsBlob().
71+
{
72+
let called = false;
73+
async function test() {
74+
const blob = getAssetAsBlob('person.jpg');
75+
const buffer = await blob.arrayBuffer();
76+
assert.deepStrictEqual(Buffer.from(buffer), binaryAssetOnDisk);
77+
const blob2 = getAssetAsBlob('utf8_test_text.txt');
78+
const text = await blob2.text();
79+
assert.strictEqual(text, textAssetOnDisk);
80+
}
81+
test().then(() => {
82+
called = true;
83+
});
84+
process.on('exit', () => {
85+
assert(called);
86+
});
87+
}

‎test/sequential/sequential.status

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ test-performance-eventloopdelay: PASS, FLAKY
4949

5050
[$system==ppc || $system==ppc64]
5151
# https://github.com/nodejs/node/issues/50740
52+
test-single-executable-application-assets: PASS, FLAKY
5253
test-single-executable-application-empty: PASS, FLAKY
5354
test-single-executable-application-snapshot-and-code-cache: PASS, FLAKY
5455
test-single-executable-application-snapshot: PASS, FLAKY
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
5+
const {
6+
injectAndCodeSign,
7+
skipIfSingleExecutableIsNotSupported,
8+
} = require('../common/sea');
9+
10+
skipIfSingleExecutableIsNotSupported();
11+
12+
// This tests the snapshot support in single executable applications.
13+
const tmpdir = require('../common/tmpdir');
14+
15+
const { copyFileSync, writeFileSync, existsSync } = require('fs');
16+
const {
17+
spawnSyncAndExit,
18+
spawnSyncAndExitWithoutError
19+
} = require('../common/child_process');
20+
const assert = require('assert');
21+
const fixtures = require('../common/fixtures');
22+
23+
tmpdir.refresh();
24+
if (!tmpdir.hasEnoughSpace(120 * 1024 * 1024)) {
25+
common.skip('Not enough disk space');
26+
}
27+
28+
const configFile = tmpdir.resolve('sea-config.json');
29+
const seaPrepBlob = tmpdir.resolve('sea-prep.blob');
30+
const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea');
31+
32+
{
33+
tmpdir.refresh();
34+
copyFileSync(fixtures.path('sea', 'get-asset.js'), tmpdir.resolve('sea.js'));
35+
writeFileSync(configFile, `
36+
{
37+
"main": "sea.js",
38+
"output": "sea-prep.blob",
39+
"assets": "invalid"
40+
}
41+
`);
42+
43+
spawnSyncAndExit(
44+
process.execPath,
45+
['--experimental-sea-config', 'sea-config.json'],
46+
{
47+
cwd: tmpdir.path
48+
},
49+
{
50+
status: 1,
51+
signal: null,
52+
stderr: /"assets" field of sea-config\.json is not a map of strings/
53+
});
54+
}
55+
56+
{
57+
tmpdir.refresh();
58+
copyFileSync(fixtures.path('sea', 'get-asset.js'), tmpdir.resolve('sea.js'));
59+
writeFileSync(configFile, `
60+
{
61+
"main": "sea.js",
62+
"output": "sea-prep.blob",
63+
"assets": {
64+
"nonexistent": "nonexistent.txt"
65+
}
66+
}
67+
`);
68+
69+
spawnSyncAndExit(
70+
process.execPath,
71+
['--experimental-sea-config', 'sea-config.json'],
72+
{
73+
cwd: tmpdir.path
74+
},
75+
{
76+
status: 1,
77+
signal: null,
78+
stderr: /Cannot read asset nonexistent\.txt: no such file or directory/
79+
});
80+
}
81+
82+
{
83+
tmpdir.refresh();
84+
copyFileSync(fixtures.path('sea', 'get-asset.js'), tmpdir.resolve('sea.js'));
85+
copyFileSync(fixtures.utf8TestTextPath, tmpdir.resolve('utf8_test_text.txt'));
86+
copyFileSync(fixtures.path('person.jpg'), tmpdir.resolve('person.jpg'));
87+
writeFileSync(configFile, `
88+
{
89+
"main": "sea.js",
90+
"output": "sea-prep.blob",
91+
"assets": {
92+
"utf8_test_text.txt": "utf8_test_text.txt",
93+
"person.jpg": "person.jpg"
94+
}
95+
}
96+
`, 'utf8');
97+
98+
spawnSyncAndExitWithoutError(
99+
process.execPath,
100+
['--experimental-sea-config', 'sea-config.json'],
101+
{
102+
env: {
103+
NODE_DEBUG_NATIVE: 'SEA',
104+
...process.env,
105+
},
106+
cwd: tmpdir.path
107+
},
108+
{});
109+
110+
assert(existsSync(seaPrepBlob));
111+
112+
copyFileSync(process.execPath, outputFile);
113+
injectAndCodeSign(outputFile, seaPrepBlob);
114+
115+
spawnSyncAndExitWithoutError(
116+
outputFile,
117+
{
118+
env: {
119+
...process.env,
120+
NODE_DEBUG_NATIVE: 'SEA',
121+
__TEST_PERSON_JPG: fixtures.path('person.jpg'),
122+
__TEST_UTF8_TEXT_PATH: fixtures.path('utf8_test_text.txt'),
123+
}
124+
},
125+
{
126+
trim: true,
127+
stdout: fixtures.utf8TestText,
128+
}
129+
);
130+
}

0 commit comments

Comments
 (0)
Please sign in to comment.