@zenfs/core 0.17.1 → 0.18.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 (69) hide show
  1. package/dist/backends/backend.d.ts +2 -3
  2. package/dist/backends/fetch.js +2 -2
  3. package/dist/backends/file_index.d.ts +14 -15
  4. package/dist/backends/file_index.js +3 -9
  5. package/dist/backends/overlay.d.ts +21 -22
  6. package/dist/backends/overlay.js +111 -114
  7. package/dist/backends/port/fs.d.ts +21 -22
  8. package/dist/backends/port/fs.js +23 -23
  9. package/dist/backends/store/fs.d.ts +20 -21
  10. package/dist/backends/store/fs.js +70 -138
  11. package/dist/browser.min.js +4 -4
  12. package/dist/browser.min.js.map +4 -4
  13. package/dist/config.js +2 -2
  14. package/dist/{cred.d.ts → credentials.d.ts} +3 -2
  15. package/dist/credentials.js +16 -0
  16. package/dist/emulation/async.d.ts +19 -4
  17. package/dist/emulation/async.js +55 -8
  18. package/dist/emulation/dir.d.ts +4 -7
  19. package/dist/emulation/dir.js +16 -24
  20. package/dist/emulation/promises.d.ts +3 -3
  21. package/dist/emulation/promises.js +103 -46
  22. package/dist/emulation/shared.d.ts +0 -3
  23. package/dist/emulation/shared.js +0 -6
  24. package/dist/emulation/sync.d.ts +3 -4
  25. package/dist/emulation/sync.js +107 -65
  26. package/dist/emulation/watchers.d.ts +40 -3
  27. package/dist/emulation/watchers.js +115 -9
  28. package/dist/error.d.ts +1 -1
  29. package/dist/error.js +1 -1
  30. package/dist/filesystem.d.ts +20 -21
  31. package/dist/filesystem.js +4 -4
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.js +1 -1
  34. package/dist/mixins/async.d.ts +13 -14
  35. package/dist/mixins/async.js +45 -47
  36. package/dist/mixins/mutexed.d.ts +1 -1
  37. package/dist/mixins/mutexed.js +61 -53
  38. package/dist/mixins/readonly.d.ts +12 -13
  39. package/dist/mixins/readonly.js +12 -12
  40. package/dist/mixins/sync.js +20 -20
  41. package/dist/stats.d.ts +12 -5
  42. package/dist/stats.js +11 -2
  43. package/dist/utils.d.ts +3 -4
  44. package/dist/utils.js +7 -17
  45. package/package.json +2 -2
  46. package/src/backends/backend.ts +2 -3
  47. package/src/backends/fetch.ts +2 -2
  48. package/src/backends/file_index.ts +3 -12
  49. package/src/backends/overlay.ts +112 -116
  50. package/src/backends/port/fs.ts +25 -26
  51. package/src/backends/store/fs.ts +72 -151
  52. package/src/config.ts +3 -2
  53. package/src/{cred.ts → credentials.ts} +11 -2
  54. package/src/emulation/async.ts +72 -16
  55. package/src/emulation/dir.ts +21 -29
  56. package/src/emulation/promises.ts +107 -46
  57. package/src/emulation/shared.ts +0 -8
  58. package/src/emulation/sync.ts +109 -66
  59. package/src/emulation/watchers.ts +140 -10
  60. package/src/error.ts +1 -1
  61. package/src/filesystem.ts +22 -23
  62. package/src/index.ts +1 -1
  63. package/src/mixins/async.ts +54 -55
  64. package/src/mixins/mutexed.ts +62 -55
  65. package/src/mixins/readonly.ts +24 -25
  66. package/src/mixins/sync.ts +21 -22
  67. package/src/stats.ts +15 -5
  68. package/src/utils.ts +9 -26
  69. package/dist/cred.js +0 -8
@@ -2,14 +2,16 @@ import { Buffer } from 'buffer';
2
2
  import type * as fs from 'node:fs';
3
3
  import { Errno, ErrnoError } from '../error.js';
4
4
  import type { File } from '../file.js';
5
- import { isAppendable, isExclusive, isReadable, isTruncating, isWriteable, parseFlag } from '../file.js';
5
+ import { flagToMode, isAppendable, isExclusive, isReadable, isTruncating, isWriteable, parseFlag } from '../file.js';
6
6
  import type { FileContents } from '../filesystem.js';
7
7
  import { BigIntStats, type Stats } from '../stats.js';
8
8
  import { normalizeMode, normalizeOptions, normalizePath, normalizeTime } from '../utils.js';
9
- import { COPYFILE_EXCL, F_OK, S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK } from './constants.js';
9
+ import * as constants from './constants.js';
10
10
  import { Dir, Dirent } from './dir.js';
11
11
  import { dirname, join, parse } from './path.js';
12
- import { _statfs, cred, fd2file, fdMap, file2fd, fixError, mounts, resolveMount } from './shared.js';
12
+ import { _statfs, fd2file, fdMap, file2fd, fixError, mounts, resolveMount } from './shared.js';
13
+ import { credentials } from '../credentials.js';
14
+ import { emitChange } from './watchers.js';
13
15
 
14
16
  /**
15
17
  * Synchronous rename.
@@ -19,18 +21,23 @@ import { _statfs, cred, fd2file, fdMap, file2fd, fixError, mounts, resolveMount
19
21
  export function renameSync(oldPath: fs.PathLike, newPath: fs.PathLike): void {
20
22
  oldPath = normalizePath(oldPath);
21
23
  newPath = normalizePath(newPath);
22
- const _old = resolveMount(oldPath);
23
- const _new = resolveMount(newPath);
24
- const paths = { [_old.path]: oldPath, [_new.path]: newPath };
24
+ const oldMount = resolveMount(oldPath);
25
+ const newMount = resolveMount(newPath);
26
+ if (!statSync(dirname(oldPath)).hasAccess(constants.W_OK, credentials)) {
27
+ throw ErrnoError.With('EACCES', oldPath, 'rename');
28
+ }
25
29
  try {
26
- if (_old === _new) {
27
- return _old.fs.renameSync(_old.path, _new.path, cred);
30
+ if (oldMount === newMount) {
31
+ oldMount.fs.renameSync(oldMount.path, newMount.path);
32
+ emitChange('rename', oldPath.toString());
33
+ return;
28
34
  }
29
35
 
30
36
  writeFileSync(newPath, readFileSync(oldPath));
31
37
  unlinkSync(oldPath);
38
+ emitChange('rename', oldPath.toString());
32
39
  } catch (e) {
33
- throw fixError(e as Error, paths);
40
+ throw fixError(e as Error, { [oldMount.path]: oldPath, [newMount.path]: newPath });
34
41
  }
35
42
  }
36
43
  renameSync satisfies typeof fs.renameSync;
@@ -43,7 +50,7 @@ export function existsSync(path: fs.PathLike): boolean {
43
50
  path = normalizePath(path);
44
51
  try {
45
52
  const { fs, path: resolvedPath } = resolveMount(realpathSync(path));
46
- return fs.existsSync(resolvedPath, cred);
53
+ return fs.existsSync(resolvedPath);
47
54
  } catch (e) {
48
55
  if ((e as ErrnoError).errno == Errno.ENOENT) {
49
56
  return false;
@@ -65,7 +72,10 @@ export function statSync(path: fs.PathLike, options?: fs.StatOptions): Stats | B
65
72
  path = normalizePath(path);
66
73
  const { fs, path: resolved } = resolveMount(existsSync(path) ? realpathSync(path) : path);
67
74
  try {
68
- const stats = fs.statSync(resolved, cred);
75
+ const stats = fs.statSync(resolved);
76
+ if (!stats.hasAccess(constants.R_OK, credentials)) {
77
+ throw ErrnoError.With('EACCES', path, 'stat');
78
+ }
69
79
  return options?.bigint ? new BigIntStats(stats) : stats;
70
80
  } catch (e) {
71
81
  throw fixError(e as Error, { [resolved]: path });
@@ -85,7 +95,7 @@ export function lstatSync(path: fs.PathLike, options?: fs.StatOptions): Stats |
85
95
  path = normalizePath(path);
86
96
  const { fs, path: resolved } = resolveMount(path);
87
97
  try {
88
- const stats = fs.statSync(resolved, cred);
98
+ const stats = fs.statSync(resolved);
89
99
  return options?.bigint ? new BigIntStats(stats) : stats;
90
100
  } catch (e) {
91
101
  throw fixError(e as Error, { [resolved]: path });
@@ -116,7 +126,11 @@ export function unlinkSync(path: fs.PathLike): void {
116
126
  path = normalizePath(path);
117
127
  const { fs, path: resolved } = resolveMount(path);
118
128
  try {
119
- return fs.unlinkSync(resolved, cred);
129
+ if (!fs.statSync(resolved).hasAccess(constants.W_OK, credentials)) {
130
+ throw ErrnoError.With('EACCES', resolved, 'unlink');
131
+ }
132
+ fs.unlinkSync(resolved);
133
+ emitChange('rename', path.toString());
120
134
  } catch (e) {
121
135
  throw fixError(e as Error, { [resolved]: path });
122
136
  }
@@ -131,21 +145,24 @@ function _openSync(path: fs.PathLike, _flag: fs.OpenMode, _mode?: fs.Mode | null
131
145
  path = resolveSymlinks && existsSync(path) ? realpathSync(path) : path;
132
146
  const { fs, path: resolved } = resolveMount(path);
133
147
 
134
- if (!fs.existsSync(resolved, cred)) {
148
+ if (!fs.existsSync(resolved)) {
135
149
  if ((!isWriteable(flag) && !isAppendable(flag)) || flag == 'r+') {
136
150
  throw ErrnoError.With('ENOENT', path, '_open');
137
151
  }
138
152
  // Create the file
139
- const parentStats: Stats = fs.statSync(dirname(resolved), cred);
153
+ const parentStats: Stats = fs.statSync(dirname(resolved));
154
+ if (!parentStats.hasAccess(constants.W_OK, credentials)) {
155
+ throw ErrnoError.With('EACCES', dirname(path), '_open');
156
+ }
140
157
  if (!parentStats.isDirectory()) {
141
158
  throw ErrnoError.With('ENOTDIR', dirname(path), '_open');
142
159
  }
143
- return fs.createFileSync(resolved, flag, mode, cred);
160
+ return fs.createFileSync(resolved, flag, mode);
144
161
  }
145
162
 
146
- const stats: Stats = fs.statSync(resolved, cred);
163
+ const stats: Stats = fs.statSync(resolved);
147
164
 
148
- if (!stats.hasAccess(mode, cred)) {
165
+ if (!stats.hasAccess(mode, credentials) || !stats.hasAccess(flagToMode(flag), credentials)) {
149
166
  throw ErrnoError.With('EACCES', path, '_open');
150
167
  }
151
168
 
@@ -153,19 +170,14 @@ function _openSync(path: fs.PathLike, _flag: fs.OpenMode, _mode?: fs.Mode | null
153
170
  throw ErrnoError.With('EEXIST', path, '_open');
154
171
  }
155
172
 
156
- if (!isTruncating(flag)) {
157
- return fs.openFileSync(resolved, flag, cred);
173
+ const file = fs.openFileSync(resolved, flag);
174
+
175
+ if (isTruncating(flag)) {
176
+ file.truncateSync(0);
177
+ file.syncSync();
158
178
  }
159
179
 
160
- // Delete file.
161
- fs.unlinkSync(resolved, cred);
162
- /*
163
- Create file. Use the same mode as the old file.
164
- Node itself modifies the ctime when this occurs, so this action
165
- will preserve that behavior if the underlying file system
166
- supports those properties.
167
- */
168
- return fs.createFileSync(resolved, flag, stats.mode, cred);
180
+ return file;
169
181
  }
170
182
 
171
183
  /**
@@ -176,7 +188,7 @@ function _openSync(path: fs.PathLike, _flag: fs.OpenMode, _mode?: fs.Mode | null
176
188
  * @param mode Mode to use to open the file. Can be ignored if the
177
189
  * filesystem doesn't support permissions.
178
190
  */
179
- export function openSync(path: fs.PathLike, flag: fs.OpenMode, mode: fs.Mode | null = F_OK): number {
191
+ export function openSync(path: fs.PathLike, flag: fs.OpenMode, mode: fs.Mode | null = constants.F_OK): number {
180
192
  return file2fd(_openSync(path, flag, mode, true));
181
193
  }
182
194
  openSync satisfies typeof fs.openSync;
@@ -252,6 +264,7 @@ export function writeFileSync(path: fs.PathOrFileDescriptor, data: FileContents,
252
264
  }
253
265
  using file = _openSync(typeof path == 'number' ? fd2file(path).path : path.toString(), flag, options.mode, true);
254
266
  file.writeSync(encodedData, 0, encodedData.byteLength, 0);
267
+ emitChange('change', path.toString());
255
268
  }
256
269
  writeFileSync satisfies typeof fs.writeFileSync;
257
270
 
@@ -371,7 +384,9 @@ export function writeSync(fd: number, data: FileContents, posOrOff?: number | nu
371
384
 
372
385
  const file = fd2file(fd);
373
386
  position ??= file.position;
374
- return file.writeSync(buffer, offset, length, position);
387
+ const bytesWritten = file.writeSync(buffer, offset, length, position);
388
+ emitChange('change', file.path);
389
+ return bytesWritten;
375
390
  }
376
391
  writeSync satisfies typeof fs.writeSync;
377
392
 
@@ -451,7 +466,11 @@ export function rmdirSync(path: fs.PathLike): void {
451
466
  path = normalizePath(path);
452
467
  const { fs, path: resolved } = resolveMount(existsSync(path) ? realpathSync(path) : path);
453
468
  try {
454
- fs.rmdirSync(resolved, cred);
469
+ if (!fs.statSync(resolved).hasAccess(constants.W_OK, credentials)) {
470
+ throw ErrnoError.With('EACCES', resolved, 'rmdir');
471
+ }
472
+ fs.rmdirSync(resolved);
473
+ emitChange('rename', path.toString());
455
474
  } catch (e) {
456
475
  throw fixError(e as Error, { [resolved]: path });
457
476
  }
@@ -462,7 +481,6 @@ rmdirSync satisfies typeof fs.rmdirSync;
462
481
  * Synchronous `mkdir`.
463
482
  * @param path
464
483
  * @param mode defaults to o777
465
- * @todo Implement recursion
466
484
  */
467
485
  export function mkdirSync(path: fs.PathLike, options: fs.MakeDirectoryOptions & { recursive: true }): string | undefined;
468
486
  export function mkdirSync(path: fs.PathLike, options?: fs.Mode | (fs.MakeDirectoryOptions & { recursive?: false }) | null): void;
@@ -478,16 +496,23 @@ export function mkdirSync(path: fs.PathLike, options?: fs.Mode | fs.MakeDirector
478
496
 
479
497
  try {
480
498
  if (!options?.recursive) {
481
- return fs.mkdirSync(resolved, mode, cred);
499
+ if (!fs.statSync(dirname(resolved)).hasAccess(constants.W_OK, credentials)) {
500
+ throw ErrnoError.With('EACCES', dirname(resolved), 'mkdir');
501
+ }
502
+ return fs.mkdirSync(resolved, mode);
482
503
  }
483
504
 
484
505
  const dirs: string[] = [];
485
- for (let dir = resolved, original = path; !fs.existsSync(dir, cred); dir = dirname(dir), original = dirname(original)) {
506
+ for (let dir = resolved, original = path; !fs.existsSync(dir); dir = dirname(dir), original = dirname(original)) {
486
507
  dirs.unshift(dir);
487
508
  errorPaths[dir] = original;
488
509
  }
489
510
  for (const dir of dirs) {
490
- fs.mkdirSync(dir, mode, cred);
511
+ if (!fs.statSync(dirname(dir)).hasAccess(constants.W_OK, credentials)) {
512
+ throw ErrnoError.With('EACCES', dirname(dir), 'mkdir');
513
+ }
514
+ fs.mkdirSync(dir, mode);
515
+ emitChange('rename', dir);
491
516
  }
492
517
  return dirs[0];
493
518
  } catch (e) {
@@ -511,8 +536,11 @@ export function readdirSync(
511
536
  path = normalizePath(path);
512
537
  const { fs, path: resolved } = resolveMount(existsSync(path) ? realpathSync(path) : path);
513
538
  let entries: string[];
539
+ if (!statSync(path).hasAccess(constants.R_OK, credentials)) {
540
+ throw ErrnoError.With('EACCES', path, 'readdir');
541
+ }
514
542
  try {
515
- entries = fs.readdirSync(resolved, cred);
543
+ entries = fs.readdirSync(resolved);
516
544
  } catch (e) {
517
545
  throw fixError(e as Error, { [resolved]: path });
518
546
  }
@@ -545,17 +573,31 @@ readdirSync satisfies typeof fs.readdirSync;
545
573
 
546
574
  /**
547
575
  * Synchronous `link`.
548
- * @param existing
549
- * @param newpath
576
+ * @param targetPath
577
+ * @param linkPath
550
578
  */
551
- export function linkSync(existing: fs.PathLike, newpath: fs.PathLike): void {
552
- existing = normalizePath(existing);
553
- newpath = normalizePath(newpath);
554
- const { fs, path: resolved } = resolveMount(existing);
579
+ export function linkSync(targetPath: fs.PathLike, linkPath: fs.PathLike): void {
580
+ targetPath = normalizePath(targetPath);
581
+ if (!statSync(dirname(targetPath)).hasAccess(constants.R_OK, credentials)) {
582
+ throw ErrnoError.With('EACCES', dirname(targetPath), 'link');
583
+ }
584
+ linkPath = normalizePath(linkPath);
585
+ if (!statSync(dirname(linkPath)).hasAccess(constants.W_OK, credentials)) {
586
+ throw ErrnoError.With('EACCES', dirname(linkPath), 'link');
587
+ }
588
+
589
+ const { fs, path } = resolveMount(targetPath);
590
+ const link = resolveMount(linkPath);
591
+ if (fs != link.fs) {
592
+ throw ErrnoError.With('EXDEV', linkPath, 'link');
593
+ }
555
594
  try {
556
- return fs.linkSync(resolved, newpath, cred);
595
+ if (!fs.statSync(path).hasAccess(constants.W_OK, credentials)) {
596
+ throw ErrnoError.With('EACCES', path, 'link');
597
+ }
598
+ return fs.linkSync(path, linkPath);
557
599
  } catch (e) {
558
- throw fixError(e as Error, { [resolved]: existing });
600
+ throw fixError(e as Error, { [path]: targetPath, [link.path]: linkPath });
559
601
  }
560
602
  }
561
603
  linkSync satisfies typeof fs.linkSync;
@@ -576,7 +618,7 @@ export function symlinkSync(target: fs.PathLike, path: fs.PathLike, type: fs.sym
576
618
 
577
619
  writeFileSync(path, target.toString());
578
620
  const file = _openSync(path, 'r+', 0o644, false);
579
- file._setTypeSync(S_IFLNK);
621
+ file._setTypeSync(constants.S_IFLNK);
580
622
  }
581
623
  symlinkSync satisfies typeof fs.symlinkSync;
582
624
 
@@ -692,7 +734,7 @@ export function realpathSync(path: fs.PathLike, options?: fs.EncodingOption | fs
692
734
  const { fs, path: resolvedPath, mountPoint } = resolveMount(lpath);
693
735
 
694
736
  try {
695
- const stats = fs.statSync(resolvedPath, cred);
737
+ const stats = fs.statSync(resolvedPath);
696
738
  if (!stats.isSymbolicLink()) {
697
739
  return lpath;
698
740
  }
@@ -711,7 +753,7 @@ realpathSync satisfies Omit<typeof fs.realpathSync, 'native'>;
711
753
  */
712
754
  export function accessSync(path: fs.PathLike, mode: number = 0o600): void {
713
755
  const stats = statSync(path);
714
- if (!stats.hasAccess(mode, cred)) {
756
+ if (!stats.hasAccess(mode, credentials)) {
715
757
  throw new ErrnoError(Errno.EACCES);
716
758
  }
717
759
  }
@@ -726,8 +768,8 @@ export function rmSync(path: fs.PathLike, options?: fs.RmOptions): void {
726
768
 
727
769
  const stats = statSync(path);
728
770
 
729
- switch (stats.mode & S_IFMT) {
730
- case S_IFDIR:
771
+ switch (stats.mode & constants.S_IFMT) {
772
+ case constants.S_IFDIR:
731
773
  if (options?.recursive) {
732
774
  for (const entry of readdirSync(path)) {
733
775
  rmSync(join(path, entry), options);
@@ -736,14 +778,14 @@ export function rmSync(path: fs.PathLike, options?: fs.RmOptions): void {
736
778
 
737
779
  rmdirSync(path);
738
780
  return;
739
- case S_IFREG:
740
- case S_IFLNK:
781
+ case constants.S_IFREG:
782
+ case constants.S_IFLNK:
741
783
  unlinkSync(path);
742
784
  return;
743
- case S_IFBLK:
744
- case S_IFCHR:
745
- case S_IFIFO:
746
- case S_IFSOCK:
785
+ case constants.S_IFBLK:
786
+ case constants.S_IFCHR:
787
+ case constants.S_IFIFO:
788
+ case constants.S_IFSOCK:
747
789
  default:
748
790
  throw new ErrnoError(Errno.EPERM, 'File type not supported', path, 'rm');
749
791
  }
@@ -780,11 +822,12 @@ export function copyFileSync(src: fs.PathLike, dest: fs.PathLike, flags?: number
780
822
  src = normalizePath(src);
781
823
  dest = normalizePath(dest);
782
824
 
783
- if (flags && flags & COPYFILE_EXCL && existsSync(dest)) {
825
+ if (flags && flags & constants.COPYFILE_EXCL && existsSync(dest)) {
784
826
  throw new ErrnoError(Errno.EEXIST, 'Destination file already exists.', dest, 'copyFile');
785
827
  }
786
828
 
787
829
  writeFileSync(dest, readFileSync(src));
830
+ emitChange('rename', dest.toString());
788
831
  }
789
832
  copyFileSync satisfies typeof fs.copyFileSync;
790
833
 
@@ -834,7 +877,7 @@ writevSync satisfies typeof fs.writevSync;
834
877
  */
835
878
  export function opendirSync(path: fs.PathLike, options?: fs.OpenDirOptions): Dir {
836
879
  path = normalizePath(path);
837
- return new Dir(path); // Re-use existing `Dir` class
880
+ return new Dir(path);
838
881
  }
839
882
  opendirSync satisfies typeof fs.opendirSync;
840
883
 
@@ -860,8 +903,8 @@ export function cpSync(source: fs.PathLike, destination: fs.PathLike, opts?: fs.
860
903
  throw new ErrnoError(Errno.EEXIST, 'Destination file or directory already exists.', destination, 'cp');
861
904
  }
862
905
 
863
- switch (srcStats.mode & S_IFMT) {
864
- case S_IFDIR:
906
+ switch (srcStats.mode & constants.S_IFMT) {
907
+ case constants.S_IFDIR:
865
908
  if (!opts?.recursive) {
866
909
  throw new ErrnoError(Errno.EISDIR, source + ' is a directory (not copied)', source, 'cp');
867
910
  }
@@ -873,14 +916,14 @@ export function cpSync(source: fs.PathLike, destination: fs.PathLike, opts?: fs.
873
916
  cpSync(join(source, dirent.name), join(destination, dirent.name), opts);
874
917
  }
875
918
  break;
876
- case S_IFREG:
877
- case S_IFLNK:
919
+ case constants.S_IFREG:
920
+ case constants.S_IFLNK:
878
921
  copyFileSync(source, destination);
879
922
  break;
880
- case S_IFBLK:
881
- case S_IFCHR:
882
- case S_IFIFO:
883
- case S_IFSOCK:
923
+ case constants.S_IFBLK:
924
+ case constants.S_IFCHR:
925
+ case constants.S_IFIFO:
926
+ case constants.S_IFSOCK:
884
927
  default:
885
928
  throw new ErrnoError(Errno.EPERM, 'File type not supported', source, 'rm');
886
929
  }
@@ -2,7 +2,17 @@ import { EventEmitter } from 'eventemitter3';
2
2
  import type { EventEmitter as NodeEventEmitter } from 'node:events';
3
3
  import type * as fs from 'node:fs';
4
4
  import { ErrnoError } from '../error.js';
5
+ import { isStatsEqual, type Stats } from '../stats.js';
6
+ import { normalizePath } from '../utils.js';
7
+ import { dirname, basename } from './path.js';
8
+ import { statSync } from './sync.js';
5
9
 
10
+ /**
11
+ * Base class for file system watchers.
12
+ * Provides event handling capabilities for watching file system changes.
13
+ *
14
+ * @template TEvents The type of events emitted by the watcher.
15
+ */
6
16
  class Watcher<TEvents extends Record<string, unknown[]> = Record<string, unknown[]>> extends EventEmitter<TEvents> implements NodeEventEmitter {
7
17
  /* eslint-disable @typescript-eslint/no-explicit-any */
8
18
  public off<T extends EventEmitter.EventNames<TEvents>>(event: T, fn?: (...args: any[]) => void, context?: any, once?: boolean): this {
@@ -14,24 +24,28 @@ class Watcher<TEvents extends Record<string, unknown[]> = Record<string, unknown
14
24
  }
15
25
  /* eslint-enable @typescript-eslint/no-explicit-any */
16
26
 
27
+ public constructor(public readonly path: string) {
28
+ super();
29
+ }
30
+
17
31
  public setMaxListeners(): never {
18
- throw ErrnoError.With('ENOTSUP');
32
+ throw ErrnoError.With('ENOSYS', this.path, 'Watcher.setMaxListeners');
19
33
  }
20
34
 
21
35
  public getMaxListeners(): never {
22
- throw ErrnoError.With('ENOTSUP');
36
+ throw ErrnoError.With('ENOSYS', this.path, 'Watcher.getMaxListeners');
23
37
  }
24
38
 
25
39
  public prependListener(): never {
26
- throw ErrnoError.With('ENOTSUP');
40
+ throw ErrnoError.With('ENOSYS', this.path, 'Watcher.prependListener');
27
41
  }
28
42
 
29
43
  public prependOnceListener(): never {
30
- throw ErrnoError.With('ENOTSUP');
44
+ throw ErrnoError.With('ENOSYS', this.path, 'Watcher.prependOnceListener');
31
45
  }
32
46
 
33
47
  public rawListeners(): never {
34
- throw ErrnoError.With('ENOTSUP');
48
+ throw ErrnoError.With('ENOSYS', this.path, 'Watcher.rawListeners');
35
49
  }
36
50
 
37
51
  public ref(): this {
@@ -44,7 +58,9 @@ class Watcher<TEvents extends Record<string, unknown[]> = Record<string, unknown
44
58
  }
45
59
 
46
60
  /**
47
- * @todo Actually emit events
61
+ * Watches for changes on the file system.
62
+ *
63
+ * @template T The type of the filename, either `string` or `Buffer`.
48
64
  */
49
65
  export class FSWatcher<T extends string | Buffer = string | Buffer>
50
66
  extends Watcher<{
@@ -54,10 +70,124 @@ export class FSWatcher<T extends string | Buffer = string | Buffer>
54
70
  }>
55
71
  implements fs.FSWatcher
56
72
  {
57
- public constructor(public readonly options: fs.WatchOptions) {
58
- super();
73
+ public constructor(
74
+ path: string,
75
+ public readonly options: fs.WatchOptions
76
+ ) {
77
+ super(path);
78
+ addWatcher(path.toString(), this);
79
+ }
80
+
81
+ public close(): void {
82
+ super.emit('close');
83
+ removeWatcher(this.path.toString(), this);
84
+ }
85
+
86
+ public [Symbol.dispose](): void {
87
+ this.close();
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Watches for changes to a file's stats.
93
+ *
94
+ * Instances of `StatWatcher` are used by `fs.watchFile()` to monitor changes to a file's statistics.
95
+ */
96
+ export class StatWatcher
97
+ extends Watcher<{
98
+ change: [current: Stats, previous: Stats];
99
+ close: [];
100
+ error: [error: Error];
101
+ }>
102
+ implements fs.StatWatcher
103
+ {
104
+ private intervalId?: NodeJS.Timeout | number;
105
+ private previous?: Stats;
106
+
107
+ public constructor(
108
+ path: string,
109
+ private options: { persistent?: boolean; interval?: number }
110
+ ) {
111
+ super(path);
112
+ this.start();
113
+ }
114
+
115
+ protected onInterval() {
116
+ try {
117
+ const current = statSync(this.path);
118
+ if (!isStatsEqual(this.previous!, current)) {
119
+ this.emit('change', current, this.previous!);
120
+ this.previous = current;
121
+ }
122
+ } catch (e) {
123
+ this.emit('error', e as Error);
124
+ }
125
+ }
126
+
127
+ protected start() {
128
+ const interval = this.options.interval || 5000;
129
+ try {
130
+ this.previous = statSync(this.path);
131
+ } catch (e) {
132
+ this.emit('error', e as Error);
133
+ return;
134
+ }
135
+ this.intervalId = setInterval(this.onInterval.bind(this), interval);
136
+ if (!this.options.persistent && typeof this.intervalId == 'object') {
137
+ this.intervalId.unref();
138
+ }
139
+ }
140
+
141
+ /**
142
+ * @internal
143
+ */
144
+ public stop() {
145
+ if (this.intervalId) {
146
+ clearInterval(this.intervalId);
147
+ this.intervalId = undefined;
148
+ }
149
+ this.removeAllListeners();
59
150
  }
60
- public close(): void {}
61
151
  }
62
152
 
63
- export class StatWatcher extends Watcher implements fs.StatWatcher {}
153
+ const watchers: Map<string, Set<FSWatcher>> = new Map();
154
+
155
+ export function addWatcher(path: string, watcher: FSWatcher) {
156
+ const normalizedPath = normalizePath(path);
157
+ if (!watchers.has(normalizedPath)) {
158
+ watchers.set(normalizedPath, new Set());
159
+ }
160
+ watchers.get(normalizedPath)!.add(watcher);
161
+ }
162
+
163
+ export function removeWatcher(path: string, watcher: FSWatcher) {
164
+ const normalizedPath = normalizePath(path);
165
+ if (watchers.has(normalizedPath)) {
166
+ watchers.get(normalizedPath)!.delete(watcher);
167
+ if (watchers.get(normalizedPath)!.size === 0) {
168
+ watchers.delete(normalizedPath);
169
+ }
170
+ }
171
+ }
172
+
173
+ export function emitChange(eventType: fs.WatchEventType, filename: string) {
174
+ let normalizedFilename: string = normalizePath(filename);
175
+ // Notify watchers on the specific file
176
+ if (watchers.has(normalizedFilename)) {
177
+ for (const watcher of watchers.get(normalizedFilename)!) {
178
+ watcher.emit('change', eventType, basename(filename));
179
+ }
180
+ }
181
+
182
+ // Notify watchers on parent directories if they are watching recursively
183
+ let parent = dirname(normalizedFilename);
184
+ while (parent !== normalizedFilename && parent !== '/') {
185
+ if (watchers.has(parent)) {
186
+ for (const watcher of watchers.get(parent)!) {
187
+ watcher.emit('change', eventType, basename(filename));
188
+ }
189
+ }
190
+ normalizedFilename = parent;
191
+ parent = dirname(parent);
192
+ }
193
+ }
package/src/error.ts CHANGED
@@ -284,7 +284,7 @@ export class ErrnoError extends Error implements NodeJS.ErrnoException {
284
284
  }
285
285
 
286
286
  /**
287
- * @return A friendly error message.
287
+ * @returns A friendly error message.
288
288
  */
289
289
  public toString(): string {
290
290
  return this.message;