worker-fs-mount 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,832 @@
1
+ /**
2
+ * Replacement module for node:fs/promises that routes mounted paths
3
+ * to their WorkerFilesystem implementations.
4
+ *
5
+ * Users should alias this in their wrangler.toml:
6
+ *
7
+ * [alias]
8
+ * "node:fs/promises" = "worker-fs-mount/fs"
9
+ *
10
+ * This module implements derived operations (readFile, writeFile, truncate,
11
+ * cp, access, rename) on top of the core streaming interface.
12
+ */
13
+
14
+ import { Buffer } from 'node:buffer';
15
+ import type { BigIntStats, Dirent, Stats } from 'node:fs';
16
+ // Import the SYNC fs module and use .promises to avoid alias loop
17
+ import * as nodeFs from 'node:fs';
18
+ import { findMount } from './registry.js';
19
+ import type { DirEntry, Stat, WorkerFilesystem } from './types.js';
20
+
21
+ // Get the real fs/promises from the sync module
22
+ const realFs = nodeFs.promises;
23
+
24
+ /**
25
+ * Extract a string path from various PathLike types.
26
+ */
27
+ function getPath(pathLike: unknown): string | null {
28
+ if (typeof pathLike === 'string') return pathLike;
29
+ if (pathLike instanceof URL) return pathLike.pathname;
30
+ if (Buffer.isBuffer(pathLike)) return pathLike.toString('utf8');
31
+ return null;
32
+ }
33
+
34
+ /**
35
+ * Create a Node.js-style filesystem error.
36
+ */
37
+ function createFsError(
38
+ code: string,
39
+ syscall: string,
40
+ path: string,
41
+ message?: string
42
+ ): NodeJS.ErrnoException {
43
+ const msg = message ?? `${code}: ${syscall} '${path}'`;
44
+ const err = new Error(msg) as NodeJS.ErrnoException;
45
+ err.code = code;
46
+ err.syscall = syscall;
47
+ err.path = path;
48
+ return err;
49
+ }
50
+
51
+ /**
52
+ * Convert our Stat type to a Node.js Stats-like object.
53
+ */
54
+ function toNodeStats(s: Stat): Stats {
55
+ const isFile = s.type === 'file';
56
+ const isDir = s.type === 'directory';
57
+ const isSymlink = s.type === 'symlink';
58
+
59
+ const mtime = s.lastModified ?? new Date(0);
60
+ const birthtime = s.created ?? new Date(0);
61
+
62
+ const stats = {
63
+ isFile: () => isFile,
64
+ isDirectory: () => isDir,
65
+ isSymbolicLink: () => isSymlink,
66
+ isBlockDevice: () => false,
67
+ isCharacterDevice: () => false,
68
+ isFIFO: () => false,
69
+ isSocket: () => false,
70
+ dev: 0,
71
+ ino: 0,
72
+ mode: isDir ? 0o755 : 0o644,
73
+ nlink: 1,
74
+ uid: 0,
75
+ gid: 0,
76
+ rdev: 0,
77
+ size: s.size,
78
+ blksize: 4096,
79
+ blocks: Math.ceil(s.size / 512),
80
+ atimeMs: mtime.getTime(),
81
+ mtimeMs: mtime.getTime(),
82
+ ctimeMs: mtime.getTime(),
83
+ birthtimeMs: birthtime.getTime(),
84
+ atime: mtime,
85
+ mtime: mtime,
86
+ ctime: mtime,
87
+ birthtime: birthtime,
88
+ };
89
+
90
+ return stats as Stats;
91
+ }
92
+
93
+ /**
94
+ * Convert our DirEntry to a Node.js Dirent-like object.
95
+ */
96
+ function toNodeDirent(entry: DirEntry, parentPath: string): Dirent {
97
+ const isFile = entry.type === 'file';
98
+ const isDir = entry.type === 'directory';
99
+ const isSymlink = entry.type === 'symlink';
100
+
101
+ const dirent = {
102
+ name: entry.name,
103
+ parentPath: parentPath,
104
+ path: parentPath,
105
+ isFile: () => isFile,
106
+ isDirectory: () => isDir,
107
+ isSymbolicLink: () => isSymlink,
108
+ isBlockDevice: () => false,
109
+ isCharacterDevice: () => false,
110
+ isFIFO: () => false,
111
+ isSocket: () => false,
112
+ };
113
+
114
+ return dirent as Dirent;
115
+ }
116
+
117
+ // === Helper functions for derived operations ===
118
+
119
+ /**
120
+ * Collect all chunks from a ReadableStream into a single Uint8Array.
121
+ */
122
+ async function collectStream(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
123
+ const reader = stream.getReader();
124
+ const chunks: Uint8Array[] = [];
125
+ let totalLength = 0;
126
+
127
+ while (true) {
128
+ const { done, value } = await reader.read();
129
+ if (done) break;
130
+ chunks.push(value);
131
+ totalLength += value.length;
132
+ }
133
+
134
+ const result = new Uint8Array(totalLength);
135
+ let offset = 0;
136
+ for (const chunk of chunks) {
137
+ result.set(chunk, offset);
138
+ offset += chunk.length;
139
+ }
140
+
141
+ return result;
142
+ }
143
+
144
+ /**
145
+ * Write data to a WritableStream.
146
+ */
147
+ async function writeToStream(stream: WritableStream<Uint8Array>, data: Uint8Array): Promise<void> {
148
+ const writer = stream.getWriter();
149
+ try {
150
+ await writer.write(data);
151
+ } finally {
152
+ await writer.close();
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Pipe a ReadableStream to a WritableStream.
158
+ */
159
+ async function pipeStreams(
160
+ readable: ReadableStream<Uint8Array>,
161
+ writable: WritableStream<Uint8Array>
162
+ ): Promise<void> {
163
+ const reader = readable.getReader();
164
+ const writer = writable.getWriter();
165
+
166
+ try {
167
+ while (true) {
168
+ const { done, value } = await reader.read();
169
+ if (done) break;
170
+ await writer.write(value);
171
+ }
172
+ } finally {
173
+ await writer.close();
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Recursively copy a directory using streaming.
179
+ */
180
+ async function copyDirectoryRecursive(
181
+ srcStub: WorkerFilesystem,
182
+ srcPath: string,
183
+ destStub: WorkerFilesystem,
184
+ destPath: string
185
+ ): Promise<void> {
186
+ // Create destination directory
187
+ await destStub.mkdir(destPath, { recursive: true });
188
+
189
+ // List source directory
190
+ const entries = await srcStub.readdir(srcPath);
191
+
192
+ for (const entry of entries) {
193
+ const srcChildPath = srcPath === '/' ? `/${entry.name}` : `${srcPath}/${entry.name}`;
194
+ const destChildPath = destPath === '/' ? `/${entry.name}` : `${destPath}/${entry.name}`;
195
+
196
+ if (entry.type === 'directory') {
197
+ await copyDirectoryRecursive(srcStub, srcChildPath, destStub, destChildPath);
198
+ } else if (entry.type === 'file') {
199
+ const readStream = await srcStub.createReadStream(srcChildPath);
200
+ const writeStream = await destStub.createWriteStream(destChildPath);
201
+ await pipeStreams(readStream, writeStream);
202
+ } else if (entry.type === 'symlink' && srcStub.readlink && destStub.symlink) {
203
+ const target = await srcStub.readlink(srcChildPath);
204
+ await destStub.symlink(destChildPath, target);
205
+ }
206
+ }
207
+ }
208
+
209
+ // === Wrapped Functions ===
210
+
211
+ export async function readFile(
212
+ path: Parameters<typeof realFs.readFile>[0],
213
+ options?: Parameters<typeof realFs.readFile>[1]
214
+ ): Promise<Buffer | string> {
215
+ const pathStr = getPath(path);
216
+ if (pathStr) {
217
+ const match = findMount(pathStr);
218
+ if (match) {
219
+ // Use streaming to read file
220
+ const stream = await match.mount.stub.createReadStream(match.relativePath);
221
+ const data = await collectStream(stream);
222
+ const buffer = Buffer.from(data);
223
+
224
+ const encoding =
225
+ typeof options === 'string'
226
+ ? options
227
+ : typeof options === 'object' && options !== null
228
+ ? options.encoding
229
+ : undefined;
230
+
231
+ if (encoding) {
232
+ return buffer.toString(encoding as BufferEncoding);
233
+ }
234
+ return buffer;
235
+ }
236
+ }
237
+ return realFs.readFile(path, options) as Promise<Buffer | string>;
238
+ }
239
+
240
+ export async function writeFile(
241
+ path: Parameters<typeof realFs.writeFile>[0],
242
+ data: Parameters<typeof realFs.writeFile>[1],
243
+ options?: Parameters<typeof realFs.writeFile>[2]
244
+ ): Promise<void> {
245
+ const pathStr = getPath(path);
246
+ if (pathStr) {
247
+ const match = findMount(pathStr);
248
+ if (match) {
249
+ let bytes: Uint8Array;
250
+ if (typeof data === 'string') {
251
+ bytes = new TextEncoder().encode(data);
252
+ } else if (Buffer.isBuffer(data)) {
253
+ bytes = new Uint8Array(data);
254
+ } else if (data instanceof Uint8Array) {
255
+ bytes = data;
256
+ } else if (ArrayBuffer.isView(data)) {
257
+ bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
258
+ } else {
259
+ const chunks: Uint8Array[] = [];
260
+ for await (const chunk of data as AsyncIterable<string | Uint8Array>) {
261
+ if (typeof chunk === 'string') {
262
+ chunks.push(new TextEncoder().encode(chunk));
263
+ } else {
264
+ chunks.push(new Uint8Array(chunk));
265
+ }
266
+ }
267
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
268
+ bytes = new Uint8Array(totalLength);
269
+ let offset = 0;
270
+ for (const chunk of chunks) {
271
+ bytes.set(chunk, offset);
272
+ offset += chunk.length;
273
+ }
274
+ }
275
+
276
+ const flag = typeof options === 'object' && options !== null ? options.flag : undefined;
277
+ const isAppend = flag === 'a' || flag === 'a+';
278
+ const isExclusive = flag === 'wx' || flag === 'xw';
279
+
280
+ // Check exclusive flag
281
+ if (isExclusive) {
282
+ const existing = await match.mount.stub.stat(match.relativePath);
283
+ if (existing) {
284
+ throw createFsError('EEXIST', 'writeFile', pathStr);
285
+ }
286
+ }
287
+
288
+ // Use streaming to write file
289
+ const stream = await match.mount.stub.createWriteStream(match.relativePath, {
290
+ flags: isAppend ? 'a' : 'w',
291
+ });
292
+ await writeToStream(stream, bytes);
293
+ return;
294
+ }
295
+ }
296
+ return realFs.writeFile(path, data, options);
297
+ }
298
+
299
+ export async function appendFile(
300
+ path: Parameters<typeof realFs.appendFile>[0],
301
+ data: Parameters<typeof realFs.appendFile>[1],
302
+ options?: Parameters<typeof realFs.appendFile>[2]
303
+ ): Promise<void> {
304
+ const pathStr = getPath(path);
305
+ if (pathStr) {
306
+ const match = findMount(pathStr);
307
+ if (match) {
308
+ let bytes: Uint8Array;
309
+ if (typeof data === 'string') {
310
+ bytes = new TextEncoder().encode(data);
311
+ } else {
312
+ bytes = new Uint8Array(data);
313
+ }
314
+
315
+ // Use streaming with append flag
316
+ const stream = await match.mount.stub.createWriteStream(match.relativePath, {
317
+ flags: 'a',
318
+ });
319
+ await writeToStream(stream, bytes);
320
+ return;
321
+ }
322
+ }
323
+ return realFs.appendFile(path, data, options);
324
+ }
325
+
326
+ export async function stat(
327
+ path: Parameters<typeof realFs.stat>[0],
328
+ options?: Parameters<typeof realFs.stat>[1]
329
+ ): Promise<Stats | BigIntStats> {
330
+ const pathStr = getPath(path);
331
+ if (pathStr) {
332
+ const match = findMount(pathStr);
333
+ if (match) {
334
+ const s = await match.mount.stub.stat(match.relativePath, { followSymlinks: true });
335
+ if (!s) {
336
+ throw createFsError('ENOENT', 'stat', pathStr);
337
+ }
338
+ return toNodeStats(s);
339
+ }
340
+ }
341
+ return realFs.stat(path, options) as Promise<Stats | BigIntStats>;
342
+ }
343
+
344
+ export async function lstat(
345
+ path: Parameters<typeof realFs.lstat>[0],
346
+ options?: Parameters<typeof realFs.lstat>[1]
347
+ ): Promise<Stats | BigIntStats> {
348
+ const pathStr = getPath(path);
349
+ if (pathStr) {
350
+ const match = findMount(pathStr);
351
+ if (match) {
352
+ const s = await match.mount.stub.stat(match.relativePath, { followSymlinks: false });
353
+ if (!s) {
354
+ throw createFsError('ENOENT', 'lstat', pathStr);
355
+ }
356
+ return toNodeStats(s);
357
+ }
358
+ }
359
+ return realFs.lstat(path, options) as Promise<Stats | BigIntStats>;
360
+ }
361
+
362
+ export async function readdir(
363
+ path: Parameters<typeof realFs.readdir>[0],
364
+ options?: Parameters<typeof realFs.readdir>[1]
365
+ ): Promise<string[] | Buffer[] | Dirent[]> {
366
+ const pathStr = getPath(path);
367
+ if (pathStr) {
368
+ const match = findMount(pathStr);
369
+ if (match) {
370
+ const opts = typeof options === 'object' && options !== null ? options : {};
371
+ const recursive = 'recursive' in opts ? opts.recursive === true : false;
372
+ const entries = await match.mount.stub.readdir(match.relativePath, { recursive });
373
+
374
+ const withFileTypes = 'withFileTypes' in opts ? opts.withFileTypes === true : false;
375
+
376
+ if (withFileTypes) {
377
+ return entries.map((e) => toNodeDirent(e, pathStr));
378
+ }
379
+
380
+ const encoding =
381
+ typeof options === 'string'
382
+ ? options
383
+ : typeof options === 'object' && options !== null && 'encoding' in options
384
+ ? options.encoding
385
+ : undefined;
386
+
387
+ if (encoding === 'buffer') {
388
+ return entries.map((e) => Buffer.from(e.name));
389
+ }
390
+
391
+ return entries.map((e) => e.name);
392
+ }
393
+ }
394
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
395
+ return realFs.readdir(path, options as any) as Promise<string[] | Buffer[] | Dirent[]>;
396
+ }
397
+
398
+ export async function mkdir(
399
+ path: Parameters<typeof realFs.mkdir>[0],
400
+ options?: Parameters<typeof realFs.mkdir>[1]
401
+ ): Promise<string | undefined> {
402
+ const pathStr = getPath(path);
403
+ if (pathStr) {
404
+ const match = findMount(pathStr);
405
+ if (match) {
406
+ const recursive =
407
+ typeof options === 'object' && options !== null && options.recursive === true;
408
+ return match.mount.stub.mkdir(match.relativePath, { recursive });
409
+ }
410
+ }
411
+ return realFs.mkdir(path, options) as Promise<string | undefined>;
412
+ }
413
+
414
+ export async function rm(
415
+ path: Parameters<typeof realFs.rm>[0],
416
+ options?: Parameters<typeof realFs.rm>[1]
417
+ ): Promise<void> {
418
+ const pathStr = getPath(path);
419
+ if (pathStr) {
420
+ const match = findMount(pathStr);
421
+ if (match) {
422
+ const recursive = options?.recursive === true;
423
+ const force = options?.force === true;
424
+ return match.mount.stub.rm(match.relativePath, { recursive, force });
425
+ }
426
+ }
427
+ return realFs.rm(path, options);
428
+ }
429
+
430
+ export async function rmdir(
431
+ path: Parameters<typeof realFs.rmdir>[0],
432
+ options?: { maxRetries?: number; retryDelay?: number }
433
+ ): Promise<void> {
434
+ const pathStr = getPath(path);
435
+ if (pathStr) {
436
+ const match = findMount(pathStr);
437
+ if (match) {
438
+ return match.mount.stub.rm(match.relativePath, { recursive: false, force: false });
439
+ }
440
+ }
441
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
442
+ return (realFs.rmdir as any)(path, options);
443
+ }
444
+
445
+ export async function unlink(path: Parameters<typeof realFs.unlink>[0]): Promise<void> {
446
+ const pathStr = getPath(path);
447
+ if (pathStr) {
448
+ const match = findMount(pathStr);
449
+ if (match) {
450
+ // Derive unlink from stat + rm
451
+ const s = await match.mount.stub.stat(match.relativePath);
452
+ if (!s) {
453
+ throw createFsError('ENOENT', 'unlink', pathStr);
454
+ }
455
+ if (s.type === 'directory') {
456
+ throw createFsError('EISDIR', 'unlink', pathStr);
457
+ }
458
+ return match.mount.stub.rm(match.relativePath);
459
+ }
460
+ }
461
+ return realFs.unlink(path);
462
+ }
463
+
464
+ export async function rename(
465
+ oldPath: Parameters<typeof realFs.rename>[0],
466
+ newPath: Parameters<typeof realFs.rename>[1]
467
+ ): Promise<void> {
468
+ const oldPathStr = getPath(oldPath);
469
+ const newPathStr = getPath(newPath);
470
+
471
+ if (oldPathStr && newPathStr) {
472
+ const oldMatch = findMount(oldPathStr);
473
+ const newMatch = findMount(newPathStr);
474
+
475
+ // Cross-mount rename not supported
476
+ if (oldMatch?.mount !== newMatch?.mount) {
477
+ throw createFsError('EXDEV', 'rename', oldPathStr, 'Cross-mount rename not supported');
478
+ }
479
+
480
+ if (oldMatch && newMatch) {
481
+ // Derive rename as copy + delete
482
+ const srcStat = await oldMatch.mount.stub.stat(oldMatch.relativePath);
483
+ if (!srcStat) {
484
+ throw createFsError('ENOENT', 'rename', oldPathStr);
485
+ }
486
+
487
+ if (srcStat.type === 'directory') {
488
+ // Recursive directory copy + delete
489
+ await copyDirectoryRecursive(
490
+ oldMatch.mount.stub,
491
+ oldMatch.relativePath,
492
+ newMatch.mount.stub,
493
+ newMatch.relativePath
494
+ );
495
+ await oldMatch.mount.stub.rm(oldMatch.relativePath, { recursive: true });
496
+ } else if (srcStat.type === 'file') {
497
+ // Stream copy + delete
498
+ const readStream = await oldMatch.mount.stub.createReadStream(oldMatch.relativePath);
499
+ const writeStream = await newMatch.mount.stub.createWriteStream(newMatch.relativePath);
500
+ await pipeStreams(readStream, writeStream);
501
+ await oldMatch.mount.stub.rm(oldMatch.relativePath);
502
+ } else if (srcStat.type === 'symlink') {
503
+ // Copy symlink + delete
504
+ if (oldMatch.mount.stub.readlink && newMatch.mount.stub.symlink) {
505
+ const target = await oldMatch.mount.stub.readlink(oldMatch.relativePath);
506
+ await newMatch.mount.stub.symlink(newMatch.relativePath, target);
507
+ await oldMatch.mount.stub.rm(oldMatch.relativePath);
508
+ } else {
509
+ throw createFsError('ENOSYS', 'rename', oldPathStr, 'symlink operations not supported');
510
+ }
511
+ }
512
+ return;
513
+ }
514
+ }
515
+
516
+ return realFs.rename(oldPath, newPath);
517
+ }
518
+
519
+ export async function copyFile(
520
+ src: Parameters<typeof realFs.copyFile>[0],
521
+ dest: Parameters<typeof realFs.copyFile>[1],
522
+ mode?: Parameters<typeof realFs.copyFile>[2]
523
+ ): Promise<void> {
524
+ const srcStr = getPath(src);
525
+ const destStr = getPath(dest);
526
+
527
+ if (srcStr && destStr) {
528
+ const srcMatch = findMount(srcStr);
529
+ const destMatch = findMount(destStr);
530
+
531
+ if (srcMatch || destMatch) {
532
+ // Use streaming for copy
533
+ if (srcMatch && destMatch) {
534
+ // Both on mounts - pipe streams
535
+ const readStream = await srcMatch.mount.stub.createReadStream(srcMatch.relativePath);
536
+ const writeStream = await destMatch.mount.stub.createWriteStream(destMatch.relativePath);
537
+ await pipeStreams(readStream, writeStream);
538
+ } else if (srcMatch) {
539
+ // Source on mount, dest on real fs
540
+ const readStream = await srcMatch.mount.stub.createReadStream(srcMatch.relativePath);
541
+ const data = await collectStream(readStream);
542
+ await realFs.writeFile(dest, data);
543
+ } else if (destMatch) {
544
+ // Source on real fs, dest on mount
545
+ const buffer = await realFs.readFile(src);
546
+ const writeStream = await destMatch.mount.stub.createWriteStream(destMatch.relativePath);
547
+ await writeToStream(writeStream, new Uint8Array(buffer));
548
+ }
549
+ return;
550
+ }
551
+ }
552
+
553
+ return realFs.copyFile(src, dest, mode);
554
+ }
555
+
556
+ export async function cp(
557
+ src: Parameters<typeof realFs.cp>[0],
558
+ dest: Parameters<typeof realFs.cp>[1],
559
+ options?: Parameters<typeof realFs.cp>[2]
560
+ ): Promise<void> {
561
+ const srcStr = getPath(src);
562
+ const destStr = getPath(dest);
563
+
564
+ if (srcStr && destStr) {
565
+ const srcMatch = findMount(srcStr);
566
+ const destMatch = findMount(destStr);
567
+
568
+ if (srcMatch || destMatch) {
569
+ // Check if source is a directory
570
+ let srcStat: Stat | null | undefined;
571
+ let isDirectory = false;
572
+
573
+ if (srcMatch) {
574
+ srcStat = await srcMatch.mount.stub.stat(srcMatch.relativePath);
575
+ isDirectory = srcStat?.type === 'directory';
576
+ } else {
577
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
578
+ const realStat = await realFs.stat(src as any);
579
+ isDirectory = realStat.isDirectory();
580
+ }
581
+
582
+ if (isDirectory) {
583
+ if (!options?.recursive) {
584
+ throw createFsError('EISDIR', 'cp', srcStr, 'cp requires recursive for directories');
585
+ }
586
+
587
+ if (srcMatch && destMatch) {
588
+ await copyDirectoryRecursive(
589
+ srcMatch.mount.stub,
590
+ srcMatch.relativePath,
591
+ destMatch.mount.stub,
592
+ destMatch.relativePath
593
+ );
594
+ } else {
595
+ throw createFsError(
596
+ 'EXDEV',
597
+ 'cp',
598
+ srcStr,
599
+ 'Directory copy across mount boundary not supported'
600
+ );
601
+ }
602
+ } else {
603
+ // File copy using streaming
604
+ if (srcMatch && destMatch) {
605
+ const readStream = await srcMatch.mount.stub.createReadStream(srcMatch.relativePath);
606
+ const writeStream = await destMatch.mount.stub.createWriteStream(destMatch.relativePath);
607
+ await pipeStreams(readStream, writeStream);
608
+ } else if (srcMatch) {
609
+ const readStream = await srcMatch.mount.stub.createReadStream(srcMatch.relativePath);
610
+ const data = await collectStream(readStream);
611
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
612
+ await realFs.writeFile(dest as any, data);
613
+ } else if (destMatch) {
614
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
615
+ const buffer = await realFs.readFile(src as any);
616
+ const writeStream = await destMatch.mount.stub.createWriteStream(destMatch.relativePath);
617
+ await writeToStream(writeStream, new Uint8Array(buffer));
618
+ }
619
+ }
620
+ return;
621
+ }
622
+ }
623
+
624
+ return realFs.cp(src, dest, options);
625
+ }
626
+
627
+ export async function access(
628
+ path: Parameters<typeof realFs.access>[0],
629
+ mode?: Parameters<typeof realFs.access>[1]
630
+ ): Promise<void> {
631
+ const pathStr = getPath(path);
632
+ if (pathStr) {
633
+ const match = findMount(pathStr);
634
+ if (match) {
635
+ // Derive from stat - just check if it exists
636
+ const s = await match.mount.stub.stat(match.relativePath);
637
+ if (!s) {
638
+ throw createFsError('ENOENT', 'access', pathStr);
639
+ }
640
+ return;
641
+ }
642
+ }
643
+ return realFs.access(path, mode);
644
+ }
645
+
646
+ export async function truncate(
647
+ path: Parameters<typeof realFs.truncate>[0],
648
+ len?: Parameters<typeof realFs.truncate>[1]
649
+ ): Promise<void> {
650
+ const pathStr = getPath(path);
651
+ if (pathStr) {
652
+ const match = findMount(pathStr);
653
+ if (match) {
654
+ const length = len ?? 0;
655
+
656
+ // Derive truncate using streams
657
+ const srcStat = await match.mount.stub.stat(match.relativePath);
658
+ if (!srcStat) {
659
+ throw createFsError('ENOENT', 'truncate', pathStr);
660
+ }
661
+ if (srcStat.type !== 'file') {
662
+ throw createFsError('EISDIR', 'truncate', pathStr);
663
+ }
664
+
665
+ let newData: Uint8Array;
666
+ if (length === 0) {
667
+ // Truncate to empty
668
+ newData = new Uint8Array(0);
669
+ } else if (length >= srcStat.size) {
670
+ // Extend with zeros
671
+ const readStream = await match.mount.stub.createReadStream(match.relativePath);
672
+ const existingData = await collectStream(readStream);
673
+ newData = new Uint8Array(length);
674
+ newData.set(existingData, 0);
675
+ // Rest is already zeros
676
+ } else {
677
+ // Truncate to smaller size - read only what we need
678
+ const readStream = await match.mount.stub.createReadStream(match.relativePath, {
679
+ start: 0,
680
+ end: length - 1,
681
+ });
682
+ newData = await collectStream(readStream);
683
+ }
684
+
685
+ const writeStream = await match.mount.stub.createWriteStream(match.relativePath);
686
+ await writeToStream(writeStream, newData);
687
+ return;
688
+ }
689
+ }
690
+ return realFs.truncate(path, len);
691
+ }
692
+
693
+ export async function symlink(
694
+ target: Parameters<typeof realFs.symlink>[0],
695
+ path: Parameters<typeof realFs.symlink>[1],
696
+ type?: Parameters<typeof realFs.symlink>[2]
697
+ ): Promise<void> {
698
+ const pathStr = getPath(path);
699
+ const targetStr = getPath(target);
700
+
701
+ if (pathStr && targetStr) {
702
+ const match = findMount(pathStr);
703
+ if (match) {
704
+ if (!match.mount.stub.symlink) {
705
+ throw createFsError('ENOSYS', 'symlink', pathStr, 'symlink not supported');
706
+ }
707
+ return match.mount.stub.symlink(match.relativePath, targetStr);
708
+ }
709
+ }
710
+ return realFs.symlink(target, path, type);
711
+ }
712
+
713
+ export async function readlink(
714
+ path: Parameters<typeof realFs.readlink>[0],
715
+ options?: Parameters<typeof realFs.readlink>[1]
716
+ ): Promise<string | Buffer> {
717
+ const pathStr = getPath(path);
718
+ if (pathStr) {
719
+ const match = findMount(pathStr);
720
+ if (match) {
721
+ if (!match.mount.stub.readlink) {
722
+ throw createFsError('ENOSYS', 'readlink', pathStr, 'readlink not supported');
723
+ }
724
+ const target = await match.mount.stub.readlink(match.relativePath);
725
+
726
+ const encoding =
727
+ typeof options === 'string'
728
+ ? options
729
+ : typeof options === 'object' && options !== null
730
+ ? options.encoding
731
+ : undefined;
732
+
733
+ if (encoding === 'buffer') {
734
+ return Buffer.from(target);
735
+ }
736
+ return target;
737
+ }
738
+ }
739
+ return realFs.readlink(path, options) as Promise<string | Buffer>;
740
+ }
741
+
742
+ export async function realpath(
743
+ path: Parameters<typeof realFs.realpath>[0],
744
+ options?: Parameters<typeof realFs.realpath>[1]
745
+ ): Promise<string | Buffer> {
746
+ const pathStr = getPath(path);
747
+ if (pathStr) {
748
+ const match = findMount(pathStr);
749
+ if (match) {
750
+ const encoding =
751
+ typeof options === 'string'
752
+ ? options
753
+ : typeof options === 'object' && options !== null
754
+ ? options.encoding
755
+ : undefined;
756
+
757
+ if ((encoding as string) === 'buffer') {
758
+ return Buffer.from(pathStr);
759
+ }
760
+ return pathStr;
761
+ }
762
+ }
763
+ return realFs.realpath(path, options) as Promise<string | Buffer>;
764
+ }
765
+
766
+ export async function utimes(
767
+ path: Parameters<typeof realFs.utimes>[0],
768
+ atime: Parameters<typeof realFs.utimes>[1],
769
+ mtime: Parameters<typeof realFs.utimes>[2]
770
+ ): Promise<void> {
771
+ const pathStr = getPath(path);
772
+ if (pathStr) {
773
+ const match = findMount(pathStr);
774
+ if (match) {
775
+ // utimes is not supported on mounted filesystems
776
+ // Just verify the file exists
777
+ const s = await match.mount.stub.stat(match.relativePath);
778
+ if (!s) {
779
+ throw createFsError('ENOENT', 'utimes', pathStr);
780
+ }
781
+ return;
782
+ }
783
+ }
784
+ return realFs.utimes(path, atime, mtime);
785
+ }
786
+
787
+ // Re-export functions we don't need to wrap
788
+ export const chmod = realFs.chmod;
789
+ export const chown = realFs.chown;
790
+ export const lchmod = realFs.lchmod;
791
+ export const lchown = realFs.lchown;
792
+ export const lutimes = realFs.lutimes;
793
+ export const link = realFs.link;
794
+ export const open = realFs.open;
795
+ export const opendir = realFs.opendir;
796
+ export const mkdtemp = realFs.mkdtemp;
797
+ export const watch = realFs.watch;
798
+ export const constants = realFs.constants;
799
+
800
+ // Default export for `import fs from 'node:fs/promises'` style imports
801
+ export default {
802
+ readFile,
803
+ writeFile,
804
+ appendFile,
805
+ stat,
806
+ lstat,
807
+ readdir,
808
+ mkdir,
809
+ rm,
810
+ rmdir,
811
+ unlink,
812
+ rename,
813
+ copyFile,
814
+ cp,
815
+ access,
816
+ truncate,
817
+ symlink,
818
+ readlink,
819
+ realpath,
820
+ utimes,
821
+ chmod,
822
+ chown,
823
+ lchmod,
824
+ lchown,
825
+ lutimes,
826
+ link,
827
+ open,
828
+ opendir,
829
+ mkdtemp,
830
+ watch,
831
+ constants,
832
+ };