Skip to content
Open
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
590 changes: 590 additions & 0 deletions doc/api/fs.md

Large diffs are not rendered by default.

84 changes: 84 additions & 0 deletions doc/api/single-executable-applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,90 @@ 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
> Stability: 1 - Experimental
Instead of using the `node:sea` API to access individual assets, you can use
the Virtual File System (VFS) to access bundled assets through standard `fs`
APIs. The VFS automatically populates itself with all assets defined in the
SEA configuration and mounts them at a virtual path (default: `/sea`).
To use the VFS with SEA:
```cjs
const fs = require('node:fs');
const sea = require('node:sea');
// Check if SEA assets are available
if (sea.hasAssets()) {
// Initialize and mount the SEA VFS
const vfs = sea.getVfs();
// Now you can use standard fs APIs to read bundled assets
const config = JSON.parse(fs.readFileSync('/sea/config.json', 'utf8'));
const data = fs.readFileSync('/sea/data/file.txt');
// Directory operations work too
const files = fs.readdirSync('/sea/assets');
// Check if a bundled file exists
if (fs.existsSync('/sea/optional.json')) {
// ...
}
}
```
The VFS supports the following `fs` operations on bundled assets:
* `readFileSync()` / `readFile()` / `promises.readFile()`
* `statSync()` / `stat()` / `promises.stat()`
* `lstatSync()` / `lstat()` / `promises.lstat()`
* `readdirSync()` / `readdir()` / `promises.readdir()`
* `existsSync()`
* `realpathSync()` / `realpath()` / `promises.realpath()`
* `accessSync()` / `access()` / `promises.access()`
* `openSync()` / `open()` - for reading
* `createReadStream()`
#### Loading modules from VFS in SEA
Once the VFS is initialized with `sea.getVfs()`, you can use `require()` directly
with absolute VFS paths:
```cjs
const sea = require('node:sea');
// Initialize VFS - this must be called first
sea.getVfs();
// Now you can require bundled modules directly
const myModule = require('/sea/lib/mymodule.js');
const utils = require('/sea/utils/helpers.js');
```
The SEA's `require()` function automatically detects VFS paths (paths starting
with the VFS mount point, e.g., `/sea/`) and loads modules from the virtual
file system.
#### Custom mount prefix
By default, the VFS is mounted at `/sea`. You can specify a custom prefix
when initializing the VFS:
```cjs
const fs = require('node:fs');
const sea = require('node:sea');

const vfs = sea.getSeaVfs({ prefix: '/app' });

// Assets are now accessible under /app
const config = fs.readFileSync('/app/config.json', 'utf8');
```

Note: `sea.getVfs()` returns a singleton. The `prefix` option is only used
on the first call; subsequent calls return the same cached instance.

### Startup snapshot support

The `useSnapshot` field can be used to enable startup snapshot support. In this
Expand Down
88 changes: 88 additions & 0 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -2334,6 +2334,94 @@ test('mocks a counting function', (t) => {
});
```

### `mock.fs([options])`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.0 - Early development

* `options` {Object} Optional configuration options for the mock file system.
The following properties are supported:
* `prefix` {string} The mount point prefix for the virtual file system.
**Default:** `'/mock'`.
* `files` {Object} An optional object where keys are file paths (relative to
the VFS root) and values are the file contents. Contents can be strings,
Buffers, or functions that return strings/Buffers.
* Returns: {MockFSContext} An object that can be used to manage the mock file
system.

This function creates a mock file system using the Virtual File System (VFS).
The mock file system is automatically cleaned up when the test completes.

## Class: `MockFSContext`

The `MockFSContext` object is returned by `mock.fs()` and provides the
following methods and properties:

* `vfs` {VirtualFileSystem} The underlying VFS instance.
* `prefix` {string} The mount prefix.
* `addFile(path, content)` Adds a file to the mock file system.
* `addDirectory(path[, populate])` Adds a directory to the mock file system.
* `existsSync(path)` Checks if a path exists (path is relative to prefix).
* `restore()` Manually restores the file system to its original state.

The following example demonstrates how to create a mock file system for testing:

```js
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');

test('reads configuration from mock file', (t) => {
const mockFs = t.mock.fs({
prefix: '/app',
files: {
'/config.json': JSON.stringify({ debug: true }),
'/data/users.txt': 'user1\nuser2\nuser3',
},
});

// Files are accessible via standard fs APIs
const config = JSON.parse(fs.readFileSync('/app/config.json', 'utf8'));
assert.strictEqual(config.debug, true);

// Check file existence
assert.strictEqual(fs.existsSync('/app/config.json'), true);
assert.strictEqual(fs.existsSync('/app/missing.txt'), false);

// Use mockFs.existsSync for paths relative to prefix
assert.strictEqual(mockFs.existsSync('/config.json'), true);
});

test('supports dynamic file content', (t) => {
let counter = 0;
const mockFs = t.mock.fs({ prefix: '/dynamic' });

mockFs.addFile('/counter.txt', () => {
counter++;
return String(counter);
});

// Each read calls the function
assert.strictEqual(fs.readFileSync('/dynamic/counter.txt', 'utf8'), '1');
assert.strictEqual(fs.readFileSync('/dynamic/counter.txt', 'utf8'), '2');
});

test('supports require from mock files', (t) => {
t.mock.fs({
prefix: '/modules',
files: {
'/math.js': 'module.exports = { add: (a, b) => a + b };',
},
});

const math = require('/modules/math.js');
assert.strictEqual(math.add(2, 3), 5);
});
```

### `mock.getter(object, methodName[, implementation][, options])`

<!-- YAML
Expand Down
14 changes: 14 additions & 0 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -3206,6 +3206,19 @@ function globSync(pattern, options) {
return new Glob(pattern, options).globSync();
}

const lazyVfs = getLazy(() => require('internal/vfs/virtual_fs').VirtualFileSystem);

/**
* Creates a new virtual file system instance.
* @param {object} [options] Configuration options
* @param {boolean} [options.fallthrough] Whether to fall through to real fs on miss
* @param {boolean} [options.moduleHooks] Whether to enable require/import hooks
* @returns {VirtualFileSystem}
*/
function createVirtual(options) {
const VirtualFileSystem = lazyVfs();
return new VirtualFileSystem(options);
}

module.exports = fs = {
appendFile,
Expand All @@ -3223,6 +3236,7 @@ module.exports = fs = {
cp,
cpSync,
createReadStream,
createVirtual,
createWriteStream,
exists,
existsSync,
Expand Down
72 changes: 60 additions & 12 deletions lib/internal/main/embedding.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,22 +97,70 @@ function embedderRunCjs(content) {

let warnedAboutBuiltins = false;

// Lazy-loaded SEA VFS support
let seaVfsInitialized = false;
let seaVfs = null;
let seaVfsMountPoint = null;

function initSeaVfs() {
if (seaVfsInitialized) return;
seaVfsInitialized = true;

if (!isLoadingSea) return;

// Check if SEA has assets (VFS support)
const { hasSeaAssets, getSeaVfs } = require('internal/vfs/sea');
if (hasSeaAssets()) {
seaVfs = getSeaVfs();
if (seaVfs) {
seaVfsMountPoint = seaVfs.mountPoint;
}
}
}

function embedderRequire(id) {
const normalizedId = normalizeRequirableId(id);
if (!normalizedId) {
if (isBuiltinWarningNeeded && !warnedAboutBuiltins) {
emitWarningSync(
'Currently the require() provided to the main script embedded into ' +
'single-executable applications only supports loading built-in modules.\n' +
'To load a module from disk after the single executable application is ' +
'launched, use require("module").createRequire().\n' +
'Support for bundled module loading or virtual file systems are under ' +
'discussions in https://github.com/nodejs/single-executable');
warnedAboutBuiltins = true;
if (normalizedId) {
// Built-in module
return require(normalizedId);
}

// Not a built-in module - check if it's a VFS path in SEA
if (isLoadingSea) {
initSeaVfs();

if (seaVfs && seaVfsMountPoint) {
// Check if the path is within the VFS mount point
// Support both absolute paths (/sea/...) and relative to mount point
let modulePath = id;
if (id.startsWith(seaVfsMountPoint) || id.startsWith('/')) {
// Absolute path - resolve within VFS
if (!id.startsWith(seaVfsMountPoint) && id.startsWith('/')) {
// Path like '/modules/foo.js' - prepend mount point
modulePath = seaVfsMountPoint + id;
}

// Check if the file exists in VFS
if (seaVfs.existsSync(modulePath)) {
// Use Module._load to load the module, which will use VFS hooks
return Module._load(modulePath, embedderRequire.main, false);
}
}
}
throw new ERR_UNKNOWN_BUILTIN_MODULE(id);
}
return require(normalizedId);

// No VFS or file not in VFS - show warning and throw
if (isBuiltinWarningNeeded && !warnedAboutBuiltins) {
emitWarningSync(
'Currently the require() provided to the main script embedded into ' +
'single-executable applications only supports loading built-in modules.\n' +
'To load a module from disk after the single executable application is ' +
'launched, use require("module").createRequire().\n' +
'Support for bundled module loading or virtual file systems are under ' +
'discussions in https://github.com/nodejs/single-executable');
warnedAboutBuiltins = true;
}
throw new ERR_UNKNOWN_BUILTIN_MODULE(id);
}

return [process, embedderRequire, embedderRunCjs];
Loading
Loading