diff --git a/doc/api/fs.md b/doc/api/fs.md index 6e86072ca6031e..4b15c3c0fb6c86 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -8285,6 +8285,596 @@ The following constants are meant for use with the {fs.Stats} object's On Windows, only `S_IRUSR` and `S_IWUSR` are available. +## Virtual file system + + + +> Stability: 1 - Experimental + +The virtual file system (VFS) allows creating in-memory file system overlays +that integrate seamlessly with the Node.js `fs` module and module loader. Virtual +files and directories can be accessed using standard `fs` operations and can be +`require()`d or `import`ed like regular files. + +### Creating a virtual file system + +Use `fs.createVirtual()` to create a new VFS instance: + +```cjs +const fs = require('node:fs'); + +const vfs = fs.createVirtual(); + +// Add files to the VFS +vfs.addFile('/config.json', JSON.stringify({ debug: true })); +vfs.addFile('/data.txt', 'Hello, World!'); + +// Mount the VFS at a specific path +vfs.mount('/app'); + +// Now files are accessible via standard fs APIs +const config = JSON.parse(fs.readFileSync('/app/config.json', 'utf8')); +console.log(config.debug); // true +``` + +```mjs +import fs from 'node:fs'; + +const vfs = fs.createVirtual(); + +// Add files to the VFS +vfs.addFile('/config.json', JSON.stringify({ debug: true })); +vfs.addFile('/data.txt', 'Hello, World!'); + +// Mount the VFS at a specific path +vfs.mount('/app'); + +// Now files are accessible via standard fs APIs +const config = JSON.parse(fs.readFileSync('/app/config.json', 'utf8')); +console.log(config.debug); // true +``` + +### `fs.createVirtual([options])` + + + +* `options` {Object} + * `fallthrough` {boolean} When `true`, operations on paths not in the VFS + fall through to the real file system. **Default:** `true`. + * `moduleHooks` {boolean} When `true`, enables hooks for `require()` and + `import` to load modules from the VFS. **Default:** `true`. + * `virtualCwd` {boolean} When `true`, enables virtual working directory + support via `vfs.chdir()` and `vfs.cwd()`. **Default:** `false`. +* Returns: {VirtualFileSystem} + +Creates a new virtual file system instance. + +```cjs +const fs = require('node:fs'); + +// Create a VFS that falls through to real fs for unmatched paths +const vfs = fs.createVirtual({ fallthrough: true }); + +// Create a VFS that only serves virtual files +const isolatedVfs = fs.createVirtual({ fallthrough: false }); + +// Create a VFS without module loading hooks (fs operations only) +const fsOnlyVfs = fs.createVirtual({ moduleHooks: false }); +``` + +### Class: `VirtualFileSystem` + + + +A `VirtualFileSystem` instance manages virtual files and directories and +provides methods to mount them into the file system namespace. + +#### `vfs.addFile(path, content)` + + + +* `path` {string} The virtual path for the file. +* `content` {string|Buffer|Function} The file content, or a function that + returns the content. + +Adds a virtual file. The `content` can be: + +* A `string` or `Buffer` for static content +* A synchronous function `() => string|Buffer` for dynamic content +* An async function `async () => string|Buffer` for async dynamic content + +```cjs +const fs = require('node:fs'); + +const vfs = fs.createVirtual(); + +// Static content +vfs.addFile('/config.json', '{"debug": true}'); + +// Dynamic content (evaluated on each read) +vfs.addFile('/timestamp.txt', () => Date.now().toString()); + +// Async dynamic content +vfs.addFile('/data.json', async () => { + const data = await fetchData(); + return JSON.stringify(data); +}); +``` + +#### `vfs.addDirectory(path[, populate])` + + + +* `path` {string} The virtual path for the directory. +* `populate` {Function} Optional callback to dynamically populate the directory. + +Adds a virtual directory. If `populate` is provided, it receives a scoped VFS +for adding files and subdirectories within this directory. The callback is +invoked lazily on first access. + +```cjs +const fs = require('node:fs'); + +const vfs = fs.createVirtual(); + +// Empty directory +vfs.addDirectory('/empty'); + +// Directory with static contents +vfs.addDirectory('/lib'); +vfs.addFile('/lib/utils.js', 'module.exports = {}'); + +// Dynamic directory (populated on first access) +vfs.addDirectory('/plugins', (dir) => { + dir.addFile('a.js', 'module.exports = "plugin a"'); + dir.addFile('b.js', 'module.exports = "plugin b"'); +}); +``` + +#### `vfs.mount(prefix)` + + + +* `prefix` {string} The path prefix where the VFS will be mounted. + +Mounts the VFS at a specific path prefix. All paths in the VFS become accessible +under this prefix. Only one mount point can be active at a time. + +```cjs +const fs = require('node:fs'); + +const vfs = fs.createVirtual(); +vfs.addFile('/module.js', 'module.exports = "hello"'); +vfs.mount('/virtual'); + +// Now accessible at /virtual/module.js +const content = fs.readFileSync('/virtual/module.js', 'utf8'); +const mod = require('/virtual/module.js'); +``` + +#### `vfs.overlay()` + + + +Enables overlay mode, where the VFS is checked first for all file system +operations. If a path exists in the VFS, it is used; otherwise, the operation +falls through to the real file system (if `fallthrough` is enabled). + +```cjs +const fs = require('node:fs'); + +const vfs = fs.createVirtual(); +vfs.addFile('/etc/myapp/config.json', '{"virtual": true}'); +vfs.overlay(); + +// Virtual file is returned +fs.readFileSync('/etc/myapp/config.json', 'utf8'); // '{"virtual": true}' + +// Real file system used for non-virtual paths +fs.readFileSync('/etc/hosts', 'utf8'); // Real file contents +``` + +#### `vfs.unmount()` + + + +Unmounts the VFS, removing it from the file system namespace. After unmounting, +the virtual files are no longer accessible through standard `fs` operations. + +```cjs +const fs = require('node:fs'); + +const vfs = fs.createVirtual(); +vfs.addFile('/test.txt', 'content'); +vfs.mount('/vfs'); + +fs.existsSync('/vfs/test.txt'); // true + +vfs.unmount(); + +fs.existsSync('/vfs/test.txt'); // false +``` + +#### `vfs.has(path)` + + + +* `path` {string} The path to check. +* Returns: {boolean} + +Returns `true` if the VFS contains a file or directory at the given path. + +#### `vfs.remove(path)` + + + +* `path` {string} The path to remove. +* Returns: {boolean} `true` if the entry was removed, `false` if not found. + +Removes a file or directory from the VFS. + +#### `vfs.virtualCwdEnabled` + + + +* {boolean} + +Returns `true` if virtual working directory support is enabled for this VFS +instance. This is determined by the `virtualCwd` option passed to +`fs.createVirtual()`. + +#### `vfs.cwd()` + + + +* Returns: {string|null} The current virtual working directory, or `null` if + not set. + +Gets the virtual current working directory. Throws `ERR_INVALID_STATE` if +`virtualCwd` option was not enabled when creating the VFS. + +```cjs +const fs = require('node:fs'); + +const vfs = fs.createVirtual({ virtualCwd: true }); +vfs.addDirectory('/project'); +vfs.mount('/app'); + +console.log(vfs.cwd()); // null (not set yet) + +vfs.chdir('/app/project'); +console.log(vfs.cwd()); // '/app/project' +``` + +#### `vfs.chdir(path)` + + + +* `path` {string} The directory path to set as the current working directory. + +Sets the virtual current working directory. The path must exist in the VFS and +must be a directory. Throws `ENOENT` if the path does not exist, `ENOTDIR` if +the path is not a directory, or `ERR_INVALID_STATE` if `virtualCwd` option was +not enabled. + +```cjs +const fs = require('node:fs'); + +const vfs = fs.createVirtual({ virtualCwd: true }); +vfs.addDirectory('/project'); +vfs.addDirectory('/project/src'); +vfs.addFile('/project/src/index.js', 'module.exports = "hello";'); +vfs.mount('/app'); + +vfs.chdir('/app/project'); +console.log(vfs.cwd()); // '/app/project' + +vfs.chdir('/app/project/src'); +console.log(vfs.cwd()); // '/app/project/src' +``` + +##### `process.chdir()` and `process.cwd()` interception + +When `virtualCwd` is enabled and the VFS is mounted or in overlay mode, +`process.chdir()` and `process.cwd()` are intercepted to support transparent +virtual working directory operations: + +* `process.chdir(path)` - When called with a path that resolves to the VFS, + the virtual cwd is updated instead of changing the real process working + directory. Paths outside the VFS fall through to the real `process.chdir()`. + +* `process.cwd()` - When a virtual cwd is set, returns the virtual cwd. + Otherwise, returns the real process working directory. + +```cjs +const fs = require('node:fs'); + +const vfs = fs.createVirtual({ virtualCwd: true }); +vfs.addDirectory('/project'); +vfs.mount('/virtual'); + +const originalCwd = process.cwd(); + +// Change to a VFS directory using process.chdir +process.chdir('/virtual/project'); +console.log(process.cwd()); // '/virtual/project' +console.log(vfs.cwd()); // '/virtual/project' + +// Change to a real directory (falls through) +process.chdir('/tmp'); +console.log(process.cwd()); // '/tmp' (real cwd) + +// Restore and unmount +process.chdir(originalCwd); +vfs.unmount(); +``` + +When the VFS is unmounted, `process.chdir()` and `process.cwd()` are restored +to their original implementations. + +> **Note:** VFS hooks are not automatically shared with worker threads. Each +> worker thread has its own `process` object and must set up its own VFS +> instance if virtual cwd support is needed. + +#### `vfs.resolvePath(path)` + + + +* `path` {string} The path to resolve. +* Returns: {string} The resolved absolute path. + +Resolves a path relative to the virtual current working directory. If the path +is absolute, it is returned as-is (normalized). If `virtualCwd` is enabled and +a virtual cwd is set, relative paths are resolved against it. Otherwise, +relative paths are resolved using the real process working directory. + +```cjs +const fs = require('node:fs'); + +const vfs = fs.createVirtual({ virtualCwd: true }); +vfs.addDirectory('/project'); +vfs.addDirectory('/project/src'); +vfs.mount('/app'); + +vfs.chdir('/app/project'); + +// Absolute paths returned as-is +console.log(vfs.resolvePath('/other/path')); // '/other/path' + +// Relative paths resolved against virtual cwd +console.log(vfs.resolvePath('src/index.js')); // '/app/project/src/index.js' +console.log(vfs.resolvePath('./src/index.js')); // '/app/project/src/index.js' +``` + +### VFS file system operations + +The `VirtualFileSystem` instance provides direct access to file system +operations that bypass the real file system entirely. These methods have the +same signatures as their `fs` module counterparts. + +#### Synchronous methods + +* `vfs.readFileSync(path[, options])` - Read file contents +* `vfs.statSync(path[, options])` - Get file stats +* `vfs.lstatSync(path[, options])` - Get file stats (same as statSync for VFS) +* `vfs.readdirSync(path[, options])` - List directory contents +* `vfs.existsSync(path)` - Check if path exists +* `vfs.realpathSync(path[, options])` - Resolve path (normalizes `.` and `..`) +* `vfs.accessSync(path[, mode])` - Check file accessibility +* `vfs.openSync(path[, flags[, mode]])` - Open file and return file descriptor +* `vfs.closeSync(fd)` - Close file descriptor +* `vfs.readSync(fd, buffer, offset, length, position)` - Read from file descriptor +* `vfs.fstatSync(fd[, options])` - Get stats from file descriptor + +```cjs +const fs = require('node:fs'); + +const vfs = fs.createVirtual(); +vfs.addFile('/data.txt', 'Hello, World!'); + +// Direct VFS operations (no mounting required) +const content = vfs.readFileSync('/data.txt', 'utf8'); +const stats = vfs.statSync('/data.txt'); +console.log(content); // 'Hello, World!' +console.log(stats.size); // 13 +``` + +#### Callback methods + +* `vfs.readFile(path[, options], callback)` - Read file contents +* `vfs.stat(path[, options], callback)` - Get file stats +* `vfs.lstat(path[, options], callback)` - Get file stats +* `vfs.readdir(path[, options], callback)` - List directory contents +* `vfs.realpath(path[, options], callback)` - Resolve path +* `vfs.access(path[, mode], callback)` - Check file accessibility +* `vfs.open(path[, flags[, mode]], callback)` - Open file +* `vfs.close(fd, callback)` - Close file descriptor +* `vfs.read(fd, buffer, offset, length, position, callback)` - Read from fd +* `vfs.fstat(fd[, options], callback)` - Get stats from file descriptor + +```cjs +const fs = require('node:fs'); + +const vfs = fs.createVirtual(); +vfs.addFile('/async.txt', 'Async content'); + +vfs.readFile('/async.txt', 'utf8', (err, data) => { + if (err) throw err; + console.log(data); // 'Async content' +}); +``` + +#### Promise methods + +The `vfs.promises` object provides promise-based versions of the file system +methods: + +* `vfs.promises.readFile(path[, options])` - Read file contents +* `vfs.promises.stat(path[, options])` - Get file stats +* `vfs.promises.lstat(path[, options])` - Get file stats +* `vfs.promises.readdir(path[, options])` - List directory contents +* `vfs.promises.realpath(path[, options])` - Resolve path +* `vfs.promises.access(path[, mode])` - Check file accessibility + +```cjs +const fs = require('node:fs'); + +const vfs = fs.createVirtual(); +vfs.addFile('/promise.txt', 'Promise content'); + +(async () => { + const data = await vfs.promises.readFile('/promise.txt', 'utf8'); + console.log(data); // 'Promise content' +})(); +``` + +#### Streams + +* `vfs.createReadStream(path[, options])` - Create a readable stream + +```cjs +const fs = require('node:fs'); + +const vfs = fs.createVirtual(); +vfs.addFile('/stream.txt', 'Streaming content'); + +const stream = vfs.createReadStream('/stream.txt', { encoding: 'utf8' }); +stream.on('data', (chunk) => console.log(chunk)); +stream.on('end', () => console.log('Done')); +``` + +The readable stream supports the following options: + +* `encoding` {string} Character encoding for string output. +* `start` {integer} Byte position to start reading from. +* `end` {integer} Byte position to stop reading at (inclusive). +* `highWaterMark` {integer} Maximum number of bytes to buffer. +* `autoClose` {boolean} Automatically close the stream on end. **Default:** `true`. + +### Module loading from VFS + +Virtual files can be loaded as modules using `require()` or `import`. The VFS +integrates with the Node.js module loaders automatically when mounted or in +overlay mode. + +```cjs +const fs = require('node:fs'); + +const vfs = fs.createVirtual(); + +// Add a CommonJS module +vfs.addFile('/app/math.js', ` + module.exports = { + add: (a, b) => a + b, + multiply: (a, b) => a * b + }; +`); + +// Add a package.json +vfs.addFile('/app/package.json', '{"name": "virtual-app", "main": "math.js"}'); + +vfs.mount('/app'); + +// Require the virtual module +const math = require('/app/math.js'); +console.log(math.add(2, 3)); // 5 + +// Require the package +const pkg = require('/app'); +console.log(pkg.multiply(4, 5)); // 20 +``` + +```mjs +import fs from 'node:fs'; + +const vfs = fs.createVirtual(); + +// Add an ES module +vfs.addFile('/esm/module.mjs', ` + export const value = 42; + export default function greet() { return 'Hello'; } +`); + +vfs.mount('/esm'); + +// Dynamic import of virtual ES module +const mod = await import('/esm/module.mjs'); +console.log(mod.value); // 42 +console.log(mod.default()); // 'Hello' +``` + +### Glob support + +The VFS integrates with `fs.glob()`, `fs.globSync()`, and `fs/promises.glob()` +when mounted or in overlay mode: + +```cjs +const fs = require('node:fs'); + +const vfs = fs.createVirtual(); +vfs.addFile('/src/index.js', 'export default 1;'); +vfs.addFile('/src/utils.js', 'export const util = 1;'); +vfs.addFile('/src/lib/helper.js', 'export const helper = 1;'); +vfs.mount('/virtual'); + +// Sync glob +const files = fs.globSync('/virtual/src/**/*.js'); +console.log(files); +// ['/virtual/src/index.js', '/virtual/src/utils.js', '/virtual/src/lib/helper.js'] + +// Async glob with callback +fs.glob('/virtual/src/*.js', (err, matches) => { + console.log(matches); // ['/virtual/src/index.js', '/virtual/src/utils.js'] +}); + +// Async glob with promises (returns async iterator) +const { glob } = require('node:fs/promises'); +(async () => { + for await (const file of glob('/virtual/src/**/*.js')) { + console.log(file); + } +})(); +``` + +### Limitations + +The current VFS implementation has the following limitations: + +* **Read-only**: Files can only be set via `addFile()`. Write operations + (`writeFile`, `appendFile`, etc.) are not supported. +* **No file watching**: `fs.watch()` and `fs.watchFile()` do not work with + virtual files. +* **No real file descriptor**: Virtual file descriptors (10000+) are managed + separately from real file descriptors. + ## Notes ### Ordering of callback and promise-based operations diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index b0ee939b4be1db..0b0432f3f4b91a 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -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 diff --git a/doc/api/test.md b/doc/api/test.md index 927208af853d38..4fda764b3b0ddb 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -2334,6 +2334,94 @@ test('mocks a counting function', (t) => { }); ``` +### `mock.fs([options])` + + + +> 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])`