@componentor/fs 1.1.7
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/LICENSE +21 -0
- package/README.md +742 -0
- package/dist/index.d.ts +544 -0
- package/dist/index.js +2551 -0
- package/dist/index.js.map +1 -0
- package/dist/opfs-hybrid.d.ts +198 -0
- package/dist/opfs-hybrid.js +2552 -0
- package/dist/opfs-hybrid.js.map +1 -0
- package/dist/opfs-worker-proxy.d.ts +224 -0
- package/dist/opfs-worker-proxy.js +274 -0
- package/dist/opfs-worker-proxy.js.map +1 -0
- package/dist/opfs-worker.js +2732 -0
- package/dist/opfs-worker.js.map +1 -0
- package/package.json +66 -0
- package/src/constants.ts +52 -0
- package/src/errors.ts +88 -0
- package/src/file-handle.ts +100 -0
- package/src/global.d.ts +57 -0
- package/src/handle-manager.ts +250 -0
- package/src/index.ts +1404 -0
- package/src/opfs-hybrid.ts +265 -0
- package/src/opfs-worker-proxy.ts +374 -0
- package/src/opfs-worker.ts +253 -0
- package/src/packed-storage.ts +426 -0
- package/src/path-utils.ts +97 -0
- package/src/streams.ts +109 -0
- package/src/symlink-manager.ts +329 -0
- package/src/types.ts +285 -0
package/README.md
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
# @componentor/fs
|
|
2
|
+
|
|
3
|
+
> ๐ A blazing-fast, Node.js-compatible filesystem interface for the browser using the Origin Private File System API
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@componentor/fs)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## โจ Features
|
|
9
|
+
|
|
10
|
+
- ๐ฅ **Lightning Fast** - Leverages sync access handles for optimal performance
|
|
11
|
+
- ๐ **Browser Native** - Built on the modern Origin Private File System API
|
|
12
|
+
- ๐ **Drop-in Replacement** - Compatible with Node.js fs/promises API
|
|
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** - 199 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
|
|
19
|
+
|
|
20
|
+
## ๐ Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @componentor/fs
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
yarn add @componentor/fs
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pnpm add @componentor/fs
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## ๐ง Quick Start
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
import OPFS from '@componentor/fs'
|
|
38
|
+
|
|
39
|
+
const fs = new OPFS()
|
|
40
|
+
|
|
41
|
+
// Write a file
|
|
42
|
+
await fs.writeFile('hello.txt', 'Hello, OPFS World!')
|
|
43
|
+
|
|
44
|
+
// Read it back
|
|
45
|
+
const content = await fs.readFile('hello.txt', { encoding: 'utf8' })
|
|
46
|
+
console.log(content) // "Hello, OPFS World!"
|
|
47
|
+
|
|
48
|
+
// Create directories
|
|
49
|
+
await fs.mkdir('projects/my-app', { recursive: true })
|
|
50
|
+
|
|
51
|
+
// List directory contents
|
|
52
|
+
const files = await fs.readdir('.')
|
|
53
|
+
console.log(files) // ['hello.txt', 'projects']
|
|
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 Comparison
|
|
68
|
+
|
|
69
|
+
| Operation | localStorage | IndexedDB | OPFS-FS |
|
|
70
|
+
|-----------|-------------|-----------|---------|
|
|
71
|
+
| Small Files | ~50ms | ~20ms | **~5ms** |
|
|
72
|
+
| Large Files | Memory limited | ~100ms | **~15ms** |
|
|
73
|
+
| Directory Ops | Not supported | Complex | **Native** |
|
|
74
|
+
|
|
75
|
+
> **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`.
|
|
76
|
+
|
|
77
|
+
## ๐ API Reference
|
|
78
|
+
|
|
79
|
+
### Constructor
|
|
80
|
+
|
|
81
|
+
#### `new OPFS(options?)`
|
|
82
|
+
|
|
83
|
+
Creates a new OPFS filesystem instance.
|
|
84
|
+
|
|
85
|
+
**Parameters:**
|
|
86
|
+
- `options.useSync` (boolean, default: `true`) - Use synchronous access handles when available
|
|
87
|
+
- `options.workerUrl` (URL | string, optional) - Worker script URL. When provided, enables **hybrid mode** for optimal performance
|
|
88
|
+
- `options.read` ('main' | 'worker', default: 'main') - Backend for read operations in hybrid mode
|
|
89
|
+
- `options.write` ('main' | 'worker', default: 'worker') - Backend for write operations in hybrid mode
|
|
90
|
+
- `options.verbose` (boolean, default: `false`) - Enable verbose logging
|
|
91
|
+
|
|
92
|
+
**Example:**
|
|
93
|
+
```javascript
|
|
94
|
+
// Use sync handles (recommended for workers)
|
|
95
|
+
const fs = new OPFS({ useSync: true })
|
|
96
|
+
|
|
97
|
+
// Force async mode
|
|
98
|
+
const fsAsync = new OPFS({ useSync: false })
|
|
99
|
+
|
|
100
|
+
// Use hybrid mode (recommended for main thread - best performance!)
|
|
101
|
+
const fs = new OPFS({
|
|
102
|
+
workerUrl: new URL('./opfs-worker.js', import.meta.url)
|
|
103
|
+
})
|
|
104
|
+
await fs.ready() // Wait for worker to initialize
|
|
105
|
+
|
|
106
|
+
// Don't forget to terminate when done
|
|
107
|
+
fs.terminate()
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Hybrid Mode (Recommended)
|
|
111
|
+
|
|
112
|
+
Hybrid mode provides the **best performance** by routing operations to optimal backends:
|
|
113
|
+
- **Reads on main thread**: No message passing overhead
|
|
114
|
+
- **Writes on worker**: Sync access handles are faster
|
|
115
|
+
|
|
116
|
+
```javascript
|
|
117
|
+
import OPFS from '@componentor/fs'
|
|
118
|
+
|
|
119
|
+
// Create with hybrid mode
|
|
120
|
+
const fs = new OPFS({
|
|
121
|
+
workerUrl: new URL('@componentor/fs/worker-script', import.meta.url)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// Wait for worker to be ready
|
|
125
|
+
await fs.ready()
|
|
126
|
+
|
|
127
|
+
// Use like normal - hybrid routing happens automatically
|
|
128
|
+
await fs.writeFile('test.txt', 'Hello World') // Routed to worker
|
|
129
|
+
const data = await fs.readFile('test.txt') // Routed to main thread
|
|
130
|
+
|
|
131
|
+
// For long-running apps, periodically call gc() to prevent memory leaks
|
|
132
|
+
await fs.gc()
|
|
133
|
+
|
|
134
|
+
// Clean up when done
|
|
135
|
+
fs.terminate()
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Performance comparison** (100 iterations benchmark):
|
|
139
|
+
| Mode | Average Time |
|
|
140
|
+
|------|-------------|
|
|
141
|
+
| Main Thread | ~335ms |
|
|
142
|
+
| Worker Only | ~274ms |
|
|
143
|
+
| **Hybrid** | **~262ms** |
|
|
144
|
+
|
|
145
|
+
### File Operations
|
|
146
|
+
|
|
147
|
+
#### `readFile(path, options?)`
|
|
148
|
+
|
|
149
|
+
Reads the entire contents of a file.
|
|
150
|
+
|
|
151
|
+
**Parameters:**
|
|
152
|
+
- `path` (string) - File path
|
|
153
|
+
- `options.encoding` (string, optional) - Text encoding ('utf8' for string output)
|
|
154
|
+
|
|
155
|
+
**Returns:** `Promise<Uint8Array | string>`
|
|
156
|
+
|
|
157
|
+
**Examples:**
|
|
158
|
+
```javascript
|
|
159
|
+
// Read as binary
|
|
160
|
+
const buffer = await fs.readFile('image.png')
|
|
161
|
+
|
|
162
|
+
// Read as text
|
|
163
|
+
const text = await fs.readFile('config.json', { encoding: 'utf8' })
|
|
164
|
+
|
|
165
|
+
// Parse JSON
|
|
166
|
+
const config = JSON.parse(await fs.readFile('config.json', { encoding: 'utf8' }))
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### `writeFile(path, data, options?)`
|
|
170
|
+
|
|
171
|
+
Writes data to a file, creating it if it doesn't exist.
|
|
172
|
+
|
|
173
|
+
**Parameters:**
|
|
174
|
+
- `path` (string) - File path
|
|
175
|
+
- `data` (string | Uint8Array) - Data to write
|
|
176
|
+
- `options` (object, optional) - Write options
|
|
177
|
+
|
|
178
|
+
**Returns:** `Promise<void>`
|
|
179
|
+
|
|
180
|
+
**Examples:**
|
|
181
|
+
```javascript
|
|
182
|
+
// Write text
|
|
183
|
+
await fs.writeFile('note.txt', 'Hello World')
|
|
184
|
+
|
|
185
|
+
// Write binary data
|
|
186
|
+
await fs.writeFile('data.bin', new Uint8Array([1, 2, 3, 4]))
|
|
187
|
+
|
|
188
|
+
// Write JSON
|
|
189
|
+
await fs.writeFile('config.json', JSON.stringify({ theme: 'dark' }))
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
#### `unlink(path)`
|
|
193
|
+
|
|
194
|
+
Deletes a file.
|
|
195
|
+
|
|
196
|
+
**Parameters:**
|
|
197
|
+
- `path` (string) - File path to delete
|
|
198
|
+
|
|
199
|
+
**Returns:** `Promise<void>`
|
|
200
|
+
|
|
201
|
+
**Example:**
|
|
202
|
+
```javascript
|
|
203
|
+
await fs.unlink('temp.txt')
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### `rename(oldPath, newPath)`
|
|
207
|
+
|
|
208
|
+
Moves/renames a file.
|
|
209
|
+
|
|
210
|
+
**Parameters:**
|
|
211
|
+
- `oldPath` (string) - Current file path
|
|
212
|
+
- `newPath` (string) - New file path
|
|
213
|
+
|
|
214
|
+
**Returns:** `Promise<void>`
|
|
215
|
+
|
|
216
|
+
**Example:**
|
|
217
|
+
```javascript
|
|
218
|
+
await fs.rename('old-name.txt', 'new-name.txt')
|
|
219
|
+
await fs.rename('file.txt', 'backup/file.txt')
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### `stat(path)`
|
|
223
|
+
|
|
224
|
+
Gets file statistics (follows symlinks).
|
|
225
|
+
|
|
226
|
+
**Parameters:**
|
|
227
|
+
- `path` (string) - File path
|
|
228
|
+
|
|
229
|
+
**Returns:** `Promise<FileStats>`
|
|
230
|
+
|
|
231
|
+
**Example:**
|
|
232
|
+
```javascript
|
|
233
|
+
const stats = await fs.stat('large-file.zip')
|
|
234
|
+
console.log(`Size: ${stats.size} bytes`)
|
|
235
|
+
console.log(`Modified: ${new Date(stats.mtimeMs)}`)
|
|
236
|
+
console.log(`Is file: ${stats.isFile()}`)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### `lstat(path)`
|
|
240
|
+
|
|
241
|
+
Gets file statistics without following symlinks.
|
|
242
|
+
|
|
243
|
+
**Parameters:**
|
|
244
|
+
- `path` (string) - File path
|
|
245
|
+
|
|
246
|
+
**Returns:** `Promise<FileStats>`
|
|
247
|
+
|
|
248
|
+
**Example:**
|
|
249
|
+
```javascript
|
|
250
|
+
const stats = await fs.lstat('link.txt')
|
|
251
|
+
if (stats.isSymbolicLink()) {
|
|
252
|
+
console.log(`Symlink pointing to: ${stats.target}`)
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Symlink Operations
|
|
257
|
+
|
|
258
|
+
#### `symlink(target, path)`
|
|
259
|
+
|
|
260
|
+
Creates a symbolic link.
|
|
261
|
+
|
|
262
|
+
**Parameters:**
|
|
263
|
+
- `target` (string) - Target path the symlink points to
|
|
264
|
+
- `path` (string) - Path where the symlink will be created
|
|
265
|
+
|
|
266
|
+
**Returns:** `Promise<void>`
|
|
267
|
+
|
|
268
|
+
**Example:**
|
|
269
|
+
```javascript
|
|
270
|
+
await fs.writeFile('config.json', '{"key": "value"}')
|
|
271
|
+
await fs.symlink('config.json', 'current-config.json')
|
|
272
|
+
|
|
273
|
+
// Read through symlink
|
|
274
|
+
const content = await fs.readFile('current-config.json', { encoding: 'utf8' })
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
#### `readlink(path)`
|
|
278
|
+
|
|
279
|
+
Reads the target of a symbolic link.
|
|
280
|
+
|
|
281
|
+
**Parameters:**
|
|
282
|
+
- `path` (string) - Symlink path
|
|
283
|
+
|
|
284
|
+
**Returns:** `Promise<string>` - The target path
|
|
285
|
+
|
|
286
|
+
**Example:**
|
|
287
|
+
```javascript
|
|
288
|
+
const target = await fs.readlink('my-link.txt')
|
|
289
|
+
console.log(`Link points to: ${target}`)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
#### `symlinkBatch(links)`
|
|
293
|
+
|
|
294
|
+
Creates multiple symbolic links efficiently in a single operation.
|
|
295
|
+
|
|
296
|
+
**Parameters:**
|
|
297
|
+
- `links` (Array<{target: string, path: string}>) - Array of symlink definitions
|
|
298
|
+
|
|
299
|
+
**Returns:** `Promise<void>`
|
|
300
|
+
|
|
301
|
+
**Example:**
|
|
302
|
+
```javascript
|
|
303
|
+
// Create multiple symlinks with a single metadata write
|
|
304
|
+
await fs.symlinkBatch([
|
|
305
|
+
{ target: '/configs/prod.json', path: '/current-config.json' },
|
|
306
|
+
{ target: '/data/latest.db', path: '/current-db.db' },
|
|
307
|
+
{ target: '/logs/today.log', path: '/current.log' }
|
|
308
|
+
])
|
|
309
|
+
|
|
310
|
+
// 60-70% faster than individual symlink() calls
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Directory Operations
|
|
314
|
+
|
|
315
|
+
#### `mkdir(path, options?)`
|
|
316
|
+
|
|
317
|
+
Creates a directory.
|
|
318
|
+
|
|
319
|
+
**Parameters:**
|
|
320
|
+
- `path` (string) - Directory path
|
|
321
|
+
- `options.recursive` (boolean, optional) - Create parent directories
|
|
322
|
+
|
|
323
|
+
**Returns:** `Promise<void>`
|
|
324
|
+
|
|
325
|
+
**Examples:**
|
|
326
|
+
```javascript
|
|
327
|
+
// Create single directory
|
|
328
|
+
await fs.mkdir('uploads')
|
|
329
|
+
|
|
330
|
+
// Create nested directories
|
|
331
|
+
await fs.mkdir('projects/webapp/src', { recursive: true })
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
#### `rmdir(path)`
|
|
335
|
+
|
|
336
|
+
Removes a directory and all its contents.
|
|
337
|
+
|
|
338
|
+
**Parameters:**
|
|
339
|
+
- `path` (string) - Directory path
|
|
340
|
+
|
|
341
|
+
**Returns:** `Promise<void>`
|
|
342
|
+
|
|
343
|
+
**Example:**
|
|
344
|
+
```javascript
|
|
345
|
+
await fs.rmdir('temp-folder')
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
#### `readdir(path)`
|
|
349
|
+
|
|
350
|
+
Lists directory contents.
|
|
351
|
+
|
|
352
|
+
**Parameters:**
|
|
353
|
+
- `path` (string) - Directory path
|
|
354
|
+
|
|
355
|
+
**Returns:** `Promise<string[]>`
|
|
356
|
+
|
|
357
|
+
**Example:**
|
|
358
|
+
```javascript
|
|
359
|
+
const files = await fs.readdir('documents')
|
|
360
|
+
console.log('Files:', files)
|
|
361
|
+
|
|
362
|
+
// List root directory
|
|
363
|
+
const rootFiles = await fs.readdir('.')
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Additional File Operations
|
|
367
|
+
|
|
368
|
+
#### `access(path, mode?)`
|
|
369
|
+
|
|
370
|
+
Tests file accessibility. Throws if the file doesn't exist.
|
|
371
|
+
|
|
372
|
+
```javascript
|
|
373
|
+
await fs.access('/path/to/file') // Throws if not accessible
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
#### `appendFile(path, data)`
|
|
377
|
+
|
|
378
|
+
Appends data to a file, creating it if it doesn't exist.
|
|
379
|
+
|
|
380
|
+
```javascript
|
|
381
|
+
await fs.appendFile('log.txt', 'New log entry\n')
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
#### `copyFile(src, dest, mode?)`
|
|
385
|
+
|
|
386
|
+
Copies a file from source to destination.
|
|
387
|
+
|
|
388
|
+
```javascript
|
|
389
|
+
await fs.copyFile('original.txt', 'backup.txt')
|
|
390
|
+
// With COPYFILE_EXCL flag to fail if dest exists
|
|
391
|
+
await fs.copyFile('src.txt', 'dest.txt', fs.constants.COPYFILE_EXCL)
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
#### `cp(src, dest, options?)`
|
|
395
|
+
|
|
396
|
+
Copies files or directories recursively.
|
|
397
|
+
|
|
398
|
+
```javascript
|
|
399
|
+
// Copy single file
|
|
400
|
+
await fs.cp('file.txt', 'copy.txt')
|
|
401
|
+
|
|
402
|
+
// Copy directory recursively
|
|
403
|
+
await fs.cp('source-dir', 'dest-dir', { recursive: true })
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
#### `exists(path)`
|
|
407
|
+
|
|
408
|
+
Returns true if the path exists, false otherwise (doesn't throw).
|
|
409
|
+
|
|
410
|
+
```javascript
|
|
411
|
+
if (await fs.exists('config.json')) {
|
|
412
|
+
// File exists
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
#### `realpath(path)`
|
|
417
|
+
|
|
418
|
+
Resolves symlinks to get the real path.
|
|
419
|
+
|
|
420
|
+
```javascript
|
|
421
|
+
const realPath = await fs.realpath('my-symlink')
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
#### `rm(path, options?)`
|
|
425
|
+
|
|
426
|
+
Removes files or directories.
|
|
427
|
+
|
|
428
|
+
```javascript
|
|
429
|
+
await fs.rm('file.txt')
|
|
430
|
+
await fs.rm('directory', { recursive: true })
|
|
431
|
+
await fs.rm('maybe-exists', { force: true }) // No error if doesn't exist
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
#### `truncate(path, len?)`
|
|
435
|
+
|
|
436
|
+
Truncates a file to the specified length.
|
|
437
|
+
|
|
438
|
+
```javascript
|
|
439
|
+
await fs.truncate('file.txt', 100) // Truncate to 100 bytes
|
|
440
|
+
await fs.truncate('file.txt') // Truncate to 0 bytes
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
#### `mkdtemp(prefix)`
|
|
444
|
+
|
|
445
|
+
Creates a unique temporary directory.
|
|
446
|
+
|
|
447
|
+
```javascript
|
|
448
|
+
const tempDir = await fs.mkdtemp('/tmp/myapp-')
|
|
449
|
+
console.log(tempDir) // e.g., "/tmp/myapp-1234567890-abc123"
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
#### `open(path, flags?, mode?)`
|
|
453
|
+
|
|
454
|
+
Opens a file and returns a FileHandle.
|
|
455
|
+
|
|
456
|
+
```javascript
|
|
457
|
+
const handle = await fs.open('file.txt', 'r')
|
|
458
|
+
const buffer = new Uint8Array(100)
|
|
459
|
+
await handle.read(buffer)
|
|
460
|
+
await handle.close()
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
#### `opendir(path)`
|
|
464
|
+
|
|
465
|
+
Opens a directory for iteration.
|
|
466
|
+
|
|
467
|
+
```javascript
|
|
468
|
+
const dir = await fs.opendir('/my-dir')
|
|
469
|
+
for await (const entry of dir) {
|
|
470
|
+
console.log(entry.name, entry.isFile(), entry.isDirectory())
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
#### `createReadStream(path, options?)`
|
|
475
|
+
|
|
476
|
+
Creates a readable stream for a file.
|
|
477
|
+
|
|
478
|
+
```javascript
|
|
479
|
+
const stream = fs.createReadStream('large-file.bin')
|
|
480
|
+
const reader = stream.getReader()
|
|
481
|
+
// Read chunks...
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
#### `createWriteStream(path, options?)`
|
|
485
|
+
|
|
486
|
+
Creates a writable stream for a file.
|
|
487
|
+
|
|
488
|
+
```javascript
|
|
489
|
+
const stream = fs.createWriteStream('output.txt')
|
|
490
|
+
const writer = stream.getWriter()
|
|
491
|
+
await writer.write(new TextEncoder().encode('data'))
|
|
492
|
+
await writer.close()
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
#### `watch(path, options?)`
|
|
496
|
+
|
|
497
|
+
Watches for file/directory changes (basic implementation).
|
|
498
|
+
|
|
499
|
+
```javascript
|
|
500
|
+
const watcher = fs.watch('/my-dir')
|
|
501
|
+
for await (const event of watcher) {
|
|
502
|
+
console.log(event.eventType, event.filename)
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Compatibility Methods (No-ops for OPFS)
|
|
507
|
+
|
|
508
|
+
The following methods are implemented for API compatibility but are no-ops since OPFS doesn't support these features:
|
|
509
|
+
|
|
510
|
+
- `chmod(path, mode)` - File modes not supported
|
|
511
|
+
- `chown(path, uid, gid)` - File ownership not supported
|
|
512
|
+
- `utimes(path, atime, mtime)` - Timestamps are read-only
|
|
513
|
+
- `lutimes(path, atime, mtime)` - Symlink timestamps are read-only
|
|
514
|
+
|
|
515
|
+
### Lifecycle Methods (Hybrid Mode)
|
|
516
|
+
|
|
517
|
+
These methods are used when running in hybrid mode (with `workerUrl`):
|
|
518
|
+
|
|
519
|
+
#### `ready()`
|
|
520
|
+
|
|
521
|
+
Wait for the worker to be initialized. Call this before performing any operations.
|
|
522
|
+
|
|
523
|
+
```javascript
|
|
524
|
+
const fs = new OPFS({ workerUrl: '...' })
|
|
525
|
+
await fs.ready() // Wait for worker
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
#### `terminate()`
|
|
529
|
+
|
|
530
|
+
Terminate the background worker. Call this when you're done using the filesystem.
|
|
531
|
+
|
|
532
|
+
```javascript
|
|
533
|
+
fs.terminate() // Clean up worker
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
#### `gc()`
|
|
537
|
+
|
|
538
|
+
Force garbage collection by reinitializing the worker's OPFS instance. Use this for long-running applications to prevent memory leaks.
|
|
539
|
+
|
|
540
|
+
```javascript
|
|
541
|
+
// Periodically call gc() in long-running apps
|
|
542
|
+
await fs.gc()
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
#### `resetCache()`
|
|
546
|
+
|
|
547
|
+
Reset internal caches (symlinks, directory handles). Lighter than `gc()`.
|
|
548
|
+
|
|
549
|
+
```javascript
|
|
550
|
+
fs.resetCache()
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
## ๐ฏ Real-World Examples
|
|
554
|
+
|
|
555
|
+
### Working with Isomorphic Git
|
|
556
|
+
|
|
557
|
+
```javascript
|
|
558
|
+
import git from 'isomorphic-git'
|
|
559
|
+
import OPFS from '@componentor/fs'
|
|
560
|
+
|
|
561
|
+
// Use hybrid mode for best performance with git operations
|
|
562
|
+
const fs = new OPFS({
|
|
563
|
+
workerUrl: new URL('@componentor/fs/worker-script', import.meta.url)
|
|
564
|
+
})
|
|
565
|
+
await fs.ready()
|
|
566
|
+
|
|
567
|
+
// Clone a repository
|
|
568
|
+
await git.clone({
|
|
569
|
+
fs,
|
|
570
|
+
http: fetch,
|
|
571
|
+
dir: '/my-repo',
|
|
572
|
+
url: 'https://github.com/user/repo.git'
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
// Read a file from the repo
|
|
576
|
+
const readme = await fs.readFile('/my-repo/README.md', { encoding: 'utf8' })
|
|
577
|
+
console.log(readme)
|
|
578
|
+
|
|
579
|
+
// Clean up when done
|
|
580
|
+
fs.terminate()
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Building a Code Editor
|
|
584
|
+
|
|
585
|
+
```javascript
|
|
586
|
+
import OPFS from '@componentor/fs'
|
|
587
|
+
|
|
588
|
+
class CodeEditor {
|
|
589
|
+
constructor(workerUrl) {
|
|
590
|
+
// Use hybrid mode for optimal performance
|
|
591
|
+
this.fs = new OPFS({ workerUrl })
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async init() {
|
|
595
|
+
await this.fs.ready()
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
destroy() {
|
|
599
|
+
this.fs.terminate()
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async createProject(name) {
|
|
603
|
+
await this.fs.mkdir(`projects/${name}/src`)
|
|
604
|
+
await this.fs.writeFile(`projects/${name}/package.json`, JSON.stringify({
|
|
605
|
+
name,
|
|
606
|
+
version: '1.0.0',
|
|
607
|
+
main: 'src/index.js'
|
|
608
|
+
}, null, 2))
|
|
609
|
+
await this.fs.writeFile(`projects/${name}/src/index.js`, '// Your code here\n')
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async saveFile(path, content) {
|
|
613
|
+
await this.fs.writeFile(path, content)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async loadFile(path) {
|
|
617
|
+
return await this.fs.readFile(path, { encoding: 'utf8' })
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async getProjectFiles(projectName) {
|
|
621
|
+
return await this.fs.readdir(`projects/${projectName}`)
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### File Upload Handler
|
|
627
|
+
|
|
628
|
+
```javascript
|
|
629
|
+
import OPFS from '@componentor/fs'
|
|
630
|
+
|
|
631
|
+
const fs = new OPFS()
|
|
632
|
+
|
|
633
|
+
async function handleFileUpload(file) {
|
|
634
|
+
// Create uploads directory
|
|
635
|
+
await fs.mkdir('uploads', { recursive: true })
|
|
636
|
+
|
|
637
|
+
// Save uploaded file
|
|
638
|
+
const buffer = new Uint8Array(await file.arrayBuffer())
|
|
639
|
+
const filename = `uploads/${Date.now()}-${file.name}`
|
|
640
|
+
await fs.writeFile(filename, buffer)
|
|
641
|
+
|
|
642
|
+
// Get file info
|
|
643
|
+
const stats = await fs.stat(filename)
|
|
644
|
+
console.log(`Saved ${file.name} (${stats.size} bytes)`)
|
|
645
|
+
|
|
646
|
+
return filename
|
|
647
|
+
}
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
## ๐ Browser Support
|
|
651
|
+
|
|
652
|
+
@componentor/fs requires browsers that support the Origin Private File System API:
|
|
653
|
+
|
|
654
|
+
- โ
Chrome 86+
|
|
655
|
+
- โ
Edge 86+
|
|
656
|
+
- โ
Firefox 111+
|
|
657
|
+
- โ
Safari 15.2+
|
|
658
|
+
|
|
659
|
+
### Feature Detection
|
|
660
|
+
|
|
661
|
+
```javascript
|
|
662
|
+
if ('storage' in navigator && 'getDirectory' in navigator.storage) {
|
|
663
|
+
const fs = new OPFS()
|
|
664
|
+
// OPFS is supported
|
|
665
|
+
} else {
|
|
666
|
+
console.warn('OPFS not supported in this browser')
|
|
667
|
+
// Fallback to other storage solutions
|
|
668
|
+
}
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
## ๐ฆ Error Handling
|
|
672
|
+
|
|
673
|
+
OPFS-FS throws standard filesystem errors:
|
|
674
|
+
|
|
675
|
+
```javascript
|
|
676
|
+
try {
|
|
677
|
+
await fs.readFile('nonexistent.txt')
|
|
678
|
+
} catch (error) {
|
|
679
|
+
if (error.message.includes('ENOENT')) {
|
|
680
|
+
console.log('File not found')
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
try {
|
|
685
|
+
await fs.mkdir('existing-dir')
|
|
686
|
+
} catch (error) {
|
|
687
|
+
if (error.message.includes('EEXIST')) {
|
|
688
|
+
console.log('Directory already exists')
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
## ๐งช Testing
|
|
694
|
+
|
|
695
|
+
@componentor/fs comes with a comprehensive test suite covering all functionality:
|
|
696
|
+
|
|
697
|
+
```bash
|
|
698
|
+
# Run all tests
|
|
699
|
+
npm test
|
|
700
|
+
|
|
701
|
+
# Run tests in watch mode
|
|
702
|
+
npm run test:watch
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
**Test Coverage:**
|
|
706
|
+
- โ
199 tests with 100% pass rate
|
|
707
|
+
- โ
File read/write operations (text and binary)
|
|
708
|
+
- โ
Directory operations (create, remove, list)
|
|
709
|
+
- โ
File metadata and statistics
|
|
710
|
+
- โ
Path normalization and edge cases
|
|
711
|
+
- โ
Symlink operations and resolution
|
|
712
|
+
- โ
Error handling and edge cases
|
|
713
|
+
- โ
Concurrent operations
|
|
714
|
+
- โ
Large file handling
|
|
715
|
+
- โ
Performance benchmarks
|
|
716
|
+
- โ
Git integration with symlinks (isomorphic-git compatibility)
|
|
717
|
+
- โ
Node.js fs compatibility (access, appendFile, copyFile, cp, rm, truncate, open, opendir, streams)
|
|
718
|
+
|
|
719
|
+
See [SYMLINK_IMPLEMENTATION.md](SYMLINK_IMPLEMENTATION.md) for details on symlink support and [PERFORMANCE.md](PERFORMANCE.md) for performance analysis.
|
|
720
|
+
|
|
721
|
+
## ๐ค Contributing
|
|
722
|
+
|
|
723
|
+
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.
|
|
724
|
+
|
|
725
|
+
When contributing, please ensure:
|
|
726
|
+
- All tests pass (`npm test`)
|
|
727
|
+
- New features include corresponding tests
|
|
728
|
+
- Code follows the existing style
|
|
729
|
+
|
|
730
|
+
## ๐ License
|
|
731
|
+
|
|
732
|
+
MIT ยฉ Componentor
|
|
733
|
+
|
|
734
|
+
## ๐ Acknowledgments
|
|
735
|
+
|
|
736
|
+
- Built on the powerful [Origin Private File System API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API)
|
|
737
|
+
- Inspired by Node.js fs/promises module
|
|
738
|
+
- Perfect companion for [isomorphic-git](https://github.com/isomorphic-git/isomorphic-git)
|
|
739
|
+
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
**Made with โค๏ธ for the modern web**
|