Skip to content

Commit 65499d8

Browse files
mcollinaclaude
andcommitted
vfs: address review comments
- doc: expand VFS examples to show addFile and mount usage - vfs: add comment explaining async event emission in streams - vfs: use createEBADF error code instead of generic Error - test: expand test-vfs-sea.js with more comprehensive tests - test: add full SEA VFS integration test Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 10b55a5 commit 65499d8

File tree

4 files changed

+200
-12
lines changed

4 files changed

+200
-12
lines changed

doc/api/fs.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8306,12 +8306,34 @@ Use `fs.createVirtual()` to create a new VFS instance:
83068306
const fs = require('node:fs');
83078307

83088308
const vfs = fs.createVirtual();
8309+
8310+
// Add files to the VFS
8311+
vfs.addFile('/config.json', JSON.stringify({ debug: true }));
8312+
vfs.addFile('/data.txt', 'Hello, World!');
8313+
8314+
// Mount the VFS at a specific path
8315+
vfs.mount('/app');
8316+
8317+
// Now files are accessible via standard fs APIs
8318+
const config = JSON.parse(fs.readFileSync('/app/config.json', 'utf8'));
8319+
console.log(config.debug); // true
83098320
```
83108321
83118322
```mjs
83128323
import fs from 'node:fs';
83138324

83148325
const vfs = fs.createVirtual();
8326+
8327+
// Add files to the VFS
8328+
vfs.addFile('/config.json', JSON.stringify({ debug: true }));
8329+
vfs.addFile('/data.txt', 'Hello, World!');
8330+
8331+
// Mount the VFS at a specific path
8332+
vfs.mount('/app');
8333+
8334+
// Now files are accessible via standard fs APIs
8335+
const config = JSON.parse(fs.readFileSync('/app/config.json', 'utf8'));
8336+
console.log(config.debug); // true
83158337
```
83168338
83178339
### `fs.createVirtual([options])`

lib/internal/vfs/streams.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const { Readable } = require('stream');
4+
const { createEBADF } = require('internal/vfs/errors');
45

56
/**
67
* Creates a readable stream for a virtual file.
@@ -57,6 +58,8 @@ class VirtualReadStream extends Readable {
5758

5859
/**
5960
* Opens the virtual file.
61+
* Events are emitted synchronously within this method, which runs
62+
* asynchronously via process.nextTick - matching real fs behavior.
6063
* @private
6164
*/
6265
_openFile() {
@@ -84,7 +87,7 @@ class VirtualReadStream extends Readable {
8487
try {
8588
const vfd = require('internal/vfs/fd').getVirtualFd(this._fd);
8689
if (!vfd) {
87-
this.destroy(new Error('Invalid file descriptor'));
90+
this.destroy(createEBADF('read'));
8891
return;
8992
}
9093
this._content = vfd.getContentSync();

test/parallel/test-vfs-sea.js

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,48 @@
11
'use strict';
22

3+
// Tests for SEA VFS functions when NOT running as a Single Executable Application.
4+
// For full SEA VFS integration tests, see test/sea/test-single-executable-application-vfs.js
5+
36
const common = require('../common');
47
const assert = require('assert');
58
const fs = require('fs');
69

7-
// Test that SEA functions exist
8-
assert.strictEqual(typeof fs.getSeaVfs, 'function');
9-
assert.strictEqual(typeof fs.hasSeaAssets, 'function');
10+
// Test that SEA functions are exported from fs module
11+
assert.strictEqual(typeof fs.getSeaVfs, 'function', 'fs.getSeaVfs should be a function');
12+
assert.strictEqual(typeof fs.hasSeaAssets, 'function', 'fs.hasSeaAssets should be a function');
1013

11-
// Test that SEA functions return appropriate values when not in SEA
14+
// Test hasSeaAssets() returns false when not running as SEA
1215
{
13-
// hasSeaAssets should return false when not running as SEA
1416
const hasAssets = fs.hasSeaAssets();
15-
assert.strictEqual(hasAssets, false);
17+
assert.strictEqual(hasAssets, false, 'hasSeaAssets() should return false when not in SEA');
18+
}
1619

17-
// getSeaVfs should return null when not running as SEA
20+
// Test getSeaVfs() returns null when not running as SEA
21+
{
1822
const seaVfs = fs.getSeaVfs();
19-
assert.strictEqual(seaVfs, null);
23+
assert.strictEqual(seaVfs, null, 'getSeaVfs() should return null when not in SEA');
24+
}
25+
26+
// Test getSeaVfs() with options still returns null when not in SEA
27+
{
28+
const seaVfs = fs.getSeaVfs({ prefix: '/custom-sea' });
29+
assert.strictEqual(seaVfs, null, 'getSeaVfs() with prefix option should return null when not in SEA');
30+
}
31+
32+
{
33+
const seaVfs = fs.getSeaVfs({ moduleHooks: false });
34+
assert.strictEqual(seaVfs, null, 'getSeaVfs() with moduleHooks option should return null when not in SEA');
35+
}
36+
37+
{
38+
const seaVfs = fs.getSeaVfs({ prefix: '/my-app', moduleHooks: true });
39+
assert.strictEqual(seaVfs, null, 'getSeaVfs() with multiple options should return null when not in SEA');
2040
}
2141

22-
// Test with custom options (should still return null when not in SEA)
42+
// Verify that calling getSeaVfs multiple times is safe (caching behavior)
2343
{
24-
const seaVfs = fs.getSeaVfs({ prefix: '/custom-sea', moduleHooks: false });
25-
assert.strictEqual(seaVfs, null);
44+
const vfs1 = fs.getSeaVfs();
45+
const vfs2 = fs.getSeaVfs();
46+
assert.strictEqual(vfs1, vfs2, 'Multiple calls to getSeaVfs() should return the same value');
47+
assert.strictEqual(vfs1, null, 'Both should be null when not in SEA');
2648
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
'use strict';
2+
3+
require('../common');
4+
5+
const {
6+
generateSEA,
7+
skipIfSingleExecutableIsNotSupported,
8+
} = require('../common/sea');
9+
10+
skipIfSingleExecutableIsNotSupported();
11+
12+
// This tests the SEA VFS integration - fs.getSeaVfs() and fs.hasSeaAssets()
13+
14+
const tmpdir = require('../common/tmpdir');
15+
const { writeFileSync } = require('fs');
16+
const { spawnSyncAndAssert } = require('../common/child_process');
17+
const { join } = require('path');
18+
19+
const configFile = tmpdir.resolve('sea-config.json');
20+
const seaPrepBlob = tmpdir.resolve('sea-prep.blob');
21+
const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea');
22+
23+
// The script that will run inside the SEA
24+
const seaMain = `
25+
'use strict';
26+
const fs = require('fs');
27+
const assert = require('assert');
28+
29+
// Test hasSeaAssets() returns true when we have assets
30+
const hasAssets = fs.hasSeaAssets();
31+
assert.strictEqual(hasAssets, true, 'hasSeaAssets() should return true');
32+
console.log('hasSeaAssets:', hasAssets);
33+
34+
// Test getSeaVfs() returns a VFS instance
35+
const vfs = fs.getSeaVfs();
36+
assert.ok(vfs !== null, 'getSeaVfs() should not return null');
37+
console.log('getSeaVfs returned VFS instance');
38+
39+
// Test that the VFS is mounted at /sea by default
40+
// and contains our assets
41+
42+
// Read the config file through standard fs (via VFS hooks)
43+
const configContent = fs.readFileSync('/sea/config.json', 'utf8');
44+
const config = JSON.parse(configContent);
45+
assert.strictEqual(config.name, 'test-app', 'config.name should match');
46+
assert.strictEqual(config.version, '1.0.0', 'config.version should match');
47+
console.log('Read config.json:', config);
48+
49+
// Read a text file
50+
const greeting = fs.readFileSync('/sea/data/greeting.txt', 'utf8');
51+
assert.strictEqual(greeting, 'Hello from SEA VFS!', 'greeting should match');
52+
console.log('Read greeting.txt:', greeting);
53+
54+
// Test existsSync
55+
assert.strictEqual(fs.existsSync('/sea/config.json'), true);
56+
assert.strictEqual(fs.existsSync('/sea/data/greeting.txt'), true);
57+
assert.strictEqual(fs.existsSync('/sea/nonexistent.txt'), false);
58+
console.log('existsSync tests passed');
59+
60+
// Test statSync
61+
const configStat = fs.statSync('/sea/config.json');
62+
assert.strictEqual(configStat.isFile(), true);
63+
assert.strictEqual(configStat.isDirectory(), false);
64+
console.log('statSync tests passed');
65+
66+
// Test readdirSync
67+
const entries = fs.readdirSync('/sea');
68+
assert.ok(entries.includes('config.json'), 'Should include config.json');
69+
assert.ok(entries.includes('data'), 'Should include data directory');
70+
console.log('readdirSync tests passed, entries:', entries);
71+
72+
// Test requiring a module from SEA VFS
73+
const mathModule = require('/sea/modules/math.js');
74+
assert.strictEqual(mathModule.add(2, 3), 5, 'math.add should work');
75+
assert.strictEqual(mathModule.multiply(4, 5), 20, 'math.multiply should work');
76+
console.log('require from VFS tests passed');
77+
78+
// Test getSeaVfs with custom prefix
79+
const customVfs = fs.getSeaVfs({ prefix: '/custom' });
80+
// Note: getSeaVfs is a singleton, so it returns the same instance
81+
// with the same mount point (/sea) regardless of options passed after first call
82+
assert.strictEqual(customVfs, vfs, 'Should return the same cached instance');
83+
console.log('Cached VFS instance test passed');
84+
85+
console.log('All SEA VFS tests passed!');
86+
`;
87+
88+
// Create the main script file
89+
writeFileSync(tmpdir.resolve('sea-main.js'), seaMain);
90+
91+
// Create asset files
92+
writeFileSync(tmpdir.resolve('config.json'), JSON.stringify({
93+
name: 'test-app',
94+
version: '1.0.0',
95+
}));
96+
97+
writeFileSync(tmpdir.resolve('greeting.txt'), 'Hello from SEA VFS!');
98+
99+
writeFileSync(tmpdir.resolve('math.js'), `
100+
module.exports = {
101+
add: (a, b) => a + b,
102+
multiply: (a, b) => a * b,
103+
};
104+
`);
105+
106+
// Create SEA config with assets
107+
writeFileSync(configFile, JSON.stringify({
108+
main: 'sea-main.js',
109+
output: 'sea-prep.blob',
110+
assets: {
111+
'config.json': 'config.json',
112+
'data/greeting.txt': 'greeting.txt',
113+
'modules/math.js': 'math.js',
114+
},
115+
}));
116+
117+
// Generate the SEA prep blob
118+
spawnSyncAndAssert(
119+
process.execPath,
120+
['--experimental-sea-config', 'sea-config.json'],
121+
{ cwd: tmpdir.path },
122+
{}
123+
);
124+
125+
// Generate the SEA executable
126+
generateSEA(outputFile, process.execPath, seaPrepBlob);
127+
128+
// Run the SEA and verify output
129+
spawnSyncAndAssert(
130+
outputFile,
131+
[],
132+
{
133+
env: {
134+
...process.env,
135+
NODE_DEBUG_NATIVE: undefined,
136+
},
137+
},
138+
{
139+
stdout: /All SEA VFS tests passed!/,
140+
}
141+
);

0 commit comments

Comments
 (0)