diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index 41e4bd7383ef7f..d1000bcbef269a 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -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()` + + + +* 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 diff --git a/lib/sea.js b/lib/sea.js index f7727014c4e3c9..5da9a75d095d7a 100644 --- a/lib/sea.js +++ b/lib/sea.js @@ -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, }; diff --git a/src/node_sea.cc b/src/node_sea.cc index 49071304262f10..e66a2299db74eb 100644 --- a/src/node_sea.cc +++ b/src/node_sea.cc @@ -29,6 +29,7 @@ #include using node::ExitCode; +using v8::Array; using v8::ArrayBuffer; using v8::BackingStore; using v8::Context; @@ -807,6 +808,25 @@ void GetAsset(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(ab); } +void GetAssetKeys(const FunctionCallbackInfo& args) { + CHECK_EQ(args.Length(), 0); + Isolate* isolate = args.GetIsolate(); + SeaResource sea_resource = FindSingleExecutableResource(); + + Local context = isolate->GetCurrentContext(); + LocalVector keys(isolate); + keys.reserve(sea_resource.assets.size()); + for (const auto& [key, _] : sea_resource.assets) { + Local key_str; + if (!ToV8Value(context, key).ToLocal(&key_str)) { + return; + } + keys.push_back(key_str); + } + Local result = Array::New(isolate, keys.data(), keys.size()); + args.GetReturnValue().Set(result); +} + MaybeLocal LoadSingleExecutableApplication( const StartExecutionCallbackInfo& info) { // Here we are currently relying on the fact that in NodeMainInstance::Run(), @@ -858,12 +878,14 @@ void Initialize(Local 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 diff --git a/test/fixtures/sea/get-asset-keys.js b/test/fixtures/sea/get-asset-keys.js new file mode 100644 index 00000000000000..2330b8fa3d9549 --- /dev/null +++ b/test/fixtures/sea/get-asset-keys.js @@ -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())); diff --git a/test/parallel/test-sea-get-asset-keys.js b/test/parallel/test-sea-get-asset-keys.js new file mode 100644 index 00000000000000..8fe7fed4f0c78a --- /dev/null +++ b/test/parallel/test-sea-get-asset-keys.js @@ -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' +}); diff --git a/test/sequential/sequential.status b/test/sequential/sequential.status index 63414f94193595..b1cdd9c368d5d6 100644 --- a/test/sequential/sequential.status +++ b/test/sequential/sequential.status @@ -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 diff --git a/test/sequential/test-single-executable-application-asset-keys-empty.js b/test/sequential/test-single-executable-application-asset-keys-empty.js new file mode 100644 index 00000000000000..77892699a95794 --- /dev/null +++ b/test/sequential/test-single-executable-application-asset-keys-empty.js @@ -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: \[\]/, + } +); diff --git a/test/sequential/test-single-executable-application-asset-keys.js b/test/sequential/test-single-executable-application-asset-keys.js new file mode 100644 index 00000000000000..e210f61579b342 --- /dev/null +++ b/test/sequential/test-single-executable-application-asset-keys.js @@ -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"\]/, + } +);