-
-
Notifications
You must be signed in to change notification settings - Fork 36.2k
vfs: integrate with CJS and ESM module loaders #63653
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
66713f6
90d8eb4
b28a077
d11997b
f94cdbe
ee06f2e
7bedf50
133a8a1
cefef62
722e10d
e7dd654
de17e81
a17ee29
023516e
e378435
7e94c3d
2203b59
f2f7b04
2462d17
902254d
2716996
6fafe07
6e3f1b9
a95717a
ffd68a2
7b6e3de
2e463ca
5b71a94
30d5115
337c130
0d5fb61
0a98b98
4c417d0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| 'use strict'; | ||
|
|
||
| // Measures VFS dispatch overhead on fs ops against real-filesystem paths | ||
| // (paths no VFS claims). Layers=0 means the VFS module is never loaded. | ||
|
|
||
| const common = require('../common.js'); | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
|
|
||
| const bench = common.createBenchmark(main, { | ||
| n: [3e5], | ||
| op: ['statSync', 'existsSync', 'accessSync', 'readFileSync'], | ||
| layers: [0, 1, 2, 5, 10], | ||
| }, { | ||
| flags: ['--experimental-vfs', '--no-warnings'], | ||
| }); | ||
|
|
||
| function mountLayers(count) { | ||
| const vfs = require('node:vfs'); | ||
| const handles = []; | ||
| for (let i = 0; i < count; i++) { | ||
| const v = vfs.create(); | ||
| v.mount(); | ||
| handles.push(v); | ||
| } | ||
| return handles; | ||
| } | ||
|
|
||
| function main({ n, op, layers }) { | ||
| const handles = layers > 0 ? mountLayers(layers) : null; | ||
|
|
||
| const target = layers === 0 ? __filename : path.join(__dirname, path.basename(__filename)); | ||
|
|
||
| for (let i = 0; i < 1000; i++) { | ||
| if (op === 'statSync') fs.statSync(target); | ||
| else if (op === 'existsSync') fs.existsSync(target); | ||
| else if (op === 'accessSync') fs.accessSync(target); | ||
| else fs.readFileSync(target); | ||
| } | ||
|
|
||
| bench.start(); | ||
| if (op === 'statSync') { | ||
| for (let i = 0; i < n; i++) fs.statSync(target); | ||
| } else if (op === 'existsSync') { | ||
| for (let i = 0; i < n; i++) fs.existsSync(target); | ||
| } else if (op === 'accessSync') { | ||
| for (let i = 0; i < n; i++) fs.accessSync(target); | ||
| } else { | ||
| for (let i = 0; i < n; i++) fs.readFileSync(target); | ||
| } | ||
| bench.end(n); | ||
|
|
||
| if (handles) { | ||
| for (const v of handles) v.unmount(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -61,6 +61,11 @@ callback-based, and promise-based file system methods that mirror the | |||||||||||||||||||||||||||||
| shape of the [`node:fs`][] API. All paths are POSIX-style and absolute | ||||||||||||||||||||||||||||||
| (starting with `/`). | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| By default, the file tree is private to the VFS instance. To expose | ||||||||||||||||||||||||||||||
| it through the global `node:fs` module, `require()`, and `import`, | ||||||||||||||||||||||||||||||
| call [`vfs.mount()`][]; call [`vfs.unmount()`][] (or rely on a | ||||||||||||||||||||||||||||||
| `using` declaration) to detach again. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ## `vfs.create([provider][, options])` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <!-- YAML | ||||||||||||||||||||||||||||||
|
|
@@ -107,6 +112,99 @@ added: v26.4.0 | |||||||||||||||||||||||||||||
| * `emitExperimentalWarning` {boolean} Whether to emit the experimental | ||||||||||||||||||||||||||||||
| warning. **Default:** `true`. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ### `vfs.mount()` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <!-- YAML | ||||||||||||||||||||||||||||||
| added: REPLACEME | ||||||||||||||||||||||||||||||
| --> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| * Returns: {string} The absolute mount point. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Mounts the virtual file system and returns the resulting mount point. | ||||||||||||||||||||||||||||||
| After mounting, files in the VFS can be accessed through the | ||||||||||||||||||||||||||||||
| `node:fs` module and resolved through `require()` and `import` | ||||||||||||||||||||||||||||||
| using paths under the returned mount point. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Mount points always live inside a reserved namespace, | ||||||||||||||||||||||||||||||
| `${os.devNull}/vfs/<layerId>`. Because [`os.devNull`][] is a | ||||||||||||||||||||||||||||||
| character device (POSIX) or a device-namespace path (Windows) that | ||||||||||||||||||||||||||||||
| cannot have child file system entries, no real file-system path can | ||||||||||||||||||||||||||||||
| exist under this namespace: virtual paths never conflate with (or | ||||||||||||||||||||||||||||||
| shadow) real paths, and the layer that owns a path is visible in the | ||||||||||||||||||||||||||||||
| path itself. | ||||||||||||||||||||||||||||||
|
Comment on lines
+128
to
+134
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I don't think we should make the path scheme a contract. |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ```cjs | ||||||||||||||||||||||||||||||
| const vfs = require('node:vfs'); | ||||||||||||||||||||||||||||||
| const fs = require('node:fs'); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const myVfs = vfs.create(); | ||||||||||||||||||||||||||||||
| myVfs.writeFileSync('/data.txt', 'Hello'); | ||||||||||||||||||||||||||||||
| const mountPoint = myVfs.mount(); | ||||||||||||||||||||||||||||||
| // e.g. '/dev/null/vfs/0' | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| fs.readFileSync(`${mountPoint}/data.txt`, 'utf8'); // 'Hello' | ||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Each `VirtualFileSystem` instance may be mounted at most once at a | ||||||||||||||||||||||||||||||
| time. Attempting to mount an already-mounted instance throws | ||||||||||||||||||||||||||||||
| `ERR_INVALID_STATE`. Because each instance mounts inside its own | ||||||||||||||||||||||||||||||
| per-layer namespace, mounts from different instances can never | ||||||||||||||||||||||||||||||
| overlap. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| The VFS supports the [Explicit Resource Management][] proposal. Use | ||||||||||||||||||||||||||||||
| a `using` declaration to unmount automatically when leaving scope: | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ```cjs | ||||||||||||||||||||||||||||||
| const vfs = require('node:vfs'); | ||||||||||||||||||||||||||||||
| const fs = require('node:fs'); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| let mountPoint; | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| using myVfs = vfs.create(); | ||||||||||||||||||||||||||||||
| myVfs.writeFileSync('/data.txt', 'Hello'); | ||||||||||||||||||||||||||||||
| mountPoint = myVfs.mount(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| fs.readFileSync(`${mountPoint}/data.txt`, 'utf8'); // 'Hello' | ||||||||||||||||||||||||||||||
| } // VFS is automatically unmounted here | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| fs.existsSync(`${mountPoint}/data.txt`); // false | ||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ### `vfs.unmount()` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <!-- YAML | ||||||||||||||||||||||||||||||
| added: REPLACEME | ||||||||||||||||||||||||||||||
| --> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Unmounts the virtual file system. After unmounting, virtual files | ||||||||||||||||||||||||||||||
| are no longer reachable through `node:fs`, `require()`, or `import`. | ||||||||||||||||||||||||||||||
| The same instance may be mounted again by calling `mount()`. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| This method is idempotent: calling `unmount()` on a VFS that is not | ||||||||||||||||||||||||||||||
| currently mounted has no effect. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ### `vfs.mounted` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <!-- YAML | ||||||||||||||||||||||||||||||
| added: REPLACEME | ||||||||||||||||||||||||||||||
| --> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| * {boolean} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| `true` while the VFS is mounted; `false` otherwise. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ### `vfs.mountPoint` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <!-- YAML | ||||||||||||||||||||||||||||||
| added: REPLACEME | ||||||||||||||||||||||||||||||
| --> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| * {string | null} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| The current mount point as an absolute string (the value returned by | ||||||||||||||||||||||||||||||
| the last [`vfs.mount()`][] call), or `null` when the VFS is not | ||||||||||||||||||||||||||||||
| mounted. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ### `vfs.provider` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <!-- YAML | ||||||||||||||||||||||||||||||
|
|
@@ -195,6 +293,71 @@ The promise namespace mirrors `fs.promises` and includes `readFile`, | |||||||||||||||||||||||||||||
| `access`, `rm`, `truncate`, `link`, `mkdtemp`, `chmod`, `chown`, `lchown`, | ||||||||||||||||||||||||||||||
| `utimes`, `lutimes`, `open`, `lchmod`, and `watch`. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ## Module loader integration | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Once a `VirtualFileSystem` is mounted, paths under the mount point | ||||||||||||||||||||||||||||||
| participate in module resolution and loading. Both | ||||||||||||||||||||||||||||||
| `require()` / `require.resolve()` (CommonJS) and `import` / | ||||||||||||||||||||||||||||||
| `import.meta.resolve()` (ECMAScript modules) consult the VFS through | ||||||||||||||||||||||||||||||
| the same toggleable hooks that `node:fs` uses, so files served from | ||||||||||||||||||||||||||||||
| the VFS are first-class modules: `package.json` is honoured, | ||||||||||||||||||||||||||||||
| extensionless files are sniffed for Wasm vs. JavaScript, conditional | ||||||||||||||||||||||||||||||
| `exports` / `imports` work, and so on. | ||||||||||||||||||||||||||||||
|
Comment on lines
+299
to
+305
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This paragraph is listing example without actually clarifying anything - exactly how do they appear in the module lookup algorithm, and with what precedence? |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ```cjs | ||||||||||||||||||||||||||||||
| const vfs = require('node:vfs'); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const myVfs = vfs.create(); | ||||||||||||||||||||||||||||||
| myVfs.mkdirSync('/lib'); | ||||||||||||||||||||||||||||||
| myVfs.writeFileSync('/lib/greet.js', 'module.exports = () => "hi";'); | ||||||||||||||||||||||||||||||
| myVfs.writeFileSync( | ||||||||||||||||||||||||||||||
| '/lib/package.json', '{"main": "./greet.js"}'); | ||||||||||||||||||||||||||||||
| const mountPoint = myVfs.mount(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const greet = require(`${mountPoint}/lib`); | ||||||||||||||||||||||||||||||
| console.log(greet()); // 'hi' | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| myVfs.unmount(); | ||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| For ECMAScript modules, convert mounted paths to `file:` URLs before | ||||||||||||||||||||||||||||||
| passing them to dynamic `import()`. This keeps VFS imports portable on | ||||||||||||||||||||||||||||||
| Windows, where mounted paths use Windows path syntax. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ```mjs | ||||||||||||||||||||||||||||||
| import { pathToFileURL } from 'node:url'; | ||||||||||||||||||||||||||||||
| import vfs from 'node:vfs'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const myVfs = vfs.create(); | ||||||||||||||||||||||||||||||
| myVfs.writeFileSync('/mod.mjs', 'export const value = 42;'); | ||||||||||||||||||||||||||||||
| const mountPoint = myVfs.mount(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const { value } = await import( | ||||||||||||||||||||||||||||||
| pathToFileURL(`${mountPoint}/mod.mjs`).href, | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| console.log(value); // 42 | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| myVfs.unmount(); | ||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Module identity follows the path: `__filename` and `module.filename` | ||||||||||||||||||||||||||||||
| are the plain absolute path of the module under the mount point, and | ||||||||||||||||||||||||||||||
| `import.meta.url` is the corresponding `file:` URL, with no synthetic | ||||||||||||||||||||||||||||||
| decorations. Importing the same virtual path repeatedly, including | ||||||||||||||||||||||||||||||
| through `import.meta.resolve()`, yields the same module instance, | ||||||||||||||||||||||||||||||
| exactly as for real files. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Calling [`vfs.unmount()`][] invalidates the modules that were loaded | ||||||||||||||||||||||||||||||
| from the mount point: a subsequent `require()` or `import` of a path | ||||||||||||||||||||||||||||||
| under a re-created mount re-reads the file from the newly mounted | ||||||||||||||||||||||||||||||
| VFS rather than returning a stale module. Modules loaded from other | ||||||||||||||||||||||||||||||
| VFS instances or from the real file system are unaffected. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Mounting and unmounting do not invalidate ESM modules that are | ||||||||||||||||||||||||||||||
| already executing. As with any other module-system teardown, | ||||||||||||||||||||||||||||||
| unmounting a VFS while the import graph below it is still loading is | ||||||||||||||||||||||||||||||
| the caller's responsibility to avoid. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ## Class: `VirtualProvider` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <!-- YAML | ||||||||||||||||||||||||||||||
|
|
@@ -318,10 +481,14 @@ fields use synthetic but stable values: | |||||||||||||||||||||||||||||
| * `blocks` is `Math.ceil(size / 512)`. | ||||||||||||||||||||||||||||||
| * Times default to the moment the entry was created/last modified. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| [Explicit Resource Management]: https://github.com/tc39/proposal-explicit-resource-management | ||||||||||||||||||||||||||||||
| [`MemoryProvider`]: #class-memoryprovider | ||||||||||||||||||||||||||||||
| [`RealFSProvider`]: #class-realfsprovider | ||||||||||||||||||||||||||||||
| [`VirtualFileSystem`]: #class-virtualfilesystem | ||||||||||||||||||||||||||||||
| [`VirtualProvider`]: #class-virtualprovider | ||||||||||||||||||||||||||||||
| [`fs.BigIntStats`]: fs.md#class-fsbigintstats | ||||||||||||||||||||||||||||||
| [`fs.Stats`]: fs.md#class-fsstats | ||||||||||||||||||||||||||||||
| [`node:fs`]: fs.md | ||||||||||||||||||||||||||||||
| [`os.devNull`]: os.md#osdevnull | ||||||||||||||||||||||||||||||
| [`vfs.mount()`]: #vfsmount | ||||||||||||||||||||||||||||||
| [`vfs.unmount()`]: #vfsunmount | ||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is actually useful, do real world app really go through 1000+ calls for all these to hit the optimizing compiler? If not this is over fitting. It's much more realistic to at least e.g. benchmark the module loading performance of a big graph after mounting the VFS.