Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions doc/api/single-executable-applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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()`][]
Expand Down Expand Up @@ -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
Expand Down
16 changes: 15 additions & 1 deletion lib/sea.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
};
22 changes: 22 additions & 0 deletions src/node_sea.cc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include <vector>

using node::ExitCode;
using v8::Array;
using v8::ArrayBuffer;
using v8::BackingStore;
using v8::Context;
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions test/fixtures/sea/get-asset-keys.js
Original file line number Diff line number Diff line change
@@ -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()));
11 changes: 11 additions & 0 deletions test/parallel/test-sea-get-asset-keys.js
Original file line number Diff line number Diff line change
@@ -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'
});
2 changes: 2 additions & 0 deletions test/sequential/sequential.status
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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: \[\]/,
}
);
74 changes: 74 additions & 0 deletions test/sequential/test-single-executable-application-asset-keys.js
Original file line number Diff line number Diff line change
@@ -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"\]/,
}
);
Loading