@componentor/fs 1.2.8 โ 2.0.1
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/README.md +402 -631
- package/dist/index.cjs +2637 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +606 -0
- package/dist/index.d.ts +539 -481
- package/dist/index.js +2358 -2480
- package/dist/index.js.map +1 -1
- package/dist/kernel.js +487 -0
- package/dist/kernel.js.map +1 -0
- package/package.json +39 -45
- package/dist/opfs-hybrid.d.ts +0 -198
- package/dist/opfs-hybrid.js +0 -2743
- package/dist/opfs-hybrid.js.map +0 -1
- package/dist/opfs-worker-proxy.d.ts +0 -224
- package/dist/opfs-worker-proxy.js +0 -274
- package/dist/opfs-worker-proxy.js.map +0 -1
- package/dist/opfs-worker.js +0 -2923
- package/dist/opfs-worker.js.map +0 -1
- package/src/constants.ts +0 -52
- package/src/errors.ts +0 -88
- package/src/file-handle.ts +0 -100
- package/src/global.d.ts +0 -57
- package/src/handle-manager.ts +0 -302
- package/src/index.ts +0 -1416
- package/src/opfs-hybrid.ts +0 -265
- package/src/opfs-worker-proxy.ts +0 -374
- package/src/opfs-worker.ts +0 -253
- package/src/packed-storage.ts +0 -604
- package/src/path-utils.ts +0 -97
- package/src/streams.ts +0 -109
- package/src/symlink-manager.ts +0 -338
- package/src/types.ts +0 -289
package/README.md
CHANGED
|
@@ -1,738 +1,509 @@
|
|
|
1
1
|
# @componentor/fs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Battle-tested OPFS-based Node.js `fs` polyfill with sync and async APIs**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
A high-performance browser filesystem with native OPFS backend and synchronous API support.
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
```typescript
|
|
8
|
+
import { fs } from '@componentor/fs';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- โก **Isomorphic Git Ready** - Perfect companion for browser-based Git operations
|
|
14
|
-
- ๐ **Symlink Support** - Full symbolic link emulation for advanced file operations
|
|
15
|
-
- ๐ฆ **Zero Dependencies** - Lightweight and efficient
|
|
16
|
-
- โ
**Fully Tested** - 214 comprehensive tests with 100% pass rate
|
|
17
|
-
- ๐ **Full fs Compatibility** - access, appendFile, copyFile, cp, rm, truncate, open, opendir, streams, and more
|
|
18
|
-
- ๐ **Hybrid Mode** - Optimal performance with reads on main thread and writes on worker
|
|
10
|
+
// Sync API (requires crossOriginIsolated)
|
|
11
|
+
fs.writeFileSync('/hello.txt', 'Hello World!');
|
|
12
|
+
const data = fs.readFileSync('/hello.txt', 'utf8');
|
|
19
13
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
npm install @componentor/fs
|
|
14
|
+
// Async API (always available)
|
|
15
|
+
await fs.promises.writeFile('/async.txt', 'Async data');
|
|
16
|
+
const content = await fs.promises.readFile('/async.txt', 'utf8');
|
|
24
17
|
```
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Node.js Compatible** - Drop-in replacement for `fs` module
|
|
22
|
+
- **Sync API** - `readFileSync`, `writeFileSync`, etc. (requires COOP/COEP)
|
|
23
|
+
- **Async API** - `promises.readFile`, `promises.writeFile`, etc.
|
|
24
|
+
- **Cross-tab Safe** - Uses `navigator.locks` for multi-tab coordination
|
|
25
|
+
- **isomorphic-git Ready** - Full compatibility with git operations
|
|
26
|
+
- **Zero Config** - Works out of the box, no worker files needed
|
|
27
|
+
- **TypeScript First** - Complete type definitions included
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
29
30
|
|
|
30
31
|
```bash
|
|
31
|
-
|
|
32
|
+
npm install @componentor/fs
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
##
|
|
35
|
+
## Quick Start
|
|
35
36
|
|
|
36
|
-
```
|
|
37
|
-
import
|
|
37
|
+
```typescript
|
|
38
|
+
import { fs, path } from '@componentor/fs';
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
// Create a directory
|
|
41
|
+
await fs.promises.mkdir('/projects/my-app', { recursive: true });
|
|
40
42
|
|
|
41
43
|
// Write a file
|
|
42
|
-
await fs.writeFile('
|
|
43
|
-
|
|
44
|
-
// Read it back
|
|
45
|
-
const content = await fs.readFile('hello.txt', { encoding: 'utf8' })
|
|
46
|
-
console.log(content) // "Hello, OPFS World!"
|
|
44
|
+
await fs.promises.writeFile('/projects/my-app/index.js', 'console.log("Hello!");');
|
|
47
45
|
|
|
48
|
-
//
|
|
49
|
-
await fs.
|
|
46
|
+
// Read a file
|
|
47
|
+
const code = await fs.promises.readFile('/projects/my-app/index.js', 'utf8');
|
|
48
|
+
console.log(code); // 'console.log("Hello!");'
|
|
50
49
|
|
|
51
50
|
// List directory contents
|
|
52
|
-
const files = await fs.readdir('
|
|
53
|
-
console.log(files) // ['
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## ๐ก Why It's Fast
|
|
57
|
-
|
|
58
|
-
The Origin Private File System API provides **direct access to the device's storage** with significantly better performance characteristics than traditional browser storage solutions:
|
|
59
|
-
|
|
60
|
-
### ๐๏ธ Performance Advantages
|
|
61
|
-
|
|
62
|
-
- **Sync Access Handles**: When available, operations bypass the async overhead for read/write operations
|
|
63
|
-
- **Native File System**: Direct integration with the operating system's file system
|
|
64
|
-
- **Optimized I/O**: Reduced serialization overhead compared to IndexedDB or localStorage
|
|
65
|
-
- **Streaming Support**: Efficient handling of large files without memory constraints
|
|
66
|
-
|
|
67
|
-
### ๐ Performance vs LightningFS
|
|
68
|
-
|
|
69
|
-
Benchmarked against [LightningFS](https://github.com/isomorphic-git/lightning-fs) (100 iterations):
|
|
70
|
-
|
|
71
|
-
| Operation | LightningFS | OPFS-FS (Hybrid) | Speedup |
|
|
72
|
-
|-----------|-------------|------------------|---------|
|
|
73
|
-
| Batch Writes | 25.57ms | 1.73ms | **14.8x faster** |
|
|
74
|
-
| Batch Reads | 12.64ms | 1.56ms | **8.1x faster** |
|
|
75
|
-
| Single Writes | 66.45ms | 71.37ms | ~1x |
|
|
76
|
-
| Single Reads | 66.76ms | 66.93ms | ~1x |
|
|
77
|
-
| **Total** | **172.85ms** | **144.30ms** | **1.20x faster** |
|
|
78
|
-
|
|
79
|
-
> **Note:** This package was previously published as `@componentor/opfs-fs`. If you're upgrading, simply change your imports from `@componentor/opfs-fs` to `@componentor/fs`.
|
|
80
|
-
|
|
81
|
-
## ๐ API Reference
|
|
82
|
-
|
|
83
|
-
### Constructor
|
|
84
|
-
|
|
85
|
-
#### `new OPFS(options?)`
|
|
86
|
-
|
|
87
|
-
Creates a new OPFS filesystem instance.
|
|
88
|
-
|
|
89
|
-
**Parameters:**
|
|
90
|
-
- `options.useSync` (boolean, default: `true`) - Use synchronous access handles when available
|
|
91
|
-
- `options.workerUrl` (URL | string, optional) - Worker script URL. When provided, enables **hybrid mode** for optimal performance
|
|
92
|
-
- `options.read` ('main' | 'worker', default: 'main') - Backend for read operations in hybrid mode
|
|
93
|
-
- `options.write` ('main' | 'worker', default: 'worker') - Backend for write operations in hybrid mode
|
|
94
|
-
- `options.verbose` (boolean, default: `false`) - Enable verbose logging
|
|
95
|
-
- `options.useCompression` (boolean, default: `false`) - Enable gzip compression for batch writes. Can improve performance for text-heavy workloads.
|
|
96
|
-
- `options.useChecksum` (boolean, default: `true`) - Enable CRC32 checksum for batch writes. Disable for maximum performance if data integrity verification is not needed.
|
|
97
|
-
|
|
98
|
-
**Example:**
|
|
99
|
-
```javascript
|
|
100
|
-
// Use sync handles (recommended for workers)
|
|
101
|
-
const fs = new OPFS({ useSync: true })
|
|
51
|
+
const files = await fs.promises.readdir('/projects/my-app');
|
|
52
|
+
console.log(files); // ['index.js']
|
|
102
53
|
|
|
103
|
-
//
|
|
104
|
-
const
|
|
54
|
+
// Get file stats
|
|
55
|
+
const stats = await fs.promises.stat('/projects/my-app/index.js');
|
|
56
|
+
console.log(stats.size); // 23
|
|
105
57
|
|
|
106
|
-
// Use
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
await fs.ready() // Wait for worker to initialize
|
|
111
|
-
|
|
112
|
-
// Don't forget to terminate when done
|
|
113
|
-
fs.terminate()
|
|
58
|
+
// Use path utilities
|
|
59
|
+
console.log(path.join('/projects', 'my-app', 'src')); // '/projects/my-app/src'
|
|
60
|
+
console.log(path.dirname('/projects/my-app/index.js')); // '/projects/my-app'
|
|
61
|
+
console.log(path.basename('/projects/my-app/index.js')); // 'index.js'
|
|
114
62
|
```
|
|
115
63
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
Hybrid mode provides the **best performance** by routing operations to optimal backends:
|
|
119
|
-
- **Reads on main thread**: No message passing overhead
|
|
120
|
-
- **Writes on worker**: Sync access handles are faster
|
|
64
|
+
## Performance Tiers
|
|
121
65
|
|
|
122
|
-
|
|
123
|
-
import OPFS from '@componentor/fs'
|
|
66
|
+
@componentor/fs operates in two performance tiers based on browser capabilities:
|
|
124
67
|
|
|
125
|
-
|
|
126
|
-
const fs = new OPFS({
|
|
127
|
-
workerUrl: new URL('@componentor/fs/worker-script', import.meta.url)
|
|
128
|
-
})
|
|
68
|
+
### Tier 1: Sync (Fastest)
|
|
129
69
|
|
|
130
|
-
|
|
131
|
-
await fs.ready()
|
|
70
|
+
**Requirements:** `crossOriginIsolated` context (COOP/COEP headers)
|
|
132
71
|
|
|
133
|
-
|
|
134
|
-
await fs.writeFile('test.txt', 'Hello World') // Routed to worker
|
|
135
|
-
const data = await fs.readFile('test.txt') // Routed to main thread
|
|
72
|
+
Uses `SharedArrayBuffer` + `Atomics` for zero-copy data transfer between main thread and worker. Enables **synchronous** filesystem operations.
|
|
136
73
|
|
|
137
|
-
|
|
138
|
-
|
|
74
|
+
```typescript
|
|
75
|
+
// Tier 1 unlocks sync APIs
|
|
76
|
+
fs.writeFileSync('/file.txt', 'data');
|
|
77
|
+
const data = fs.readFileSync('/file.txt', 'utf8');
|
|
78
|
+
fs.mkdirSync('/dir', { recursive: true });
|
|
79
|
+
fs.existsSync('/file.txt'); // true
|
|
139
80
|
```
|
|
140
81
|
|
|
141
|
-
###
|
|
142
|
-
|
|
143
|
-
#### `readFile(path, options?)`
|
|
82
|
+
### Tier 2: Async (Always Available)
|
|
144
83
|
|
|
145
|
-
|
|
84
|
+
Works in any browser context without special headers. Uses Web Worker with `postMessage` for async operations.
|
|
146
85
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
**Examples:**
|
|
154
|
-
```javascript
|
|
155
|
-
// Read as binary
|
|
156
|
-
const buffer = await fs.readFile('image.png')
|
|
157
|
-
|
|
158
|
-
// Read as text
|
|
159
|
-
const text = await fs.readFile('config.json', { encoding: 'utf8' })
|
|
160
|
-
|
|
161
|
-
// Parse JSON
|
|
162
|
-
const config = JSON.parse(await fs.readFile('config.json', { encoding: 'utf8' }))
|
|
86
|
+
```typescript
|
|
87
|
+
// Tier 2 - promises API always works
|
|
88
|
+
await fs.promises.writeFile('/file.txt', 'data');
|
|
89
|
+
const data = await fs.promises.readFile('/file.txt', 'utf8');
|
|
90
|
+
await fs.promises.mkdir('/dir', { recursive: true });
|
|
91
|
+
await fs.promises.exists('/file.txt'); // true
|
|
163
92
|
```
|
|
164
93
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
Writes data to a file, creating it if it doesn't exist.
|
|
168
|
-
|
|
169
|
-
**Parameters:**
|
|
170
|
-
- `path` (string) - File path
|
|
171
|
-
- `data` (string | Uint8Array) - Data to write
|
|
172
|
-
- `options` (object, optional) - Write options
|
|
173
|
-
|
|
174
|
-
**Returns:** `Promise<void>`
|
|
175
|
-
|
|
176
|
-
**Examples:**
|
|
177
|
-
```javascript
|
|
178
|
-
// Write text
|
|
179
|
-
await fs.writeFile('note.txt', 'Hello World')
|
|
94
|
+
## COOP/COEP Headers (Required for Tier 1)
|
|
180
95
|
|
|
181
|
-
|
|
182
|
-
await fs.writeFile('data.bin', new Uint8Array([1, 2, 3, 4]))
|
|
96
|
+
To enable Tier 1 (sync) performance, your server must send these headers:
|
|
183
97
|
|
|
184
|
-
// Write JSON
|
|
185
|
-
await fs.writeFile('config.json', JSON.stringify({ theme: 'dark' }))
|
|
186
98
|
```
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
Deletes a file.
|
|
191
|
-
|
|
192
|
-
**Parameters:**
|
|
193
|
-
- `path` (string) - File path to delete
|
|
194
|
-
|
|
195
|
-
**Returns:** `Promise<void>`
|
|
196
|
-
|
|
197
|
-
**Example:**
|
|
198
|
-
```javascript
|
|
199
|
-
await fs.unlink('temp.txt')
|
|
99
|
+
Cross-Origin-Opener-Policy: same-origin
|
|
100
|
+
Cross-Origin-Embedder-Policy: require-corp
|
|
200
101
|
```
|
|
201
102
|
|
|
202
|
-
|
|
103
|
+
### Vite Configuration
|
|
203
104
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
await fs.rename('old-name.txt', 'new-name.txt')
|
|
215
|
-
await fs.rename('file.txt', 'backup/file.txt')
|
|
105
|
+
```typescript
|
|
106
|
+
// vite.config.ts
|
|
107
|
+
export default defineConfig({
|
|
108
|
+
server: {
|
|
109
|
+
headers: {
|
|
110
|
+
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
111
|
+
'Cross-Origin-Embedder-Policy': 'require-corp',
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
216
115
|
```
|
|
217
116
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
Gets file statistics (follows symlinks).
|
|
221
|
-
|
|
222
|
-
**Parameters:**
|
|
223
|
-
- `path` (string) - File path
|
|
117
|
+
### Express/Node.js
|
|
224
118
|
|
|
225
|
-
**Returns:** `Promise<FileStats>`
|
|
226
|
-
|
|
227
|
-
**Example:**
|
|
228
119
|
```javascript
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
120
|
+
app.use((req, res, next) => {
|
|
121
|
+
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
|
122
|
+
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
|
|
123
|
+
next();
|
|
124
|
+
});
|
|
233
125
|
```
|
|
234
126
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
Gets file statistics without following symlinks.
|
|
238
|
-
|
|
239
|
-
**Parameters:**
|
|
240
|
-
- `path` (string) - File path
|
|
241
|
-
|
|
242
|
-
**Returns:** `Promise<FileStats>`
|
|
127
|
+
### Vercel
|
|
243
128
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
129
|
+
```json
|
|
130
|
+
// vercel.json
|
|
131
|
+
{
|
|
132
|
+
"headers": [
|
|
133
|
+
{
|
|
134
|
+
"source": "/(.*)",
|
|
135
|
+
"headers": [
|
|
136
|
+
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
137
|
+
{ "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
]
|
|
249
141
|
}
|
|
250
142
|
```
|
|
251
143
|
|
|
252
|
-
###
|
|
253
|
-
|
|
254
|
-
#### `symlink(target, path)`
|
|
255
|
-
|
|
256
|
-
Creates a symbolic link.
|
|
257
|
-
|
|
258
|
-
**Parameters:**
|
|
259
|
-
- `target` (string) - Target path the symlink points to
|
|
260
|
-
- `path` (string) - Path where the symlink will be created
|
|
261
|
-
|
|
262
|
-
**Returns:** `Promise<void>`
|
|
263
|
-
|
|
264
|
-
**Example:**
|
|
265
|
-
```javascript
|
|
266
|
-
await fs.writeFile('config.json', '{"key": "value"}')
|
|
267
|
-
await fs.symlink('config.json', 'current-config.json')
|
|
268
|
-
|
|
269
|
-
// Read through symlink
|
|
270
|
-
const content = await fs.readFile('current-config.json', { encoding: 'utf8' })
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
#### `readlink(path)`
|
|
274
|
-
|
|
275
|
-
Reads the target of a symbolic link.
|
|
276
|
-
|
|
277
|
-
**Parameters:**
|
|
278
|
-
- `path` (string) - Symlink path
|
|
279
|
-
|
|
280
|
-
**Returns:** `Promise<string>` - The target path
|
|
281
|
-
|
|
282
|
-
**Example:**
|
|
283
|
-
```javascript
|
|
284
|
-
const target = await fs.readlink('my-link.txt')
|
|
285
|
-
console.log(`Link points to: ${target}`)
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
#### `symlinkBatch(links)`
|
|
289
|
-
|
|
290
|
-
Creates multiple symbolic links efficiently in a single operation.
|
|
291
|
-
|
|
292
|
-
**Parameters:**
|
|
293
|
-
- `links` (Array<{target: string, path: string}>) - Array of symlink definitions
|
|
294
|
-
|
|
295
|
-
**Returns:** `Promise<void>`
|
|
296
|
-
|
|
297
|
-
**Example:**
|
|
298
|
-
```javascript
|
|
299
|
-
// Create multiple symlinks with a single metadata write
|
|
300
|
-
await fs.symlinkBatch([
|
|
301
|
-
{ target: '/configs/prod.json', path: '/current-config.json' },
|
|
302
|
-
{ target: '/data/latest.db', path: '/current-db.db' },
|
|
303
|
-
{ target: '/logs/today.log', path: '/current.log' }
|
|
304
|
-
])
|
|
305
|
-
|
|
306
|
-
// 60-70% faster than individual symlink() calls
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
### Directory Operations
|
|
310
|
-
|
|
311
|
-
#### `mkdir(path, options?)`
|
|
312
|
-
|
|
313
|
-
Creates a directory.
|
|
314
|
-
|
|
315
|
-
**Parameters:**
|
|
316
|
-
- `path` (string) - Directory path
|
|
317
|
-
- `options.recursive` (boolean, optional) - Create parent directories
|
|
318
|
-
|
|
319
|
-
**Returns:** `Promise<void>`
|
|
320
|
-
|
|
321
|
-
**Examples:**
|
|
322
|
-
```javascript
|
|
323
|
-
// Create single directory
|
|
324
|
-
await fs.mkdir('uploads')
|
|
325
|
-
|
|
326
|
-
// Create nested directories
|
|
327
|
-
await fs.mkdir('projects/webapp/src', { recursive: true })
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
#### `rmdir(path)`
|
|
331
|
-
|
|
332
|
-
Removes a directory and all its contents.
|
|
333
|
-
|
|
334
|
-
**Parameters:**
|
|
335
|
-
- `path` (string) - Directory path
|
|
336
|
-
|
|
337
|
-
**Returns:** `Promise<void>`
|
|
338
|
-
|
|
339
|
-
**Example:**
|
|
340
|
-
```javascript
|
|
341
|
-
await fs.rmdir('temp-folder')
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
#### `readdir(path)`
|
|
345
|
-
|
|
346
|
-
Lists directory contents.
|
|
347
|
-
|
|
348
|
-
**Parameters:**
|
|
349
|
-
- `path` (string) - Directory path
|
|
350
|
-
|
|
351
|
-
**Returns:** `Promise<string[]>`
|
|
352
|
-
|
|
353
|
-
**Example:**
|
|
354
|
-
```javascript
|
|
355
|
-
const files = await fs.readdir('documents')
|
|
356
|
-
console.log('Files:', files)
|
|
357
|
-
|
|
358
|
-
// List root directory
|
|
359
|
-
const rootFiles = await fs.readdir('.')
|
|
360
|
-
```
|
|
144
|
+
### Check if Tier 1 is Available
|
|
361
145
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
await fs.access('/path/to/file') // Throws if not accessible
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
#### `appendFile(path, data)`
|
|
373
|
-
|
|
374
|
-
Appends data to a file, creating it if it doesn't exist.
|
|
375
|
-
|
|
376
|
-
```javascript
|
|
377
|
-
await fs.appendFile('log.txt', 'New log entry\n')
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
#### `copyFile(src, dest, mode?)`
|
|
381
|
-
|
|
382
|
-
Copies a file from source to destination.
|
|
383
|
-
|
|
384
|
-
```javascript
|
|
385
|
-
await fs.copyFile('original.txt', 'backup.txt')
|
|
386
|
-
// With COPYFILE_EXCL flag to fail if dest exists
|
|
387
|
-
await fs.copyFile('src.txt', 'dest.txt', fs.constants.COPYFILE_EXCL)
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
#### `cp(src, dest, options?)`
|
|
391
|
-
|
|
392
|
-
Copies files or directories recursively.
|
|
393
|
-
|
|
394
|
-
```javascript
|
|
395
|
-
// Copy single file
|
|
396
|
-
await fs.cp('file.txt', 'copy.txt')
|
|
397
|
-
|
|
398
|
-
// Copy directory recursively
|
|
399
|
-
await fs.cp('source-dir', 'dest-dir', { recursive: true })
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
#### `exists(path)`
|
|
403
|
-
|
|
404
|
-
Returns true if the path exists, false otherwise (doesn't throw).
|
|
405
|
-
|
|
406
|
-
```javascript
|
|
407
|
-
if (await fs.exists('config.json')) {
|
|
408
|
-
// File exists
|
|
146
|
+
```typescript
|
|
147
|
+
if (crossOriginIsolated) {
|
|
148
|
+
console.log('Tier 1 (sync) available!');
|
|
149
|
+
fs.writeFileSync('/fast.txt', 'blazing fast');
|
|
150
|
+
} else {
|
|
151
|
+
console.log('Tier 2 (async) only');
|
|
152
|
+
await fs.promises.writeFile('/fast.txt', 'still fast');
|
|
409
153
|
}
|
|
410
154
|
```
|
|
411
155
|
|
|
412
|
-
|
|
156
|
+
## Benchmarks
|
|
413
157
|
|
|
414
|
-
|
|
158
|
+
Tested against LightningFS (IndexedDB-based filesystem) in Chrome with Tier 1 enabled:
|
|
415
159
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
await fs.rm('maybe-exists', { force: true }) // No error if doesn't exist
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
#### `truncate(path, len?)`
|
|
431
|
-
|
|
432
|
-
Truncates a file to the specified length.
|
|
433
|
-
|
|
434
|
-
```javascript
|
|
435
|
-
await fs.truncate('file.txt', 100) // Truncate to 100 bytes
|
|
436
|
-
await fs.truncate('file.txt') // Truncate to 0 bytes
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
#### `mkdtemp(prefix)`
|
|
160
|
+
| Operation | @componentor/fs | LightningFS | Winner |
|
|
161
|
+
|-----------|-----------------|-------------|--------|
|
|
162
|
+
| Write 100 x 1KB | 131ms (763 ops/s) | 317ms (316 ops/s) | **OPFS 2.4x** |
|
|
163
|
+
| Write 100 x 4KB | 145ms (690 ops/s) | 49ms (2061 ops/s) | LightningFS |
|
|
164
|
+
| Read 100 x 1KB | 11ms (9170 ops/s) | 17ms (5824 ops/s) | **OPFS 1.6x** |
|
|
165
|
+
| Read 100 x 4KB | 10ms (10493 ops/s) | 16ms (6431 ops/s) | **OPFS 1.6x** |
|
|
166
|
+
| Large 10 x 1MB | 19ms (538 ops/s) | 11ms (910 ops/s) | LightningFS |
|
|
167
|
+
| Batch Write 500 | 416ms (1202 ops/s) | 125ms (4014 ops/s) | LightningFS |
|
|
168
|
+
| Batch Read 500 | 311ms (1608 ops/s) | 74ms (6736 ops/s) | LightningFS |
|
|
169
|
+
| **Git Clone** | 427ms | 1325ms | **OPFS 3.1x** |
|
|
170
|
+
| Git Status 10x | 53ms | 39ms | LightningFS |
|
|
440
171
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
#### `open(path, flags?, mode?)`
|
|
449
|
-
|
|
450
|
-
Opens a file and returns a FileHandle.
|
|
451
|
-
|
|
452
|
-
```javascript
|
|
453
|
-
const handle = await fs.open('file.txt', 'r')
|
|
454
|
-
const buffer = new Uint8Array(100)
|
|
455
|
-
await handle.read(buffer)
|
|
456
|
-
await handle.close()
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
#### `opendir(path)`
|
|
460
|
-
|
|
461
|
-
Opens a directory for iteration.
|
|
462
|
-
|
|
463
|
-
```javascript
|
|
464
|
-
const dir = await fs.opendir('/my-dir')
|
|
465
|
-
for await (const entry of dir) {
|
|
466
|
-
console.log(entry.name, entry.isFile(), entry.isDirectory())
|
|
467
|
-
}
|
|
468
|
-
```
|
|
172
|
+
**Key takeaways:**
|
|
173
|
+
- **Git clone is 2-3x faster** - the most important real-world operation
|
|
174
|
+
- **Reads are 1.6x faster** - OPFS excels at read operations
|
|
175
|
+
- **Small writes (1KB) are 2.4x faster** - great for config files and metadata
|
|
176
|
+
- LightningFS wins on batch operations and larger sequential writes
|
|
469
177
|
|
|
470
|
-
|
|
178
|
+
*Results from Chrome 120+ with crossOriginIsolated enabled. Performance varies by browser and hardware.*
|
|
471
179
|
|
|
472
|
-
|
|
180
|
+
Run benchmarks yourself:
|
|
473
181
|
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
const reader = stream.getReader()
|
|
477
|
-
// Read chunks...
|
|
182
|
+
```bash
|
|
183
|
+
npm run benchmark:open
|
|
478
184
|
```
|
|
479
185
|
|
|
480
|
-
|
|
186
|
+
## API Reference
|
|
187
|
+
|
|
188
|
+
### Sync API (Tier 1 Only)
|
|
481
189
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
await writer.write(new TextEncoder().encode('data'))
|
|
488
|
-
await writer.close()
|
|
489
|
-
```
|
|
190
|
+
```typescript
|
|
191
|
+
// Read/Write
|
|
192
|
+
fs.readFileSync(path: string, options?: { encoding?: string }): Uint8Array | string
|
|
193
|
+
fs.writeFileSync(path: string, data: Uint8Array | string, options?: { flush?: boolean }): void
|
|
194
|
+
fs.appendFileSync(path: string, data: Uint8Array | string): void
|
|
490
195
|
|
|
491
|
-
|
|
196
|
+
// Directories
|
|
197
|
+
fs.mkdirSync(path: string, options?: { recursive?: boolean }): void
|
|
198
|
+
fs.rmdirSync(path: string, options?: { recursive?: boolean }): void
|
|
199
|
+
fs.readdirSync(path: string): string[]
|
|
492
200
|
|
|
493
|
-
|
|
201
|
+
// File Operations
|
|
202
|
+
fs.unlinkSync(path: string): void
|
|
203
|
+
fs.renameSync(oldPath: string, newPath: string): void
|
|
204
|
+
fs.copyFileSync(src: string, dest: string): void
|
|
205
|
+
fs.truncateSync(path: string, len?: number): void
|
|
494
206
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
207
|
+
// Info
|
|
208
|
+
fs.statSync(path: string): Stats
|
|
209
|
+
fs.existsSync(path: string): boolean
|
|
210
|
+
fs.accessSync(path: string, mode?: number): void
|
|
500
211
|
```
|
|
501
212
|
|
|
502
|
-
###
|
|
503
|
-
|
|
504
|
-
The following methods are implemented for API compatibility but are no-ops since OPFS doesn't support these features:
|
|
213
|
+
### Async API (Always Available)
|
|
505
214
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
215
|
+
```typescript
|
|
216
|
+
// Read/Write
|
|
217
|
+
fs.promises.readFile(path: string, options?: ReadOptions): Promise<Uint8Array | string>
|
|
218
|
+
fs.promises.writeFile(path: string, data: Uint8Array | string, options?: WriteOptions): Promise<void>
|
|
219
|
+
fs.promises.appendFile(path: string, data: Uint8Array | string): Promise<void>
|
|
510
220
|
|
|
511
|
-
|
|
221
|
+
// Directories
|
|
222
|
+
fs.promises.mkdir(path: string, options?: { recursive?: boolean }): Promise<void>
|
|
223
|
+
fs.promises.rmdir(path: string, options?: { recursive?: boolean }): Promise<void>
|
|
224
|
+
fs.promises.readdir(path: string, options?: { withFileTypes?: boolean }): Promise<string[] | Dirent[]>
|
|
512
225
|
|
|
513
|
-
|
|
226
|
+
// File Operations
|
|
227
|
+
fs.promises.unlink(path: string): Promise<void>
|
|
228
|
+
fs.promises.rename(oldPath: string, newPath: string): Promise<void>
|
|
229
|
+
fs.promises.copyFile(src: string, dest: string): Promise<void>
|
|
230
|
+
fs.promises.truncate(path: string, len?: number): Promise<void>
|
|
231
|
+
fs.promises.rm(path: string, options?: { recursive?: boolean, force?: boolean }): Promise<void>
|
|
514
232
|
|
|
515
|
-
|
|
233
|
+
// Info
|
|
234
|
+
fs.promises.stat(path: string): Promise<Stats>
|
|
235
|
+
fs.promises.lstat(path: string): Promise<Stats>
|
|
236
|
+
fs.promises.exists(path: string): Promise<boolean>
|
|
237
|
+
fs.promises.access(path: string, mode?: number): Promise<void>
|
|
238
|
+
fs.promises.realpath(path: string): Promise<string>
|
|
516
239
|
|
|
517
|
-
|
|
240
|
+
// Advanced
|
|
241
|
+
fs.promises.open(path: string, flags?: string, mode?: number): Promise<FileHandle>
|
|
242
|
+
fs.promises.opendir(path: string): Promise<Dir>
|
|
243
|
+
fs.promises.mkdtemp(prefix: string): Promise<string>
|
|
244
|
+
fs.promises.symlink(target: string, path: string): Promise<void>
|
|
245
|
+
fs.promises.readlink(path: string): Promise<string>
|
|
246
|
+
fs.promises.link(existingPath: string, newPath: string): Promise<void>
|
|
518
247
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
248
|
+
// Cache Management
|
|
249
|
+
fs.promises.flush(): Promise<void> // Flush pending writes
|
|
250
|
+
fs.promises.purge(): Promise<void> // Clear all caches
|
|
522
251
|
```
|
|
523
252
|
|
|
524
|
-
|
|
253
|
+
### Path Utilities
|
|
525
254
|
|
|
526
|
-
|
|
255
|
+
```typescript
|
|
256
|
+
import { path } from '@componentor/fs';
|
|
527
257
|
|
|
528
|
-
|
|
529
|
-
|
|
258
|
+
path.join('/foo', 'bar', 'baz') // '/foo/bar/baz'
|
|
259
|
+
path.resolve('foo', 'bar') // '/foo/bar'
|
|
260
|
+
path.dirname('/foo/bar/baz.txt') // '/foo/bar'
|
|
261
|
+
path.basename('/foo/bar/baz.txt') // 'baz.txt'
|
|
262
|
+
path.extname('/foo/bar/baz.txt') // '.txt'
|
|
263
|
+
path.normalize('/foo//bar/../baz') // '/foo/baz'
|
|
264
|
+
path.isAbsolute('/foo') // true
|
|
265
|
+
path.relative('/foo/bar', '/foo/baz') // '../baz'
|
|
266
|
+
path.parse('/foo/bar/baz.txt') // { root, dir, base, ext, name }
|
|
267
|
+
path.format({ dir: '/foo', name: 'bar', ext: '.txt' }) // '/foo/bar.txt'
|
|
530
268
|
```
|
|
531
269
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
Force garbage collection by reinitializing the worker's OPFS instance. Use this for long-running applications to prevent memory leaks.
|
|
270
|
+
### Constants
|
|
535
271
|
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
await fs.gc()
|
|
539
|
-
```
|
|
272
|
+
```typescript
|
|
273
|
+
import { constants } from '@componentor/fs';
|
|
540
274
|
|
|
541
|
-
|
|
275
|
+
constants.F_OK // 0 - File exists
|
|
276
|
+
constants.R_OK // 4 - File is readable
|
|
277
|
+
constants.W_OK // 2 - File is writable
|
|
278
|
+
constants.X_OK // 1 - File is executable
|
|
542
279
|
|
|
543
|
-
|
|
280
|
+
constants.COPYFILE_EXCL // 1 - Fail if dest exists
|
|
544
281
|
|
|
545
|
-
|
|
546
|
-
|
|
282
|
+
constants.O_RDONLY // 0
|
|
283
|
+
constants.O_WRONLY // 1
|
|
284
|
+
constants.O_RDWR // 2
|
|
285
|
+
constants.O_CREAT // 64
|
|
286
|
+
constants.O_EXCL // 128
|
|
287
|
+
constants.O_TRUNC // 512
|
|
288
|
+
constants.O_APPEND // 1024
|
|
547
289
|
```
|
|
548
290
|
|
|
549
|
-
##
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
```
|
|
554
|
-
import
|
|
555
|
-
import
|
|
556
|
-
|
|
557
|
-
// Use hybrid mode for best performance with git operations
|
|
558
|
-
const fs = new OPFS({
|
|
559
|
-
workerUrl: new URL('@componentor/fs/worker-script', import.meta.url)
|
|
560
|
-
})
|
|
561
|
-
await fs.ready()
|
|
291
|
+
## isomorphic-git Integration
|
|
292
|
+
|
|
293
|
+
@componentor/fs works seamlessly with isomorphic-git:
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import { fs } from '@componentor/fs';
|
|
297
|
+
import git from 'isomorphic-git';
|
|
298
|
+
import http from 'isomorphic-git/http/web';
|
|
562
299
|
|
|
563
300
|
// Clone a repository
|
|
564
301
|
await git.clone({
|
|
565
302
|
fs,
|
|
566
|
-
http
|
|
567
|
-
dir: '/
|
|
568
|
-
url: 'https://github.com/user/repo
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
//
|
|
576
|
-
fs.
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
```
|
|
303
|
+
http,
|
|
304
|
+
dir: '/repo',
|
|
305
|
+
url: 'https://github.com/user/repo',
|
|
306
|
+
corsProxy: 'https://cors.isomorphic-git.org',
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Check status
|
|
310
|
+
const status = await git.statusMatrix({ fs, dir: '/repo' });
|
|
311
|
+
|
|
312
|
+
// Stage and commit
|
|
313
|
+
await git.add({ fs, dir: '/repo', filepath: '.' });
|
|
314
|
+
await git.commit({
|
|
315
|
+
fs,
|
|
316
|
+
dir: '/repo',
|
|
317
|
+
message: 'Initial commit',
|
|
318
|
+
author: { name: 'User', email: 'user@example.com' },
|
|
319
|
+
});
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Architecture
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
326
|
+
โ Main Thread โ
|
|
327
|
+
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
328
|
+
โ โ Sync API โ โ Async API โ โ Path Utilities โ โ
|
|
329
|
+
โ โ readFileSyncโ โ promises. โ โ join, dirname, etc. โ โ
|
|
330
|
+
โ โwriteFileSyncโ โ readFile โ โโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
331
|
+
โ โโโโโโโโฌโโโโโโโ โโโโโโโโฌโโโโโโโ โ
|
|
332
|
+
โ โ โ โ
|
|
333
|
+
โ โ Atomics.wait โ postMessage โ
|
|
334
|
+
โ โ (Tier 1) โ (Tier 2) โ
|
|
335
|
+
โโโโโโโโโโโผโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
336
|
+
โ โ
|
|
337
|
+
โผ โผ
|
|
338
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
339
|
+
โ Web Worker โ
|
|
340
|
+
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
341
|
+
โ โ OPFS Kernel โ โ
|
|
342
|
+
โ โ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โ โ
|
|
343
|
+
โ โ โ Sync Handle โ โ Directory โ โ navigator โ โ โ
|
|
344
|
+
โ โ โ Cache โ โ Cache โ โ .locks โ โ โ
|
|
345
|
+
โ โ โ (100 max) โ โ โ โ (cross-tab) โ โ โ
|
|
346
|
+
โ โ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โ โ
|
|
347
|
+
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
|
|
348
|
+
โ โ โ
|
|
349
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
350
|
+
โ
|
|
351
|
+
โผ
|
|
352
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
353
|
+
โ OPFS โ
|
|
354
|
+
โ Origin Private File System โ
|
|
355
|
+
โ (Browser Storage API) โ
|
|
356
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## Feature Comparison
|
|
360
|
+
|
|
361
|
+
### API Compatibility
|
|
362
|
+
|
|
363
|
+
| Feature | Node.js fs | @componentor/fs v2 | @componentor/fs v1 | LightningFS |
|
|
364
|
+
|---------|------------|--------------------|--------------------|-------------|
|
|
365
|
+
| `readFile` | โ
| โ
| โ
| โ
|
|
|
366
|
+
| `writeFile` | โ
| โ
| โ
| โ
|
|
|
367
|
+
| `readFileSync` | โ
| โ
Tier 1 | โ | โ |
|
|
368
|
+
| `writeFileSync` | โ
| โ
Tier 1 | โ | โ |
|
|
369
|
+
| `mkdir` / `mkdirSync` | โ
| โ
| โ
| โ
|
|
|
370
|
+
| `readdir` / `readdirSync` | โ
| โ
| โ
| โ
|
|
|
371
|
+
| `stat` / `statSync` | โ
| โ
| โ
| โ
|
|
|
372
|
+
| `unlink` / `unlinkSync` | โ
| โ
| โ
| โ
|
|
|
373
|
+
| `rename` / `renameSync` | โ
| โ
| โ
| โ
|
|
|
374
|
+
| `rm` (recursive) | โ
| โ
| โ
| โ |
|
|
375
|
+
| `copyFile` | โ
| โ
| โ
| โ |
|
|
376
|
+
| `symlink` / `readlink` | โ
| โ
| โ
| โ
|
|
|
377
|
+
| `watch` / `watchFile` | โ
| โ
| โ | โ |
|
|
378
|
+
| `open` / `FileHandle` | โ
| โ
| โ | โ |
|
|
379
|
+
| `opendir` / `Dir` | โ
| โ
| โ | โ |
|
|
380
|
+
| `mkdtemp` | โ
| โ
| โ | โ |
|
|
381
|
+
| Streams | โ
| โ | โ | โ |
|
|
382
|
+
|
|
383
|
+
### Performance Tiers
|
|
384
|
+
|
|
385
|
+
| Capability | Tier 1 Sync | Tier 1 Promises | Tier 2 | Legacy v1 | LightningFS |
|
|
386
|
+
|------------|-------------|-----------------|--------|-----------|-------------|
|
|
387
|
+
| **Sync API** | โ
| โ | โ | โ | โ |
|
|
388
|
+
| **Async API** | โ
| โ
| โ
| โ
| โ
|
|
|
389
|
+
| **Requires COOP/COEP** | โ
| โ
| โ | โ | โ |
|
|
390
|
+
| **SharedArrayBuffer** | โ
| โ
| โ | โ | โ |
|
|
391
|
+
| **Handle Caching** | โ
| โ
| โ | โ | N/A |
|
|
392
|
+
| **Zero-copy Transfer** | โ
| โ | โ | โ | โ |
|
|
393
|
+
| **Cross-tab Safety** | โ
| โ
| โ
| โ
| โ |
|
|
394
|
+
| **Storage Backend** | OPFS | OPFS | OPFS | OPFS | IndexedDB |
|
|
395
|
+
|
|
396
|
+
### Architecture Comparison
|
|
397
|
+
|
|
398
|
+
| Aspect | @componentor/fs v2 | @componentor/fs v1 | LightningFS |
|
|
399
|
+
|--------|--------------------|--------------------|-------------|
|
|
400
|
+
| **Storage** | OPFS (native FS) | OPFS | IndexedDB |
|
|
401
|
+
| **Worker** | Dedicated kernel | Shared worker | None |
|
|
402
|
+
| **Sync Method** | Atomics.wait | N/A | N/A |
|
|
403
|
+
| **Handle Strategy** | Cached (100 max) | Per-operation | N/A |
|
|
404
|
+
| **Locking** | navigator.locks | navigator.locks | None |
|
|
405
|
+
| **Bundle Size** | ~16KB | ~12KB | ~25KB |
|
|
406
|
+
| **TypeScript** | Full | Full | Partial |
|
|
407
|
+
|
|
408
|
+
## Browser Support
|
|
409
|
+
|
|
410
|
+
| Browser | Tier 1 (Sync) | Tier 2 (Async) |
|
|
411
|
+
|---------|---------------|----------------|
|
|
412
|
+
| Chrome 102+ | Yes | Yes |
|
|
413
|
+
| Edge 102+ | Yes | Yes |
|
|
414
|
+
| Firefox 111+ | Yes* | Yes |
|
|
415
|
+
| Safari 15.2+ | No** | Yes |
|
|
416
|
+
| Opera 88+ | Yes | Yes |
|
|
417
|
+
|
|
418
|
+
\* Firefox requires `dom.workers.modules.enabled` flag
|
|
419
|
+
\** Safari doesn't support `createSyncAccessHandle` in workers
|
|
420
|
+
|
|
421
|
+
## Troubleshooting
|
|
422
|
+
|
|
423
|
+
### "SharedArrayBuffer is not defined"
|
|
424
|
+
|
|
425
|
+
Your page is not crossOriginIsolated. Add COOP/COEP headers:
|
|
426
|
+
|
|
427
|
+
```
|
|
428
|
+
Cross-Origin-Opener-Policy: same-origin
|
|
429
|
+
Cross-Origin-Embedder-Policy: require-corp
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### "Atomics.wait cannot be called in this context"
|
|
433
|
+
|
|
434
|
+
`Atomics.wait` can only be called from a Worker. The library handles this automatically - use the async API on the main thread.
|
|
435
|
+
|
|
436
|
+
### "NotAllowedError: Access handle is already open"
|
|
437
|
+
|
|
438
|
+
Another tab or operation has the file open. The library uses `navigator.locks` to prevent this, but if you're using multiple filesystem instances, ensure they coordinate.
|
|
439
|
+
|
|
440
|
+
### Slow Performance
|
|
441
|
+
|
|
442
|
+
1. Check if Tier 1 is enabled: `console.log(crossOriginIsolated)`
|
|
443
|
+
2. Use batch operations when possible
|
|
444
|
+
3. Disable flush for bulk writes: `{ flush: false }`
|
|
445
|
+
4. Call `fs.promises.flush()` after bulk operations
|
|
446
|
+
|
|
447
|
+
## Changelog
|
|
621
448
|
|
|
622
|
-
###
|
|
449
|
+
### v2.0.1 (2025)
|
|
623
450
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
async function handleFileUpload(file) {
|
|
630
|
-
// Create uploads directory
|
|
631
|
-
await fs.mkdir('uploads', { recursive: true })
|
|
632
|
-
|
|
633
|
-
// Save uploaded file
|
|
634
|
-
const buffer = new Uint8Array(await file.arrayBuffer())
|
|
635
|
-
const filename = `uploads/${Date.now()}-${file.name}`
|
|
636
|
-
await fs.writeFile(filename, buffer)
|
|
637
|
-
|
|
638
|
-
// Get file info
|
|
639
|
-
const stats = await fs.stat(filename)
|
|
640
|
-
console.log(`Saved ${file.name} (${stats.size} bytes)`)
|
|
641
|
-
|
|
642
|
-
return filename
|
|
643
|
-
}
|
|
644
|
-
```
|
|
451
|
+
**Bug Fixes:**
|
|
452
|
+
- Fixed mtime not updating correctly when files are modified
|
|
453
|
+
- `stat()` now always returns accurate `lastModified` from OPFS instead of approximation
|
|
454
|
+
- Ensures git status and other mtime-dependent operations work correctly
|
|
645
455
|
|
|
646
|
-
|
|
456
|
+
### v2.0.0 (2025)
|
|
647
457
|
|
|
648
|
-
|
|
458
|
+
**Major rewrite with sync API support and performance tiers.**
|
|
649
459
|
|
|
650
|
-
|
|
651
|
-
-
|
|
652
|
-
-
|
|
653
|
-
-
|
|
460
|
+
**New Features:**
|
|
461
|
+
- Synchronous API (`readFileSync`, `writeFileSync`, etc.) via Atomics
|
|
462
|
+
- Performance tiers (Tier 1 Sync, Tier 1 Promises, Tier 2)
|
|
463
|
+
- Dedicated worker kernel with handle caching (100 max)
|
|
464
|
+
- `watch()` and `watchFile()` for file change notifications
|
|
465
|
+
- `FileHandle` API (`fs.promises.open()`)
|
|
466
|
+
- `Dir` API (`fs.promises.opendir()`)
|
|
467
|
+
- `mkdtemp()` for temporary directories
|
|
468
|
+
- `flush()` and `purge()` for cache management
|
|
469
|
+
- Full `Dirent` support with `withFileTypes` option
|
|
654
470
|
|
|
655
|
-
|
|
471
|
+
**Performance:**
|
|
472
|
+
- 2-3x faster git clone vs LightningFS
|
|
473
|
+
- 1.6x faster reads
|
|
474
|
+
- Handle caching eliminates repeated open/close overhead
|
|
475
|
+
- Zero-copy data transfer with SharedArrayBuffer (Tier 1)
|
|
656
476
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
} else {
|
|
662
|
-
console.warn('OPFS not supported in this browser')
|
|
663
|
-
// Fallback to other storage solutions
|
|
664
|
-
}
|
|
665
|
-
```
|
|
477
|
+
**Breaking Changes:**
|
|
478
|
+
- Requires `crossOriginIsolated` for Tier 1 (sync) features
|
|
479
|
+
- New architecture - not backwards compatible with v1 internals
|
|
480
|
+
- Minimum browser versions increased
|
|
666
481
|
|
|
667
|
-
|
|
482
|
+
### v1.2.8 (2024)
|
|
668
483
|
|
|
669
|
-
|
|
484
|
+
- Final release of v1 branch
|
|
485
|
+
- OPFS-based async filesystem
|
|
486
|
+
- Basic isomorphic-git compatibility
|
|
487
|
+
- Cross-tab locking with `navigator.locks`
|
|
670
488
|
|
|
671
|
-
|
|
672
|
-
try {
|
|
673
|
-
await fs.readFile('nonexistent.txt')
|
|
674
|
-
} catch (error) {
|
|
675
|
-
if (error.message.includes('ENOENT')) {
|
|
676
|
-
console.log('File not found')
|
|
677
|
-
}
|
|
678
|
-
}
|
|
489
|
+
### v1.0.0 (2024)
|
|
679
490
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
console.log('Directory already exists')
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
```
|
|
688
|
-
|
|
689
|
-
## ๐งช Testing
|
|
491
|
+
- Initial release
|
|
492
|
+
- Async-only OPFS filesystem
|
|
493
|
+
- Node.js `fs.promises` compatible API
|
|
494
|
+
- Basic directory and file operations
|
|
690
495
|
|
|
691
|
-
|
|
496
|
+
## Contributing
|
|
692
497
|
|
|
693
498
|
```bash
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
npm
|
|
499
|
+
git clone https://github.com/componentor/fs
|
|
500
|
+
cd fs
|
|
501
|
+
npm install
|
|
502
|
+
npm run dev # Watch mode
|
|
503
|
+
npm test # Run tests
|
|
504
|
+
npm run benchmark:open # Run benchmarks
|
|
699
505
|
```
|
|
700
506
|
|
|
701
|
-
|
|
702
|
-
- โ
214 tests with 100% pass rate
|
|
703
|
-
- โ
File read/write operations (text and binary)
|
|
704
|
-
- โ
Directory operations (create, remove, list)
|
|
705
|
-
- โ
File metadata and statistics
|
|
706
|
-
- โ
Path normalization and edge cases
|
|
707
|
-
- โ
Symlink operations and resolution
|
|
708
|
-
- โ
Error handling and edge cases
|
|
709
|
-
- โ
Concurrent operations
|
|
710
|
-
- โ
Large file handling
|
|
711
|
-
- โ
Performance benchmarks
|
|
712
|
-
- โ
Git integration with symlinks (isomorphic-git compatibility)
|
|
713
|
-
- โ
Node.js fs compatibility (access, appendFile, copyFile, cp, rm, truncate, open, opendir, streams)
|
|
714
|
-
|
|
715
|
-
See [SYMLINK_IMPLEMENTATION.md](SYMLINK_IMPLEMENTATION.md) for details on symlink support and [PERFORMANCE.md](PERFORMANCE.md) for performance analysis.
|
|
716
|
-
|
|
717
|
-
## ๐ค Contributing
|
|
718
|
-
|
|
719
|
-
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
|
|
720
|
-
|
|
721
|
-
When contributing, please ensure:
|
|
722
|
-
- All tests pass (`npm test`)
|
|
723
|
-
- New features include corresponding tests
|
|
724
|
-
- Code follows the existing style
|
|
725
|
-
|
|
726
|
-
## ๐ License
|
|
727
|
-
|
|
728
|
-
MIT ยฉ Componentor
|
|
729
|
-
|
|
730
|
-
## ๐ Acknowledgments
|
|
731
|
-
|
|
732
|
-
- Built on the powerful [Origin Private File System API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API)
|
|
733
|
-
- Inspired by Node.js fs/promises module
|
|
734
|
-
- Perfect companion for [isomorphic-git](https://github.com/isomorphic-git/isomorphic-git)
|
|
735
|
-
|
|
736
|
-
---
|
|
507
|
+
## License
|
|
737
508
|
|
|
738
|
-
|
|
509
|
+
MIT
|