sea: implement sea.getAssetKeys()

This adds a new API to allow the bundled script in SEA to query
the list of assets.

PR-URL: https://github.com/nodejs/node/pull/59661
Refs: https://github.com/nodejs/single-executable/discussions/112
Reviewed-By: Darshan Sen <raisinten@gmail.com>
This commit is contained in:
Joyee Cheung 2025-09-04 13:58:50 +02:00 committed by GitHub
parent 66fcca4328
commit 6428e2e4ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 218 additions and 4 deletions

View File

@ -221,7 +221,10 @@ executable, users can retrieve the assets using the [`sea.getAsset()`][] and
The single-executable application can access the assets as follows:
```cjs
const { getAsset, getAssetAsBlob, getRawAsset } = require('node:sea');
const { getAsset, getAssetAsBlob, getRawAsset, getAssetKeys } = require('node:sea');
// Get all asset keys.
const keys = getAssetKeys();
console.log(keys); // ['a.jpg', 'b.txt']
// Returns a copy of the data in an ArrayBuffer.
const image = getAsset('a.jpg');
// Returns a string decoded from the asset as UTF8.
@ -232,8 +235,8 @@ const blob = getAssetAsBlob('a.jpg');
const raw = getRawAsset('a.jpg');
```
See documentation of the [`sea.getAsset()`][], [`sea.getAssetAsBlob()`][] and [`sea.getRawAsset()`][]
APIs for more information.
See documentation of the [`sea.getAsset()`][], [`sea.getAssetAsBlob()`][],
[`sea.getRawAsset()`][] and [`sea.getAssetKeys()`][] APIs for more information.
### Startup snapshot support
@ -429,6 +432,19 @@ writes to the returned array buffer is likely to result in a crash.
`assets` field in the single-executable application configuration.
* Returns: {ArrayBuffer}
### `sea.getAssetKeys()`
<!-- YAML
added: REPLACEME
-->
* Returns {string\[]} An array containing all the keys of the assets
embedded in the executable. If no assets are embedded, returns an empty array.
This method can be used to retrieve an array of all the keys of assets
embedded into the single-executable application.
An error is thrown when not running inside a single-executable application.
### `require(id)` in the injected main script is not file based
`require()` in the injected main script is not the same as the [`require()`][]
@ -503,6 +519,7 @@ to help us document them.
[`require.main`]: modules.md#accessing-the-main-module
[`sea.getAsset()`]: #seagetassetkey-encoding
[`sea.getAssetAsBlob()`]: #seagetassetasblobkey-options
[`sea.getAssetKeys()`]: #seagetassetkeys
[`sea.getRawAsset()`]: #seagetrawassetkey
[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data
[`v8.startupSnapshot` API]: v8.md#startup-snapshot-api

View File

@ -3,7 +3,7 @@ const {
ArrayBufferPrototypeSlice,
} = primordials;
const { isSea, getAsset: getAssetInternal } = internalBinding('sea');
const { isSea, getAsset: getAssetInternal, getAssetKeys: getAssetKeysInternal } = internalBinding('sea');
const { TextDecoder } = require('internal/encoding');
const { validateString } = require('internal/validators');
const {
@ -68,9 +68,23 @@ function getAssetAsBlob(key, options) {
return new Blob([asset], options);
}
/**
* Returns an array of all the keys of assets embedded into the
* single-executable application.
* @returns {string[]}
*/
function getAssetKeys() {
if (!isSea()) {
throw new ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION();
}
return getAssetKeysInternal() || [];
}
module.exports = {
isSea,
getAsset,
getRawAsset,
getAssetAsBlob,
getAssetKeys,
};

View File

@ -29,6 +29,7 @@
#include <vector>
using node::ExitCode;
using v8::Array;
using v8::ArrayBuffer;
using v8::BackingStore;
using v8::Context;
@ -807,6 +808,25 @@ void GetAsset(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(ab);
}
void GetAssetKeys(const FunctionCallbackInfo<Value>& args) {
CHECK_EQ(args.Length(), 0);
Isolate* isolate = args.GetIsolate();
SeaResource sea_resource = FindSingleExecutableResource();
Local<Context> context = isolate->GetCurrentContext();
LocalVector<Value> keys(isolate);
keys.reserve(sea_resource.assets.size());
for (const auto& [key, _] : sea_resource.assets) {
Local<Value> key_str;
if (!ToV8Value(context, key).ToLocal(&key_str)) {
return;
}
keys.push_back(key_str);
}
Local<Array> result = Array::New(isolate, keys.data(), keys.size());
args.GetReturnValue().Set(result);
}
MaybeLocal<Value> LoadSingleExecutableApplication(
const StartExecutionCallbackInfo& info) {
// Here we are currently relying on the fact that in NodeMainInstance::Run(),
@ -858,12 +878,14 @@ void Initialize(Local<Object> target,
"isExperimentalSeaWarningNeeded",
IsExperimentalSeaWarningNeeded);
SetMethod(context, target, "getAsset", GetAsset);
SetMethod(context, target, "getAssetKeys", GetAssetKeys);
}
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(IsSea);
registry->Register(IsExperimentalSeaWarningNeeded);
registry->Register(GetAsset);
registry->Register(GetAssetKeys);
}
} // namespace sea

9
test/fixtures/sea/get-asset-keys.js vendored Normal file
View File

@ -0,0 +1,9 @@
'use strict';
const { isSea, getAssetKeys } = require('node:sea');
const assert = require('node:assert');
assert(isSea());
const keys = getAssetKeys();
console.log('Asset keys:', JSON.stringify(keys.sort()));

View File

@ -0,0 +1,11 @@
'use strict';
require('../common');
const { getAssetKeys } = require('node:sea');
const assert = require('node:assert');
// Test that getAssetKeys throws when not in SEA
assert.throws(() => getAssetKeys(), {
code: 'ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION'
});

View File

@ -57,6 +57,8 @@ test-watch-mode-inspect: SKIP
test-single-executable-application: SKIP
test-single-executable-application-assets: SKIP
test-single-executable-application-assets-raw: SKIP
test-single-executable-application-asset-keys-empty: SKIP
test-single-executable-application-asset-keys: SKIP
test-single-executable-application-disable-experimental-sea-warning: SKIP
test-single-executable-application-empty: SKIP
test-single-executable-application-exec-argv: SKIP

View File

@ -0,0 +1,65 @@
'use strict';
// This test verifies that the `getAssetKeys()` function works correctly
// in a single executable application without any assets.
require('../common');
const {
generateSEA,
skipIfSingleExecutableIsNotSupported,
} = require('../common/sea');
skipIfSingleExecutableIsNotSupported();
const tmpdir = require('../common/tmpdir');
const { copyFileSync, writeFileSync, existsSync } = require('fs');
const {
spawnSyncAndExitWithoutError,
spawnSyncAndAssert,
} = require('../common/child_process');
const assert = require('assert');
const fixtures = require('../common/fixtures');
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-keys.js'), tmpdir.resolve('sea.js'));
writeFileSync(tmpdir.resolve('sea-config.json'), `
{
"main": "sea.js",
"output": "sea-prep.blob"
}
`, 'utf8');
spawnSyncAndExitWithoutError(
process.execPath,
['--experimental-sea-config', 'sea-config.json'],
{
env: {
NODE_DEBUG_NATIVE: 'SEA',
...process.env,
},
cwd: tmpdir.path
},
{});
assert(existsSync(seaPrepBlob));
generateSEA(outputFile, process.execPath, seaPrepBlob);
spawnSyncAndAssert(
outputFile,
{
env: {
...process.env,
NODE_DEBUG_NATIVE: 'SEA',
}
},
{
stdout: /Asset keys: \[\]/,
}
);

View File

@ -0,0 +1,74 @@
'use strict';
// This test verifies that the `getAssetKeys()` function works correctly
// in a single executable application with assets.
require('../common');
const {
generateSEA,
skipIfSingleExecutableIsNotSupported,
} = require('../common/sea');
skipIfSingleExecutableIsNotSupported();
const tmpdir = require('../common/tmpdir');
const { copyFileSync, writeFileSync, existsSync } = require('fs');
const {
spawnSyncAndExitWithoutError,
spawnSyncAndAssert,
} = require('../common/child_process');
const assert = require('assert');
const fixtures = require('../common/fixtures');
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-keys.js'), tmpdir.resolve('sea.js'));
writeFileSync(tmpdir.resolve('asset-1.txt'), 'This is asset 1');
writeFileSync(tmpdir.resolve('asset-2.txt'), 'This is asset 2');
writeFileSync(tmpdir.resolve('asset-3.txt'), 'This is asset 3');
writeFileSync(configFile, `
{
"main": "sea.js",
"output": "sea-prep.blob",
"assets": {
"asset-1.txt": "asset-1.txt",
"asset-2.txt": "asset-2.txt",
"asset-3.txt": "asset-3.txt"
}
}
`, 'utf8');
spawnSyncAndExitWithoutError(
process.execPath,
['--experimental-sea-config', 'sea-config.json'],
{
env: {
NODE_DEBUG_NATIVE: 'SEA',
...process.env,
},
cwd: tmpdir.path
},
{});
assert(existsSync(seaPrepBlob));
generateSEA(outputFile, process.execPath, seaPrepBlob);
spawnSyncAndAssert(
outputFile,
{
env: {
...process.env,
NODE_DEBUG_NATIVE: 'SEA',
}
},
{
stdout: /Asset keys: \["asset-1\.txt","asset-2\.txt","asset-3\.txt"\]/,
}
);