From b35c59147bb195ecf3dbd9cb52d231e73491c75b Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Tue, 14 Nov 2023 19:13:03 +0100 Subject: [PATCH] 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 without copying. 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. --- doc/api/errors.md | 14 ++ doc/api/single-executable-applications.md | 110 +++++++++++++-- lib/internal/bootstrap/realm.js | 1 + lib/internal/errors.js | 4 + lib/sea.js | 47 +++++++ src/json_parser.cc | 47 +++++++ src/json_parser.h | 3 + src/node_sea.cc | 94 ++++++++++++- src/node_sea.h | 3 + test/fixtures/sea/get-asset.js | 87 ++++++++++++ test/sequential/sequential.status | 1 + ...st-single-executable-application-assets.js | 130 ++++++++++++++++++ 12 files changed, 529 insertions(+), 12 deletions(-) create mode 100644 lib/sea.js create mode 100644 test/fixtures/sea/get-asset.js create mode 100644 test/sequential/test-single-executable-application-assets.js diff --git a/doc/api/errors.md b/doc/api/errors.md index 38a29a1aae5000..d34b3b8e780ee0 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -2360,6 +2360,13 @@ 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. + + +### `ERR_NOT_IN_SEA` + +The operation cannot be performed when it's not in a single-executable +application. + ### `ERR_NOT_SUPPORTED_IN_SNAPSHOT` @@ -2490,6 +2497,13 @@ example, Ctrl+C was pressed.) Script execution timed out, possibly due to bugs in the script being executed. + + +### `ERR_SEA_ASSET_NOT_FOUND` + +A key was passed to single exectuable application APIs to identify an asset, +but no match could be found. + ### `ERR_SERVER_ALREADY_LISTEN` diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 12c9e34a9805f3..473dc71e0a68e8 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -178,7 +178,11 @@ 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" + } } ``` @@ -186,6 +190,40 @@ 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 without copying. +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()` + + + +* Returns: {boolean} Whether this script is running inside a single-executable + application. + +### `sea.getAsset(key[, encoding])` + + + +This method can be used to retrieve the assets configured to be bundled into the +single-executable application at build time. + +* `key` {string} the key for the asset in the dictionary specified by the + `assets` field in the single-executable applciation configuratoin. +* `encoding` {undefined|string} If specified, the asset will be decoded as + a string. Any encoding supported by the `TextDecoder` is accpeted. + If unspecified, an `ArrayBuffer` containing a copy of the asset would be + returned instead. +* Returns: {string|ArrayBuffer} + +### `sea.getAssetAsBlob(key[, options])` + + + +Similar to [`sea.getAsset()`][], but returns the result in a [`Blob`][] +which allows users to read the data from the embedded asset without +copying it first. + +* `key` {string} the key for the asset in the dictionary specified by the + `assets` field in the single-executable applciation configuratoin. +* `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 diff --git a/lib/internal/bootstrap/realm.js b/lib/internal/bootstrap/realm.js index 93a939cc7e4645..cb6f39a559473b 100644 --- a/lib/internal/bootstrap/realm.js +++ b/lib/internal/bootstrap/realm.js @@ -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', ]); diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 39e512762a470c..9e7fe25700c350 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1601,6 +1601,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_SEA', + '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); @@ -1681,6 +1683,8 @@ E('ERR_REQUIRE_ESM', }, Error); E('ERR_SCRIPT_EXECUTION_INTERRUPTED', 'Script execution was interrupted by `SIGINT`', Error); +E('ERR_SEA_ASSET_NOT_FOUND', + 'Cannot find asset %s for the single executable application', Error); 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); diff --git a/lib/sea.js b/lib/sea.js new file mode 100644 index 00000000000000..20e4413644314d --- /dev/null +++ b/lib/sea.js @@ -0,0 +1,47 @@ +'use strict'; + +const { isSea, getAsset: getAssetInternal } = internalBinding('sea'); +const { TextDecoder } = require('internal/encoding'); +const { validateString } = require('internal/validators'); +const { + ERR_NOT_IN_SEA, + ERR_SEA_ASSET_NOT_FOUND, +} = require('internal/errors').codes; +const { Blob } = require('internal/blob'); + +function getRawAsset(key) { + validateString(key, 'key'); + + if (!isSea()) { + throw new ERR_NOT_IN_SEA(); + } + + const asset = getAssetInternal(key); + if (asset === undefined) { + throw new ERR_SEA_ASSET_NOT_FOUND(key); + } + return asset; +} + +function getAsset(key, encoding) { + if (encoding !== undefined) { + validateString(encoding, 'encoding'); + } + const asset = getRawAsset(key); + if (encoding === undefined) { + return asset.slice(); + } + const decoder = new TextDecoder(encoding); + return decoder.decode(asset); +} + +function getAssetAsBlob(key, options) { + const asset = getRawAsset(key); + return new Blob([asset], options); +} + +module.exports = { + isSea, + getAsset, + getAssetAsBlob, +}; diff --git a/src/json_parser.cc b/src/json_parser.cc index 1b445193bc8ceb..168c75a83dc91e 100644 --- a/src/json_parser.cc +++ b/src/json_parser.cc @@ -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,50 @@ std::optional JSONParser::GetTopLevelBoolField(std::string_view field) { return value->BooleanValue(isolate); } +std::optional> +JSONParser::GetTopLevelDictOfStrings(std::string_view field) { + Isolate* isolate = isolate_.get(); + v8::HandleScope handle_scope(isolate); + Local context = context_.Get(isolate); + Local content_object = content_.Get(isolate); + Local 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 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 std::map{}; + } + if (!content_object->Get(context, field_local).ToLocal(&value) || + !value->IsObject()) { + return std::nullopt; + } + Local dict = value.As(); + Local keys; + if (!dict->GetOwnPropertyNames(context).ToLocal(&keys)) { + return std::nullopt; + } + std::map result; + uint32_t length = keys->Length(); + for (uint32_t i = 0; i < length; ++i) { + Local key; + Local value; + if (!keys->Get(context, i).ToLocal(&key) || !key->IsString()) return {}; + if (!dict->Get(context, key).ToLocal(&value) || !value->IsString()) + return {}; + + Utf8Value key_utf8(isolate, key); + Utf8Value value_utf8(isolate, value); + result.emplace(*key_utf8, *value_utf8); + } + return result; +} + } // namespace node diff --git a/src/json_parser.h b/src/json_parser.h index 9e6c00ef8d8712..654302bd6f968f 100644 --- a/src/json_parser.h +++ b/src/json_parser.h @@ -3,6 +3,7 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#include #include #include #include @@ -20,6 +21,8 @@ class JSONParser { bool Parse(const std::string& content); std::optional GetTopLevelStringField(std::string_view field); std::optional GetTopLevelBoolField(std::string_view field); + std::optional> GetTopLevelDictOfStrings( + std::string_view field); private: // We might want a lighter-weight JSON parser for this use case. But for now diff --git a/src/node_sea.cc b/src/node_sea.cc index d1ab5051032d76..29b2a61c3dd8ec 100644 --- a/src/node_sea.cc +++ b/src/node_sea.cc @@ -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(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::map assets; + if (static_cast(flags & SeaFlags::kIncludeAssets)) { + size_t assets_size = ReadArithmetic(); + 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::map assets; }; std::optional ParseSingleExecutableConfig( @@ -371,6 +400,18 @@ std::optional ParseSingleExecutableConfig( result.flags |= SeaFlags::kUseCodeCache; } + std::optional> assets_opt = + parser.GetTopLevelDictOfStrings("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; } @@ -464,6 +505,21 @@ std::optional GenerateCodeCache(std::string_view main_path, return code_cache; } +int BuildAssets(const std::map& config, + std::map* assets) { + for (auto const& [key, path] : config) { + std::string blob; + int r = ReadFileSync(&blob, key.c_str()); + if (r != 0) { + const char* err = uv_strerror(r); + FPrintF(stderr, "Cannot read asset %s: %s\n", key.c_str(), err); + return r; + } + assets->emplace(key, std::move(blob)); + } + return 0; +} + ExitCode GenerateSingleExecutableBlob( const SeaConfig& config, const std::vector& args, @@ -506,13 +562,22 @@ ExitCode GenerateSingleExecutableBlob( } } + std::map assets; + if (!config.assets.empty() && BuildAssets(config.assets, &assets) != 0) { + return ExitCode::kGenericUserError; + } + std::map 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); @@ -547,6 +612,29 @@ ExitCode BuildSingleExecutableBlob(const std::string& config_path, return ExitCode::kGenericUserError; } +void GetAsset(const FunctionCallbackInfo& 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 store = ArrayBuffer::NewBackingStore( + const_cast(it->second.data()), + it->second.size(), + [](void*, size_t, void*) {}, + nullptr); + Local ab = ArrayBuffer::New(args.GetIsolate(), std::move(store)); + args.GetReturnValue().Set(ab); +} + void Initialize(Local target, Local unused, Local context, @@ -558,6 +646,7 @@ void Initialize(Local target, IsExperimentalSeaWarningNeeded); SetMethod(context, target, "getCodePath", GetCodePath); SetMethod(context, target, "getCodeCache", GetCodeCache); + SetMethod(context, target, "getAsset", GetAsset); } void RegisterExternalReferences(ExternalReferenceRegistry* registry) { @@ -565,6 +654,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(IsExperimentalSeaWarningNeeded); registry->Register(GetCodePath); registry->Register(GetCodeCache); + registry->Register(GetAsset); } } // namespace sea diff --git a/src/node_sea.h b/src/node_sea.h index c443de9e4d0adc..2b59feb55d17e8 100644 --- a/src/node_sea.h +++ b/src/node_sea.h @@ -6,6 +6,7 @@ #if !defined(DISABLE_SINGLE_EXECUTABLE_APPLICATION) #include +#include #include #include #include @@ -27,6 +28,7 @@ enum class SeaFlags : uint32_t { kDisableExperimentalSeaWarning = 1 << 0, kUseSnapshot = 1 << 1, kUseCodeCache = 1 << 2, + kIncludeAssets = 1 << 3, }; struct SeaResource { @@ -34,6 +36,7 @@ struct SeaResource { std::string_view code_path; std::string_view main_code_or_snapshot; std::optional code_cache; + std::map assets; bool use_snapshot() const; static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags); diff --git a/test/fixtures/sea/get-asset.js b/test/fixtures/sea/get-asset.js new file mode 100644 index 00000000000000..4c76129a1c5d01 --- /dev/null +++ b/test/fixtures/sea/get-asset.js @@ -0,0 +1,87 @@ +'use strict'; + +const { isSea, getAsset, getAssetAsBlob } = require('node:sea'); +const { readFileSync } = require('fs'); +const assert = require('assert'); + +assert(isSea()); + +// Test invalid getAsset() calls. +{ + assert.throws(() => getAsset('utf8_test_text.txt', 'invalid'), { + code: 'ERR_ENCODING_NOT_SUPPORTED' + }); + + assert.throws(() => getAsset(null), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => getAsset(1), { + code: 'ERR_INVALID_ARG_TYPE' + }); + + assert.throws(() => getAsset('nonexistent'), { + code: 'ERR_SEA_ASSET_NOT_FOUND' + }); +} + +// Test invalid getAssetAsBlob() calls. +{ + // Invalid options argument. + assert.throws(() => { + getAssetAsBlob('utf8_test_text.txt', 123) + }, { + code: 'ERR_INVALID_ARG_TYPE' + }); + + assert.throws(() => getAssetAsBlob('nonexistent'), { + code: 'ERR_SEA_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); + }); +} diff --git a/test/sequential/sequential.status b/test/sequential/sequential.status index a5e2d38ff31aa8..b8e3aa37bbaaaf 100644 --- a/test/sequential/sequential.status +++ b/test/sequential/sequential.status @@ -49,6 +49,7 @@ test-performance-eventloopdelay: PASS, FLAKY [$system==ppc || $system==ppc64] # https://github.com/nodejs/node/issues/50740 +test-single-executable-application-assets: PASS, FLAKY test-single-executable-application-empty: PASS, FLAKY test-single-executable-application-snapshot-and-code-cache: PASS, FLAKY test-single-executable-application-snapshot: PASS, FLAKY diff --git a/test/sequential/test-single-executable-application-assets.js b/test/sequential/test-single-executable-application-assets.js new file mode 100644 index 00000000000000..e5a47b9cd2141a --- /dev/null +++ b/test/sequential/test-single-executable-application-assets.js @@ -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: 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, + } + ); +}