memfs 4.57.4 → 4.57.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,134 @@
1
+ `memfs` bridges its two APIs in both directions:
2
+
3
+ - **Node `fs`-to-FSA** (`memfs/lib/node-to-fsa`) --- expose any `fs`-like
4
+ filesystem (the real `fs`, a `memfs` volume, anything) through browser FSA
5
+ handles.
6
+ - **FSA-to-Node `fs`** (`memfs/lib/fsa-to-node`) --- run `fs`-based code on
7
+ top of a real FSA directory (e.g. the browser's OPFS or a user-picked folder).
8
+
9
+ ## Node `fs`-to-FSA
10
+
11
+ `nodeToFsa` wraps a folder of an `fs`-like filesystem into a `FileSystemDirectoryHandle`:
12
+
13
+ ```ts
14
+ import {nodeToFsa} from 'memfs/lib/node-to-fsa';
15
+
16
+ nodeToFsa(fs, dirPath: string, ctx?: {
17
+ mode?: 'read' | 'readwrite'; // default 'read'
18
+ syncHandleAllowed?: boolean;
19
+ separator?: '/' | '\\';
20
+ }): FileSystemDirectoryHandle;
21
+ ```
22
+
23
+ The `fs` argument can be Node's real `fs` module or any `fs`-like object,
24
+ including a `memfs` instance:
25
+
26
+ ```ts
27
+ import { memfs } from 'memfs';
28
+ import { nodeToFsa } from 'memfs/lib/node-to-fsa';
29
+
30
+ const { fs } = memfs({ '/files/note.txt': 'hi' });
31
+ const dir = nodeToFsa(fs, '/files', { mode: 'readwrite' });
32
+
33
+ const handle = await dir.getFileHandle('note.txt');
34
+ await (await handle.getFile()).text(); // 'hi'
35
+
36
+ const created = await dir.getFileHandle('new.txt', { create: true });
37
+ const writable = await created.createWritable();
38
+ await writable.write('data');
39
+ await writable.close();
40
+ ```
41
+
42
+ From here you have a real FSA
43
+ handle: `getDirectoryHandle`, `removeEntry`, `entries()`, `createWritable()`,
44
+ and (when `syncHandleAllowed`) `createSyncAccessHandle()` all work --- see
45
+ [File System Access](/libs/memfs/file-system-access) for the handle surface.
46
+
47
+ ```jj.note
48
+ The writable stream writes to a temporary `.crswap` swap file and atomically
49
+ renames it over the target on `close()`, mirroring how Chrome implements FSA
50
+ writes.
51
+ ```
52
+
53
+ ## FSA-to-Node `fs`
54
+
55
+ `FsaNodeFs` implements the Node `fs` API on top of an FSA `FileSystemDirectoryHandle`.
56
+ This lets `fs`-based packages run in the browser against OPFS or a directory the
57
+ user granted access to.
58
+
59
+ ```ts
60
+ import { FsaNodeFs } from 'memfs/lib/fsa-to-node';
61
+
62
+ const fs = new FsaNodeFs(dir); // dir: a FileSystemDirectoryHandle (or a Promise of one)
63
+
64
+ await fs.promises.writeFile('/hello.txt', 'Hello World!');
65
+ await fs.promises.readFile('/hello.txt', 'utf8'); // 'Hello World!'
66
+ ```
67
+
68
+ Out of the box the **asynchronous** methods are supported --- the callback API,
69
+ the promises API, `createReadStream`, and `createWriteStream`:
70
+
71
+ ```ts
72
+ fs.mkdir('/dir', err => {
73
+ /* ... */
74
+ });
75
+ fs.createWriteStream('/out.bin').end(Buffer.from([1, 2, 3]));
76
+ ```
77
+
78
+ ### Synchronous API
79
+
80
+ The FSA API is asynchronous, so synchronous `fs` methods (`readFileSync`, `writeFileSync`, ...)
81
+ need a helper that blocks the calling thread. `memfs` does this with a **Web Worker**
82
+ plus `Atomics`/`SharedArrayBuffer`: the sync call parks on the main thread while
83
+ the worker performs the async FSA work.
84
+
85
+ Wire up the sync adapter and pass it to `FsaNodeFs`:
86
+
87
+ ```ts
88
+ import { FsaNodeFs, FsaNodeSyncAdapterWorker } from 'memfs/lib/fsa-to-node';
89
+
90
+ const adapter = await FsaNodeSyncAdapterWorker.start('https://<path>/worker.js', dir);
91
+ const fs = new FsaNodeFs(dir, adapter);
92
+
93
+ fs.writeFileSync('/hello.txt', 'Hello World!'); // now synchronous methods work
94
+ ```
95
+
96
+ The worker file instantiates a `FsaNodeSyncWorker` (imported from the
97
+ underlying package --- it is not re-exported through the `memfs/lib/fsa-to-node`
98
+ entry point):
99
+
100
+ ```ts
101
+ import { FsaNodeSyncWorker } from '@jsonjoy.com/fs-fsa-to-node/lib/worker/FsaNodeSyncWorker';
102
+
103
+ if (typeof window === 'undefined') {
104
+ const worker = new FsaNodeSyncWorker();
105
+ worker.start();
106
+ }
107
+ ```
108
+
109
+ `SharedArrayBuffer` and `Atomics` require the page to be
110
+ [cross-origin isolated](https://web.dev/cross-origin-isolation-guide/): serve
111
+ over HTTPS and send these response headers.
112
+
113
+ ```ts
114
+ // webpack devServer
115
+ {
116
+ devServer: {
117
+ https: true,
118
+ headers: {
119
+ 'Cross-Origin-Opener-Policy': 'same-origin',
120
+ 'Cross-Origin-Embedder-Policy': 'require-corp',
121
+ },
122
+ },
123
+ }
124
+ ```
125
+
126
+ With that in place, most synchronous methods work too.
127
+
128
+ ## Demos
129
+
130
+ The repository ships runnable browser demos:
131
+
132
+ - **Async API and `WriteStream`** --- `yarn demo:fsa-to-node-zipfile`
133
+ - **Synchronous API over a worker** --- `yarn demo:fsa-to-node-sync-tests`
134
+ - **`isomorphic-git` on OPFS / FSA** --- `yarn demo:git-opfs`, `yarn demo:git-fsa`
package/docs/fsa.md ADDED
@@ -0,0 +1,133 @@
1
+ The browser [File System Access (FSA) API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API)
2
+ hands you a `FileSystemDirectoryHandle` and you navigate from
3
+ there --- `getDirectoryHandle`, `getFileHandle`, `createWritable`, and so on. `memfs`
4
+ provides a complete **in-memory** implementation of that API, useful for
5
+ testing FSA/OPFS code in Node and for sandboxes that have no real disk.
6
+
7
+ ```ts
8
+ import { fsa } from 'memfs/lib/fsa';
9
+
10
+ const { dir, core } = fsa({ mode: 'readwrite' });
11
+
12
+ const folder = await dir.getDirectoryHandle('new-folder', { create: true });
13
+ const file = await folder.getFileHandle('file.txt', { create: true });
14
+ await (await file.createWritable()).write('Hello, world!');
15
+
16
+ core.toJSON(); // {'/new-folder/file.txt': 'Hello, world!'}
17
+ ```
18
+
19
+ ## `fsa()`
20
+
21
+ ```ts
22
+ fsa(
23
+ ctx?: Partial<CoreFsaContext>,
24
+ core?: Superblock, // defaults to a fresh in-memory filesystem
25
+ dirPath?: string, // root for the returned handle, defaults to '/'
26
+ ): {
27
+ core: Superblock; // the backing filesystem (toJSON / fromJSON)
28
+ dir: FileSystemDirectoryHandle; // handle rooted at dirPath
29
+ FileSystemObserver: ...; // change-observer constructor
30
+ };
31
+ ```
32
+
33
+ The context controls behaviour:
34
+
35
+ | Option | Default | Meaning |
36
+ | ------------------- | -------- | ----------------------------------------------------------------- |
37
+ | `mode` | `'read'` | `'read'` or `'readwrite'`. Write operations require `'readwrite'` |
38
+ | `separator` | `'/'` | Path separator used by `core.toJSON` / `fromJSON` |
39
+ | `syncHandleAllowed` | `false` | Enables `createSyncAccessHandle()` on file handles |
40
+ | `locks` | --- | A `FileLockManager` controlling concurrent-write locks |
41
+
42
+ `core` is a `Superblock` --- the same in-memory store that backs `Volume` ---
43
+ so you can serialize and seed the filesystem directly:
44
+
45
+ ```ts
46
+ const { dir, core } = fsa({ mode: 'readwrite' });
47
+
48
+ core.fromJSON({
49
+ 'documents/readme.txt': 'Welcome!',
50
+ 'photos/vacation.jpg': Buffer.from('...'),
51
+ 'empty-folder': null,
52
+ });
53
+
54
+ const docs = await dir.getDirectoryHandle('documents');
55
+ const readme = await docs.getFileHandle('readme.txt');
56
+ await (await readme.getFile()).text(); // 'Welcome!'
57
+ ```
58
+
59
+ ## Directory handles
60
+
61
+ A directory handle (`FileSystemDirectoryHandle`) supports the standard async
62
+ methods:
63
+
64
+ | Method | Description |
65
+ | ------------------------------------- | ------------------------------------------------------------------ |
66
+ | `getDirectoryHandle(name, {create?})` | Get or create a subdirectory |
67
+ | `getFileHandle(name, {create?})` | Get or create a file |
68
+ | `removeEntry(name, {recursive?})` | Delete a file or directory |
69
+ | `keys()` / `values()` / `entries()` | Async iterators over children |
70
+ | `resolve(handle)` | Relative path (as `string[]`) from here to a descendant, or `null` |
71
+
72
+ ```ts
73
+ for await (const [name, handle] of dir.entries()) {
74
+ handle.kind; // 'file' | 'directory'
75
+ }
76
+ ```
77
+
78
+ ## File handles
79
+
80
+ A file handle (`FileSystemFileHandle`) reads via `getFile()` and writes via a
81
+ writable stream:
82
+
83
+ | Method | Description |
84
+ | ------------------------------------- | --------------------------------------------------------------- |
85
+ | `getFile()` | Returns a `File` (use `.text()`, `.arrayBuffer()`, `.stream()`) |
86
+ | `createWritable({keepExistingData?})` | Opens a `FileSystemWritableFileStream` |
87
+ | `createSyncAccessHandle()` | Synchronous read/write handle (only when `syncHandleAllowed`) |
88
+
89
+ ## Writable streams
90
+
91
+ `createWritable()` returns a `FileSystemWritableFileStream`. Write plain data,
92
+ or structured write commands to seek and truncate:
93
+
94
+ ```ts
95
+ const file = await dir.getFileHandle('log.csv', { create: true });
96
+ const writable = await file.createWritable();
97
+
98
+ await writable.write('timestamp,level\n'); // append data
99
+ await writable.write({ type: 'write', position: 0, data: 'X' }); // write at offset
100
+ await writable.write({ type: 'seek', position: 4 }); // move cursor
101
+ await writable.write({ type: 'truncate', size: 10 }); // resize
102
+ await writable.close(); // commit
103
+ ```
104
+
105
+ ```jj.note
106
+ Writable streams take a **lock** on the file for the duration. A second
107
+ `createWritable()` on the same file while one is open is rejected --- matching
108
+ browser behaviour. The lock is released on `close()` or `abort()`.
109
+ ```
110
+
111
+ ## Sync access handles
112
+
113
+ When `syncHandleAllowed` is set, file handles expose
114
+ `createSyncAccessHandle()` --- the OPFS worker API with
115
+ `read`/`write`/`getSize`/`truncate`/`flush`/`close`:
116
+
117
+ ```ts
118
+ const { dir } = fsa({ mode: 'readwrite', syncHandleAllowed: true });
119
+ const file = await dir.getFileHandle('data.bin', { create: true });
120
+ const access = await file.createSyncAccessHandle();
121
+
122
+ await access.write(new Uint8Array([1, 2, 3]), { at: 0 });
123
+ await access.getSize(); // 3
124
+ await access.close();
125
+ ```
126
+
127
+ ```jj.note
128
+ The browser spec defines the sync-access-handle methods as *synchronous*. This
129
+ in-memory implementation returns promises instead, so `await` them.
130
+ ```
131
+
132
+ To go the other direction --- an `fs`-like API on top of an FSA handle, or FSA
133
+ handles on top of an `fs` filesystem --- see [Adapters](/libs/memfs/adapters).
package/docs/index.ts ADDED
@@ -0,0 +1,55 @@
1
+ import type { LibPage } from './types';
2
+
3
+ export const page: LibPage = {
4
+ name: 'memfs',
5
+ title: 'memfs',
6
+ type: 'lib',
7
+ subtitle: 'In-memory Node.js fs API and browser File System Access API.',
8
+ pkg: 'memfs',
9
+ group: 'tooling',
10
+ repo: 'streamich/memfs',
11
+ repoPath: 'tree/master/packages/memfs',
12
+ tech: 'TypeScript',
13
+ techIcon: { set: 'lineicons', icon: 'typescript' },
14
+ showContentsTable: true,
15
+ children: [
16
+ {
17
+ name: 'Node fs API',
18
+ subtitle: 'The in-memory fs module: fs, vol, Volume, memfs(), and the supported method surface.',
19
+ // @ts-ignore
20
+ src: async () => (await import('./node.md')).default,
21
+ },
22
+ {
23
+ name: 'Volumes',
24
+ subtitle: 'Create volumes from JSON, export them back, and combine memfs with unionfs and fs-monkey.',
25
+ // @ts-ignore
26
+ src: async () => (await import('./volumes.md')).default,
27
+ },
28
+ {
29
+ name: 'File System (Access)',
30
+ subtitle: 'An in-memory implementation of the browser File System (Access) (FSA) API.',
31
+ // @ts-ignore
32
+ src: async () => (await import('./fsa.md')).default,
33
+ },
34
+ {
35
+ name: 'Adapters',
36
+ subtitle: 'Bridge between the Node fs API and the FSA API in either direction, including a sync bridge.',
37
+ // @ts-ignore
38
+ src: async () => (await import('./adapters.md')).default,
39
+ },
40
+ {
41
+ name: 'Snapshots',
42
+ subtitle: 'POJO, CBOR, and JSON snapshots of any fs directory, preserving symlinks and binary data.',
43
+ // @ts-ignore
44
+ src: async () => (await import('./snapshot.md')).default,
45
+ },
46
+ {
47
+ name: 'Tree printing',
48
+ subtitle: 'Render an ASCII tree of any fs directory.',
49
+ // @ts-ignore
50
+ src: async () => (await import('./print.md')).default,
51
+ },
52
+ ],
53
+ // @ts-ignore
54
+ src: async () => (await import('./text.md')).default,
55
+ };
package/docs/node.md ADDED
@@ -0,0 +1,207 @@
1
+ `memfs` implements [Node's `fs` API](https://nodejs.org/api/fs.html) in memory:
2
+ synchronous, callback, and promise-based methods; read/write streams; file and
3
+ directory watching; hard links and symlinks; i-nodes; file descriptors; and the
4
+ `fs.constants`. It throws same errors as Node (with `.code` set, e.g.
5
+ `ENOENT`), so code that branches on error codes keeps working.
6
+
7
+ ```ts
8
+ import { fs } from 'memfs';
9
+
10
+ fs.writeFileSync('/hello.txt', 'World!');
11
+ fs.readFileSync('/hello.txt', 'utf8'); // 'World!'
12
+ ```
13
+
14
+ ## `memfs()` --- isolated instances
15
+
16
+ `fs`/`vol` are a single shared default volume. For tests, prefer `memfs()`,
17
+ which returns a brand-new, isolated pair so volumes never leak into each other:
18
+
19
+ ```ts
20
+ import { memfs } from 'memfs';
21
+
22
+ const { fs, vol } = memfs();
23
+ ```
24
+
25
+ Seed it from a [nested JSON tree](/libs/memfs/volumes):
26
+
27
+ ```ts
28
+ const { fs } = memfs({
29
+ '/app': {
30
+ 'index.js': 'console.log(1)',
31
+ 'package.json': '{"name": "app"}',
32
+ },
33
+ });
34
+
35
+ fs.readdirSync('/app'); // ['index.js', 'package.json']
36
+ ```
37
+
38
+ The second argument is a cwd string or an options object:
39
+
40
+ ```ts
41
+ interface MemfsOptions {
42
+ /** Working directory for resolving relative paths. Defaults to '/'. */
43
+ cwd?: string;
44
+ /** A process-like object controlling platform, uid, gid, and cwd(). */
45
+ process?: IProcess;
46
+ }
47
+
48
+ memfs(json?: NestedDirectoryJSON, cwdOrOpts?: string | MemfsOptions): {fs: IFs; vol: Volume};
49
+ ```
50
+
51
+ ```ts
52
+ const { fs } = memfs({ './README.md': '# Hi' }, '/repo');
53
+ fs.readFileSync('/repo/README.md', 'utf8'); // '# Hi'
54
+ ```
55
+
56
+ ## `fs` vs `vol`
57
+
58
+ The package exports both `fs` and `vol`. They back onto the **same storage** but
59
+ differ in shape:
60
+
61
+ ```ts
62
+ import { fs, vol } from 'memfs';
63
+ ```
64
+
65
+ - **`vol`** is a `Volume` instance --- it implements every `fs` method, plus
66
+ volume helpers like `fromJSON`/`toJSON`/`reset`/`toTree`. Its methods are
67
+ _not_ bound and it carries no `constants`:
68
+
69
+ ```ts
70
+ vol.writeFileSync('/foo', 'bar');
71
+ vol.F_OK; // undefined
72
+ ```
73
+
74
+ - **`fs`** is an _fs-like_ object built from `vol` with `createFsFromVolume(vol)`.
75
+ All methods are **bound** (safe to destructure) and
76
+ it carries `constants`, `Stats`, `Dirent`, `ReadStream`, `promises`, etc. ---
77
+ identical in shape to `require('fs')`:
78
+
79
+ ```ts
80
+ const { readFileSync, writeFileSync } = fs; // bound, safe to destructure
81
+ fs.constants.O_RDONLY; // 0
82
+ ```
83
+
84
+ Every member of the `fs` object is also re-exported at the top level, so you can
85
+ treat `memfs` itself as the `fs` module:
86
+
87
+ ```ts
88
+ import { readFileSync, F_OK, ReadStream } from 'memfs';
89
+ ```
90
+
91
+ Use `vol` when you want the volume helpers; use `fs` (or `memfs()`) when you want
92
+ a faithful `fs` drop-in.
93
+
94
+ ## `Volume` and `createFsFromVolume`
95
+
96
+ `memfs()` is sugar over two lower-level pieces you can use directly:
97
+
98
+ ```ts
99
+ import { Volume, createFsFromVolume } from 'memfs';
100
+
101
+ const vol = new Volume();
102
+ const fs = createFsFromVolume(vol);
103
+
104
+ fs.writeFileSync('/foo', 'bar');
105
+ ```
106
+
107
+ `new Volume()` is an empty filesystem. `createFsFromVolume(vol)` wraps it into
108
+ the bound, `constants`-carrying `fs`-like object described above. Construct as
109
+ many independent volumes as you need --- see [Volumes](/libs/memfs/volumes).
110
+
111
+ ## The promises API
112
+
113
+ `fs.promises` (equivalently `vol.promises`) mirrors `fs/promises`:
114
+
115
+ ```ts
116
+ const { fs } = memfs();
117
+
118
+ await fs.promises.writeFile('/note.txt', 'hi');
119
+ await fs.promises.readFile('/note.txt', 'utf8'); // 'hi'
120
+
121
+ const handle = await fs.promises.open('/note.txt', 'r');
122
+ const { bytesRead, buffer } = await handle.read(Buffer.alloc(2), 0, 2, 0);
123
+ await handle.close();
124
+ ```
125
+
126
+ ## Streams
127
+
128
+ `createReadStream` and `createWriteStream` return Node-compatible streams:
129
+
130
+ ```ts
131
+ const { fs } = memfs({ '/in.txt': 'stream me' });
132
+
133
+ const out = fs.createWriteStream('/out.txt');
134
+ fs.createReadStream('/in.txt').pipe(out);
135
+ out.on('finish', () => fs.readFileSync('/out.txt', 'utf8')); // 'stream me'
136
+ ```
137
+
138
+ ## Watching
139
+
140
+ Both `fs.watch` (inode events) and `fs.watchFile` (stat polling) are
141
+ implemented:
142
+
143
+ ```ts
144
+ const { fs } = memfs({ '/log.txt': '' });
145
+
146
+ const watcher = fs.watch('/log.txt', (eventType, filename) => {
147
+ // eventType: 'change' | 'rename'
148
+ });
149
+
150
+ fs.appendFileSync('/log.txt', 'entry\n');
151
+ watcher.close();
152
+ ```
153
+
154
+ ## Supporting objects
155
+
156
+ These are available both as named exports and as properties of the `fs` object.
157
+
158
+ | Object | Description |
159
+ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
160
+ | `Stats` | Result of `stat`/`lstat`/`fstat`. `Stats<bigint>` when `{bigint: true}` |
161
+ | `Dirent` | Directory entry from `readdir({withFileTypes: true})` and `Dir`. Exposes `name`, `parentPath`, and `isFile()`/`isDirectory()`/`isSymbolicLink()`/... (the legacy `path` getter is deprecated --- use `parentPath`) |
162
+ | `Dir` | Async directory iterator from `opendir`. Implements `AsyncIterable<Dirent>` and `Symbol.asyncDispose`, so it works with `await using` |
163
+ | `StatFs` | Result of `statfs`/`statfsSync` |
164
+ | `FileHandle` | Returned by `fs.promises.open()` |
165
+ | `StatWatcher` | Returned by `watchFile` |
166
+ | `FSWatcher` | Returned by `watch` |
167
+
168
+ `Dir` with `await using` (auto-closes on scope exit):
169
+
170
+ ```ts
171
+ const { fs } = memfs({ '/d': { a: '', b: '' } });
172
+
173
+ await using dir = await fs.promises.opendir('/d');
174
+ for await (const entry of dir) {
175
+ entry.name; // 'a' then 'b'
176
+ entry.parentPath; // '/d'
177
+ entry.isFile(); // true
178
+ }
179
+ ```
180
+
181
+ ## Relative paths
182
+
183
+ Absolute paths behave as you would expect. **Relative** paths are resolved
184
+ against `process.cwd()` --- which points at your _on-disk_ working directory,
185
+ a folder that almost certainly does not exist inside the in-memory volume. The
186
+ safe choice is to always use absolute paths.
187
+
188
+ If you must use relative paths, either create the cwd inside the volume:
189
+
190
+ ```ts
191
+ vol.mkdirSync(process.cwd(), { recursive: true });
192
+ ```
193
+
194
+ or point the process at `/`, which exists in every volume:
195
+
196
+ ```ts
197
+ process.chdir('/');
198
+ ```
199
+
200
+ (You can also pass a `cwd` to [`memfs()`](/libs/memfs/node-fs-api) or a custom
201
+ `process` object to control this per volume.)
202
+
203
+ ## Dependencies
204
+
205
+ The Node `fs` implementation relies on the `buffer`, `events`, `stream`, and
206
+ `path` built-ins, and uses the `process` and `setImmediate` globals (mocking
207
+ them when unavailable), so it bundles cleanly for the browser.
package/docs/print.md ADDED
@@ -0,0 +1,78 @@
1
+ `toTreeSync` renders an ASCII tree of a directory from any `fs`-like
2
+ filesystem. Pass the filesystem and a starting folder; get back a string.
3
+
4
+ It is published as its own package, which `memfs` already depends on:
5
+
6
+ ```ts
7
+ import { toTreeSync } from '@jsonjoy.com/fs-print';
8
+ ```
9
+
10
+ ```ts
11
+ import * as fs from 'fs';
12
+ import { toTreeSync } from '@jsonjoy.com/fs-print';
13
+
14
+ console.log(toTreeSync(fs, { dir: process.cwd() + '/src' }));
15
+ // src/
16
+ // ├─ __tests__/
17
+ // │ ├─ index.test.ts
18
+ // │ └─ node.test.ts
19
+ // ├─ Dirent.ts
20
+ // ├─ Stats.ts
21
+ // ...
22
+ ```
23
+
24
+ Any `fs` implementation works, including an in-memory `memfs` instance:
25
+
26
+ ```ts
27
+ import { memfs } from 'memfs';
28
+ import { toTreeSync } from '@jsonjoy.com/fs-print';
29
+
30
+ const { fs } = memfs({
31
+ '/src': {
32
+ 'index.ts': '...',
33
+ util: { 'print.ts': '...' },
34
+ },
35
+ });
36
+
37
+ console.log(toTreeSync(fs, { dir: '/src' }));
38
+ // src/
39
+ // ├─ index.ts
40
+ // └─ util/
41
+ // └─ print.ts
42
+ ```
43
+
44
+ Symlinks are rendered with an arrow to their target:
45
+
46
+ ```ts
47
+ // /
48
+ // ├─ a/
49
+ // │ └─ b/
50
+ // │ └─ file.txt
51
+ // └─ goto → /a/b/file.txt
52
+ ```
53
+
54
+ ## Options
55
+
56
+ ```ts
57
+ toTreeSync(fs: FsSynchronousApi, opts?: ToTreeOptions): string;
58
+
59
+ interface ToTreeOptions {
60
+ dir?: string; // starting directory, default '/'
61
+ depth?: number; // max recursion depth, default 10
62
+ separator?: '/' | '\\'; // path separator, default '/'
63
+ tab?: string; // prefix prepended to every line, default ''
64
+ sort?: boolean; // sort entries (dirs first), default true
65
+ }
66
+ ```
67
+
68
+ By default entries are sorted alphabetically with folders first. Set
69
+ `sort: false` to print them in raw filesystem order:
70
+
71
+ ```ts
72
+ console.log(toTreeSync(fs, { sort: false }));
73
+ ```
74
+
75
+ ```jj.note
76
+ `toTreeSync` is synchronous only --- there is no async variant. A `Volume` also
77
+ has a built-in [`toTree()`](/libs/memfs/volumes) for the common in-memory case.
78
+ ```
@@ -0,0 +1,124 @@
1
+ The snapshot utility captures a directory (or single file) from any `fs`-like
2
+ filesystem into a portable value, and restores it later --- recursively, and
3
+ preserving **symlinks** and **binary** content. It comes in three encodings:
4
+ a plain POJO, a compact CBOR `Uint8Array`, and a JSON `Uint8Array`.
5
+
6
+ It is published as its own package, which `memfs` already depends on:
7
+
8
+ ```ts
9
+ import * as snapshot from '@jsonjoy.com/fs-snapshot';
10
+ ```
11
+
12
+ ```jj.note
13
+ For the simple "string/Buffer contents only" case, a `Volume` has built-in
14
+ [`toJSON` / `fromJSON`](/libs/memfs/volumes). Reach for snapshots when you need
15
+ to preserve symlinks, round-trip binary data faithfully, or serialize to bytes
16
+ (CBOR / JSON) for storage or transport.
17
+ ```
18
+
19
+ ## Options
20
+
21
+ Every function takes a target descriptor. The synchronous functions want a
22
+ synchronous `fs`; the async ones want a promises API:
23
+
24
+ ```ts
25
+ // sync functions
26
+ {fs: FsSynchronousApi, path?: string, separator?: '/' | '\\'}
27
+ // async functions
28
+ {fs: FsPromisesApi, path?: string, separator?: '/' | '\\'}
29
+ ```
30
+
31
+ `path` defaults to `'/'`. Any `fs`-like object works --- `memfs`, the real
32
+ `fs`, or an [adapter](/libs/memfs/adapters).
33
+
34
+ ## POJO snapshot
35
+
36
+ `toSnapshot*` returns a `SnapshotNode` (a nested tuple, see below);
37
+ `fromSnapshot*` writes it back into a filesystem.
38
+
39
+ ```ts
40
+ const snap = snapshot.toSnapshotSync({ fs, path: '/app' });
41
+ snapshot.fromSnapshotSync(snap, { fs: fs2, path: '/restored' });
42
+ ```
43
+
44
+ ```ts
45
+ const snap = await snapshot.toSnapshot({ fs: fs.promises, path: '/app' });
46
+ await snapshot.fromSnapshot(snap, { fs: fs2.promises, path: '/restored' });
47
+ ```
48
+
49
+ ## Binary snapshot (CBOR)
50
+
51
+ Encoded as a CBOR `Uint8Array` --- compact and binary-safe, good for storing or
52
+ sending a whole tree over the wire.
53
+
54
+ ```ts
55
+ const bytes = snapshot.toBinarySnapshotSync({ fs, path: '/app' }); // Uint8Array
56
+ snapshot.fromBinarySnapshotSync(bytes, { fs: fs2, path: '/app' });
57
+ ```
58
+
59
+ ```ts
60
+ const bytes = await snapshot.toBinarySnapshot({ fs: fs.promises, path: '/app' });
61
+ await snapshot.fromBinarySnapshot(bytes, { fs: fs2.promises, path: '/app' });
62
+ ```
63
+
64
+ ## JSON snapshot
65
+
66
+ Same idea, JSON-encoded into a `Uint8Array`. Binary file contents are carried as
67
+ Base64 data-URL strings, so the result is valid JSON yet still round-trips
68
+ binary data.
69
+
70
+ ```ts
71
+ const bytes = snapshot.toJsonSnapshotSync({ fs, path: '/app' }); // Uint8Array
72
+ snapshot.fromJsonSnapshotSync(bytes, { fs: fs2, path: '/app' });
73
+ ```
74
+
75
+ ## Function reference
76
+
77
+ | Format | To snapshot | From snapshot | Returns |
78
+ | ------ | ------------------------------------------- | ----------------------------------------------- | -------------- |
79
+ | POJO | `toSnapshotSync` / `toSnapshot` | `fromSnapshotSync` / `fromSnapshot` | `SnapshotNode` |
80
+ | CBOR | `toBinarySnapshotSync` / `toBinarySnapshot` | `fromBinarySnapshotSync` / `fromBinarySnapshot` | `Uint8Array` |
81
+ | JSON | `toJsonSnapshotSync` / `toJsonSnapshot` | `fromJsonSnapshotSync` / `fromJsonSnapshot` | `Uint8Array` |
82
+
83
+ The `*Sync` variants take a synchronous `fs`; the others take `fs.promises` and
84
+ return a `Promise`.
85
+
86
+ ## Encoding format
87
+
88
+ A snapshot follows the [Compact JSON](https://jsonjoy.com/specs/compact-json)
89
+ scheme: each node is a tuple whose first element is its type.
90
+
91
+ ```ts
92
+ const enum SnapshotNodeType {
93
+ Folder = 0,
94
+ File = 1,
95
+ Symlink = 2,
96
+ }
97
+ ```
98
+
99
+ **Folder** --- type, metadata, and a map of children:
100
+
101
+ ```ts
102
+ [
103
+ 0,
104
+ {},
105
+ {
106
+ 'file.bin': [1, {}, new Uint8Array([1, 2, 3])],
107
+ },
108
+ ];
109
+ ```
110
+
111
+ **File** --- type, metadata, and contents as a `Uint8Array`:
112
+
113
+ ```ts
114
+ [1, {}, new Uint8Array([1, 2, 3])];
115
+ ```
116
+
117
+ **Symlink** --- type and metadata carrying the link `target`:
118
+
119
+ ```ts
120
+ [2, { target: 'file.bin' }];
121
+ ```
122
+
123
+ Because the format is structural and stable, a snapshot taken, restored into a
124
+ fresh filesystem, and snapshotted again is deep-equal to the original.
package/docs/text.md ADDED
@@ -0,0 +1,71 @@
1
+ `memfs` is an in-memory file system. It implements two APIs:
2
+
3
+ - **Node's [`fs` module](https://nodejs.org/api/fs.html)** --- a drop-in
4
+ replacement you can use anywhere the `fs` module is expected (tests, mocks,
5
+ bundlers, sandboxes). Files live in memory instead of on disk.
6
+ - **The browser [File System Access (FSA) API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API)**
7
+ --- the same `FileSystemDirectoryHandle` interface a browser exposes, backed
8
+ by memory.
9
+
10
+ It also ships adapters that translate _between_ those two APIs, so you can run `fs`-based
11
+ code in the browser on top of a real directory, or expose an `fs`-like filesystem through
12
+ FSA handles. It runs in Node.js, Deno, Bun, and browsers, and stores file contents
13
+ in `Uint8Array` buffers.
14
+
15
+ ```ts
16
+ import { fs } from 'memfs';
17
+
18
+ fs.writeFileSync('/hello.txt', 'Hello, World!');
19
+ fs.readFileSync('/hello.txt', 'utf8'); // 'Hello, World!'
20
+
21
+ fs.mkdirSync('/dir');
22
+ fs.readdirSync('/'); // ['dir', 'hello.txt']
23
+ ```
24
+
25
+ ## Install
26
+
27
+ ```
28
+ npm install memfs
29
+ ```
30
+
31
+ ## Two ways in
32
+
33
+ The default export gives you a ready-to-use filesystem. `fs` is a bound, `require('fs')`-shaped
34
+ object --- `vol` is the `Volume` behind it. They share the same storage.
35
+
36
+ ```ts
37
+ import { fs, vol } from 'memfs';
38
+
39
+ fs.writeFileSync('/script.sh', 'sudo rm -rf *');
40
+ vol.toJSON(); // {'/script.sh': 'sudo rm -rf *'}
41
+ ```
42
+
43
+ For an isolated filesystem (recommended for tests, so volumes never bleed into
44
+ each other), call `memfs()` --- it returns a fresh `fs`/`vol` pair, optionally
45
+ seeded from a JSON tree:
46
+
47
+ ```ts
48
+ import { memfs } from 'memfs';
49
+
50
+ const { fs, vol } = memfs({ '/app/index.js': 'console.log(1)' });
51
+
52
+ fs.readFileSync('/app/index.js', 'utf8'); // 'console.log(1)'
53
+ ```
54
+
55
+ ## Surface map
56
+
57
+ | Page | Import | What it covers |
58
+ | ---------------------------------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------- |
59
+ | [Node fs API](/libs/memfs/node-fs-api) | `memfs` | The `fs`/`vol`/`Volume`/`memfs()` model and the full Node `fs` method surface |
60
+ | [Volumes](/libs/memfs/volumes) | `memfs` | Building volumes from JSON, exporting them, and `unionfs` / `fs-monkey` integration |
61
+ | [File System Access](/libs/memfs/file-system-access) | `memfs/lib/fsa` | An in-memory FSA filesystem (`FileSystemDirectoryHandle` and friends) |
62
+ | [Adapters](/libs/memfs/adapters) | `memfs/lib/node-to-fsa`, `memfs/lib/fsa-to-node` | Convert between the `fs` API and the FSA API in either direction |
63
+ | [Snapshots](/libs/memfs/snapshots) | `@jsonjoy.com/fs-snapshot` | POJO / CBOR / JSON snapshots of any `fs` directory |
64
+ | [Tree printing](/libs/memfs/tree-printing) | `@jsonjoy.com/fs-print` | Print an ASCII tree of any `fs` directory |
65
+
66
+ ```jj.note
67
+ `memfs` is built from a set of smaller `@jsonjoy.com/fs-*` packages
68
+ (`fs-node`, `fs-fsa`, `fs-node-to-fsa`, `fs-fsa-to-node`, `fs-snapshot`,
69
+ `fs-print`, ...). Installing `memfs` pulls them all in; the snapshot and
70
+ tree-printing utilities are also publishable on their own.
71
+ ```
package/docs/types.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Minimal, self-contained vendored copy of the `LibPage` / `ContentPage`
3
+ * documentation types from `@jsonjoy.com/ui`.
4
+ */
5
+
6
+ export type PageTypes = 'blog' | 'resource' | 'spec' | 'spec-note' | 'lib';
7
+
8
+ /** Landing-page category a library belongs to. */
9
+ export type LibGroupId = 'tooling' | 'plain-text' | 'rich-text' | 'ui' | 'sync';
10
+
11
+ /** Icon reference understood by the site renderer. */
12
+ export interface IconSpec {
13
+ set: string;
14
+ icon: string;
15
+ }
16
+
17
+ /** A node in the documentation page tree. */
18
+ export interface ContentPage {
19
+ /** Name of the item, shown in menus. Also used to derive the URL slug. */
20
+ name: string;
21
+ /** Type of the page. */
22
+ type?: PageTypes;
23
+ /** As displayed in the main page title. Defaults to `name`. */
24
+ title?: string;
25
+ /** One-line subtitle shown under the page title / in cards. */
26
+ subtitle?: string;
27
+ /** Up to a paragraph short description. */
28
+ about?: string;
29
+ /** Main Markdown body of the page, loaded lazily. */
30
+ src?: () => Promise<string>;
31
+ /** Whether a contents table is shown under the Markdown body. */
32
+ showContentsTable?: boolean;
33
+ /** Child pages. */
34
+ children?: ContentPage[];
35
+ /** NPM package name. */
36
+ pkg?: string;
37
+ /** GitHub repo name, e.g. `streamich/memfs`. */
38
+ repo?: string;
39
+ /** Path within the repo, e.g. `tree/master/packages/memfs`. */
40
+ repoPath?: string;
41
+ }
42
+
43
+ /** A code-library documentation page (root of a library's docs tree). */
44
+ export interface LibPage extends ContentPage {
45
+ /** Published npm package name, e.g. `memfs`. */
46
+ pkg?: string;
47
+ /** Primary language or runtime, e.g. `TypeScript`, `Node.js`, `Web`. */
48
+ tech?: string;
49
+ /** Icon shown next to `tech` in the card. */
50
+ techIcon?: IconSpec;
51
+ /** Library technical identifier. */
52
+ libId?: string;
53
+ /** Landing-page category. Defaults to `tooling`. */
54
+ group?: LibGroupId;
55
+ /** Highlight this lib as a card at the top of the landing page. */
56
+ featured?: boolean;
57
+ }
@@ -0,0 +1,175 @@
1
+ A `Volume` is one isolated in-memory filesystem. You can spin up as many as you
2
+ like, seed them from a plain JSON object, and export their contents back to
3
+ JSON --- which makes `memfs` convenient for fixtures and assertions.
4
+
5
+ ```ts
6
+ import { Volume } from 'memfs';
7
+
8
+ const vol = Volume.fromJSON({ '/foo': 'bar' });
9
+ vol.readFileSync('/foo', 'utf8'); // 'bar'
10
+ ```
11
+
12
+ ## Building from JSON
13
+
14
+ There are two JSON shapes. **Flat** maps full paths to contents:
15
+
16
+ ```ts
17
+ type DirectoryJSON = { [path: string]: string | Buffer | null };
18
+ ```
19
+
20
+ ```ts
21
+ const vol = Volume.fromJSON(
22
+ {
23
+ './README.md': '1',
24
+ './src/index.js': '2',
25
+ './node_modules/debug/index.js': '3',
26
+ },
27
+ '/app', // cwd: resolves the relative keys above
28
+ );
29
+
30
+ vol.readFileSync('/app/README.md', 'utf8'); // '1'
31
+ vol.readFileSync('/app/src/index.js', 'utf8'); // '2'
32
+ ```
33
+
34
+ **Nested** lets directories nest as objects --- handy for deeper trees:
35
+
36
+ ```ts
37
+ const vol = Volume.fromNestedJSON({
38
+ '/app': {
39
+ 'index.js': '...',
40
+ src: {
41
+ 'main.ts': '...',
42
+ util: { 'log.ts': '...' },
43
+ },
44
+ },
45
+ });
46
+ ```
47
+
48
+ In both shapes the value `null` means an **empty directory** and an empty
49
+ string `''` means an **empty file**. Values can be `string` or `Buffer`
50
+ (binary). The instance methods `vol.fromJSON(json, cwd?)` and
51
+ `vol.fromNestedJSON(json, cwd?)` add files into an existing volume; the static
52
+ `Volume.fromJSON` / `Volume.fromNestedJSON` create a new one.
53
+
54
+ ```ts
55
+ const vol = new Volume();
56
+ vol.fromJSON({ '/a.txt': 'A' });
57
+ vol.fromJSON({ '/b.txt': 'B' }); // merges in
58
+ ```
59
+
60
+ ```jj.note
61
+ `vol.mountSync(mountpoint, json)` is a legacy alias for adding a flat JSON tree
62
+ rooted at a given path. New code should use `fromJSON` with a cwd.
63
+ ```
64
+
65
+ ## Exporting to JSON
66
+
67
+ `toJSON` walks the volume and returns a flat object --- the inverse of
68
+ `fromJSON`. This is the workhorse for test assertions:
69
+
70
+ ```ts
71
+ vol.writeFileSync('/foo', 'bar');
72
+ vol.toJSON(); // {'/foo': 'bar'}
73
+ ```
74
+
75
+ ```ts
76
+ expect(vol.toJSON()).toEqual({ '/foo': 'bar' });
77
+ ```
78
+
79
+ The full signature lets you scope and shape the output:
80
+
81
+ ```ts
82
+ vol.toJSON(
83
+ paths?: PathLike | PathLike[], // restrict to these paths; omit for everything
84
+ json?: {}, // object to populate (for merging exports)
85
+ isRelative?: boolean, // emit relative instead of absolute paths
86
+ asBuffer?: boolean, // emit Buffer contents instead of strings
87
+ ): DirectoryJSON;
88
+ ```
89
+
90
+ ```ts
91
+ const vol = Volume.fromJSON({ '/dir/a': 'b', '/dir2/a': 'b', '/dir2/c': 'd' });
92
+ vol.toJSON('/dir2'); // {'/dir2/a': 'b', '/dir2/c': 'd'}
93
+ ```
94
+
95
+ ## Reset
96
+
97
+ `reset()` empties a volume so you can reuse it between tests:
98
+
99
+ ```ts
100
+ vol.fromJSON({ '/index.js': '...' });
101
+ vol.toJSON(); // {'/index.js': '...'}
102
+ vol.reset();
103
+ vol.toJSON(); // {}
104
+ ```
105
+
106
+ ## Inspecting as a tree
107
+
108
+ `toTree()` renders the volume as an ASCII tree (a quick built-in; for arbitrary
109
+ `fs` filesystems and more options see [Tree printing](/libs/memfs/tree-printing)):
110
+
111
+ ```ts
112
+ const { vol } = memfs({
113
+ '/src': {
114
+ 'index.ts': '...',
115
+ util: { 'print.ts': '...' },
116
+ },
117
+ });
118
+
119
+ console.log(vol.toTree());
120
+ // /
121
+ // └─ src/
122
+ // ├─ index.ts
123
+ // └─ util/
124
+ // └─ print.ts
125
+ ```
126
+
127
+ `toTree(opts?)` accepts a sub-folder, a `depth`, and a `separator`.
128
+
129
+ ## Many volumes
130
+
131
+ Each `Volume` is fully independent:
132
+
133
+ ```ts
134
+ const a = Volume.fromJSON({ '/foo': 'bar' });
135
+ const b = Volume.fromJSON({ '/foo': 'baz' });
136
+
137
+ a.readFileSync('/foo', 'utf8'); // 'bar'
138
+ b.readFileSync('/foo', 'utf8'); // 'baz'
139
+ ```
140
+
141
+ Reach for `memfs()` when you also want the bound, `constants`-carrying `fs`
142
+ object alongside the volume --- see the [Node fs API](/libs/memfs/node-fs-api)
143
+ page.
144
+
145
+ ## Combine with the real disk: `unionfs`
146
+
147
+ [`unionfs`](https://github.com/streamich/unionfs) layers several filesystems
148
+ into one. Overlay an in-memory volume on top of the real disk:
149
+
150
+ ```ts
151
+ import * as realFs from 'fs';
152
+ import { ufs } from 'unionfs';
153
+ import { Volume } from 'memfs';
154
+
155
+ const vol = Volume.fromJSON({ '/foo': 'bar' });
156
+
157
+ ufs.use(realFs).use(vol);
158
+ ufs.readFileSync('/foo', 'utf8'); // 'bar', served from the volume
159
+ ```
160
+
161
+ ## Patch `require`: `fs-monkey`
162
+
163
+ [`fs-monkey`](https://github.com/streamich/fs-monkey) can point Node's module
164
+ loader at a volume, so `require()` resolves modules out of memory:
165
+
166
+ ```ts
167
+ import { patchRequire } from 'fs-monkey';
168
+
169
+ vol.writeFileSync('/index.js', 'console.log("hi world")');
170
+ patchRequire(vol);
171
+ require('/index'); // logs: hi world
172
+ ```
173
+
174
+ `fs-monkey` also exposes `patchFs(vol)` to monkey-patch the global `fs` module
175
+ itself.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memfs",
3
- "version": "4.57.4",
3
+ "version": "4.57.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -49,6 +49,7 @@
49
49
  "lib",
50
50
  "dist",
51
51
  "README.md",
52
+ "docs",
52
53
  "LICENSE",
53
54
  "demo/runkit.js"
54
55
  ],
@@ -127,14 +128,14 @@
127
128
  "tslib": "2"
128
129
  },
129
130
  "dependencies": {
130
- "@jsonjoy.com/fs-core": "4.57.4",
131
- "@jsonjoy.com/fs-fsa": "4.57.4",
132
- "@jsonjoy.com/fs-node": "4.57.4",
133
- "@jsonjoy.com/fs-node-builtins": "4.57.4",
134
- "@jsonjoy.com/fs-node-to-fsa": "4.57.4",
135
- "@jsonjoy.com/fs-node-utils": "4.57.4",
136
- "@jsonjoy.com/fs-print": "4.57.4",
137
- "@jsonjoy.com/fs-snapshot": "4.57.4",
131
+ "@jsonjoy.com/fs-core": "4.57.5",
132
+ "@jsonjoy.com/fs-fsa": "4.57.5",
133
+ "@jsonjoy.com/fs-node": "4.57.5",
134
+ "@jsonjoy.com/fs-node-builtins": "4.57.5",
135
+ "@jsonjoy.com/fs-node-to-fsa": "4.57.5",
136
+ "@jsonjoy.com/fs-node-utils": "4.57.5",
137
+ "@jsonjoy.com/fs-print": "4.57.5",
138
+ "@jsonjoy.com/fs-snapshot": "4.57.5",
138
139
  "@jsonjoy.com/json-pack": "^1.11.0",
139
140
  "@jsonjoy.com/util": "^1.9.0",
140
141
  "glob-to-regex.js": "^1.0.1",