@zenfs/core 2.6.3 → 2.6.5

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 (47) hide show
  1. package/README.md +1 -1
  2. package/dist/backends/cow.js +11 -3
  3. package/dist/internal/file_index.d.ts +2 -0
  4. package/dist/internal/file_index.js +10 -2
  5. package/dist/polyfills.js +4 -1
  6. package/package.json +11 -11
  7. package/scripts/test.js +15 -8
  8. package/tests/backend/single-buffer.test.ts +2 -2
  9. package/tests/backend/single-buffer.worker.js +1 -1
  10. package/tests/common/file_index.test.ts +51 -0
  11. package/tests/common/glob.test.ts +1 -1
  12. package/tests/common/inode.test.ts +1 -1
  13. package/tests/common/utf8stream.test.ts +1 -1
  14. package/tests/common.ts +4 -2
  15. package/tests/fetch/fetch.ts +1 -1
  16. package/tests/fetch/run.sh +1 -1
  17. package/tests/fs/append.test.ts +1 -1
  18. package/tests/fs/concurrency.test.ts +1 -1
  19. package/tests/fs/dir.test.ts +1 -1
  20. package/tests/fs/directory.test.ts +3 -3
  21. package/tests/fs/errors.test.ts +1 -1
  22. package/tests/fs/exists.test.ts +3 -3
  23. package/tests/fs/links.test.ts +5 -5
  24. package/tests/fs/open.test.ts +5 -5
  25. package/tests/fs/permissions.test.ts +5 -5
  26. package/tests/fs/read.test.ts +3 -3
  27. package/tests/fs/readFile.test.ts +2 -2
  28. package/tests/fs/rename.test.ts +5 -5
  29. package/tests/fs/scaling.test.ts +1 -1
  30. package/tests/fs/stat.test.ts +2 -2
  31. package/tests/fs/streams.test.ts +1 -1
  32. package/tests/fs/tempdir.test.ts +1 -1
  33. package/tests/fs/times.test.ts +2 -2
  34. package/tests/fs/truncate.test.ts +1 -1
  35. package/tests/fs/watch.test.ts +1 -1
  36. package/tests/fs/write.test.ts +2 -2
  37. package/tests/fs/xattr.test.ts +1 -1
  38. package/tests/{native/setup.ts → native-setup.ts} +5 -5
  39. package/tests/setup/context.ts +1 -1
  40. package/tests/setup/cow.ts +1 -1
  41. package/tests/setup/index.ts +1 -1
  42. package/tests/setup/memory.ts +1 -1
  43. package/tests/setup/port.ts +1 -1
  44. package/tests/setup/single-buffer.ts +1 -1
  45. package/tests/setup.ts +1 -1
  46. package/tests/tsconfig.json +2 -1
  47. package/tests/native/run.sh +0 -3
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ZenFS is a cross-platform library that emulates the [Node.js filesystem API](http://nodejs.org/api/fs.html).
4
4
  It works using a system of backends, which are used by ZenFS to store and retrieve data.
5
- ZenFS can also integrate with other tools.
5
+ ZenFS should cover the full API surface of the latest Node.js version, though complex changes may lag a little bit.
6
6
 
7
7
  ## Backends
8
8
 
@@ -116,11 +116,19 @@ export class CopyOnWriteFS extends FileSystem {
116
116
  _nextIno = 1;
117
117
  /** Moves the public inode numbers for a rename, including a directory's descendants */
118
118
  renameInos(oldPath, newPath) {
119
- for (const [path, ino] of [...this.inos]) {
119
+ const changes = [];
120
+ for (const [path, ino] of this.inos) {
120
121
  if (path != oldPath && !path.startsWith(oldPath + '/'))
121
122
  continue;
122
- this.inos.delete(path);
123
- this.inos.set(newPath + path.slice(oldPath.length), ino);
123
+ changes.push({
124
+ path,
125
+ add: newPath + path.slice(oldPath.length),
126
+ ino,
127
+ });
128
+ }
129
+ for (const change of changes) {
130
+ this.inos.delete(change.path);
131
+ this.inos.set(change.add, change.ino);
124
132
  }
125
133
  }
126
134
  /** Replaces the layer's inode number with the public one for `path` */
@@ -17,6 +17,7 @@ export declare const version = 1;
17
17
  * @internal
18
18
  */
19
19
  export declare class Index extends Map<string, Inode> {
20
+ #private;
20
21
  maxSize: number;
21
22
  /**
22
23
  * Converts the index to JSON
@@ -26,6 +27,7 @@ export declare class Index extends Map<string, Inode> {
26
27
  * Converts the index to a string
27
28
  */
28
29
  toString(): string;
30
+ clear(): void;
29
31
  /**
30
32
  * Get the size in bytes of the index (including the size reported for each entry)
31
33
  */
@@ -21,7 +21,8 @@ export class Index extends Map {
21
21
  return {
22
22
  version,
23
23
  maxSize: this.maxSize,
24
- entries: Object.fromEntries([...this].map(([k, v]) => [k, v.toJSON()])),
24
+ // @todo use `Iterator.map` #296
25
+ entries: Object.fromEntries(Array.from(this).map(([k, v]) => [k, v.toJSON()])),
25
26
  };
26
27
  }
27
28
  /**
@@ -30,6 +31,10 @@ export class Index extends Map {
30
31
  toString() {
31
32
  return JSON.stringify(this.toJSON());
32
33
  }
34
+ clear() {
35
+ super.clear();
36
+ this.#lastAlloc = 0;
37
+ }
33
38
  /**
34
39
  * Get the size in bytes of the index (including the size reported for each entry)
35
40
  */
@@ -74,12 +79,15 @@ export class Index extends Map {
74
79
  }
75
80
  return entries;
76
81
  }
82
+ #lastAlloc = 0;
77
83
  /**
78
84
  * Get the next available ID in the index
79
85
  * @internal
80
86
  */
81
87
  _alloc() {
82
- return Math.max(...[...this.values()].flatMap(i => [i.ino, i.data])) + 1;
88
+ // @todo use `Iterator.flatMap` #296
89
+ this.#lastAlloc ||= Array.from(this.values()).reduce((max, inode) => Math.max(max, inode.ino, inode.data), 0);
90
+ return ++this.#lastAlloc;
83
91
  }
84
92
  /**
85
93
  * Gets a list of entries for each directory in the index.
package/dist/polyfills.js CHANGED
@@ -43,7 +43,10 @@ Uint8Array.fromBase64 ??=
43
43
  Uint8Array.prototype.toHex ??=
44
44
  (warn('Using a polyfill of Uint8Array.prototype.toHex'),
45
45
  function toHex() {
46
- return [...this].map(b => b.toString(16).padStart(2, '0')).join('');
46
+ // @todo use `Iterator.map` #296
47
+ return Array.from(this)
48
+ .map(b => b.toString(16).padStart(2, '0'))
49
+ .join('');
47
50
  });
48
51
  Uint8Array.fromHex ??=
49
52
  (warn('Using a polyfill of Uint8Array.fromHex'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "2.6.3",
3
+ "version": "2.6.5",
4
4
  "description": "A filesystem, anywhere",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -11,7 +11,9 @@
11
11
  "keywords": [
12
12
  "filesystem",
13
13
  "node",
14
- "storage"
14
+ "storage",
15
+ "fs",
16
+ "browser"
15
17
  ],
16
18
  "bin": {
17
19
  "make-index": "scripts/make-index.js",
@@ -44,14 +46,15 @@
44
46
  },
45
47
  "exports": {
46
48
  ".": "./dist/index.js",
47
- "./*": "./dist/*",
49
+ "./*.js": "./dist/*.js",
50
+ "./*": "./dist/*.js",
48
51
  "./emulation/*": "./dist/node/*",
49
52
  "./promises": "./dist/node/promises.js",
50
53
  "./readline": "./dist/node/readline.js",
51
- "./constants": "./dist/constants.js",
52
- "./path": "./dist/path.js",
53
54
  "./eslint": "./eslint.shared.js",
54
- "./tests/*": "./tests/*",
55
+ "./tests/*.ts": "./tests/*.ts",
56
+ "./tests/*.js": "./tests/*.js",
57
+ "./tests/*": "./tests/*.js",
55
58
  "./types/*": "./types/*"
56
59
  },
57
60
  "publishConfig": {
@@ -64,7 +67,8 @@
64
67
  "spdx:check": "npx lice -a src tests/*.ts tests/**/*.test.ts",
65
68
  "spdx:fix": "npx lice -aw src tests/*.ts tests/**/*.test.ts",
66
69
  "lint": "eslint src tests",
67
- "test": "npx --silent zenfs-test --clean; npx --silent zenfs-test -abcp; tests/fetch/run.sh; npx --silent zenfs-test --report",
70
+ "test": "npx -s zenfs-test --clean; npx -s zenfs-test -abcpn; tests/fetch/run.sh; npx -s zenfs-test --report-only",
71
+ "test:native": "npx -s zenfs-test tests/native-setup.ts --verbose",
68
72
  "build": "tsc -p tsconfig.json",
69
73
  "build:docs": "typedoc",
70
74
  "dev": "tsc -p tsconfig.json --watch",
@@ -86,12 +90,8 @@
86
90
  "eslint": "^10.1.0",
87
91
  "globals": "^17.3.0",
88
92
  "prettier": "^3.2.5",
89
- "tsx": "^4.19.1",
90
93
  "typedoc": "^0.28.18",
91
94
  "typescript": "^6.0.0",
92
95
  "typescript-eslint": "^8.58.0"
93
- },
94
- "allowScripts": {
95
- "esbuild": true
96
96
  }
97
97
  }
package/scripts/test.js CHANGED
@@ -26,11 +26,14 @@ const { values: options, positionals } = parseArgs({
26
26
  skip: { short: 's', type: 'string', multiple: true, default: [] },
27
27
  'exit-on-fail': { short: 'e', type: 'boolean' },
28
28
  runs: { short: 'r', type: 'string', default: '1' },
29
+ // @todo [breaking] make using node the default and remove tsx completely
30
+ node: { short: 'n', type: 'boolean', default: false },
29
31
 
30
32
  // Coverage and performance
31
33
  coverage: { type: 'string', default: 'tests/.coverage' },
32
34
  preserve: { short: 'p', type: 'boolean' },
33
35
  report: { type: 'boolean', default: false },
36
+ 'report-only': { type: 'boolean', default: false },
34
37
  clean: { type: 'boolean', default: false },
35
38
  profile: { type: 'boolean', default: false },
36
39
  },
@@ -42,7 +45,8 @@ const { values: options, positionals } = parseArgs({
42
45
  * @param {...any} args
43
46
  */
44
47
  function debug(...args) {
45
- if (options.debug) console.debug(styleText('dim', '[debug]'), ...args.map(a => (typeof a === 'string' ? styleText('dim', a) : a)));
48
+ if (options.debug || process.env.DEBUG)
49
+ console.debug(styleText('dim', '[debug]'), ...args.map(a => (typeof a === 'string' ? styleText('dim', a) : a)));
46
50
  }
47
51
 
48
52
  if (options.help) {
@@ -61,6 +65,7 @@ Behavior:
61
65
  -I, --inspect Use the inspector for debugging
62
66
  -s, --skip <pattern> Skip tests with names matching the given pattern. Can be specified multiple times.
63
67
  -d, --debug Output debug messages from the test runner
68
+ -n, --node Use plain node instead of tsx. Requires use of erasable TS syntax only
64
69
 
65
70
  Output:
66
71
  -h, --help Outputs this help message
@@ -74,9 +79,10 @@ Output:
74
79
  Coverage:
75
80
  --coverage <dir> Override the default coverage data directory
76
81
  -p, --preserve Do not delete or report coverage data
77
- --report ONLY report coverage
82
+ --report Report coverage along with test results
83
+ --report-only ONLY report coverage
78
84
  --clean ONLY clean up coverage directory
79
- --profile Record performance profiles`);
85
+ --profile Record performance profiles`);
80
86
  process.exit();
81
87
  }
82
88
 
@@ -105,12 +111,13 @@ function report() {
105
111
  }
106
112
  }
107
113
 
108
- if (options.report) {
114
+ if (options['report-only']) {
109
115
  report();
110
116
  process.exit();
111
117
  }
112
118
 
113
- const nRuns = Number.isSafeInteger(parseInt(options.runs)) ? parseInt(options.runs) : 1;
119
+ const nRuns = Number.isSafeInteger(parseInt(options.runs)) ? parseInt(options.runs) : 1,
120
+ nodeCommand = options.node ? 'node' : 'tsx';
114
121
 
115
122
  /** @type {typeof import('./ci.js')} */
116
123
  let ci;
@@ -175,7 +182,7 @@ function makeCommand(profileName, ...rest) {
175
182
  ]
176
183
  .filter(v => v)
177
184
  .filter(v => typeof v == 'string');
178
- if (!options.quiet) debug('command: tsx', command.join(' '));
185
+ if (!options.quiet) debug('command: ' + nodeCommand, command.join(' '));
179
186
 
180
187
  return command;
181
188
  }
@@ -235,7 +242,7 @@ async function runTests(config) {
235
242
  }
236
243
 
237
244
  try {
238
- execFileSync('tsx', command, {
245
+ execFileSync(nodeCommand, command, {
239
246
  stdio: ['ignore', options.verbose ? 'inherit' : 'ignore', 'inherit'],
240
247
  });
241
248
  if (!options.quiet) console.log(`${styleText('green', 'passed')}${identText} ${time()}`);
@@ -283,4 +290,4 @@ for (const setupFile of positionals) {
283
290
  });
284
291
  }
285
292
 
286
- if (!options.preserve) report();
293
+ if (!options.preserve && options.report) report();
@@ -1,11 +1,11 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
- import assert from 'node:assert';
2
+ import assert from 'node:assert/strict';
3
3
  import { randomBytes } from 'node:crypto';
4
4
  import { suite, test } from 'node:test';
5
5
  import { Worker } from 'worker_threads';
6
6
  import { sizeof } from 'memium';
7
7
  import { fs, mount, resolveMountConfig, SingleBuffer, vfs } from '@zenfs/core';
8
- import { MetadataBlock, SuperBlock } from '@zenfs/core/backends/single_buffer.js';
8
+ import { MetadataBlock, SuperBlock } from '@zenfs/core/backends/single_buffer';
9
9
  import { setupLogs } from '../logs.js';
10
10
 
11
11
  setupLogs();
@@ -1,4 +1,4 @@
1
- import assert from 'node:assert';
1
+ import assert from 'node:assert/strict';
2
2
  import { writeFileSync as _write } from 'node:fs';
3
3
  import { parentPort, workerData } from 'node:worker_threads';
4
4
  import { configureSingle, fs, SingleBuffer } from '../../dist/index.js';
@@ -0,0 +1,51 @@
1
+ // SPDX-License-Identifier: LGPL-3.0-or-later
2
+ import { Index, Inode } from '@zenfs/core';
3
+ import assert from 'node:assert/strict';
4
+ import { suite, test } from 'node:test';
5
+
6
+ /** Build an index with `n` file entries, using IDs 2..2n+1 */
7
+ function buildIndex(n: number): Index {
8
+ const index = new Index();
9
+ index.set('/', new Inode({ ino: 0, data: 1, mode: 0o40755, nlink: 1 }));
10
+ for (let i = 0; i < n; i++) {
11
+ index.set(`/f${i}`, new Inode({ ino: 2 * i + 2, data: 2 * i + 3, mode: 0o100644, nlink: 1 }));
12
+ }
13
+ return index;
14
+ }
15
+
16
+ /** Calls `fn` with `depth` extra frames on the stack */
17
+ function atDepth<T>(depth: number, fn: () => T): T {
18
+ return depth > 0 ? atDepth(depth - 1, fn) : fn();
19
+ }
20
+
21
+ suite('Index', () => {
22
+ test('_alloc returns an unused ID', () => {
23
+ const index = buildIndex(10);
24
+ assert.equal(index._alloc(), 22);
25
+ });
26
+
27
+ test('_alloc does not re-use IDs', () => {
28
+ const index = buildIndex(10);
29
+ const ids = new Set<number>();
30
+
31
+ for (let i = 0; i < 100; i++) {
32
+ const id = index._alloc();
33
+ assert.equal(index.getByID(id), undefined, `\`_alloc\` returned in-use ID ${id}`);
34
+ assert(!ids.has(id), `\`_alloc\` returned duplicate ID ${id}`);
35
+ ids.add(id);
36
+ }
37
+ });
38
+
39
+ test('_alloc works with a large index #312', () => {
40
+ const index = buildIndex(100_000);
41
+ assert.equal(index._alloc(), 200_002);
42
+ });
43
+
44
+ test('_alloc works with a large index and a deep stack #312', () => {
45
+ const index = buildIndex(60_000);
46
+ assert.equal(
47
+ atDepth(1000, () => index._alloc()),
48
+ 120_002
49
+ );
50
+ });
51
+ });
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import assert from 'node:assert/strict';
3
3
  import { suite, test } from 'node:test';
4
- import { fs } from '../common.js';
4
+ import { fs } from '../common.ts';
5
5
 
6
6
  // Set up a directory structure for glob tests
7
7
  fs.mkdirSync('/glob');
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import { Inode } from '@zenfs/core';
3
- import assert from 'node:assert';
3
+ import assert from 'node:assert/strict';
4
4
  import { suite, test } from 'node:test';
5
5
 
6
6
  suite('Inode manipulation', () => {
@@ -2,7 +2,7 @@
2
2
  import { Buffer } from 'buffer';
3
3
  import assert from 'node:assert/strict';
4
4
  import { suite, test } from 'node:test';
5
- import { config, fs } from '../common.js';
5
+ import { config, fs } from '../common.ts';
6
6
 
7
7
  suite('Utf8Stream', config('write'), () => {
8
8
  test('writes synchronously', config('sync'), () => {
package/tests/common.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import { fs as defaultFS } from '@zenfs/core';
3
- import type { NodeFS } from '@zenfs/core/node/types.js';
3
+ import type { NodeFS } from '@zenfs/core/node/types';
4
4
  import { join, resolve } from 'node:path';
5
5
  import { styleText } from 'node:util';
6
6
  import { setupLogs } from './logs.js';
@@ -41,7 +41,9 @@ export type TestFlag =
41
41
  | 'xattr'
42
42
  | 'times'
43
43
  | 'tempdir'
44
- | 'lchmod';
44
+ | 'lchmod'
45
+ | 'promises.exists'
46
+ | 'root';
45
47
 
46
48
  /**
47
49
  * Whether a backend supports a flag.
@@ -1,6 +1,6 @@
1
1
  import { configure, Fetch } from '@zenfs/core';
2
2
  import { log } from 'kerium';
3
- import type { TestFlag, TestFlagState } from '../common.js';
3
+ import type { TestFlag, TestFlagState } from '../common.ts';
4
4
  import { baseUrl } from './config.js';
5
5
 
6
6
  await configure({
@@ -12,6 +12,6 @@ until nc -z localhost 26514; do
12
12
  sleep 0.25
13
13
  done
14
14
 
15
- npx --silent zenfs-test $SCRIPT_DIR/fetch.ts --preserve --force "$@"
15
+ npx --silent zenfs-test $SCRIPT_DIR/fetch.ts --preserve --force --node "$@"
16
16
 
17
17
  kill $PID
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import assert from 'node:assert/strict';
3
3
  import { suite, test } from 'node:test';
4
- import { fs, config } from '../common.js';
4
+ import { fs, config } from '../common.ts';
5
5
 
6
6
  const content = 'Sample content',
7
7
  original = 'ABCD';
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import assert from 'node:assert/strict';
3
3
  import { suite, test } from 'node:test';
4
- import { config, fs } from '../common.js';
4
+ import { config, fs } from '../common.ts';
5
5
 
6
6
  suite('Concurrency', config('write', 'async'), () => {
7
7
  test('parallel writeFile in the same directory does not lose entries #256', async () => {
@@ -2,7 +2,7 @@
2
2
  import { sync, type Dirent } from '@zenfs/core';
3
3
  import assert, { rejects } from 'node:assert/strict';
4
4
  import { suite, test } from 'node:test';
5
- import { config, fs } from '../common.js';
5
+ import { config, fs } from '../common.ts';
6
6
 
7
7
  const testFile = 'test-file.txt';
8
8
  fs.writeFileSync(testFile, 'Sample content');
@@ -2,7 +2,7 @@
2
2
  import assert from 'node:assert/strict';
3
3
  import { join } from 'node:path/posix';
4
4
  import { suite, test } from 'node:test';
5
- import { config, fs } from '../common.js';
5
+ import { config, fs } from '../common.ts';
6
6
 
7
7
  const testDir = 'test-dir';
8
8
  const testFiles = ['file1.txt', 'file2.txt', 'file3.txt'];
@@ -22,7 +22,7 @@ for (const dir of testDirectories) {
22
22
  suite('Directories', config('directories'), () => {
23
23
  test('mkdir', config('async'), async () => {
24
24
  await fs.promises.mkdir('/one', 0o755);
25
- assert(await fs.promises.exists('/one'));
25
+ assert(await fs.promises.stat('/one', { throwIfNoEntry: false }));
26
26
  await assert.rejects(fs.promises.mkdir('/one', 0o755), { code: 'EEXIST' });
27
27
  });
28
28
 
@@ -30,7 +30,7 @@ suite('Directories', config('directories'), () => {
30
30
 
31
31
  test('mkdir, nested', config('async'), async () => {
32
32
  await assert.rejects(fs.promises.mkdir('/nested/dir'), { code: 'ENOENT', path: '/nested/dir' });
33
- assert(!(await fs.promises.exists('/nested/dir')));
33
+ assert(!(await fs.promises.stat('/nested/dir', { throwIfNoEntry: false })));
34
34
  });
35
35
 
36
36
  test('mkdir, recursive', config('async'), async () => {
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import assert from 'node:assert/strict';
3
3
  import test, { suite } from 'node:test';
4
- import { config, fs } from '../common.js';
4
+ import { config, fs } from '../common.ts';
5
5
 
6
6
  const existingFile = '/exit.js';
7
7
  const path = '/non-existent';
@@ -1,17 +1,17 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import assert from 'node:assert/strict';
3
3
  import { suite, test } from 'node:test';
4
- import { config, fs } from '../common.js';
4
+ import { config, fs } from '../common.ts';
5
5
 
6
6
  suite('exists', () => {
7
7
  const f = 'x.txt';
8
8
 
9
- test('return true for an existing file', config('async'), async () => {
9
+ test('return true for an existing file', config('async', 'promises.exists'), async () => {
10
10
  const exists = await fs.promises.exists(f);
11
11
  assert(exists);
12
12
  });
13
13
 
14
- test('return false for a non-existent file', config('async'), async () => {
14
+ test('return false for a non-existent file', config('async', 'promises.exists'), async () => {
15
15
  const exists = await fs.promises.exists(f + '-NO');
16
16
  assert(!exists);
17
17
  });
@@ -2,7 +2,7 @@
2
2
  import { join } from '@zenfs/core/path';
3
3
  import assert from 'node:assert/strict';
4
4
  import { suite, test } from 'node:test';
5
- import { config, fs } from '../common.js';
5
+ import { config, fs } from '../common.ts';
6
6
 
7
7
  suite('Links', config('symlinks'), () => {
8
8
  const target = '/a1.js',
@@ -64,8 +64,8 @@ suite('Links', config('symlinks'), () => {
64
64
 
65
65
  test('unlink', async () => {
66
66
  await fs.promises.unlink(symlink);
67
- assert(!(await fs.promises.exists(symlink)));
68
- assert(await fs.promises.exists(target));
67
+ assert(!(await fs.promises.stat(symlink, { throwIfNoEntry: false })));
68
+ assert(await fs.promises.stat(target, { throwIfNoEntry: false }));
69
69
  });
70
70
 
71
71
  test('link', config('links'), async () => {
@@ -99,7 +99,7 @@ suite('Links', config('symlinks'), () => {
99
99
  // A link pointing at a directory satisfies a recursive mkdir and is left as a link
100
100
  assert.equal(await fs.promises.mkdir('/mkdir-link', { recursive: true }), undefined);
101
101
  assert((await fs.promises.lstat('/mkdir-link')).isSymbolicLink());
102
- assert(!(await fs.promises.exists('/mkdir-missing')));
102
+ assert(!(await fs.promises.stat('/mkdir-missing', { throwIfNoEntry: false })));
103
103
  });
104
104
 
105
105
  test('mkdir follows symlinks for intermediate components', config('async'), async () => {
@@ -113,7 +113,7 @@ suite('Links', config('symlinks'), () => {
113
113
 
114
114
  // Node reports the link itself here, unlike `mkdirSync` which reports the requested path with ENOENT
115
115
  await assert.rejects(fs.promises.mkdir('/mkdir-dangling/x', { recursive: true }), { code: 'ENOTDIR', path: '/mkdir-dangling' });
116
- assert(!(await fs.promises.exists('/mkdir-missing')));
116
+ assert(!(await fs.promises.stat('/mkdir-missing', { throwIfNoEntry: false })));
117
117
  });
118
118
 
119
119
  test('mkdirSync does not follow a symlink at the final component', config('sync'), () => {
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import assert from 'node:assert/strict';
3
3
  import { suite, test } from 'node:test';
4
- import { config, fs } from '../common.js';
4
+ import { config, fs } from '../common.ts';
5
5
 
6
6
  suite('Opening files', () => {
7
7
  const filename = 'a.js';
@@ -12,12 +12,12 @@ suite('Opening files', () => {
12
12
  });
13
13
 
14
14
  test('open file with mode "r"', config('async'), async () => {
15
- const { fd } = await fs.promises.open(filename, 'r');
16
- assert(fd >= -Infinity);
15
+ await using handle = await fs.promises.open(filename, 'r');
16
+ assert(handle.fd >= -Infinity);
17
17
  });
18
18
 
19
19
  test('open file with mode "rs"', config('async'), async () => {
20
- const { fd } = await fs.promises.open(filename, 'rs');
21
- assert(fd >= -Infinity);
20
+ await using handle = await fs.promises.open(filename, 'rs');
21
+ assert(handle.fd >= -Infinity);
22
22
  });
23
23
  });
@@ -1,13 +1,13 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import { R_OK, W_OK, X_OK } from '@zenfs/core/constants';
3
- import { defaultContext } from '@zenfs/core/internal/contexts.js';
4
- import { hasAccess } from '@zenfs/core/internal/inode.js';
3
+ import { defaultContext } from '@zenfs/core/internal/contexts';
4
+ import { hasAccess } from '@zenfs/core/internal/inode';
5
5
  import { join } from '@zenfs/core/path';
6
6
  import type { Exception } from 'kerium';
7
7
  import assert from 'node:assert/strict';
8
8
  import { suite, test } from 'node:test';
9
9
  import { encodeUTF8 } from 'utilium';
10
- import { config, fs } from '../common.js';
10
+ import { config, fs } from '../common.ts';
11
11
 
12
12
  const asyncMode = 0o777;
13
13
  const syncMode = 0o644;
@@ -25,7 +25,7 @@ suite('Permissions', config('permissions'), () => {
25
25
  });
26
26
 
27
27
  test('fchmod', config('sync', 'async'), async () => {
28
- const handle = await fs.promises.open(file, 'a', 0o644);
28
+ await using handle = await fs.promises.open(file, 'a', 0o644);
29
29
 
30
30
  await handle.chmod(asyncMode);
31
31
  const stats = await handle.stat();
@@ -58,7 +58,7 @@ suite('Permissions', config('permissions'), () => {
58
58
 
59
59
  function checkError(access: number) {
60
60
  return function (error: Exception) {
61
- assert(error instanceof Error);
61
+ assert(Error.isError(error));
62
62
  assert(!hasAccess(defaultContext, stats!, access));
63
63
  };
64
64
  }
@@ -5,7 +5,7 @@ import assert from 'node:assert/strict';
5
5
  import type { OpenMode, PathLike } from 'node:fs';
6
6
  import { suite, test } from 'node:test';
7
7
  import { promisify } from 'node:util';
8
- import { config, fs } from '../common.js';
8
+ import { config, fs } from '../common.ts';
9
9
 
10
10
  const filepath = 'x.txt';
11
11
  const expected = 'xyz\n';
@@ -13,7 +13,7 @@ const ellipses = '…'.repeat(10_000);
13
13
 
14
14
  suite('read', () => {
15
15
  test('read file asynchronously', config('async'), async () => {
16
- const handle = await fs.promises.open(filepath, 'r');
16
+ await using handle = await fs.promises.open(filepath, 'r');
17
17
  const { bytesRead, buffer } = await handle.read(Buffer.alloc(expected.length), 0, expected.length, 0);
18
18
  assert.equal(bytesRead, expected.length);
19
19
  assert.equal(buffer.toString(), expected);
@@ -46,7 +46,7 @@ suite('read', () => {
46
46
  const bufferSync = Buffer.alloc(expected.length);
47
47
 
48
48
  test('read file from handle asynchronously', config('async'), async () => {
49
- const handle = await fs.promises.open(filepath, 'r');
49
+ await using handle = await fs.promises.open(filepath, 'r');
50
50
  const { bytesRead } = await handle.read(bufferAsync, 0, expected.length, 0);
51
51
 
52
52
  assert.equal(bytesRead, expected.length);
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import assert from 'node:assert/strict';
3
3
  import { suite, test } from 'node:test';
4
- import { config, fs } from '../common.js';
4
+ import { config, fs } from '../common.ts';
5
5
 
6
6
  suite('Reading', () => {
7
7
  test('Cannot read a file with an invalid encoding', config('sync'), () => {
@@ -9,7 +9,7 @@ suite('Reading', () => {
9
9
  });
10
10
 
11
11
  test('Reading past the end of a file should not be an error', config('async'), async () => {
12
- const handle = await fs.promises.open('a.js', 'r');
12
+ await using handle = await fs.promises.open('a.js', 'r');
13
13
  const { bytesRead } = await handle.read(new Uint8Array(10), 0, 10, 10000);
14
14
  assert.equal(bytesRead, 0);
15
15
  });
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import assert from 'node:assert/strict';
3
3
  import { suite, test } from 'node:test';
4
- import { config, fs } from '../common.js';
4
+ import { config, fs } from '../common.ts';
5
5
 
6
6
  suite('Rename', config('rename'), () => {
7
7
  /**
@@ -26,8 +26,8 @@ suite('Rename', config('rename'), () => {
26
26
  const subContents = await fs.promises.readdir(dir + '/_rename_me');
27
27
  assert.equal(subContents.length, 1);
28
28
 
29
- assert(await fs.promises.exists(dir + '/file.dat'));
30
- assert(await fs.promises.exists(dir + '/_rename_me/lol.txt'));
29
+ assert(await fs.promises.stat(dir + '/file.dat', { throwIfNoEntry: false }));
30
+ assert(await fs.promises.stat(dir + '/_rename_me/lol.txt', { throwIfNoEntry: false }));
31
31
  }
32
32
 
33
33
  test('rename directory', config('async'), async () => {
@@ -46,7 +46,7 @@ suite('Rename', config('rename'), () => {
46
46
 
47
47
  await check_directory(newDir);
48
48
 
49
- assert(!(await fs.promises.exists(oldDir)));
49
+ assert(!(await fs.promises.stat(oldDir, { throwIfNoEntry: false })));
50
50
 
51
51
  await fs.promises.mkdir(oldDir);
52
52
  await populate(oldDir);
@@ -67,7 +67,7 @@ suite('Rename', config('rename'), () => {
67
67
  await fs.promises.rename(one, two);
68
68
 
69
69
  assert.equal(await fs.promises.readFile(two, 'utf8'), 'hey');
70
- assert(!(await fs.promises.exists(one)));
70
+ assert(!(await fs.promises.stat(one, { throwIfNoEntry: false })));
71
71
  });
72
72
 
73
73
  test('File to Directory and Directory to File Rename', config('sync', 'async'), async () => {
@@ -2,7 +2,7 @@
2
2
  import { sync } from '@zenfs/core';
3
3
  import assert from 'node:assert/strict';
4
4
  import { suite, test } from 'node:test';
5
- import { config, fs } from '../common.js';
5
+ import { config, fs } from '../common.ts';
6
6
 
7
7
  const n_files = 130;
8
8
  const huge_size = 0x1000000;
@@ -2,7 +2,7 @@
2
2
  import { defaultContext, hasAccess, type Stats } from '@zenfs/core';
3
3
  import assert from 'node:assert/strict';
4
4
  import { suite, test } from 'node:test';
5
- import { config, fs } from '../common.js';
5
+ import { config, fs } from '../common.ts';
6
6
 
7
7
  suite('Stats', () => {
8
8
  const existing_file = 'x.txt';
@@ -35,7 +35,7 @@ suite('Stats', () => {
35
35
  fs.close(fd);
36
36
  });
37
37
 
38
- test('hasAccess for non-root access', config('sync', 'permissions'), () => {
38
+ test('hasAccess for non-root access', config('sync', 'permissions', 'root'), () => {
39
39
  const newFile = 'new.txt';
40
40
 
41
41
  fs.writeFileSync(newFile, 'hello', { mode: 0o640 });
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import assert from 'node:assert/strict';
3
3
  import { suite, test } from 'node:test';
4
- import { config, fs } from '../common.js';
4
+ import { config, fs } from '../common.ts';
5
5
 
6
6
  // Top-level initialization
7
7
  const testFilePath = 'test-file.txt';
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import { test, suite } from 'node:test';
3
- import { config, fs } from '../common.js';
3
+ import { config, fs } from '../common.ts';
4
4
  import assert from 'node:assert/strict';
5
5
  import { basename } from 'node:path/posix';
6
6
 
@@ -4,7 +4,7 @@ import type { Exception } from 'kerium';
4
4
  import assert from 'node:assert/strict';
5
5
  import { suite, test } from 'node:test';
6
6
  import { wait } from 'utilium';
7
- import { config, fs } from '../common.js';
7
+ import { config, fs } from '../common.ts';
8
8
 
9
9
  const path = 'x.txt';
10
10
 
@@ -33,7 +33,7 @@ suite('Times', config('times'), () => {
33
33
  assert.deepEqual(unixTimestamps(await fs.promises.stat(path)), times);
34
34
 
35
35
  await fs.promises.utimes('foobarbaz', atime, mtime).catch((error: Exception) => {
36
- assert(error instanceof Error);
36
+ assert(Error.isError(error));
37
37
  assert.equal(error.code, 'ENOENT');
38
38
  });
39
39
 
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import assert from 'node:assert/strict';
3
3
  import { suite, test } from 'node:test';
4
- import { config, fs } from '../common.js';
4
+ import { config, fs } from '../common.ts';
5
5
 
6
6
  const path: string = 'truncate-file.txt',
7
7
  size = 1024 * 16,
@@ -2,7 +2,7 @@
2
2
  import type { Stats } from '@zenfs/core';
3
3
  import assert from 'node:assert/strict';
4
4
  import { suite, test } from 'node:test';
5
- import { config, fs } from '../common.js';
5
+ import { config, fs } from '../common.ts';
6
6
 
7
7
  const testDir = '/test-watch-dir';
8
8
  const testFile = testDir + '/test.txt';
@@ -3,8 +3,8 @@ import assert from 'node:assert/strict';
3
3
  import { readFileSync } from 'node:fs';
4
4
  import { join } from 'node:path/posix';
5
5
  import { suite, test } from 'node:test';
6
- import { config, fs } from '../common.js';
7
- import { data as dataPath } from '../setup.js';
6
+ import { config, fs } from '../common.ts';
7
+ import { data as dataPath } from '../setup.ts';
8
8
 
9
9
  const fileName = 'write.txt';
10
10
  const utf8example = readFileSync(join(dataPath, 'utf8.txt'), 'utf8');
@@ -2,7 +2,7 @@
2
2
  import { Buffer } from 'buffer';
3
3
  import assert from 'node:assert/strict';
4
4
  import { suite, test } from 'node:test';
5
- import { config, fs } from '../common.js';
5
+ import { config, fs } from '../common.ts';
6
6
 
7
7
  suite('Extended Attributes', config('xattr'), () => {
8
8
  const testFile = 'xattr-test.txt';
@@ -2,11 +2,12 @@
2
2
  /* Run the FS test suite against native node:fs.
3
3
  Since the tests use absolute paths, everything is contained to a scratch directory using chroot-style path mapping. */
4
4
  import * as native from 'node:fs';
5
- import type { TestFlag, TestFlagState } from '../common.js';
5
+ import type { TestFlag, TestFlagState } from './common.ts';
6
6
  import { isAbsolute, join, relative, resolve } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
- import { copySync, data, tmp } from '../setup.js';
8
+ import { copySync, data, tmp } from './setup.ts';
9
9
  import { styleText } from 'node:util';
10
+ import { isRoot } from 'utilium/node';
10
11
 
11
12
  // The tests assume modes are used as-is, but native fs applies the process umask
12
13
  process.umask(0);
@@ -205,9 +206,6 @@ for (const [key, value] of Object.entries(native.promises)) {
205
206
  promises.watch = (...args: Parameters<typeof native.promises.watch>) =>
206
207
  native.promises.watch(...(mapArgs('watch', args) as Parameters<typeof native.promises.watch>));
207
208
 
208
- // ZenFS API extension
209
- promises.exists = (path: PathLike) => Promise.resolve(native.existsSync(toNative(path)));
210
-
211
209
  wrapped.promises = promises;
212
210
 
213
211
  export const fs = wrapped as unknown as typeof native;
@@ -220,6 +218,8 @@ export const flags: Partial<Record<TestFlag, TestFlagState>> = {
220
218
  lchmod: process.platform == 'darwin',
221
219
  // The suite uses ZenFS' own extended attribute API, which `node:fs` does not have
222
220
  xattr: false,
221
+ 'promises.exists': false,
222
+ root: isRoot || 'skip',
223
223
  };
224
224
 
225
225
  copySync(data, fs);
@@ -1,5 +1,5 @@
1
1
  import { fs as _fs, bindContext } from '@zenfs/core';
2
- import { copySync, data } from '../setup.js';
2
+ import { copySync, data } from '../setup.ts';
3
3
 
4
4
  _fs.mkdirSync('/new_root');
5
5
 
@@ -1,5 +1,5 @@
1
1
  import { configureSingle, CopyOnWrite, fs, InMemory, resolveMountConfig } from '@zenfs/core';
2
- import { copySync, data } from '../setup.js';
2
+ import { copySync, data } from '../setup.ts';
3
3
 
4
4
  fs.umount('/');
5
5
  const readable = await resolveMountConfig({ backend: InMemory, label: 'ro' });
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path/posix';
3
3
  import { configureSingle, CopyOnWrite, InMemory, InMemoryStore, mounts, Readonly, StoreFS } from '@zenfs/core';
4
4
  import { S_IFDIR } from '@zenfs/core/constants';
5
- import { copySync, data } from '../setup.js';
5
+ import { copySync, data } from '../setup.ts';
6
6
 
7
7
  copySync(data);
8
8
 
@@ -1,3 +1,3 @@
1
- import { copySync, data } from '../setup.js';
1
+ import { copySync, data } from '../setup.ts';
2
2
 
3
3
  copySync(data);
@@ -1,7 +1,7 @@
1
1
  import { InMemory, Port, configureSingle, fs, resolveMountConfig, resolveRemoteMount, sync } from '@zenfs/core';
2
2
  import { after, afterEach } from 'node:test';
3
3
  import { MessageChannel } from 'node:worker_threads';
4
- import { copySync, data } from '../setup.js';
4
+ import { copySync, data } from '../setup.ts';
5
5
 
6
6
  const { port1: localPort, port2: remotePort } = new MessageChannel();
7
7
 
@@ -1,5 +1,5 @@
1
1
  import { SingleBuffer, configureSingle } from '@zenfs/core';
2
- import { copySync, data } from '../setup.js';
2
+ import { copySync, data } from '../setup.ts';
3
3
 
4
4
  await configureSingle({
5
5
  backend: SingleBuffer,
package/tests/setup.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import { fs as _fs } from '@zenfs/core';
3
+ import type { NodeFS } from '@zenfs/core/node/types';
3
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs';
4
5
  import { join, relative } from 'node:path';
5
- import type { NodeFS } from '@zenfs/core/node/types.js';
6
6
 
7
7
  export const data = join(import.meta.dirname, 'data');
8
8
 
@@ -7,7 +7,8 @@
7
7
  "esModuleInterop": true,
8
8
  "allowSyntheticDefaultImports": true,
9
9
  "allowJs": true,
10
- "rootDir": ".."
10
+ "rootDir": "..",
11
+ "allowImportingTsExtensions": true
11
12
  },
12
13
  "include": ["**/*.ts", "*.ts", "**/*.js", "../types/uint8array.d.ts"]
13
14
  }
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env bash
2
-
3
- sudo npx zenfs-test -v tests/native/setup.ts