@gjsify/fs 0.1.15 → 0.2.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.
Files changed (67) hide show
  1. package/lib/esm/callback.js +22 -13
  2. package/lib/esm/cp.js +253 -0
  3. package/lib/esm/dir.js +160 -0
  4. package/lib/esm/fd-ops.js +189 -0
  5. package/lib/esm/file-handle.js +263 -84
  6. package/lib/esm/fs-watcher.js +88 -4
  7. package/lib/esm/glob.js +164 -0
  8. package/lib/esm/index.js +128 -2
  9. package/lib/esm/promises.js +90 -27
  10. package/lib/esm/read-stream.js +53 -43
  11. package/lib/esm/stat-watcher.js +121 -0
  12. package/lib/esm/statfs.js +57 -0
  13. package/lib/esm/sync.js +70 -52
  14. package/lib/esm/utils.js +7 -0
  15. package/lib/esm/utimes.js +62 -0
  16. package/lib/esm/write-stream.js +2 -5
  17. package/lib/types/cp.d.ts +18 -0
  18. package/lib/types/cp.spec.d.ts +2 -0
  19. package/lib/types/dir.d.ts +29 -0
  20. package/lib/types/dir.spec.d.ts +2 -0
  21. package/lib/types/fd-ops.d.ts +57 -0
  22. package/lib/types/fd-ops.spec.d.ts +2 -0
  23. package/lib/types/file-handle.d.ts +34 -4
  24. package/lib/types/fs-watcher.d.ts +9 -2
  25. package/lib/types/glob.d.ts +8 -0
  26. package/lib/types/glob.spec.d.ts +2 -0
  27. package/lib/types/index.d.ts +51 -1
  28. package/lib/types/promises.d.ts +31 -4
  29. package/lib/types/read-stream.d.ts +3 -1
  30. package/lib/types/stat-watcher.d.ts +21 -0
  31. package/lib/types/statfs.d.ts +35 -0
  32. package/lib/types/statfs.spec.d.ts +2 -0
  33. package/lib/types/sync.d.ts +4 -7
  34. package/lib/types/utils.d.ts +2 -0
  35. package/lib/types/utimes.d.ts +13 -0
  36. package/lib/types/utimes.spec.d.ts +2 -0
  37. package/lib/types/watch.spec.d.ts +2 -0
  38. package/lib/types/watchfile.spec.d.ts +2 -0
  39. package/lib/types/write-stream.d.ts +1 -2
  40. package/package.json +12 -12
  41. package/src/callback.ts +22 -13
  42. package/src/cp.spec.ts +181 -0
  43. package/src/cp.ts +328 -0
  44. package/src/dir.spec.ts +204 -0
  45. package/src/dir.ts +199 -0
  46. package/src/fd-ops.spec.ts +234 -0
  47. package/src/fd-ops.ts +251 -0
  48. package/src/file-handle.ts +264 -94
  49. package/src/fs-watcher.ts +101 -6
  50. package/src/glob.spec.ts +201 -0
  51. package/src/glob.ts +205 -0
  52. package/src/index.ts +74 -0
  53. package/src/promises.ts +94 -29
  54. package/src/read-stream.ts +49 -43
  55. package/src/stat-watcher.ts +116 -0
  56. package/src/statfs.spec.ts +67 -0
  57. package/src/statfs.ts +92 -0
  58. package/src/streams.spec.ts +58 -0
  59. package/src/sync.ts +75 -57
  60. package/src/test.mts +13 -2
  61. package/src/utils.ts +10 -0
  62. package/src/utimes.spec.ts +113 -0
  63. package/src/utimes.ts +97 -0
  64. package/src/watch.spec.ts +171 -0
  65. package/src/watchfile.spec.ts +185 -0
  66. package/src/write-stream.ts +5 -8
  67. package/tsconfig.tsbuildinfo +1 -1
package/src/promises.ts CHANGED
@@ -6,8 +6,26 @@ import GLib from '@girs/glib-2.0';
6
6
  import { join, dirname } from 'node:path';
7
7
  import { getEncodingFromOptions, encodeUint8Array, decode } from './encoding.js';
8
8
  import { realpathSync, readdirSync as readdirSyncFn, renameSync, copyFileSync, accessSync, appendFileSync, readlinkSync, truncateSync, chmodSync, chownSync, linkSync } from './sync.js';
9
+ import { cpAsync } from './cp.js';
10
+ import { opendirAsync, Dir } from './dir.js';
11
+ import { globAsync } from './glob.js';
12
+ import { watchAsync } from './fs-watcher.js';
13
+ import { statfsAsync } from './statfs.js';
14
+ import { utimesAsync, lutimesAsync, lchownAsync, lchmodAsync } from './utimes.js';
15
+ import {
16
+ fstatAsync,
17
+ ftruncateAsync,
18
+ fdatasyncAsync,
19
+ fsyncAsync,
20
+ fchmodAsync,
21
+ fchownAsync,
22
+ futimesAsync,
23
+ readvAsync,
24
+ writevAsync,
25
+ openAsBlob,
26
+ } from './fd-ops.js';
9
27
  import { FileHandle } from './file-handle.js';
10
- import { tempDirPath } from './utils.js';
28
+ import { tempDirPath, normalizePath } from './utils.js';
11
29
  import { Dirent } from './dirent.js';
12
30
  import { Stats, BigIntStats, STAT_ATTRIBUTES } from './stats.js';
13
31
  import { createNodeError } from './errors.js';
@@ -77,7 +95,7 @@ async function mkdir(path: PathLike, options?: Mode | MakeDirectoryOptions | nul
77
95
  _mode = options;
78
96
  }
79
97
 
80
- const pathStr = path.toString();
98
+ const pathStr = normalizePath(path);
81
99
 
82
100
  if (recursive) {
83
101
  return mkdirRecursiveAsync(pathStr);
@@ -90,7 +108,7 @@ async function mkdir(path: PathLike, options?: Mode | MakeDirectoryOptions | nul
90
108
  file.make_directory_finish(res);
91
109
  resolve(undefined);
92
110
  } catch (err: unknown) {
93
- reject(createNodeError(err, 'mkdir', path));
111
+ reject(createNodeError(err, 'mkdir', pathStr));
94
112
  }
95
113
  });
96
114
  });
@@ -152,7 +170,8 @@ async function mkdirRecursiveAsync(pathStr: string): Promise<string | undefined>
152
170
  }
153
171
 
154
172
  async function readFile(path: PathLike | FileHandle, options: ReadOptions = { encoding: null, flag: 'r' }) {
155
- const file = Gio.File.new_for_path(path.toString());
173
+ const pathStr = normalizePath(path as PathLike);
174
+ const file = Gio.File.new_for_path(pathStr);
156
175
 
157
176
  let ok: boolean;
158
177
  let data: Uint8Array;
@@ -167,11 +186,11 @@ async function readFile(path: PathLike | FileHandle, options: ReadOptions = { en
167
186
  });
168
187
  });
169
188
  } catch (error) {
170
- throw createNodeError(error, 'open', path.toString() as PathLike);
189
+ throw createNodeError(error, 'open', pathStr);
171
190
  }
172
191
 
173
192
  if (!ok) {
174
- throw createNodeError(new Error('failed to read file'), 'open', path.toString() as PathLike);
193
+ throw createNodeError(new Error('failed to read file'), 'open', pathStr);
175
194
  }
176
195
 
177
196
  return encodeUint8Array(getEncodingFromOptions(options, 'buffer'), data);
@@ -230,8 +249,9 @@ async function mkdtemp(prefix: string, options?: BufferEncodingOption | ObjectEn
230
249
  return decode(path, encoding);
231
250
  }
232
251
 
233
- async function writeFile(path: string, data: string | Uint8Array | unknown) {
234
- const file = Gio.File.new_for_path(path);
252
+ async function writeFile(path: PathLike, data: string | Uint8Array | unknown) {
253
+ const pathStr = normalizePath(path);
254
+ const file = Gio.File.new_for_path(pathStr);
235
255
 
236
256
  // Convert data to Uint8Array if it's a string
237
257
  let bytes: Uint8Array;
@@ -250,7 +270,7 @@ async function writeFile(path: string, data: string | Uint8Array | unknown) {
250
270
  try {
251
271
  resolve(file.replace_finish(res));
252
272
  } catch (err: unknown) {
253
- reject(createNodeError(err, 'open', path));
273
+ reject(createNodeError(err, 'open', pathStr));
254
274
  }
255
275
  });
256
276
  });
@@ -264,7 +284,7 @@ async function writeFile(path: string, data: string | Uint8Array | unknown) {
264
284
  outputStream.write_bytes_finish(res);
265
285
  resolve();
266
286
  } catch (err: unknown) {
267
- reject(createNodeError(err, 'write', path));
287
+ reject(createNodeError(err, 'write', pathStr));
268
288
  }
269
289
  });
270
290
  });
@@ -277,7 +297,7 @@ async function writeFile(path: string, data: string | Uint8Array | unknown) {
277
297
  outputStream.close_finish(res);
278
298
  resolve();
279
299
  } catch (err: unknown) {
280
- reject(createNodeError(err, 'close', path));
300
+ reject(createNodeError(err, 'close', pathStr));
281
301
  }
282
302
  });
283
303
  });
@@ -294,20 +314,21 @@ async function writeFile(path: string, data: string | Uint8Array | unknown) {
294
314
  * @return Fulfills with `undefined` upon success.
295
315
  */
296
316
  async function rmdir(path: PathLike, _options?: RmDirOptions): Promise<void> {
297
- const file = Gio.File.new_for_path(path.toString());
317
+ const pathStr = normalizePath(path);
318
+ const file = Gio.File.new_for_path(pathStr);
298
319
  // Check if it's a directory
299
320
  const info = await new Promise<Gio.FileInfo>((resolve, reject) => {
300
321
  file.query_info_async('standard::type', Gio.FileQueryInfoFlags.NONE, GLib.PRIORITY_DEFAULT, null, (_s: unknown, res: Gio.AsyncResult) => {
301
322
  try {
302
323
  resolve(file.query_info_finish(res));
303
324
  } catch (err: unknown) {
304
- reject(createNodeError(err, 'rmdir', path));
325
+ reject(createNodeError(err, 'rmdir', pathStr));
305
326
  }
306
327
  });
307
328
  });
308
329
  if (info.get_file_type() !== Gio.FileType.DIRECTORY) {
309
330
  const err = Object.assign(new Error(), { code: 4 }); // Gio.IOErrorEnum.NOT_DIRECTORY
310
- throw createNodeError(err, 'rmdir', path);
331
+ throw createNodeError(err, 'rmdir', pathStr);
311
332
  }
312
333
  // Check if empty
313
334
  const children = await new Promise<Gio.FileEnumerator>((resolve, reject) => {
@@ -315,14 +336,14 @@ async function rmdir(path: PathLike, _options?: RmDirOptions): Promise<void> {
315
336
  try {
316
337
  resolve(file.enumerate_children_finish(res));
317
338
  } catch (err: unknown) {
318
- reject(createNodeError(err, 'rmdir', path));
339
+ reject(createNodeError(err, 'rmdir', pathStr));
319
340
  }
320
341
  });
321
342
  });
322
343
  const firstChild = children.next_file(null);
323
344
  if (firstChild !== null) {
324
345
  const err = Object.assign(new Error(), { code: 5 }); // Gio.IOErrorEnum.NOT_EMPTY
325
- throw createNodeError(err, 'rmdir', path);
346
+ throw createNodeError(err, 'rmdir', pathStr);
326
347
  }
327
348
  // Delete the empty directory
328
349
  await new Promise<void>((resolve, reject) => {
@@ -331,29 +352,30 @@ async function rmdir(path: PathLike, _options?: RmDirOptions): Promise<void> {
331
352
  file.delete_finish(res);
332
353
  resolve();
333
354
  } catch (err: unknown) {
334
- reject(createNodeError(err, 'rmdir', path));
355
+ reject(createNodeError(err, 'rmdir', pathStr));
335
356
  }
336
357
  });
337
358
  });
338
359
  }
339
360
 
340
- async function unlink(path: string): Promise<void> {
341
- const file = Gio.File.new_for_path(path);
361
+ async function unlink(path: PathLike): Promise<void> {
362
+ const pathStr = normalizePath(path);
363
+ const file = Gio.File.new_for_path(pathStr);
342
364
  await new Promise<void>((resolve, reject) => {
343
365
  file.delete_async(GLib.PRIORITY_DEFAULT, null, (_s: unknown, res: Gio.AsyncResult) => {
344
366
  try {
345
367
  file.delete_finish(res);
346
368
  resolve();
347
369
  } catch (err: unknown) {
348
- reject(createNodeError(err, 'unlink', path));
370
+ reject(createNodeError(err, 'unlink', pathStr));
349
371
  }
350
372
  });
351
373
  });
352
374
  }
353
375
 
354
- async function open(path: PathLike, flags?: OpenFlags, mode?: Mode): Promise<FileHandle> {
376
+ async function open(path: PathLike, flags?: OpenFlags | number, mode?: Mode): Promise<FileHandle> {
355
377
  // FileHandle constructor maps GLib.FileError to NodeJS.ErrnoException on failure.
356
- return new FileHandle({ path, flags, mode });
378
+ return new FileHandle({ path, flags: flags as OpenFlags | undefined, mode });
357
379
  }
358
380
 
359
381
  async function write<TBuffer extends Uint8Array>(
@@ -425,14 +447,15 @@ async function _writeStr(
425
447
  // --- helpers ---
426
448
 
427
449
  function queryInfoAsync(path: PathLike, flags: Gio.FileQueryInfoFlags, syscall: string, options?: { bigint?: boolean }): Promise<Stats | BigIntStats> {
450
+ const pathStr = normalizePath(path);
428
451
  return new Promise((resolve, reject) => {
429
- const file = Gio.File.new_for_path(path.toString());
452
+ const file = Gio.File.new_for_path(pathStr);
430
453
  file.query_info_async(STAT_ATTRIBUTES, flags, GLib.PRIORITY_DEFAULT, null, (_s: unknown, res: Gio.AsyncResult) => {
431
454
  try {
432
455
  const info = file.query_info_finish(res);
433
- resolve(options?.bigint ? new BigIntStats(info, path) : new Stats(info, path));
456
+ resolve(options?.bigint ? new BigIntStats(info, pathStr) : new Stats(info, pathStr));
434
457
  } catch (err: unknown) {
435
- reject(createNodeError(err, syscall, path));
458
+ reject(createNodeError(err, syscall, pathStr));
436
459
  }
437
460
  });
438
461
  });
@@ -463,14 +486,16 @@ async function realpath(path: PathLike): Promise<string> {
463
486
  }
464
487
 
465
488
  async function symlink(target: PathLike, path: PathLike, _type?: string): Promise<void> {
489
+ const pathStr = normalizePath(path);
490
+ const targetStr = normalizePath(target);
466
491
  return new Promise((resolve, reject) => {
467
- const file = Gio.File.new_for_path(path.toString());
468
- file.make_symbolic_link_async(target.toString(), GLib.PRIORITY_DEFAULT, null, (_s: unknown, res: Gio.AsyncResult) => {
492
+ const file = Gio.File.new_for_path(pathStr);
493
+ file.make_symbolic_link_async(targetStr, GLib.PRIORITY_DEFAULT, null, (_s: unknown, res: Gio.AsyncResult) => {
469
494
  try {
470
495
  file.make_symbolic_link_finish(res);
471
496
  resolve();
472
497
  } catch (err: unknown) {
473
- reject(createNodeError(err, 'symlink', target, path));
498
+ reject(createNodeError(err, 'symlink', targetStr, pathStr));
474
499
  }
475
500
  });
476
501
  });
@@ -482,7 +507,7 @@ async function symlink(target: PathLike, path: PathLike, _type?: string): Promis
482
507
  * @return Fulfills with `undefined` upon success.
483
508
  */
484
509
  async function rm(path: PathLike, options?: RmOptions): Promise<void> {
485
- const pathStr = path.toString();
510
+ const pathStr = normalizePath(path);
486
511
  const file = Gio.File.new_for_path(pathStr);
487
512
  const recursive = options?.recursive || false;
488
513
  const force = options?.force || false;
@@ -568,6 +593,8 @@ async function link(existingPath: PathLike, newPath: PathLike): Promise<void> {
568
593
  linkSync(existingPath, newPath);
569
594
  }
570
595
 
596
+ export type { Dir };
597
+
571
598
  export {
572
599
  readFile,
573
600
  mkdir,
@@ -592,6 +619,25 @@ export {
592
619
  chmod,
593
620
  chown,
594
621
  link,
622
+ cpAsync as cp,
623
+ opendirAsync as opendir,
624
+ globAsync as glob,
625
+ watchAsync as watch,
626
+ statfsAsync as statfs,
627
+ utimesAsync as utimes,
628
+ lutimesAsync as lutimes,
629
+ lchownAsync as lchown,
630
+ lchmodAsync as lchmod,
631
+ fstatAsync as fstat,
632
+ ftruncateAsync as ftruncate,
633
+ fdatasyncAsync as fdatasync,
634
+ fsyncAsync as fsync,
635
+ fchmodAsync as fchmod,
636
+ fchownAsync as fchown,
637
+ futimesAsync as futimes,
638
+ readvAsync as readv,
639
+ writevAsync as writev,
640
+ openAsBlob,
595
641
  };
596
642
 
597
643
  export default {
@@ -618,4 +664,23 @@ export default {
618
664
  chmod,
619
665
  chown,
620
666
  link,
667
+ cp: cpAsync,
668
+ opendir: opendirAsync,
669
+ glob: globAsync,
670
+ watch: watchAsync,
671
+ statfs: statfsAsync,
672
+ utimes: utimesAsync,
673
+ lutimes: lutimesAsync,
674
+ lchown: lchownAsync,
675
+ lchmod: lchmodAsync,
676
+ fstat: fstatAsync,
677
+ ftruncate: ftruncateAsync,
678
+ fdatasync: fdatasyncAsync,
679
+ fsync: fsyncAsync,
680
+ fchmod: fchmodAsync,
681
+ fchown: fchownAsync,
682
+ futimes: futimesAsync,
683
+ readv: readvAsync,
684
+ writev: writevAsync,
685
+ openAsBlob,
621
686
  };
@@ -6,7 +6,7 @@ import Gio from '@girs/gio-2.0';
6
6
  import GLib from '@girs/glib-2.0';
7
7
  import { Buffer } from "node:buffer";
8
8
  import { Readable } from "node:stream";
9
- import { URL, fileURLToPath } from "node:url";
9
+ import { normalizePath } from './utils.js';
10
10
 
11
11
  import type { CreateReadStreamOptions } from 'node:fs/promises';
12
12
  import type { PathLike, ReadStream as IReadStream } from 'node:fs';
@@ -19,13 +19,15 @@ export class ReadStream extends Readable implements IReadStream {
19
19
 
20
20
  private _gioFile: Gio.File;
21
21
  private _inputStream: Gio.FileInputStream | null = null;
22
+ private _cancellable = new Gio.Cancellable();
22
23
  private _start: number;
23
24
  private _end: number;
24
25
  private _pos: number;
25
26
 
26
27
  close(callback?: (err?: NodeJS.ErrnoException | null) => void): void {
28
+ this._cancellable.cancel();
27
29
  if (this._inputStream) {
28
- try { this._inputStream.close(null); } catch {}
30
+ this._inputStream.close_async(GLib.PRIORITY_DEFAULT, null, () => {});
29
31
  this._inputStream = null;
30
32
  }
31
33
  this.destroy();
@@ -33,9 +35,7 @@ export class ReadStream extends Readable implements IReadStream {
33
35
  }
34
36
 
35
37
  constructor(path: PathLike, opts?: CreateReadStreamOptions) {
36
- if (path instanceof URL) {
37
- path = fileURLToPath(path);
38
- }
38
+ const pathStr = normalizePath(path);
39
39
 
40
40
  super({
41
41
  highWaterMark: opts?.highWaterMark ?? 64 * 1024,
@@ -43,40 +43,41 @@ export class ReadStream extends Readable implements IReadStream {
43
43
  objectMode: false,
44
44
  });
45
45
 
46
- this.path = path.toString();
47
- this._gioFile = Gio.File.new_for_path(this.path.toString());
46
+ this.path = pathStr;
47
+ this._gioFile = Gio.File.new_for_path(pathStr);
48
48
  this._start = (opts?.start as number) ?? 0;
49
49
  this._end = (opts?.end as number) ?? Infinity;
50
50
  this._pos = this._start;
51
+ }
51
52
 
52
- // Validate file existence eagerly (like Node.js) to emit error event
53
- Promise.resolve().then(() => {
54
- if (!this._inputStream && !this.destroyed) {
55
- try {
56
- this._inputStream = this._gioFile.read(null);
57
- this.pending = false;
58
- this.emit('open', 0);
59
- this.emit('ready');
60
- if (this._start > 0 && this._inputStream.can_seek()) {
61
- this._inputStream.seek(this._start, GLib.SeekType.SET, null);
62
- }
63
- } catch (err) {
64
- this.destroy(err as Error);
53
+ // Use _construct() for async file opening so the stream machinery defers
54
+ // _read() until the file is open. This avoids the fragile _pendingReadSize
55
+ // pattern and correctly handles backpressure via the constructed flag.
56
+ override _construct(callback: (err?: Error | null) => void): void {
57
+ this._gioFile.read_async(GLib.PRIORITY_DEFAULT, this._cancellable, (_source, asyncResult) => {
58
+ if (this.destroyed) { callback(); return; }
59
+ try {
60
+ this._inputStream = this._gioFile.read_finish(asyncResult);
61
+ this.pending = false;
62
+ this.emit('open', 0);
63
+ this.emit('ready');
64
+ if (this._start > 0 && this._inputStream!.can_seek()) {
65
+ this._inputStream!.seek(this._start, GLib.SeekType.SET, null);
66
+ }
67
+ callback();
68
+ } catch (err) {
69
+ if (!this._cancellable.is_cancelled()) {
70
+ callback(err as Error);
65
71
  }
66
72
  }
67
73
  });
68
74
  }
69
75
 
70
76
  override _read(size: number): void {
71
- // Stream is opened eagerly in constructor; if not yet ready, wait
72
- if (!this._inputStream) {
73
- if (this.destroyed) return;
74
- // Retry on next tick (constructor's async open hasn't completed yet)
75
- Promise.resolve().then(() => this._read(size));
76
- return;
77
- }
77
+ this._doRead(size);
78
+ }
78
79
 
79
- // Calculate how many bytes to read
80
+ private _doRead(size: number): void {
80
81
  let toRead = size;
81
82
  if (this._end !== Infinity) {
82
83
  const remaining = this._end - this._pos + 1;
@@ -87,27 +88,32 @@ export class ReadStream extends Readable implements IReadStream {
87
88
  toRead = Math.min(size, remaining);
88
89
  }
89
90
 
90
- try {
91
- const gbytes = this._inputStream!.read_bytes(toRead, null);
92
- const data = gbytes.get_data();
91
+ const stream = this._inputStream!;
92
+ stream.read_bytes_async(toRead, GLib.PRIORITY_DEFAULT, this._cancellable, (_source, asyncResult) => {
93
+ try {
94
+ const gbytes = stream.read_bytes_finish(asyncResult);
95
+ const data = gbytes.get_data();
93
96
 
94
- if (!data || data.length === 0) {
95
- // EOF
96
- this.push(null);
97
- return;
98
- }
97
+ if (!data || data.length === 0) {
98
+ this.push(null);
99
+ return;
100
+ }
99
101
 
100
- this.bytesRead += data.length;
101
- this._pos += data.length;
102
- this.push(Buffer.from(data));
103
- } catch (err) {
104
- this.destroy(err as Error);
105
- }
102
+ this.bytesRead += data.length;
103
+ this._pos += data.length;
104
+ this.push(Buffer.from(data));
105
+ } catch (err) {
106
+ if (!this._cancellable.is_cancelled()) {
107
+ this.destroy(err as Error);
108
+ }
109
+ }
110
+ });
106
111
  }
107
112
 
108
113
  override _destroy(error: Error | null, callback: (error?: Error | null) => void): void {
114
+ this._cancellable.cancel();
109
115
  if (this._inputStream) {
110
- try { this._inputStream.close(null); } catch {}
116
+ this._inputStream.close_async(GLib.PRIORITY_DEFAULT, null, () => {});
111
117
  this._inputStream = null;
112
118
  }
113
119
  callback(error);
@@ -0,0 +1,116 @@
1
+ // Reference: Node.js lib/internal/fs/watchers.js (StatWatcher)
2
+ // Reimplemented for GJS using setInterval polling
3
+
4
+ import { EventEmitter } from 'node:events';
5
+ import { statSync } from './sync.js';
6
+
7
+ import type { PathLike, Stats } from 'node:fs';
8
+
9
+ function zeroedStat(): Stats {
10
+ return {
11
+ dev: 0, ino: 0, mode: 0, nlink: 0, uid: 0, gid: 0, rdev: 0,
12
+ size: 0, blksize: 0, blocks: 0,
13
+ atimeMs: 0, mtimeMs: 0, ctimeMs: 0, birthtimeMs: 0,
14
+ atime: new Date(0), mtime: new Date(0), ctime: new Date(0), birthtime: new Date(0),
15
+ isFile: () => false, isDirectory: () => false, isBlockDevice: () => false,
16
+ isCharacterDevice: () => false, isSymbolicLink: () => false, isFIFO: () => false,
17
+ isSocket: () => false,
18
+ } as unknown as Stats;
19
+ }
20
+
21
+ export class StatWatcher extends EventEmitter {
22
+ private _path: string;
23
+ private _interval: number;
24
+ private _timerId: ReturnType<typeof setInterval> | null = null;
25
+ private _prev: Stats;
26
+ private _changeCount = 0;
27
+
28
+ constructor(path: string, interval: number) {
29
+ super();
30
+ this._path = path;
31
+ this._interval = interval;
32
+ this._prev = zeroedStat();
33
+ }
34
+
35
+ start(): void {
36
+ try { this._prev = statSync(this._path) as unknown as Stats; } catch {}
37
+ this._timerId = setInterval(() => {
38
+ let curr: Stats;
39
+ try { curr = statSync(this._path) as unknown as Stats; } catch { curr = zeroedStat(); }
40
+ const prev = this._prev;
41
+ if (curr.mtimeMs !== prev.mtimeMs || curr.size !== prev.size || curr.ino !== prev.ino) {
42
+ this._prev = curr;
43
+ this.emit('change', curr, prev);
44
+ }
45
+ }, this._interval);
46
+ }
47
+
48
+ stop(): void {
49
+ if (this._timerId !== null) {
50
+ clearInterval(this._timerId);
51
+ this._timerId = null;
52
+ }
53
+ this.emit('stop');
54
+ }
55
+
56
+ addChangeListener(listener: (curr: Stats, prev: Stats) => void): void {
57
+ this._changeCount++;
58
+ this.on('change', listener);
59
+ }
60
+
61
+ removeChangeListener(listener: (curr: Stats, prev: Stats) => void): void {
62
+ this._changeCount--;
63
+ this.removeListener('change', listener);
64
+ }
65
+
66
+ removeAllChangeListeners(): void {
67
+ this._changeCount = 0;
68
+ this.removeAllListeners('change');
69
+ }
70
+
71
+ get changeListenerCount(): number {
72
+ return this._changeCount;
73
+ }
74
+ }
75
+
76
+ const statWatchers = new Map<string, StatWatcher>();
77
+
78
+ export function watchFile(
79
+ filename: PathLike,
80
+ options: { persistent?: boolean; interval?: number } | ((curr: Stats, prev: Stats) => void),
81
+ listener?: (curr: Stats, prev: Stats) => void,
82
+ ): StatWatcher {
83
+ if (typeof options === 'function') {
84
+ listener = options;
85
+ options = {};
86
+ }
87
+ const interval = (options as { interval?: number }).interval ?? 5007;
88
+ const resolved = filename.toString();
89
+
90
+ let watcher = statWatchers.get(resolved);
91
+ if (!watcher) {
92
+ watcher = new StatWatcher(resolved, interval);
93
+ watcher.start();
94
+ statWatchers.set(resolved, watcher);
95
+ }
96
+ if (listener) watcher.addChangeListener(listener);
97
+ return watcher;
98
+ }
99
+
100
+ export function unwatchFile(
101
+ filename: PathLike,
102
+ listener?: (curr: Stats, prev: Stats) => void,
103
+ ): void {
104
+ const resolved = filename.toString();
105
+ const watcher = statWatchers.get(resolved);
106
+ if (!watcher) return;
107
+ if (listener) {
108
+ watcher.removeChangeListener(listener);
109
+ } else {
110
+ watcher.removeAllChangeListeners();
111
+ }
112
+ if (watcher.changeListenerCount === 0) {
113
+ watcher.stop();
114
+ statWatchers.delete(resolved);
115
+ }
116
+ }
@@ -0,0 +1,67 @@
1
+ // Ported from refs/node-test/parallel/test-fs-statfs.js (behavior)
2
+ // Original: MIT, Node.js contributors.
3
+ // Rewritten for @gjsify/unit — behavior preserved, assertion dialect adapted.
4
+
5
+ import { describe, it, expect } from '@gjsify/unit';
6
+ import { statfsSync, statfs, promises } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+
9
+ const TMP = tmpdir();
10
+
11
+ export default async () => {
12
+ await describe('fs.statfs / fs.promises.statfs', async () => {
13
+ await it('statfsSync returns object with expected shape', async () => {
14
+ const result = statfsSync(TMP);
15
+ expect(typeof result.type).toBe('number');
16
+ expect(typeof result.bsize).toBe('number');
17
+ expect(typeof result.blocks).toBe('number');
18
+ expect(typeof result.bfree).toBe('number');
19
+ expect(typeof result.bavail).toBe('number');
20
+ expect(typeof result.files).toBe('number');
21
+ expect(typeof result.ffree).toBe('number');
22
+ });
23
+
24
+ await it('statfsSync returns plausible values', async () => {
25
+ const result = statfsSync(TMP);
26
+ expect(result.bsize).toBe(4096);
27
+ expect(result.blocks).toBeGreaterThan(0);
28
+ expect(result.bfree).toBeGreaterThanOrEqual(0);
29
+ expect(result.bavail).toBeGreaterThanOrEqual(0);
30
+ });
31
+
32
+ await it('statfs callback form returns same shape', async () => {
33
+ const result = await new Promise<any>((resolve, reject) => {
34
+ statfs(TMP, (err, stats) => {
35
+ if (err) reject(err);
36
+ else resolve(stats);
37
+ });
38
+ });
39
+ expect(typeof result.type).toBe('number');
40
+ expect(typeof result.bsize).toBe('number');
41
+ expect(result.bsize).toBe(4096);
42
+ expect(result.blocks).toBeGreaterThan(0);
43
+ });
44
+
45
+ await it('promises.statfs returns same shape', async () => {
46
+ const result = await promises.statfs(TMP);
47
+ expect(typeof result.type).toBe('number');
48
+ expect(typeof result.bsize).toBe('number');
49
+ expect(result.bsize).toBe(4096);
50
+ expect(result.blocks).toBeGreaterThan(0);
51
+ });
52
+
53
+ await it('statfsSync with bigint:true returns bigint fields', async () => {
54
+ const result = statfsSync(TMP, { bigint: true });
55
+ expect(typeof result.type).toBe('bigint');
56
+ expect(typeof result.bsize).toBe('bigint');
57
+ expect(typeof result.blocks).toBe('bigint');
58
+ expect(typeof result.bfree).toBe('bigint');
59
+ expect(result.bsize).toBe(4096n);
60
+ expect(result.blocks > 0n).toBe(true);
61
+ });
62
+
63
+ await it('statfsSync throws on non-existent path', async () => {
64
+ expect(() => statfsSync('/nonexistent-gjsify-test-path-xyz')).toThrow();
65
+ });
66
+ });
67
+ };