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.
- package/docs/adapters.md +134 -0
- package/docs/fsa.md +133 -0
- package/docs/index.ts +55 -0
- package/docs/node.md +207 -0
- package/docs/print.md +78 -0
- package/docs/snapshot.md +124 -0
- package/docs/text.md +71 -0
- package/docs/types.ts +57 -0
- package/docs/volumes.md +175 -0
- package/package.json +10 -9
package/docs/adapters.md
ADDED
|
@@ -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
|
+
```
|
package/docs/snapshot.md
ADDED
|
@@ -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
|
+
}
|
package/docs/volumes.md
ADDED
|
@@ -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.
|
|
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.
|
|
131
|
-
"@jsonjoy.com/fs-fsa": "4.57.
|
|
132
|
-
"@jsonjoy.com/fs-node": "4.57.
|
|
133
|
-
"@jsonjoy.com/fs-node-builtins": "4.57.
|
|
134
|
-
"@jsonjoy.com/fs-node-to-fsa": "4.57.
|
|
135
|
-
"@jsonjoy.com/fs-node-utils": "4.57.
|
|
136
|
-
"@jsonjoy.com/fs-print": "4.57.
|
|
137
|
-
"@jsonjoy.com/fs-snapshot": "4.57.
|
|
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",
|