@zenfs/core 1.1.6 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/backends/file_index.js +0 -3
  2. package/dist/backends/overlay.js +0 -8
  3. package/dist/backends/store/fs.js +4 -17
  4. package/dist/config.d.ts +24 -1
  5. package/dist/config.js +5 -0
  6. package/dist/devices.js +0 -12
  7. package/dist/emulation/cache.d.ts +21 -0
  8. package/dist/emulation/cache.js +36 -0
  9. package/dist/emulation/config.d.ts +10 -0
  10. package/dist/emulation/config.js +10 -0
  11. package/dist/emulation/promises.d.ts +9 -14
  12. package/dist/emulation/promises.js +71 -47
  13. package/dist/emulation/shared.d.ts +16 -0
  14. package/dist/emulation/sync.d.ts +11 -20
  15. package/dist/emulation/sync.js +44 -22
  16. package/dist/file.d.ts +1 -1
  17. package/dist/file.js +6 -3
  18. package/package.json +4 -2
  19. package/readme.md +1 -1
  20. package/scripts/test.js +19 -1
  21. package/src/backends/backend.ts +160 -0
  22. package/src/backends/fetch.ts +180 -0
  23. package/src/backends/file_index.ts +206 -0
  24. package/src/backends/memory.ts +50 -0
  25. package/src/backends/overlay.ts +560 -0
  26. package/src/backends/port/fs.ts +335 -0
  27. package/src/backends/port/readme.md +54 -0
  28. package/src/backends/port/rpc.ts +167 -0
  29. package/src/backends/readme.md +3 -0
  30. package/src/backends/store/fs.ts +700 -0
  31. package/src/backends/store/readme.md +9 -0
  32. package/src/backends/store/simple.ts +146 -0
  33. package/src/backends/store/store.ts +173 -0
  34. package/src/config.ts +185 -0
  35. package/src/credentials.ts +31 -0
  36. package/src/devices.ts +459 -0
  37. package/src/emulation/async.ts +834 -0
  38. package/src/emulation/cache.ts +44 -0
  39. package/src/emulation/config.ts +11 -0
  40. package/src/emulation/constants.ts +182 -0
  41. package/src/emulation/dir.ts +138 -0
  42. package/src/emulation/index.ts +8 -0
  43. package/src/emulation/path.ts +440 -0
  44. package/src/emulation/promises.ts +1134 -0
  45. package/src/emulation/shared.ts +153 -0
  46. package/src/emulation/streams.ts +34 -0
  47. package/src/emulation/sync.ts +868 -0
  48. package/src/emulation/watchers.ts +193 -0
  49. package/src/error.ts +307 -0
  50. package/src/file.ts +662 -0
  51. package/src/filesystem.ts +174 -0
  52. package/src/index.ts +25 -0
  53. package/src/inode.ts +132 -0
  54. package/src/mixins/async.ts +208 -0
  55. package/src/mixins/index.ts +5 -0
  56. package/src/mixins/mutexed.ts +257 -0
  57. package/src/mixins/readonly.ts +96 -0
  58. package/src/mixins/shared.ts +25 -0
  59. package/src/mixins/sync.ts +58 -0
  60. package/src/polyfills.ts +21 -0
  61. package/src/stats.ts +363 -0
  62. package/src/utils.ts +288 -0
  63. package/tests/common.ts +1 -11
  64. package/tests/fs/directory.test.ts +1 -1
  65. package/tests/fs/errors.test.ts +1 -1
  66. package/tests/fs/links.test.ts +1 -1
  67. package/tests/fs/open.test.ts +1 -1
  68. package/tests/fs/permissions.test.ts +4 -4
  69. package/tests/fs/readdir.test.ts +3 -3
  70. package/tests/fs/rename.test.ts +1 -1
  71. package/tests/fs/stat.test.ts +1 -1
  72. package/tests/fs/times.test.ts +2 -2
  73. package/tests/fs/truncate.test.ts +1 -1
  74. package/tests/port/channel.test.ts +3 -3
  75. package/tests/port/config.test.ts +4 -5
  76. package/tests/port/config.worker.js +5 -0
  77. package/tests/port/remote.test.ts +4 -5
  78. package/tests/port/remote.worker.js +5 -0
  79. package/tests/port/timeout.test.ts +2 -2
  80. package/tests/setup/common.ts +1 -1
  81. package/tests/setup/cow+fetch.ts +1 -1
  82. package/tests/port/config.worker.ts +0 -5
  83. package/tests/port/remote.worker.ts +0 -5
@@ -3,6 +3,7 @@ import type * as fs from 'node:fs';
3
3
  import type { FileContents } from '../filesystem.js';
4
4
  import { BigIntStats, type Stats } from '../stats.js';
5
5
  import { Dir, Dirent } from './dir.js';
6
+ import { type InternalOptions, type ReaddirOptions } from './shared.js';
6
7
  export declare function renameSync(oldPath: fs.PathLike, newPath: fs.PathLike): void;
7
8
  /**
8
9
  * Test whether or not `path` exists by checking with the file system.
@@ -110,29 +111,19 @@ export declare function mkdirSync(path: fs.PathLike, options?: fs.Mode | (fs.Mak
110
111
  recursive?: false;
111
112
  }) | null): void;
112
113
  export declare function mkdirSync(path: fs.PathLike, options?: fs.Mode | fs.MakeDirectoryOptions | null): string | undefined;
113
- export declare function readdirSync(path: fs.PathLike, options?: {
114
- recursive?: boolean;
115
- encoding?: BufferEncoding | null;
114
+ export declare function readdirSync(path: fs.PathLike, options?: (fs.ObjectEncodingOptions & ReaddirOptions & {
116
115
  withFileTypes?: false;
117
- } | BufferEncoding | null): string[];
118
- export declare function readdirSync(path: fs.PathLike, options: {
119
- recursive?: boolean;
120
- encoding: 'buffer';
116
+ }) | BufferEncoding | null): string[];
117
+ export declare function readdirSync(path: fs.PathLike, options: fs.BufferEncodingOption & ReaddirOptions & {
121
118
  withFileTypes?: false;
122
- } | 'buffer'): Buffer[];
123
- export declare function readdirSync(path: fs.PathLike, options: {
124
- recursive?: boolean;
125
- withFileTypes: true;
126
- }): Dirent[];
127
- export declare function readdirSync(path: fs.PathLike, options?: (fs.ObjectEncodingOptions & {
119
+ }): Buffer[];
120
+ export declare function readdirSync(path: fs.PathLike, options?: (fs.ObjectEncodingOptions & ReaddirOptions & {
128
121
  withFileTypes?: false;
129
- recursive?: boolean;
130
122
  }) | BufferEncoding | null): string[] | Buffer[];
131
- export declare function readdirSync(path: fs.PathLike, options?: {
132
- withFileTypes?: boolean;
133
- recursive?: boolean;
134
- encoding?: BufferEncoding | 'buffer' | null;
135
- } | BufferEncoding | 'buffer' | null): string[] | Dirent[] | Buffer[];
123
+ export declare function readdirSync(path: fs.PathLike, options: fs.ObjectEncodingOptions & ReaddirOptions & {
124
+ withFileTypes: true;
125
+ }): Dirent[];
126
+ export declare function readdirSync(path: fs.PathLike, options?: (ReaddirOptions & (fs.ObjectEncodingOptions | fs.BufferEncodingOption)) | BufferEncoding | null): string[] | Dirent[] | Buffer[];
136
127
  export declare function linkSync(targetPath: fs.PathLike, linkPath: fs.PathLike): void;
137
128
  /**
138
129
  * Synchronous `symlink`.
@@ -163,7 +154,7 @@ export declare function accessSync(path: fs.PathLike, mode?: number): void;
163
154
  * Synchronous `rm`. Removes files or directories (recursively).
164
155
  * @param path The path to the file or directory to remove.
165
156
  */
166
- export declare function rmSync(path: fs.PathLike, options?: fs.RmOptions): void;
157
+ export declare function rmSync(path: fs.PathLike, options?: fs.RmOptions & InternalOptions): void;
167
158
  /**
168
159
  * Synchronous `mkdtemp`. Creates a unique temporary directory.
169
160
  * @param prefix The directory prefix.
@@ -54,13 +54,15 @@ import * as constants from './constants.js';
54
54
  import { Dir, Dirent } from './dir.js';
55
55
  import { dirname, join, parse } from './path.js';
56
56
  import { _statfs, fd2file, fdMap, file2fd, fixError, mounts, resolveMount } from './shared.js';
57
+ import { config } from './config.js';
57
58
  import { emitChange } from './watchers.js';
59
+ import * as cache from './cache.js';
58
60
  export function renameSync(oldPath, newPath) {
59
61
  oldPath = normalizePath(oldPath);
60
62
  newPath = normalizePath(newPath);
61
63
  const oldMount = resolveMount(oldPath);
62
64
  const newMount = resolveMount(newPath);
63
- if (!statSync(dirname(oldPath)).hasAccess(constants.W_OK)) {
65
+ if (config.checkAccess && !statSync(dirname(oldPath)).hasAccess(constants.W_OK)) {
64
66
  throw ErrnoError.With('EACCES', oldPath, 'rename');
65
67
  }
66
68
  try {
@@ -100,7 +102,7 @@ export function statSync(path, options) {
100
102
  const { fs, path: resolved } = resolveMount(realpathSync(path));
101
103
  try {
102
104
  const stats = fs.statSync(resolved);
103
- if (!stats.hasAccess(constants.R_OK)) {
105
+ if (config.checkAccess && !stats.hasAccess(constants.R_OK)) {
104
106
  throw ErrnoError.With('EACCES', resolved, 'stat');
105
107
  }
106
108
  return options?.bigint ? new BigIntStats(stats) : stats;
@@ -145,7 +147,7 @@ export function unlinkSync(path) {
145
147
  path = normalizePath(path);
146
148
  const { fs, path: resolved } = resolveMount(path);
147
149
  try {
148
- if (!fs.statSync(resolved).hasAccess(constants.W_OK)) {
150
+ if (config.checkAccess && !(cache.getStats(path) || fs.statSync(resolved)).hasAccess(constants.W_OK)) {
149
151
  throw ErrnoError.With('EACCES', resolved, 'unlink');
150
152
  }
151
153
  fs.unlinkSync(resolved);
@@ -174,7 +176,7 @@ function _openSync(path, _flag, _mode, resolveSymlinks = true) {
174
176
  }
175
177
  // Create the file
176
178
  const parentStats = fs.statSync(dirname(resolved));
177
- if (!parentStats.hasAccess(constants.W_OK)) {
179
+ if (config.checkAccess && !parentStats.hasAccess(constants.W_OK)) {
178
180
  throw ErrnoError.With('EACCES', dirname(path), '_open');
179
181
  }
180
182
  if (!parentStats.isDirectory()) {
@@ -182,7 +184,7 @@ function _openSync(path, _flag, _mode, resolveSymlinks = true) {
182
184
  }
183
185
  return fs.createFileSync(resolved, flag, mode);
184
186
  }
185
- if (!stats.hasAccess(mode) || !stats.hasAccess(flagToMode(flag))) {
187
+ if (config.checkAccess && (!stats.hasAccess(mode) || !stats.hasAccess(flagToMode(flag)))) {
186
188
  throw ErrnoError.With('EACCES', path, '_open');
187
189
  }
188
190
  if (isExclusive(flag)) {
@@ -392,7 +394,11 @@ export function rmdirSync(path) {
392
394
  path = normalizePath(path);
393
395
  const { fs, path: resolved } = resolveMount(realpathSync(path));
394
396
  try {
395
- if (!fs.statSync(resolved).hasAccess(constants.W_OK)) {
397
+ const stats = cache.getStats(path) || fs.statSync(resolved);
398
+ if (!stats.isDirectory()) {
399
+ throw ErrnoError.With('ENOTDIR', resolved, 'rmdir');
400
+ }
401
+ if (config.checkAccess && !stats.hasAccess(constants.W_OK)) {
396
402
  throw ErrnoError.With('EACCES', resolved, 'rmdir');
397
403
  }
398
404
  fs.rmdirSync(resolved);
@@ -406,12 +412,12 @@ rmdirSync;
406
412
  export function mkdirSync(path, options) {
407
413
  options = typeof options === 'object' ? options : { mode: options };
408
414
  const mode = normalizeMode(options?.mode, 0o777);
409
- path = realpathSync(normalizePath(path));
415
+ path = realpathSync(path);
410
416
  const { fs, path: resolved } = resolveMount(path);
411
417
  const errorPaths = { [resolved]: path };
412
418
  try {
413
419
  if (!options?.recursive) {
414
- if (!fs.statSync(dirname(resolved)).hasAccess(constants.W_OK)) {
420
+ if (config.checkAccess && !fs.statSync(dirname(resolved)).hasAccess(constants.W_OK)) {
415
421
  throw ErrnoError.With('EACCES', dirname(resolved), 'mkdir');
416
422
  }
417
423
  return fs.mkdirSync(resolved, mode);
@@ -422,7 +428,7 @@ export function mkdirSync(path, options) {
422
428
  errorPaths[dir] = original;
423
429
  }
424
430
  for (const dir of dirs) {
425
- if (!fs.statSync(dirname(dir)).hasAccess(constants.W_OK)) {
431
+ if (config.checkAccess && !fs.statSync(dirname(dir)).hasAccess(constants.W_OK)) {
426
432
  throw ErrnoError.With('EACCES', dirname(dir), 'mkdir');
427
433
  }
428
434
  fs.mkdirSync(dir, mode);
@@ -441,8 +447,13 @@ export function readdirSync(path, options) {
441
447
  const { fs, path: resolved } = resolveMount(realpathSync(path));
442
448
  let entries;
443
449
  try {
444
- if (!fs.statSync(resolved).hasAccess(constants.R_OK)) {
445
- throw ErrnoError.With('EACCES', path, 'readdir');
450
+ const stats = cache.getStats(path) || fs.statSync(resolved);
451
+ cache.setStats(path, stats);
452
+ if (config.checkAccess && !stats.hasAccess(constants.R_OK)) {
453
+ throw ErrnoError.With('EACCES', resolved, 'readdir');
454
+ }
455
+ if (!stats.isDirectory()) {
456
+ throw ErrnoError.With('ENOTDIR', resolved, 'readdir');
446
457
  }
447
458
  entries = fs.readdirSync(resolved);
448
459
  }
@@ -463,11 +474,12 @@ export function readdirSync(path, options) {
463
474
  // Iterate over entries and handle recursive case if needed
464
475
  const values = [];
465
476
  for (const entry of entries) {
466
- const entryStat = fs.statSync(join(resolved, entry));
477
+ const entryStat = cache.getStats(join(path, entry)) || fs.statSync(join(resolved, entry));
478
+ cache.setStats(join(path, entry), entryStat);
467
479
  if (options?.withFileTypes) {
468
480
  values.push(new Dirent(entry, entryStat));
469
481
  }
470
- else if (options?.encoding === 'buffer') {
482
+ else if (options?.encoding == 'buffer') {
471
483
  values.push(Buffer.from(entry));
472
484
  }
473
485
  else {
@@ -475,7 +487,7 @@ export function readdirSync(path, options) {
475
487
  }
476
488
  if (!entryStat.isDirectory() || !options?.recursive)
477
489
  continue;
478
- for (const subEntry of readdirSync(join(path, entry), options)) {
490
+ for (const subEntry of readdirSync(join(path, entry), { ...options, _isIndirect: true })) {
479
491
  if (subEntry instanceof Dirent) {
480
492
  subEntry.path = join(entry, subEntry.path);
481
493
  values.push(subEntry);
@@ -488,17 +500,20 @@ export function readdirSync(path, options) {
488
500
  }
489
501
  }
490
502
  }
503
+ if (!options?._isIndirect) {
504
+ cache.clearStats();
505
+ }
491
506
  return values;
492
507
  }
493
508
  readdirSync;
494
509
  // SYMLINK METHODS
495
510
  export function linkSync(targetPath, linkPath) {
496
511
  targetPath = normalizePath(targetPath);
497
- if (!statSync(dirname(targetPath)).hasAccess(constants.R_OK)) {
512
+ if (config.checkAccess && !statSync(dirname(targetPath)).hasAccess(constants.R_OK)) {
498
513
  throw ErrnoError.With('EACCES', dirname(targetPath), 'link');
499
514
  }
500
515
  linkPath = normalizePath(linkPath);
501
- if (!statSync(dirname(linkPath)).hasAccess(constants.W_OK)) {
516
+ if (config.checkAccess && !statSync(dirname(linkPath)).hasAccess(constants.W_OK)) {
502
517
  throw ErrnoError.With('EACCES', dirname(linkPath), 'link');
503
518
  }
504
519
  const { fs, path } = resolveMount(targetPath);
@@ -507,7 +522,7 @@ export function linkSync(targetPath, linkPath) {
507
522
  throw ErrnoError.With('EXDEV', linkPath, 'link');
508
523
  }
509
524
  try {
510
- if (!fs.statSync(path).hasAccess(constants.W_OK)) {
525
+ if (config.checkAccess && !fs.statSync(path).hasAccess(constants.W_OK)) {
511
526
  throw ErrnoError.With('EACCES', path, 'link');
512
527
  }
513
528
  return fs.linkSync(path, linkPath);
@@ -608,6 +623,8 @@ export function realpathSync(path, options) {
608
623
  }
609
624
  realpathSync;
610
625
  export function accessSync(path, mode = 0o600) {
626
+ if (!config.checkAccess)
627
+ return;
611
628
  if (!statSync(path).hasAccess(mode)) {
612
629
  throw new ErrnoError(Errno.EACCES);
613
630
  }
@@ -621,7 +638,7 @@ export function rmSync(path, options) {
621
638
  path = normalizePath(path);
622
639
  let stats;
623
640
  try {
624
- stats = statSync(path);
641
+ stats = cache.getStats(path) || statSync(path);
625
642
  }
626
643
  catch (error) {
627
644
  if (error.code != 'ENOENT' || !options?.force)
@@ -630,26 +647,31 @@ export function rmSync(path, options) {
630
647
  if (!stats) {
631
648
  return;
632
649
  }
650
+ cache.setStats(path, stats);
633
651
  switch (stats.mode & constants.S_IFMT) {
634
652
  case constants.S_IFDIR:
635
653
  if (options?.recursive) {
636
- for (const entry of readdirSync(path)) {
637
- rmSync(join(path, entry), options);
654
+ for (const entry of readdirSync(path, { _isIndirect: true })) {
655
+ rmSync(join(path, entry), { ...options, _isIndirect: true });
638
656
  }
639
657
  }
640
658
  rmdirSync(path);
641
- return;
659
+ break;
642
660
  case constants.S_IFREG:
643
661
  case constants.S_IFLNK:
644
662
  unlinkSync(path);
645
- return;
663
+ break;
646
664
  case constants.S_IFBLK:
647
665
  case constants.S_IFCHR:
648
666
  case constants.S_IFIFO:
649
667
  case constants.S_IFSOCK:
650
668
  default:
669
+ cache.clearStats();
651
670
  throw new ErrnoError(Errno.EPERM, 'File type not supported', path, 'rm');
652
671
  }
672
+ if (!options?._isIndirect) {
673
+ cache.clearStats();
674
+ }
653
675
  }
654
676
  rmSync;
655
677
  export function mkdtempSync(prefix, options) {
package/dist/file.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { FileReadResult } from 'node:fs/promises';
2
2
  import type { FileSystem } from './filesystem.js';
3
- import { Stats, type FileType } from './stats.js';
4
3
  import './polyfills.js';
4
+ import { Stats, type FileType } from './stats.js';
5
5
  /**
6
6
  Typescript does not include a type declaration for resizable array buffers.
7
7
  It has been standardized into ECMAScript though
package/dist/file.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { O_APPEND, O_CREAT, O_EXCL, O_RDONLY, O_RDWR, O_SYNC, O_TRUNC, O_WRONLY, S_IFMT, size_max } from './emulation/constants.js';
2
+ import { config } from './emulation/config.js';
2
3
  import { Errno, ErrnoError } from './error.js';
3
- import { Stats } from './stats.js';
4
4
  import './polyfills.js';
5
+ import { Stats } from './stats.js';
5
6
  const validFlags = ['r', 'r+', 'rs', 'rs+', 'w', 'wx', 'w+', 'wx+', 'a', 'ax', 'a+', 'ax+'];
6
7
  export function parseFlag(flag) {
7
8
  if (typeof flag === 'number') {
@@ -383,7 +384,8 @@ export class PreloadFile extends File {
383
384
  */
384
385
  async read(buffer, offset, length, position) {
385
386
  const bytesRead = this._read(buffer, offset, length, position);
386
- await this.sync();
387
+ if (config.syncOnRead)
388
+ await this.sync();
387
389
  return { bytesRead, buffer };
388
390
  }
389
391
  /**
@@ -397,7 +399,8 @@ export class PreloadFile extends File {
397
399
  */
398
400
  readSync(buffer, offset, length, position) {
399
401
  const bytesRead = this._read(buffer, offset, length, position);
400
- this.statSync();
402
+ if (config.syncOnRead)
403
+ this.syncSync();
401
404
  return bytesRead;
402
405
  }
403
406
  async chmod(mode) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "1.1.6",
3
+ "version": "1.2.1",
4
4
  "description": "A filesystem, anywhere",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -18,6 +18,7 @@
18
18
  "zenfs-test": "scripts/test.js"
19
19
  },
20
20
  "files": [
21
+ "src",
21
22
  "dist",
22
23
  "tests",
23
24
  "license.md",
@@ -48,7 +49,8 @@
48
49
  "./mixins": "./dist/mixins/index.js",
49
50
  "./path": "./dist/emulation/path.js",
50
51
  "./eslint": "./eslint.shared.js",
51
- "./tests/*": "./tests/*"
52
+ "./tests/*": "./tests/*",
53
+ "./src/*": "./src/*"
52
54
  },
53
55
  "scripts": {
54
56
  "format": "prettier --write .",
package/readme.md CHANGED
@@ -32,7 +32,7 @@ npm install @zenfs/core
32
32
  ```js
33
33
  import { fs } from '@zenfs/core'; // You can also use the default export
34
34
 
35
- fs.writeFileSync('/test.txt', 'You can do this in anywhere (including browsers)!');
35
+ fs.writeFileSync('/test.txt', 'You can do this anywhere, including browsers!');
36
36
 
37
37
  const contents = fs.readFileSync('/test.txt', 'utf-8');
38
38
  console.log(contents);
package/scripts/test.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execSync } from 'node:child_process';
4
+ import { existsSync } from 'node:fs';
4
5
  import { join } from 'node:path';
5
6
  import { parseArgs } from 'node:util';
6
7
 
@@ -8,6 +9,8 @@ const { values: options, positionals } = parseArgs({
8
9
  options: {
9
10
  help: { short: 'h', type: 'boolean', default: false },
10
11
  verbose: { type: 'boolean', default: false },
12
+ test: { type: 'string' },
13
+ forceExit: { short: 'f', type: 'boolean', default: false },
11
14
  },
12
15
  allowPositionals: true,
13
16
  });
@@ -20,12 +23,27 @@ paths: The setup files to run tests on
20
23
  options:
21
24
  --help, -h Outputs this help message
22
25
  --verbose Output verbose messages
26
+ --test Which test to run
27
+ --forceExit Whether to use --test-force-exit
23
28
  `);
24
29
  process.exit();
25
30
  }
26
31
 
32
+ if (options.verbose) console.debug('Forcing tests to exit (--test-force-exit)');
33
+
34
+ if (!existsSync(join(import.meta.dirname, '../dist'))) {
35
+ console.log('ERROR: Missing build. If you are using an installed package, please submit a bug report.');
36
+ process.exit(1);
37
+ }
38
+
39
+ const testsGlob = join(import.meta.dirname, `../tests/fs/${options.test || '*'}.test.ts`);
40
+
27
41
  for (const setupFile of positionals) {
28
42
  if (options.verbose) console.debug('Running tests for:', setupFile);
29
43
  process.env.SETUP = setupFile;
30
- execSync('tsx --test --experimental-test-coverage ' + join(import.meta.dirname, '../tests/fs/*.test.ts'), { stdio: 'inherit' });
44
+ if (!existsSync(setupFile)) {
45
+ console.log('ERROR: Skipping non-existent file:', setupFile);
46
+ continue;
47
+ }
48
+ execSync(['tsx --test --experimental-test-coverage', options.forceExit ? '--test-force-exit' : '', testsGlob, process.env.CMD].join(' '), { stdio: 'inherit' });
31
49
  }
@@ -0,0 +1,160 @@
1
+ import type { Entries, RequiredKeys } from 'utilium';
2
+ import { ErrnoError, Errno } from '../error.js';
3
+ import type { FileSystem } from '../filesystem.js';
4
+ import { levenshtein } from '../utils.js';
5
+
6
+ type OptionType = 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function';
7
+
8
+ /**
9
+ * Resolves the type of Backend.options from the options interface
10
+ */
11
+ export type OptionsConfig<T> = {
12
+ [K in keyof T]: {
13
+ /**
14
+ * The basic JavaScript type(s) for this option.
15
+ */
16
+ type: OptionType | readonly OptionType[];
17
+
18
+ /**
19
+ * Description of the option. Used in error messages and documentation.
20
+ */
21
+ description: string;
22
+
23
+ /**
24
+ * Whether or not the option is required (optional can be set to null or undefined). Defaults to false.
25
+ */
26
+ required: K extends RequiredKeys<T> ? true : false;
27
+
28
+ /**
29
+ * A custom validation function to check if the option is valid.
30
+ * When async, resolves if valid and rejects if not.
31
+ * When sync, it will throw an error if not valid.
32
+ */
33
+ validator?(opt: T[K]): void | Promise<void>;
34
+ };
35
+ };
36
+
37
+ /**
38
+ * Configuration options shared by backends and `Configuration`
39
+ */
40
+ export interface SharedConfig {
41
+ /**
42
+ * If set, disables the sync cache and sync operations on async file systems.
43
+ */
44
+ disableAsyncCache?: boolean;
45
+ }
46
+
47
+ /**
48
+ * A backend
49
+ */
50
+ export interface Backend<FS extends FileSystem = FileSystem, TOptions extends object = object> {
51
+ /**
52
+ * Create a new instance of the backend
53
+ */
54
+ create(options: TOptions & Partial<SharedConfig>): FS | Promise<FS>;
55
+
56
+ /**
57
+ * A name to identify the backend.
58
+ */
59
+ name: string;
60
+
61
+ /**
62
+ * Describes all of the options available for this backend.
63
+ */
64
+ options: OptionsConfig<TOptions>;
65
+
66
+ /**
67
+ * Whether the backend is available in the current environment.
68
+ * It supports checking synchronously and asynchronously
69
+ *
70
+ * Returns 'true' if this backend is available in the current
71
+ * environment. For example, a backend using a browser API will return
72
+ * 'false' if the API is unavailable
73
+ *
74
+ */
75
+ isAvailable(): boolean | Promise<boolean>;
76
+ }
77
+
78
+ /**
79
+ * Gets the options type of a backend
80
+ * @internal
81
+ */
82
+ export type OptionsOf<T extends Backend> = T extends Backend<FileSystem, infer TOptions> ? TOptions : never;
83
+
84
+ /**
85
+ * Gets the FileSystem type for a backend
86
+ * @internal
87
+ */
88
+ export type FilesystemOf<T extends Backend> = T extends Backend<infer FS> ? FS : never;
89
+
90
+ /** @internal */
91
+ export function isBackend(arg: unknown): arg is Backend {
92
+ return arg != null && typeof arg == 'object' && 'isAvailable' in arg && typeof arg.isAvailable == 'function' && 'create' in arg && typeof arg.create == 'function';
93
+ }
94
+
95
+ /**
96
+ * Checks that `options` object is valid for the file system options.
97
+ * @internal
98
+ */
99
+ export async function checkOptions<T extends Backend>(backend: T, options: Record<string, unknown>): Promise<void> {
100
+ if (typeof options != 'object' || options === null) {
101
+ throw new ErrnoError(Errno.EINVAL, 'Invalid options');
102
+ }
103
+
104
+ // Check for required options.
105
+ for (const [optName, opt] of Object.entries(backend.options) as Entries<OptionsConfig<Record<string, any>>>) {
106
+ const providedValue = options?.[optName];
107
+
108
+ if (providedValue === undefined || providedValue === null) {
109
+ if (!opt.required) {
110
+ continue;
111
+ }
112
+ /* Required option not provided.
113
+ if any incorrect options provided, which ones are close to the provided one?
114
+ (edit distance 5 === close)*/
115
+ const incorrectOptions = Object.keys(options)
116
+ .filter(o => !(o in backend.options))
117
+ .map((a: string) => {
118
+ return { str: a, distance: levenshtein(optName, a) };
119
+ })
120
+ .filter(o => o.distance < 5)
121
+ .sort((a, b) => a.distance - b.distance);
122
+
123
+ throw new ErrnoError(
124
+ Errno.EINVAL,
125
+ `${backend.name}: Required option '${optName}' not provided.${
126
+ incorrectOptions.length > 0 ? ` You provided '${incorrectOptions[0].str}', did you mean '${optName}'.` : ''
127
+ }`
128
+ );
129
+ }
130
+ // Option provided, check type.
131
+ const typeMatches = Array.isArray(opt.type) ? opt.type.indexOf(typeof providedValue) != -1 : typeof providedValue == opt.type;
132
+ if (!typeMatches) {
133
+ throw new ErrnoError(
134
+ Errno.EINVAL,
135
+ `${backend.name}: Value provided for option ${optName} is not the proper type. Expected ${
136
+ Array.isArray(opt.type) ? `one of {${opt.type.join(', ')}}` : (opt.type as string)
137
+ }, but received ${typeof providedValue}`
138
+ );
139
+ }
140
+
141
+ if (opt.validator) {
142
+ await opt.validator(providedValue);
143
+ }
144
+ // Otherwise: All good!
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Specifies a file system backend type and its options.
150
+ *
151
+ * Individual options can recursively contain BackendConfiguration objects for values that require file systems.
152
+ *
153
+ * The configuration for each file system corresponds to that file system's option object passed to its `create()` method.
154
+ */
155
+ export type BackendConfiguration<T extends Backend> = OptionsOf<T> & Partial<SharedConfig> & { backend: T };
156
+
157
+ /** @internal */
158
+ export function isBackendConfig<T extends Backend>(arg: unknown): arg is BackendConfiguration<T> {
159
+ return arg != null && typeof arg == 'object' && 'backend' in arg && isBackend(arg.backend);
160
+ }