mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
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. PR-URL: https://github.com/nodejs/node/pull/50960 Refs: https://github.com/nodejs/single-executable/issues/68 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
This commit is contained in:
parent
7235f69078
commit
ce8f085d26
12 changed files with 578 additions and 12 deletions
|
@ -2367,6 +2367,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`
|
||||
|
@ -2513,6 +2524,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`
|
||||
|
|
|
@ -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.
|
||||
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
|
||||
|
|
|
@ -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',
|
||||
]);
|
||||
|
|
|
@ -1632,6 +1632,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);
|
||||
|
@ -1715,6 +1717,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
lib/sea.js
Normal file
75
lib/sea.js
Normal file
|
@ -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,
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,6 +652,7 @@ 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) {
|
||||
|
@ -572,6 +660,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
|||
registry->Register(IsExperimentalSeaWarningNeeded);
|
||||
registry->Register(GetCodePath);
|
||||
registry->Register(GetCodeCache);
|
||||
registry->Register(GetAsset);
|
||||
}
|
||||
|
||||
} // namespace sea
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "node_exit_code.h"
|
||||
|
@ -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<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
test/fixtures/sea/get-asset.js
vendored
Normal file
100
test/fixtures/sea/get-asset.js
vendored
Normal file
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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
test/sequential/test-single-executable-application-assets.js
Normal file
130
test/sequential/test-single-executable-application-assets.js
Normal file
|
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue