sea: mount bundled assets as a virtual file system

Support "useVfs": true in the SEA configuration: mount the bundled
assets as a read-only VFS and run the CommonJS or ESM main script from
inside the mount.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: https://github.com/nodejs/node/pull/65675
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
This commit is contained in:
Matteo Collina authored and GitHub committed 2026-09-03 10:52:15 +00:00
1 parent 4cd3d98510
commit 4e207b1c34
28 files changed
+1059 -2

No files matched your search

+104
View File
@@ -116,6 +116,7 @@ The configuration currently reads the following top-level fields:
"disableExperimentalSEAWarning": true, // Default: false
"useSnapshot": false, // Default: false
"useCodeCache": true, // Default: false
"useVfs": true, // Default: false
"execArgv": ["--no-warnings", "--max-old-space-size=4096"], // Optional
"execArgvExtension": "env", // Default: "env", options: "none", "env", "cli"
"assets": { // Optional
@@ -175,6 +176,105 @@ const raw = getRawAsset('a.jpg');
See documentation of the [`sea.getAsset()`][], [`sea.getAssetAsBlob()`][],
[`sea.getRawAsset()`][] and [`sea.getAssetKeys()`][] APIs for more information.
### Virtual file system (VFS) for assets
<!-- YAML
added: REPLACEME
-->
> Stability: 1.0 - Early development
In addition to using the `node:sea` API to access individual assets, the
bundled assets can be exposed as a read-only [virtual file system][] and
accessed through standard `node:fs` APIs. To enable this, set
`"useVfs": true` in the SEA configuration.
A virtual file system never shadows the real file system: it is mounted at a
reserved mount point that cannot exist on the real file system, and the mount
point is chosen at runtime rather than being a fixed path. When `useVfs` is
enabled, the injected main script itself is placed at the root of the mount
and executed from there, so `__filename` and `__dirname` point inside the
virtual file system instead of reflecting [`process.execPath`][]. Bundled
code therefore reaches the assets through `__dirname`-relative paths and
relative [`require()`][] calls, without having to know the mount point:
```cjs
const fs = require('node:fs');
const path = require('node:path');
// __dirname is the root of the virtual file system holding the assets.
const rawConfig = fs.readFileSync(path.join(__dirname, 'config.json'), 'utf8');
const data = fs.readFileSync(path.join(__dirname, 'data/file.txt'));
// Directory operations work too.
const files = fs.readdirSync(path.join(__dirname, 'assets'));
// Check if a bundled file exists.
if (fs.existsSync(path.join(__dirname, 'optional.json'))) {
// ...
}
```
The VFS supports the `node:fs` operations for reading files and directories.
Since the SEA VFS is read-only, write operations fail with `EROFS`. See the
[VFS documentation][] for the full list of supported operations.
#### Loading modules from the VFS in a SEA
When `useVfs` is enabled, the main script is executed from inside the
virtual file system, and `require()` uses the [module loader
integration][] of the VFS to load modules from the bundled assets. This
supports relative requires (e.g. `require('./helper.js')`) as well as
`node_modules` package lookups, which are confined to the mount:
```cjs
// Require bundled modules using relative paths.
const myModule = require('./lib/mymodule.js');
// Packages bundled under the node_modules asset prefix also resolve.
const dep = require('some-package');
```
#### ESM entry points
`"useVfs": true` also supports `"mainFormat": "module"`. The ESM main
script is loaded from inside the mount through the ESM loader, so
`import.meta.url`, `import.meta.filename`, and `import.meta.dirname`
reflect the location of the main script in the virtual file system, and
static and dynamic imports resolve against the bundled assets:
```mjs
import fs from 'node:fs';
import path from 'node:path';
// import.meta.dirname is the root of the virtual file system.
const data = fs.readFileSync(
path.join(import.meta.dirname, 'data/file.txt'));
// Relative and bare specifier imports resolve inside the mount.
import myModule from './lib/mymodule.mjs';
const lazy = await import('./lib/lazy.mjs');
```
Module format detection works the same way as on the real file
system: name bundled ES modules with the `.mjs` extension (or provide the
relevant `package.json` files as assets) so they are interpreted as ESM.
#### Snapshot and code caching limitations
`"useVfs": true` cannot be used together with `"useSnapshot": true` or
`"useCodeCache": true`. The code cache limitation is due to incomplete
implementation, not a technical impossibility. Consider bundling the
application if startup performance matters and do not rely on module loading
from the VFS in that case.
#### Native addon limitations
Native addons (`.node` files) cannot be loaded directly from the VFS because
`process.dlopen()` requires files on the real file system. To use native
addons in a SEA with VFS, write the asset to a temporary file first. See
[Using native addons in the injected main script][] for an example.
### Startup snapshot support
The `useSnapshot` field can be used to enable startup snapshot support. In this
@@ -648,6 +748,8 @@ to help us document them.
[Generating single executable preparation blobs]: #1-generating-single-executable-preparation-blobs
[Mach-O]: https://en.wikipedia.org/wiki/Mach-O
[PE]: https://en.wikipedia.org/wiki/Portable_Executable
[Using native addons in the injected main script]: #using-native-addons-in-the-injected-main-script
[VFS documentation]: vfs.md
[Windows SDK]: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/
[`process.execPath`]: process.md#processexecpath
[`require()`]: modules.md#requireid
@@ -660,8 +762,10 @@ to help us document them.
[`v8.startupSnapshot` API]: v8.md#startup-snapshot-api
[documentation about startup snapshot support in Node.js]: cli.md#--build-snapshot
[fuse]: https://www.electronjs.org/docs/latest/tutorial/fuses
[module loader integration]: vfs.md#module-loader-integration
[postject]: https://github.com/nodejs/postject
[postject-linux-arm64-issue]: https://github.com/nodejs/postject/issues/105
[signtool]: https://learn.microsoft.com/en-us/windows/win32/seccrypto/signtool
[single executable applications]: https://github.com/nodejs/single-executable
[supported by Node.js]: https://github.com/nodejs/node/blob/main/BUILDING.md#platform-list
[virtual file system]: vfs.md
+32
View File
@@ -419,6 +419,37 @@ system, the callers are responsible for avoiding removal or
invalidation of modules in the virtual file system while they are
being loaded.
## Use with Single Executable Applications
When running as a [Single Executable Application][] built with
`"useVfs": true` in the SEA configuration, the bundled assets are
automatically mounted as a read-only virtual file system and the injected
main script is executed from the root of the mount. No additional setup is
required. Since the mount point is reserved and chosen at runtime, bundled
code accesses the assets through `__dirname`-relative paths and relative
`require()` calls rather than through a fixed path:
```cjs
// In the SEA main script, __dirname is the root of the mounted assets.
const fs = require('node:fs');
const path = require('node:path');
const config = JSON.parse(
fs.readFileSync(path.join(__dirname, 'config.json'), 'utf8'));
const template = fs.readFileSync(
path.join(__dirname, 'templates/index.html'), 'utf8');
```
ESM entry points (`"mainFormat": "module"`) are supported: the main module
is loaded from inside the mount through the ESM loader, and
`import.meta.dirname` points at the mount root.
`"useVfs"` cannot be used together with `"useSnapshot"` or `"useCodeCache"`.
The SEA configuration parser will error if either combination is detected.
See the [Single Executable Application][] documentation for more information
on creating SEA builds with assets.
## Class: `VirtualProvider`
<!-- YAML
@@ -591,6 +622,7 @@ fields use synthetic but stable values:
[CommonJS resolution algorithm]: modules.md#all-together
[ES modules resolution algorithm]: esm.md#resolution-algorithm
[Explicit Resource Management]: https://github.com/tc39/proposal-explicit-resource-management
[Single Executable Application]: single-executable-applications.md
[`MemoryProvider`]: #class-memoryprovider
[`RealFSProvider`]: #class-realfsprovider
[`VirtualFileSystem`]: #class-virtualfilesystem
+51 -2
View File
@@ -12,10 +12,15 @@
const {
prepareMainThreadExecution,
} = require('internal/process/pre_execution');
const { isExperimentalSeaWarningNeeded, isSea } = internalBinding('sea');
const {
isExperimentalSeaWarningNeeded,
isSea,
isVfsEnabled,
mainCodePath: seaMainCodePath,
} = internalBinding('sea');
const { emitExperimentalWarning } = require('internal/util');
const { emitWarningSync } = require('internal/process/warning');
const { Module } = require('internal/modules/cjs/loader');
const { Module, wrapModuleLoad } = require('internal/modules/cjs/loader');
const { compileFunctionForCJSLoader } = internalBinding('contextify');
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
const { pathToFileURL } = require('internal/url');
@@ -120,10 +125,54 @@ function embedderRunESM(content, filename) {
return wrap.getNamespace();
}
/* c8 ignore start -- only reachable in an actual SEA binary */
/**
* Mounts the SEA virtual file system with the main script placed at the
* mount point root, and returns the path of the main script inside the
* mount, or null when the VFS could not be set up.
* @param {string} content The source of the SEA main script
* @returns {string|null} The VFS path of the main script
*/
function setUpSeaVfs(content) {
const mainName = path.basename(seaMainCodePath || process.execPath);
const { initSeaVfs } = require('internal/vfs/sea');
const seaVfs = initSeaVfs({ extraFiles: { [mainName]: content } });
if (seaVfs === null) {
return null;
}
return path.join(seaVfs.mountPoint, mainName);
}
/* c8 ignore stop */
function embedderRunEntryPoint(content, format, filename) {
format ||= moduleFormats.kCommonJS;
filename ||= process.execPath;
/* c8 ignore start -- only reachable in an actual SEA binary */
if (isLoadingSea && isVfsEnabled()) {
// Run the main script from inside the SEA VFS mount so that
// `__filename`, `__dirname`, `import.meta`, relative requires and
// imports, and `node_modules` lookups all resolve against the
// bundled assets.
const vfsMain = setUpSeaVfs(content);
if (vfsMain !== null) {
if (format === moduleFormats.kCommonJS) {
return wrapModuleLoad(vfsMain, null, true);
} else if (format === moduleFormats.kModule) {
const { runEntryPointWithESMLoader } =
require('internal/modules/run_main');
const mainURL = pathToFileURL(vfsMain);
return runEntryPointWithESMLoader((cascadedLoader) => {
// Note that if the graph contains unsettled TLA, this may never
// resolve even after the event loop stops running.
return cascadedLoader.import(
mainURL, undefined, { __proto__: null }, undefined, true);
});
}
}
}
/* c8 ignore stop */
if (format === moduleFormats.kCommonJS) {
return embedderRunCjs(content, filename);
} else if (format === moduleFormats.kModule) {
+349
View File
@@ -0,0 +1,349 @@
'use strict';
const {
ArrayFrom,
ArrayPrototypeFilter,
ArrayPrototypePop,
ArrayPrototypePush,
Boolean,
ObjectKeys,
SafeMap,
SafeSet,
StringPrototypeReplaceAll,
StringPrototypeSplit,
StringPrototypeStartsWith,
Symbol,
Uint8Array,
} = primordials;
const { Buffer } = require('buffer');
const { posix: pathPosix } = require('path');
const { VirtualProvider } = require('internal/vfs/provider');
const { MemoryFileHandle } = require('internal/vfs/file_handle');
const {
codes: {
ERR_INVALID_STATE,
},
} = require('internal/errors');
const {
createENOENT,
createENOTDIR,
createEISDIR,
createEROFS,
} = require('internal/vfs/errors');
const {
createFileStats,
createDirectoryStats,
} = require('internal/vfs/stats');
const { Dirent } = require('internal/fs/utils');
const { kEmptyObject } = require('internal/util');
const {
fs: {
UV_DIRENT_FILE,
UV_DIRENT_DIR,
},
} = internalBinding('constants');
// Private symbols
const kAssets = Symbol('kAssets');
const kExtraFiles = Symbol('kExtraFiles');
const kDirectories = Symbol('kDirectories');
const kGetAsset = Symbol('kGetAsset');
const kSizes = Symbol('kSizes');
/* c8 ignore start -- the SEA provider requires an actual SEA binary to run */
/**
* Read-only provider serving the assets bundled into a Single Executable
* Application. Asset content stays in the executable's SEA blob and is
* copied into JS memory only when a file is opened.
*/
class SEAProvider extends VirtualProvider {
/**
* @param {object} [options] Options
* @param {Record<string, string|Buffer>} [options.extraFiles] Additional
* files to serve alongside the assets, keyed by path (used for the SEA
* main script, whose source lives in the SEA blob but not in the assets)
*/
constructor(options = kEmptyObject) {
super();
const { isSea, getAsset, getAssetKeys } = internalBinding('sea');
if (!isSea()) {
throw new ERR_INVALID_STATE(
'SEAProvider can only be used in a Single Executable Application');
}
this[kGetAsset] = getAsset;
// Map of normalized path -> asset key
this[kAssets] = new SafeMap();
// Map of normalized path -> Buffer content
this[kExtraFiles] = new SafeMap();
// Map of directory path -> SafeSet of child names
this[kDirectories] = new SafeMap();
// Cache of file sizes so stat does not have to copy asset content
this[kSizes] = new SafeMap();
// Root directory always exists
this[kDirectories].set('/', new SafeSet());
const keys = getAssetKeys() || [];
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
this[kAssets].set(this.#normalizePath(key), key);
}
const extraFiles = options.extraFiles;
if (extraFiles !== undefined) {
const paths = ObjectKeys(extraFiles);
for (let i = 0; i < paths.length; i++) {
const path = paths[i];
const content = extraFiles[path];
const buffer = typeof content === 'string' ?
Buffer.from(content) : content;
const normalized = this.#normalizePath(path);
this[kExtraFiles].set(normalized, buffer);
this[kSizes].set(normalized, buffer.length);
}
}
// Derive the directory tree from the file paths
for (const path of this.#filePaths()) {
const parts = ArrayPrototypeFilter(
StringPrototypeSplit(path, '/'), Boolean);
let currentPath = '/';
for (let i = 0; i < parts.length - 1; i++) {
const parentPath = currentPath;
currentPath = pathPosix.join(currentPath, parts[i]);
if (!this[kDirectories].has(currentPath)) {
this[kDirectories].set(currentPath, new SafeSet());
}
this[kDirectories].get(parentPath).add(parts[i]);
}
if (parts.length > 0) {
this[kDirectories].get(pathPosix.dirname(path)).add(
pathPosix.basename(path));
}
}
}
get readonly() {
return true;
}
get supportsSymlinks() {
return false;
}
/**
* Iterates over the normalized paths of all files.
* @yields {string} The normalized path of each file
*/
* #filePaths() {
yield* this[kAssets].keys();
yield* this[kExtraFiles].keys();
}
/**
* Normalizes a path to an absolute posix-style path.
* @param {string} path The path
* @returns {string} Normalized path
*/
#normalizePath(path) {
let normalized = StringPrototypeReplaceAll(path, '\\', '/');
if (!StringPrototypeStartsWith(normalized, '/')) {
normalized = '/' + normalized;
}
return pathPosix.normalize(normalized);
}
/**
* Checks if a normalized path is a file.
* @param {string} path Normalized path
* @returns {boolean}
*/
#isFile(path) {
return this[kAssets].has(path) || this[kExtraFiles].has(path);
}
/**
* Checks if a normalized path is a directory.
* @param {string} path Normalized path
* @returns {boolean}
*/
#isDirectory(path) {
return this[kDirectories].has(path);
}
/**
* Gets the file content as an independently mutable Buffer.
* @param {string} path Normalized path
* @returns {Buffer}
*/
#getContent(path) {
const extra = this[kExtraFiles].get(path);
if (extra !== undefined) {
return Buffer.from(extra);
}
const key = this[kAssets].get(path);
if (key === undefined) {
throw createENOENT('open', path);
}
// getAsset returns a zero-copy ArrayBuffer over the (possibly read-only)
// SEA blob in the executable; copy it so the handle owns mutable memory.
const content = Buffer.from(new Uint8Array(this[kGetAsset](key)));
this[kSizes].set(path, content.length);
return content;
}
/**
* Gets the size of a file, loading the content only on first access.
* @param {string} path Normalized path
* @returns {number}
*/
#getSize(path) {
let size = this[kSizes].get(path);
if (size === undefined) {
size = this.#getContent(path).length;
}
return size;
}
openSync(path, flags, mode) {
// Normalize numeric flags (O_RDONLY === 0) to a string
const normalizedFlags = typeof flags === 'number' ?
(flags === 0 ? 'r' : null) : flags;
if (normalizedFlags !== 'r') {
throw createEROFS('open', path);
}
const normalized = this.#normalizePath(path);
if (this.#isDirectory(normalized)) {
throw createEISDIR('open', path);
}
if (!this.#isFile(normalized)) {
throw createENOENT('open', path);
}
const content = this.#getContent(normalized);
const getStats = () => createFileStats(content.length, { mode: 0o444 });
return new MemoryFileHandle(normalized, 'r', 0o444, content, null,
getStats);
}
async open(path, flags, mode) {
return this.openSync(path, flags, mode);
}
statSync(path, options) {
const normalized = this.#normalizePath(path);
if (this.#isDirectory(normalized)) {
return createDirectoryStats({ mode: 0o555, bigint: options?.bigint });
}
if (this.#isFile(normalized)) {
return createFileStats(this.#getSize(normalized),
{ mode: 0o444, bigint: options?.bigint });
}
throw createENOENT('stat', path);
}
async stat(path, options) {
return this.statSync(path, options);
}
readdirSync(path, options) {
const normalized = this.#normalizePath(path);
if (!this.#isDirectory(normalized)) {
if (this.#isFile(normalized)) {
throw createENOTDIR('scandir', path);
}
throw createENOENT('scandir', path);
}
const withFileTypes = options?.withFileTypes === true;
const recursive = options?.recursive === true;
if (recursive) {
return this.#readdirRecursive(normalized, withFileTypes);
}
const names = ArrayFrom(this[kDirectories].get(normalized));
if (!withFileTypes) {
return names;
}
const dirents = [];
for (let i = 0; i < names.length; i++) {
const childPath = pathPosix.join(normalized, names[i]);
const type = this.#isDirectory(childPath) ?
UV_DIRENT_DIR : UV_DIRENT_FILE;
ArrayPrototypePush(dirents, new Dirent(names[i], type, normalized));
}
return dirents;
}
/**
* Recursively reads directory contents.
* @param {string} dirPath The normalized directory path
* @param {boolean} withFileTypes Whether to return Dirent objects
* @returns {string[]|Dirent[]}
*/
#readdirRecursive(dirPath, withFileTypes) {
const results = [];
// Traverse depth-first in preorder with an explicit frame stack, so a
// deeply nested asset tree cannot exhaust the call stack.
const stack = [{
path: dirPath,
relative: '',
children: ArrayFrom(this[kDirectories].get(dirPath)),
index: 0,
}];
while (stack.length > 0) {
const frame = stack[stack.length - 1];
if (frame.index >= frame.children.length) {
ArrayPrototypePop(stack);
continue;
}
const name = frame.children[frame.index++];
const childPath = pathPosix.join(frame.path, name);
const childRelative = frame.relative ?
`${frame.relative}/${name}` : name;
const isDir = this.#isDirectory(childPath);
if (withFileTypes) {
const type = isDir ? UV_DIRENT_DIR : UV_DIRENT_FILE;
ArrayPrototypePush(results,
new Dirent(childRelative, type, dirPath));
} else {
ArrayPrototypePush(results, childRelative);
}
if (isDir) {
ArrayPrototypePush(stack, {
path: childPath,
relative: childRelative,
children: ArrayFrom(this[kDirectories].get(childPath)),
index: 0,
});
}
}
return results;
}
async readdir(path, options) {
return this.readdirSync(path, options);
}
}
/* c8 ignore stop */
module.exports = {
SEAProvider,
};
+58
View File
@@ -0,0 +1,58 @@
'use strict';
const { isSea, isVfsEnabled } = internalBinding('sea');
const { kEmptyObject } = require('internal/util');
const {
codes: {
ERR_INVALID_STATE,
},
} = require('internal/errors');
let initialized = false;
/* c8 ignore start -- SEA VFS initialization requires an actual SEA binary */
/**
* Initializes the SEA virtual file system: a read-only VFS serving the
* assets bundled into the executable, mounted at its reserved mount point.
* Because a VFS never shadows the real file system, the assets live under
* the mount point returned by `vfs.mountPoint`, not at a fixed path; the
* SEA main script is placed at the mount point root so bundled code can
* reach the assets through `__dirname`-relative paths and relative
* `require()` calls.
* @param {object} [options] Configuration options
* @param {Record<string, string|Buffer>} [options.extraFiles] Additional
* files to serve alongside the assets (used for the SEA main script)
* @returns {VirtualFileSystem|null} The mounted VFS, or null if not running
* as a SEA or VFS is not enabled in the SEA configuration
* @throws {ERR_INVALID_STATE} If already initialized
*/
function initSeaVfs(options = kEmptyObject) {
if (initialized) {
throw new ERR_INVALID_STATE('SEA VFS is already initialized');
}
initialized = true;
if (!isSea() || !isVfsEnabled()) {
return null;
}
const { VirtualFileSystem } = require('internal/vfs/file_system');
const { SEAProvider } = require('internal/vfs/providers/sea');
const provider = new SEAProvider({ extraFiles: options.extraFiles });
// The SEA warning already covers the feature; don't emit the
// VirtualFileSystem experimental warning for the implicit SEA mount.
const vfs = new VirtualFileSystem(provider, {
emitExperimentalWarning: false,
});
vfs.mount();
return vfs;
}
/* c8 ignore stop */
module.exports = {
initSeaVfs,
};
+57
View File
@@ -263,6 +263,15 @@ void IsSea(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(IsSingleExecutable());
}
void IsVfsEnabled(const FunctionCallbackInfo<Value>& args) {
bool enabled = false;
if (IsSingleExecutable()) {
SeaResource sea_resource = FindSingleExecutableResource();
enabled = static_cast<bool>(sea_resource.flags & SeaFlags::kEnableVfs);
}
args.GetReturnValue().Set(enabled);
}
void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo<Value>& args) {
bool is_building_sea =
!per_process::cli_options->experimental_sea_config.empty();
@@ -447,6 +456,16 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
if (use_code_cache_value) {
result.flags |= SeaFlags::kUseCodeCache;
}
} else if (key == "useVfs") {
bool use_vfs;
if (field.value().get_bool().get(use_vfs)) {
FPrintF(
stderr, "\"useVfs\" field of %s is not a Boolean\n", config_path);
return std::nullopt;
}
if (use_vfs) {
result.flags |= SeaFlags::kEnableVfs;
}
} else if (key == "assets") {
simdjson::ondemand::object assets_object;
if (field.value().get_object().get(assets_object)) {
@@ -565,6 +584,19 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
return std::nullopt;
}
if (static_cast<bool>(result.flags & SeaFlags::kEnableVfs)) {
if (static_cast<bool>(result.flags & SeaFlags::kUseSnapshot)) {
FPrintF(stderr,
"\"useVfs\" is not supported when \"useSnapshot\" is true\n");
return std::nullopt;
}
if (static_cast<bool>(result.flags & SeaFlags::kUseCodeCache)) {
FPrintF(stderr,
"\"useVfs\" is not supported when \"useCodeCache\" is true\n");
return std::nullopt;
}
}
if (result.main_path.empty()) {
FPrintF(stderr,
"\"main\" field of %s is not a non-empty string\n",
@@ -924,7 +956,31 @@ void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
Environment* env = Environment::GetCurrent(context);
Isolate* isolate = env->isolate();
if (IsSingleExecutable()) {
SeaResource sea_resource = FindSingleExecutableResource();
// Expose the main script path recorded in the SEA config so the VFS
// integration can place the main script at the mount point root.
if (static_cast<bool>(sea_resource.flags & SeaFlags::kEnableVfs)) {
Local<String> code_path_str;
if (String::NewFromUtf8(isolate,
sea_resource.code_path.data(),
NewStringType::kNormal,
sea_resource.code_path.length())
.ToLocal(&code_path_str)) {
target
->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "mainCodePath"),
code_path_str)
.Check();
}
}
}
SetMethod(context, target, "isSea", IsSea);
SetMethod(context, target, "isVfsEnabled", IsVfsEnabled);
SetMethod(context,
target,
"isExperimentalSeaWarningNeeded",
@@ -935,6 +991,7 @@ void Initialize(Local<Object> target,
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(IsSea);
registry->Register(IsVfsEnabled);
registry->Register(IsExperimentalSeaWarningNeeded);
registry->Register(GetAsset);
registry->Register(GetAssetKeys);
+1
View File
@@ -30,6 +30,7 @@ enum class SeaFlags : uint32_t {
kUseCodeCache = 1 << 2,
kIncludeAssets = 1 << 3,
kIncludeExecArgv = 1 << 4,
kEnableVfs = 1 << 5,
};
enum class SeaExecArgvExtension : uint8_t {
+5
View File
@@ -0,0 +1,5 @@
'use strict';
// CommonJS module imported from the ESM main to test interop inside the VFS.
module.exports = {
sum: (a, b) => a + b,
};
+1
View File
@@ -0,0 +1 @@
{"name":"test-app","version":"1.0.0"}
+1
View File
@@ -0,0 +1 @@
Hello from SEA VFS!
+63
View File
@@ -0,0 +1,63 @@
// The ESM SEA main script runs from inside the VFS mount: import.meta
// reflects the mount, and static imports, dynamic imports, and bare
// specifier lookups all resolve against the bundled assets.
import fs from 'node:fs';
import path from 'node:path';
import assert from 'node:assert';
import { createRequire } from 'node:module';
// Static import of a relative ESM module from the VFS.
import { add, multiply } from './modules/math.mjs';
// Static import of a CommonJS module from the VFS (interop).
import calculator from './modules/calculator.cjs';
// Static import of a bare specifier resolved via the in-VFS node_modules.
import { name as pkgName, greet } from 'test-esm-pkg';
// The main script runs from inside the VFS, not from the executable.
assert.ok(import.meta.url.startsWith('file:'));
assert.strictEqual(path.basename(import.meta.filename), 'main.mjs');
assert.notStrictEqual(import.meta.filename, process.execPath);
console.log('main module runs from', import.meta.url);
// import.meta.dirname is the root of the mounted assets.
const config = JSON.parse(
fs.readFileSync(path.join(import.meta.dirname, 'config.json'), 'utf8'));
assert.strictEqual(config.name, 'test-app');
const greeting = fs.readFileSync(
path.join(import.meta.dirname, 'data', 'greeting.txt'), 'utf8');
assert.strictEqual(greeting, 'Hello from SEA VFS!');
console.log('fs access through import.meta.dirname passed');
// Static ESM import from the VFS.
assert.strictEqual(add(2, 3), 5);
assert.strictEqual(multiply(4, 5), 20);
console.log('static relative import passed');
// CommonJS interop from the VFS.
assert.strictEqual(calculator.sum(10, 20), 30);
console.log('static import of CommonJS module passed');
// Bare specifier resolution confined to the in-VFS node_modules.
assert.strictEqual(pkgName, 'test-esm-pkg');
assert.strictEqual(greet('World'), 'Hello, World!');
console.log('bare specifier import passed');
// Dynamic import from the VFS.
const dynamicMath = await import('./modules/math.mjs');
assert.strictEqual(dynamicMath.add(1, 1), 2);
console.log('dynamic import passed');
// createRequire against the VFS main URL.
const require = createRequire(import.meta.url);
const requiredCalculator = require('./modules/calculator.cjs');
assert.strictEqual(requiredCalculator.sum(3, 4), 7);
console.log('createRequire from import.meta.url passed');
// node:sea and the VFS serve the same content.
const { getAsset } = await import('node:sea');
assert.strictEqual(getAsset('data/greeting.txt', 'utf8'), greeting);
console.log('node:sea API and VFS coexistence passed');
console.log('All SEA VFS ESM tests passed!');
+6
View File
@@ -0,0 +1,6 @@
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
+14
View File
@@ -0,0 +1,14 @@
{
"main": "main.mjs",
"mainFormat": "module",
"output": "sea-prep.blob",
"useVfs": true,
"assets": {
"config.json": "config.json",
"data/greeting.txt": "greeting.txt",
"modules/math.mjs": "math.mjs",
"modules/calculator.cjs": "calculator.cjs",
"node_modules/test-esm-pkg/package.json": "test-esm-pkg-package.json",
"node_modules/test-esm-pkg/index.mjs": "test-esm-pkg-index.mjs"
}
}
+4
View File
@@ -0,0 +1,4 @@
export const name = 'test-esm-pkg';
export function greet(who) {
return `Hello, ${who}!`;
}
+6
View File
@@ -0,0 +1,6 @@
{
"name": "test-esm-pkg",
"version": "1.0.0",
"type": "module",
"exports": { ".": "./index.mjs" }
}
+9
View File
@@ -0,0 +1,9 @@
'use strict';
// This module tests transitive requires - it requires math.js using a
// relative path, which tests that module hooks resolve correctly.
const math = require('./math.js');
module.exports = {
sum: (a, b) => math.add(a, b),
product: (a, b) => math.multiply(a, b),
};
+1
View File
@@ -0,0 +1 @@
{"name":"test-app","version":"1.0.0"}
+1
View File
@@ -0,0 +1 @@
Hello from SEA VFS!
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
add: (a, b) => a + b,
multiply: (a, b) => a * b,
};
+15
View File
@@ -0,0 +1,15 @@
{
"main": "sea.js",
"output": "sea-prep.blob",
"useVfs": true,
"assets": {
"config.json": "config.json",
"data/greeting.txt": "greeting.txt",
"modules/math.js": "math.js",
"modules/calculator.js": "calculator.js",
"node_modules/test-pkg/package.json": "test-pkg-package.json",
"node_modules/test-pkg/index.js": "test-pkg-index.js",
"node_modules/test-exports-pkg/package.json": "test-exports-pkg-package.json",
"node_modules/test-exports-pkg/lib/entry.js": "test-exports-pkg-entry.js"
}
}
+106
View File
@@ -0,0 +1,106 @@
'use strict';
const fs = require('fs');
const path = require('path');
const assert = require('assert');
// The SEA VFS never shadows the real file system: the assets are mounted at
// a reserved mount point and the main script runs from inside the mount, so
// everything is reachable through __dirname-relative paths and relative
// requires.
// The main script runs from inside the VFS, not from the executable.
assert.notStrictEqual(__filename, process.execPath);
assert.strictEqual(path.basename(__filename), 'sea.js');
assert.strictEqual(require.main, module);
console.log('main script runs from', __filename);
// The main script itself is readable through fs.
assert.strictEqual(fs.existsSync(__filename), true);
// Read the config file through standard fs (via VFS hooks)
const configContent = fs.readFileSync(path.join(__dirname, 'config.json'), 'utf8');
const config = JSON.parse(configContent);
assert.strictEqual(config.name, 'test-app', 'config.name should match');
assert.strictEqual(config.version, '1.0.0', 'config.version should match');
console.log('Read config.json:', config);
// Read a text file
const greetingPath = path.join(__dirname, 'data', 'greeting.txt');
const greeting = fs.readFileSync(greetingPath, 'utf8');
assert.strictEqual(greeting, 'Hello from SEA VFS!', 'greeting should match');
console.log('Read greeting.txt:', greeting);
// Test existsSync
assert.strictEqual(fs.existsSync(path.join(__dirname, 'config.json')), true);
assert.strictEqual(fs.existsSync(greetingPath), true);
assert.strictEqual(fs.existsSync(path.join(__dirname, 'nonexistent.txt')), false);
console.log('existsSync tests passed');
// Test statSync
const configStat = fs.statSync(path.join(__dirname, 'config.json'));
assert.strictEqual(configStat.isFile(), true);
assert.strictEqual(configStat.isDirectory(), false);
const dirStat = fs.statSync(path.join(__dirname, 'data'));
assert.strictEqual(dirStat.isDirectory(), true);
console.log('statSync tests passed');
// Test readdirSync - the mount root lists the assets and the main script
const entries = fs.readdirSync(__dirname);
assert.ok(entries.includes('config.json'), 'Should include config.json');
assert.ok(entries.includes('data'), 'Should include data directory');
assert.ok(entries.includes('sea.js'), 'Should include the main script');
console.log('readdirSync tests passed, entries:', entries);
// The VFS is read-only
assert.throws(() => {
fs.writeFileSync(path.join(__dirname, 'new-file.txt'), 'nope');
}, { code: 'EROFS' });
console.log('read-only test passed');
// Test relative require from main script - __filename is inside the mount so
// relative paths resolve against the bundled assets via module hooks
const mathModule = require('./modules/math.js');
assert.strictEqual(mathModule.add(2, 3), 5, 'math.add should work');
assert.strictEqual(mathModule.multiply(4, 5), 20, 'math.multiply should work');
console.log('relative require from main script passed');
// Test transitive requires: calculator.js requires ./math.js internally
const calculator = require('./modules/calculator.js');
assert.strictEqual(calculator.sum(10, 20), 30, 'calculator.sum should work');
assert.strictEqual(calculator.product(3, 7), 21, 'calculator.product should work');
console.log('transitive require from VFS tests passed');
// Module lookup paths are confined to the mount
assert.deepStrictEqual(module.paths, [path.join(__dirname, 'node_modules')]);
console.log('module.paths confinement test passed');
// Test that node:sea API and VFS can load the same asset
const sea = require('node:sea');
const seaAsset = sea.getAsset('data/greeting.txt', 'utf8');
const vfsAsset = fs.readFileSync(greetingPath, 'utf8');
assert.strictEqual(seaAsset, vfsAsset, 'node:sea and VFS should return the same content');
console.log('node:sea API and VFS coexistence test passed');
// Test buffer independence: multiple reads return independent copies
const buf1 = fs.readFileSync(greetingPath);
const buf2 = fs.readFileSync(greetingPath);
const original = buf1[0];
buf1[0] = 0xFF;
assert.strictEqual(buf2[0], original, 'buf2 should be unaffected by buf1 mutation');
assert.strictEqual(buf1[0], 0xFF, 'buf1 mutation should persist');
console.log('buffer independence test passed');
// Test node_modules package lookup via VFS (resolved through "exports" field)
const testPkg = require('test-pkg');
assert.strictEqual(testPkg.name, 'test-pkg', 'package name should match');
assert.strictEqual(testPkg.greet('World'), 'Hello, World!', 'package function should work');
console.log('node_modules package lookup test passed');
// Test exports-only package (no "main" field, entry in subdirectory)
// This proves the package.json reader is VFS-aware - without it,
// "exports" would not be consulted and resolution would fail.
const exportsPkg = require('test-exports-pkg');
assert.strictEqual(exportsPkg.fromExports, true, 'exports-only package should resolve');
console.log('exports-only package lookup test passed');
console.log('All SEA VFS tests passed!');
+2
View File
@@ -0,0 +1,2 @@
'use strict';
module.exports = { fromExports: true };
+5
View File
@@ -0,0 +1,5 @@
{
"name": "test-exports-pkg",
"version": "1.0.0",
"exports": { ".": "./lib/entry.js" }
}
+5
View File
@@ -0,0 +1,5 @@
'use strict';
module.exports = {
name: 'test-pkg',
greet: (name) => `Hello, ${name}!`,
};
+5
View File
@@ -0,0 +1,5 @@
{
"name": "test-pkg",
"version": "1.0.0",
"exports": { ".": "./index.js" }
}
@@ -0,0 +1,77 @@
// This tests that --build-sea rejects "useVfs" combined with options it
// does not support.
'use strict';
require('../common');
const tmpdir = require('../common/tmpdir');
const { skipIfBuildSEAIsNotSupported } = require('../common/sea');
const { writeFileSync } = require('fs');
const { spawnSyncAndAssert } = require('../common/child_process');
skipIfBuildSEAIsNotSupported();
// Test: "useVfs" is not a Boolean
{
tmpdir.refresh();
const config = tmpdir.resolve('invalid-useVfs.json');
writeFileSync(config, `
{
"main": "bundle.js",
"output": "sea",
"useVfs": "true"
}
`, 'utf8');
spawnSyncAndAssert(
process.execPath,
['--build-sea', config], {
cwd: tmpdir.path,
}, {
status: 1,
stderr: /"useVfs" field of .*invalid-useVfs\.json is not a Boolean/,
});
}
// Test: "useVfs" with "useSnapshot"
{
tmpdir.refresh();
const config = tmpdir.resolve('vfs-snapshot.json');
writeFileSync(config, `
{
"main": "bundle.js",
"output": "sea",
"useVfs": true,
"useSnapshot": true
}
`, 'utf8');
spawnSyncAndAssert(
process.execPath,
['--build-sea', config], {
cwd: tmpdir.path,
}, {
status: 1,
stderr: /"useVfs" is not supported when "useSnapshot" is true/,
});
}
// Test: "useVfs" with "useCodeCache"
{
tmpdir.refresh();
const config = tmpdir.resolve('vfs-code-cache.json');
writeFileSync(config, `
{
"main": "bundle.js",
"output": "sea",
"useVfs": true,
"useCodeCache": true
}
`, 'utf8');
spawnSyncAndAssert(
process.execPath,
['--build-sea', config], {
cwd: tmpdir.path,
}, {
status: 1,
stderr: /"useVfs" is not supported when "useCodeCache" is true/,
});
}
@@ -0,0 +1,39 @@
'use strict';
// This tests the SEA VFS integration with an ESM entry point - the bundled
// assets are mounted as a virtual file system and the ESM main script runs
// from inside the mount through the ESM loader.
require('../common');
const {
buildSEA,
skipIfBuildSEAIsNotSupported,
} = require('../common/sea');
skipIfBuildSEAIsNotSupported();
const tmpdir = require('../common/tmpdir');
const { spawnSyncAndAssert } = require('../common/child_process');
const fixtures = require('../common/fixtures');
tmpdir.refresh();
const outputFile = buildSEA(fixtures.path('sea', 'vfs-esm'));
spawnSyncAndAssert(
outputFile,
{
env: {
...process.env,
NODE_DEBUG_NATIVE: undefined,
},
},
{
stdout: /All SEA VFS ESM tests passed!/,
stderr(stderr) {
if (/ExperimentalWarning: VirtualFileSystem/.test(stderr)) {
throw new Error('SEA VFS should not emit the public VirtualFileSystem warning');
}
},
},
);
@@ -0,0 +1,38 @@
'use strict';
// This tests the SEA VFS integration - the bundled assets are mounted as a
// virtual file system and the main script runs from inside the mount.
require('../common');
const {
buildSEA,
skipIfBuildSEAIsNotSupported,
} = require('../common/sea');
skipIfBuildSEAIsNotSupported();
const tmpdir = require('../common/tmpdir');
const { spawnSyncAndAssert } = require('../common/child_process');
const fixtures = require('../common/fixtures');
tmpdir.refresh();
const outputFile = buildSEA(fixtures.path('sea', 'vfs'));
spawnSyncAndAssert(
outputFile,
{
env: {
...process.env,
NODE_DEBUG_NATIVE: undefined,
},
},
{
stdout: /All SEA VFS tests passed!/,
stderr(stderr) {
if (/ExperimentalWarning: VirtualFileSystem/.test(stderr)) {
throw new Error('SEA VFS should not emit the public VirtualFileSystem warning');
}
},
},
);