Skip to content

Commit 0bf9775

Browse files
joyeecheungtargos
authored andcommitted
sea: implement sea.getAssetKeys()
This adds a new API to allow the bundled script in SEA to query the list of assets. PR-URL: #59661 Refs: nodejs/single-executable#112 Reviewed-By: Darshan Sen <raisinten@gmail.com>
1 parent 01b66b1 commit 0bf9775

8 files changed

+218
-4
lines changed

doc/api/single-executable-applications.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,10 @@ executable, users can retrieve the assets using the [`sea.getAsset()`][] and
221221
The single-executable application can access the assets as follows:
222222
223223
```cjs
224-
const { getAsset, getAssetAsBlob, getRawAsset } = require('node:sea');
224+
const { getAsset, getAssetAsBlob, getRawAsset, getAssetKeys } = require('node:sea');
225+
// Get all asset keys.
226+
const keys = getAssetKeys();
227+
console.log(keys); // ['a.jpg', 'b.txt']
225228
// Returns a copy of the data in an ArrayBuffer.
226229
const image = getAsset('a.jpg');
227230
// Returns a string decoded from the asset as UTF8.
@@ -232,8 +235,8 @@ const blob = getAssetAsBlob('a.jpg');
232235
const raw = getRawAsset('a.jpg');
233236
```
234237
235-
See documentation of the [`sea.getAsset()`][], [`sea.getAssetAsBlob()`][] and [`sea.getRawAsset()`][]
236-
APIs for more information.
238+
See documentation of the [`sea.getAsset()`][], [`sea.getAssetAsBlob()`][],
239+
[`sea.getRawAsset()`][] and [`sea.getAssetKeys()`][] APIs for more information.
237240
238241
### Startup snapshot support
239242
@@ -429,6 +432,19 @@ writes to the returned array buffer is likely to result in a crash.
429432
`assets` field in the single-executable application configuration.
430433
* Returns: {ArrayBuffer}
431434
435+
### `sea.getAssetKeys()`
436+
437+
<!-- YAML
438+
added: REPLACEME
439+
-->
440+
441+
* Returns {string\[]} An array containing all the keys of the assets
442+
embedded in the executable. If no assets are embedded, returns an empty array.
443+
444+
This method can be used to retrieve an array of all the keys of assets
445+
embedded into the single-executable application.
446+
An error is thrown when not running inside a single-executable application.
447+
432448
### `require(id)` in the injected main script is not file based
433449
434450
`require()` in the injected main script is not the same as the [`require()`][]
@@ -503,6 +519,7 @@ to help us document them.
503519
[`require.main`]: modules.md#accessing-the-main-module
504520
[`sea.getAsset()`]: #seagetassetkey-encoding
505521
[`sea.getAssetAsBlob()`]: #seagetassetasblobkey-options
522+
[`sea.getAssetKeys()`]: #seagetassetkeys
506523
[`sea.getRawAsset()`]: #seagetrawassetkey
507524
[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data
508525
[`v8.startupSnapshot` API]: v8.md#startup-snapshot-api

lib/sea.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const {
33
ArrayBufferPrototypeSlice,
44
} = primordials;
55

6-
const { isSea, getAsset: getAssetInternal } = internalBinding('sea');
6+
const { isSea, getAsset: getAssetInternal, getAssetKeys: getAssetKeysInternal } = internalBinding('sea');
77
const { TextDecoder } = require('internal/encoding');
88
const { validateString } = require('internal/validators');
99
const {
@@ -68,9 +68,23 @@ function getAssetAsBlob(key, options) {
6868
return new Blob([asset], options);
6969
}
7070

71+
/**
72+
* Returns an array of all the keys of assets embedded into the
73+
* single-executable application.
74+
* @returns {string[]}
75+
*/
76+
function getAssetKeys() {
77+
if (!isSea()) {
78+
throw new ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION();
79+
}
80+
81+
return getAssetKeysInternal() || [];
82+
}
83+
7184
module.exports = {
7285
isSea,
7386
getAsset,
7487
getRawAsset,
7588
getAssetAsBlob,
89+
getAssetKeys,
7690
};

src/node_sea.cc

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <vector>
3030

3131
using node::ExitCode;
32+
using v8::Array;
3233
using v8::ArrayBuffer;
3334
using v8::BackingStore;
3435
using v8::Context;
@@ -807,6 +808,25 @@ void GetAsset(const FunctionCallbackInfo<Value>& args) {
807808
args.GetReturnValue().Set(ab);
808809
}
809810

811+
void GetAssetKeys(const FunctionCallbackInfo<Value>& args) {
812+
CHECK_EQ(args.Length(), 0);
813+
Isolate* isolate = args.GetIsolate();
814+
SeaResource sea_resource = FindSingleExecutableResource();
815+
816+
Local<Context> context = isolate->GetCurrentContext();
817+
LocalVector<Value> keys(isolate);
818+
keys.reserve(sea_resource.assets.size());
819+
for (const auto& [key, _] : sea_resource.assets) {
820+
Local<Value> key_str;
821+
if (!ToV8Value(context, key).ToLocal(&key_str)) {
822+
return;
823+
}
824+
keys.push_back(key_str);
825+
}
826+
Local<Array> result = Array::New(isolate, keys.data(), keys.size());
827+
args.GetReturnValue().Set(result);
828+
}
829+
810830
MaybeLocal<Value> LoadSingleExecutableApplication(
811831
const StartExecutionCallbackInfo& info) {
812832
// Here we are currently relying on the fact that in NodeMainInstance::Run(),
@@ -858,12 +878,14 @@ void Initialize(Local<Object> target,
858878
"isExperimentalSeaWarningNeeded",
859879
IsExperimentalSeaWarningNeeded);
860880
SetMethod(context, target, "getAsset", GetAsset);
881+
SetMethod(context, target, "getAssetKeys", GetAssetKeys);
861882
}
862883

863884
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
864885
registry->Register(IsSea);
865886
registry->Register(IsExperimentalSeaWarningNeeded);
866887
registry->Register(GetAsset);
888+
registry->Register(GetAssetKeys);
867889
}
868890

869891
} // namespace sea

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use strict';
2+
3+
const { isSea, getAssetKeys } = require('node:sea');
4+
const assert = require('node:assert');
5+
6+
assert(isSea());
7+
8+
const keys = getAssetKeys();
9+
console.log('Asset keys:', JSON.stringify(keys.sort()));
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use strict';
2+
3+
require('../common');
4+
5+
const { getAssetKeys } = require('node:sea');
6+
const assert = require('node:assert');
7+
8+
// Test that getAssetKeys throws when not in SEA
9+
assert.throws(() => getAssetKeys(), {
10+
code: 'ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION'
11+
});

test/sequential/sequential.status

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ test-watch-mode-inspect: SKIP
5757
test-single-executable-application: SKIP
5858
test-single-executable-application-assets: SKIP
5959
test-single-executable-application-assets-raw: SKIP
60+
test-single-executable-application-asset-keys-empty: SKIP
61+
test-single-executable-application-asset-keys: SKIP
6062
test-single-executable-application-disable-experimental-sea-warning: SKIP
6163
test-single-executable-application-empty: SKIP
6264
test-single-executable-application-exec-argv: SKIP
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use strict';
2+
3+
// This test verifies that the `getAssetKeys()` function works correctly
4+
// in a single executable application without any assets.
5+
6+
require('../common');
7+
8+
const {
9+
generateSEA,
10+
skipIfSingleExecutableIsNotSupported,
11+
} = require('../common/sea');
12+
13+
skipIfSingleExecutableIsNotSupported();
14+
15+
const tmpdir = require('../common/tmpdir');
16+
17+
const { copyFileSync, writeFileSync, existsSync } = require('fs');
18+
const {
19+
spawnSyncAndExitWithoutError,
20+
spawnSyncAndAssert,
21+
} = require('../common/child_process');
22+
const assert = require('assert');
23+
const fixtures = require('../common/fixtures');
24+
25+
const seaPrepBlob = tmpdir.resolve('sea-prep.blob');
26+
const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea');
27+
28+
tmpdir.refresh();
29+
copyFileSync(fixtures.path('sea', 'get-asset-keys.js'), tmpdir.resolve('sea.js'));
30+
31+
writeFileSync(tmpdir.resolve('sea-config.json'), `
32+
{
33+
"main": "sea.js",
34+
"output": "sea-prep.blob"
35+
}
36+
`, 'utf8');
37+
38+
spawnSyncAndExitWithoutError(
39+
process.execPath,
40+
['--experimental-sea-config', 'sea-config.json'],
41+
{
42+
env: {
43+
NODE_DEBUG_NATIVE: 'SEA',
44+
...process.env,
45+
},
46+
cwd: tmpdir.path
47+
},
48+
{});
49+
50+
assert(existsSync(seaPrepBlob));
51+
52+
generateSEA(outputFile, process.execPath, seaPrepBlob);
53+
54+
spawnSyncAndAssert(
55+
outputFile,
56+
{
57+
env: {
58+
...process.env,
59+
NODE_DEBUG_NATIVE: 'SEA',
60+
}
61+
},
62+
{
63+
stdout: /Asset keys: \[\]/,
64+
}
65+
);
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
'use strict';
2+
3+
// This test verifies that the `getAssetKeys()` function works correctly
4+
// in a single executable application with assets.
5+
6+
require('../common');
7+
8+
const {
9+
generateSEA,
10+
skipIfSingleExecutableIsNotSupported,
11+
} = require('../common/sea');
12+
13+
skipIfSingleExecutableIsNotSupported();
14+
15+
const tmpdir = require('../common/tmpdir');
16+
17+
const { copyFileSync, writeFileSync, existsSync } = require('fs');
18+
const {
19+
spawnSyncAndExitWithoutError,
20+
spawnSyncAndAssert,
21+
} = require('../common/child_process');
22+
const assert = require('assert');
23+
const fixtures = require('../common/fixtures');
24+
25+
const configFile = tmpdir.resolve('sea-config.json');
26+
const seaPrepBlob = tmpdir.resolve('sea-prep.blob');
27+
const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea');
28+
29+
tmpdir.refresh();
30+
copyFileSync(fixtures.path('sea', 'get-asset-keys.js'), tmpdir.resolve('sea.js'));
31+
writeFileSync(tmpdir.resolve('asset-1.txt'), 'This is asset 1');
32+
writeFileSync(tmpdir.resolve('asset-2.txt'), 'This is asset 2');
33+
writeFileSync(tmpdir.resolve('asset-3.txt'), 'This is asset 3');
34+
35+
writeFileSync(configFile, `
36+
{
37+
"main": "sea.js",
38+
"output": "sea-prep.blob",
39+
"assets": {
40+
"asset-1.txt": "asset-1.txt",
41+
"asset-2.txt": "asset-2.txt",
42+
"asset-3.txt": "asset-3.txt"
43+
}
44+
}
45+
`, 'utf8');
46+
47+
spawnSyncAndExitWithoutError(
48+
process.execPath,
49+
['--experimental-sea-config', 'sea-config.json'],
50+
{
51+
env: {
52+
NODE_DEBUG_NATIVE: 'SEA',
53+
...process.env,
54+
},
55+
cwd: tmpdir.path
56+
},
57+
{});
58+
59+
assert(existsSync(seaPrepBlob));
60+
61+
generateSEA(outputFile, process.execPath, seaPrepBlob);
62+
63+
spawnSyncAndAssert(
64+
outputFile,
65+
{
66+
env: {
67+
...process.env,
68+
NODE_DEBUG_NATIVE: 'SEA',
69+
}
70+
},
71+
{
72+
stdout: /Asset keys: \["asset-1\.txt","asset-2\.txt","asset-3\.txt"\]/,
73+
}
74+
);

0 commit comments

Comments
 (0)