@zenfs/core 2.5.6 → 2.5.8
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.
- package/dist/backends/store/fs.js +4 -8
- package/dist/index.js +1 -2
- package/dist/internal/filesystem.d.ts +4 -4
- package/dist/internal/index_fs.js +6 -8
- package/dist/internal/inode.js +3 -0
- package/dist/node/compat.d.ts +1 -0
- package/dist/node/compat.js +2 -0
- package/dist/node/promises.d.ts +16 -1
- package/dist/node/promises.js +180 -0
- package/dist/node/readline.d.ts +1 -1
- package/dist/vfs/watchers.js +8 -6
- package/package.json +7 -3
- package/scripts/test.js +0 -0
- package/tests/common/handle-pull.test.ts +86 -0
- package/tests/common/inode.test.ts +8 -0
- package/tests/fs/watch.test.ts +19 -0
|
@@ -454,12 +454,10 @@ export class StoreFS extends FileSystem {
|
|
|
454
454
|
}
|
|
455
455
|
}
|
|
456
456
|
async createFile(path, options) {
|
|
457
|
-
options.mode
|
|
458
|
-
return await this.commitNew(path, options, new Uint8Array());
|
|
457
|
+
return await this.commitNew(path, { ...options, mode: options.mode | S_IFREG }, new Uint8Array());
|
|
459
458
|
}
|
|
460
459
|
createFileSync(path, options) {
|
|
461
|
-
options.mode
|
|
462
|
-
return this.commitNewSync(path, options, new Uint8Array());
|
|
460
|
+
return this.commitNewSync(path, { ...options, mode: options.mode | S_IFREG }, new Uint8Array());
|
|
463
461
|
}
|
|
464
462
|
async unlink(path) {
|
|
465
463
|
return this.remove(path, false);
|
|
@@ -478,12 +476,10 @@ export class StoreFS extends FileSystem {
|
|
|
478
476
|
this.removeSync(path, true);
|
|
479
477
|
}
|
|
480
478
|
async mkdir(path, options) {
|
|
481
|
-
options.mode
|
|
482
|
-
return await this.commitNew(path, options, encodeUTF8('{}'));
|
|
479
|
+
return await this.commitNew(path, { ...options, mode: options.mode | S_IFDIR }, encodeUTF8('{}'));
|
|
483
480
|
}
|
|
484
481
|
mkdirSync(path, options) {
|
|
485
|
-
options.mode
|
|
486
|
-
return this.commitNewSync(path, options, encodeUTF8('{}'));
|
|
482
|
+
return this.commitNewSync(path, { ...options, mode: options.mode | S_IFDIR }, encodeUTF8('{}'));
|
|
487
483
|
}
|
|
488
484
|
async readdir(path) {
|
|
489
485
|
const env_11 = { stack: [], error: void 0, hasError: false };
|
package/dist/index.js
CHANGED
|
@@ -16,5 +16,4 @@ export { fs };
|
|
|
16
16
|
export default fs;
|
|
17
17
|
export * from './node/compat.js';
|
|
18
18
|
export * as vfs from './vfs/index.js';
|
|
19
|
-
|
|
20
|
-
globalThis.__zenfs__ = Object.assign(Object.create(fs), { _version: $pkg.version });
|
|
19
|
+
globalThis.__zenfs__ = Object.create(fs);
|
|
@@ -91,21 +91,21 @@ export interface FileSystemAttributes {
|
|
|
91
91
|
* @category Internals
|
|
92
92
|
* @internal
|
|
93
93
|
*/
|
|
94
|
-
export interface CreationOptions extends Partial<InodeLike
|
|
94
|
+
export interface CreationOptions extends Readonly<Partial<InodeLike>> {
|
|
95
95
|
/**
|
|
96
96
|
* The uid to create the file.
|
|
97
97
|
* This is ignored if the FS supports setuid and the setuid bit is set
|
|
98
98
|
*/
|
|
99
|
-
uid: number;
|
|
99
|
+
readonly uid: number;
|
|
100
100
|
/**
|
|
101
101
|
* The gid to create the file.
|
|
102
102
|
* This is ignored if the FS supports setgid and the setgid bit is set
|
|
103
103
|
*/
|
|
104
|
-
gid: number;
|
|
104
|
+
readonly gid: number;
|
|
105
105
|
/**
|
|
106
106
|
* The mode to create the file with.
|
|
107
107
|
*/
|
|
108
|
-
mode: number;
|
|
108
|
+
readonly mode: number;
|
|
109
109
|
}
|
|
110
110
|
/**
|
|
111
111
|
* Options for streaming operations
|
|
@@ -94,13 +94,13 @@ export class IndexFS extends FileSystem {
|
|
|
94
94
|
const inode = this.index.get(path);
|
|
95
95
|
if (!inode)
|
|
96
96
|
throw withErrno('ENOENT');
|
|
97
|
-
return inode;
|
|
97
|
+
return new Inode(inode);
|
|
98
98
|
}
|
|
99
99
|
statSync(path) {
|
|
100
100
|
const inode = this.index.get(path);
|
|
101
101
|
if (!inode)
|
|
102
102
|
throw withErrno('ENOENT');
|
|
103
|
-
return inode;
|
|
103
|
+
return new Inode(inode);
|
|
104
104
|
}
|
|
105
105
|
async touch(path, metadata) {
|
|
106
106
|
const inode = this.index.get(path) ?? _throw(withErrno('ENOENT'));
|
|
@@ -165,21 +165,19 @@ export class IndexFS extends FileSystem {
|
|
|
165
165
|
return inode;
|
|
166
166
|
}
|
|
167
167
|
async createFile(path, options) {
|
|
168
|
-
options.mode
|
|
169
|
-
return this.create(path, options);
|
|
168
|
+
return this.create(path, { ...options, mode: options.mode | S_IFREG });
|
|
170
169
|
}
|
|
171
170
|
createFileSync(path, options) {
|
|
172
|
-
options.mode
|
|
173
|
-
return this.create(path, options);
|
|
171
|
+
return this.create(path, { ...options, mode: options.mode | S_IFREG });
|
|
174
172
|
}
|
|
175
173
|
async mkdir(path, options) {
|
|
176
|
-
options.mode
|
|
174
|
+
options = { ...options, mode: options.mode | S_IFDIR };
|
|
177
175
|
const inode = this.create(path, options);
|
|
178
176
|
await this._mkdir?.(path, options);
|
|
179
177
|
return inode;
|
|
180
178
|
}
|
|
181
179
|
mkdirSync(path, options) {
|
|
182
|
-
options.mode
|
|
180
|
+
options = { ...options, mode: options.mode | S_IFDIR };
|
|
183
181
|
const inode = this.create(path, options);
|
|
184
182
|
this._mkdirSync?.(path, options);
|
|
185
183
|
return inode;
|
package/dist/internal/inode.js
CHANGED
|
@@ -451,6 +451,9 @@ let Inode = (() => {
|
|
|
451
451
|
data = args[0];
|
|
452
452
|
args = [sizeof(Inode)];
|
|
453
453
|
}
|
|
454
|
+
if (args[0] instanceof Inode) {
|
|
455
|
+
args = [new Uint8Array(args[0].buffer, args[0].byteOffset, args[0].byteLength)];
|
|
456
|
+
}
|
|
454
457
|
super(...args);
|
|
455
458
|
__runInitializers(this, ___data_extraInitializers);
|
|
456
459
|
if (this.byteLength < sizeof(Inode)) {
|
package/dist/node/compat.d.ts
CHANGED
package/dist/node/compat.js
CHANGED
package/dist/node/promises.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Abortable } from 'node:events';
|
|
|
2
2
|
import type * as fs from 'node:fs';
|
|
3
3
|
import type * as promises from 'node:fs/promises';
|
|
4
4
|
import type { Stream } from 'node:stream';
|
|
5
|
+
import type { ByteReadableStream, Transform, Writer } from 'node:stream/iter';
|
|
5
6
|
import type { ReadableStream as NodeReadableStream } from 'node:stream/web';
|
|
6
7
|
import type { V_Context } from '../context.js';
|
|
7
8
|
import type { StreamOptions } from '../internal/filesystem.js';
|
|
@@ -157,6 +158,20 @@ export declare class FileHandle implements promises.FileHandle {
|
|
|
157
158
|
* @param options Options for the writeable stream.
|
|
158
159
|
*/
|
|
159
160
|
createWriteStream(options?: promises.CreateWriteStreamOptions): WriteStream;
|
|
161
|
+
/**
|
|
162
|
+
* Return the file contents as an async iterable using the `node:stream/iter` pull model.
|
|
163
|
+
* Reads are performed in `chunkSize`-byte chunks (default 128 KiB).
|
|
164
|
+
* If transforms are provided, they are applied via `stream/iter`'s `pull()`.
|
|
165
|
+
* The handle is closed when iteration completes if `autoClose` is set.
|
|
166
|
+
* @experimental
|
|
167
|
+
*/
|
|
168
|
+
pull(...transforms: Transform[]): ByteReadableStream;
|
|
169
|
+
pull(...args: [...transforms: Transform[], options: promises.PullOptions]): ByteReadableStream;
|
|
170
|
+
/**
|
|
171
|
+
* Return a `node:stream/iter` writer backed by this file handle.
|
|
172
|
+
* @experimental
|
|
173
|
+
*/
|
|
174
|
+
writer(options?: promises.WriterOptions): Writer;
|
|
160
175
|
}
|
|
161
176
|
export declare function rename(this: V_Context, oldPath: fs.PathLike, newPath: fs.PathLike): Promise<void>;
|
|
162
177
|
/**
|
|
@@ -224,7 +239,7 @@ export declare function writeFile(this: V_Context, path: fs.PathLike | promises.
|
|
|
224
239
|
* @option mode Defaults to `0644`.
|
|
225
240
|
* @option flag Defaults to `'a'`.
|
|
226
241
|
*/
|
|
227
|
-
export declare function appendFile(this: V_Context, path: fs.PathLike | promises.FileHandle, data: FileContents
|
|
242
|
+
export declare function appendFile(this: V_Context, path: fs.PathLike | promises.FileHandle, data: FileContents | Iterable<string | ArrayBufferView> | AsyncIterable<string | ArrayBufferView>, _options?: BufferEncoding | (fs.EncodingOption & {
|
|
228
243
|
mode?: fs.Mode;
|
|
229
244
|
flag?: fs.OpenMode;
|
|
230
245
|
}) | null): Promise<void>;
|
package/dist/node/promises.js
CHANGED
|
@@ -69,6 +69,68 @@ import { createInterface } from './readline.js';
|
|
|
69
69
|
import { BigIntStats, Stats } from './stats.js';
|
|
70
70
|
import { ReadStream, WriteStream } from './streams.js';
|
|
71
71
|
export * as constants from '../constants.js';
|
|
72
|
+
/** Coerce a `stream/iter` transform result into a flat array of `Uint8Array` chunks. */
|
|
73
|
+
async function* normalizeTransformResult(result) {
|
|
74
|
+
if (result == null)
|
|
75
|
+
return;
|
|
76
|
+
if (typeof result == 'string') {
|
|
77
|
+
yield encodeUTF8(result);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (result instanceof Uint8Array) {
|
|
81
|
+
yield result;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (ArrayBuffer.isView(result)) {
|
|
85
|
+
yield new Uint8Array(result.buffer, result.byteOffset, result.byteLength);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (result instanceof ArrayBuffer || (typeof SharedArrayBuffer != 'undefined' && result instanceof SharedArrayBuffer)) {
|
|
89
|
+
yield new Uint8Array(result);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (typeof result[Symbol.asyncIterator] == 'function'
|
|
93
|
+
|| typeof result[Symbol.iterator] == 'function') {
|
|
94
|
+
for await (const part of result)
|
|
95
|
+
yield* normalizeTransformResult(part);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
throw new TypeError('Invalid transform result');
|
|
99
|
+
}
|
|
100
|
+
/** Ponyfill for `node:stream/iter`'s `pull()` transform application */
|
|
101
|
+
async function* applyTransform(source, transform, signal) {
|
|
102
|
+
const options = { signal: signal };
|
|
103
|
+
if (typeof transform == 'function') {
|
|
104
|
+
for await (const chunks of source) {
|
|
105
|
+
signal?.throwIfAborted();
|
|
106
|
+
const out = [];
|
|
107
|
+
for await (const chunk of normalizeTransformResult(await transform(chunks, options)))
|
|
108
|
+
out.push(chunk);
|
|
109
|
+
if (out.length)
|
|
110
|
+
yield out;
|
|
111
|
+
}
|
|
112
|
+
const out = [];
|
|
113
|
+
for await (const chunk of normalizeTransformResult(await transform(null, options)))
|
|
114
|
+
out.push(chunk);
|
|
115
|
+
if (out.length)
|
|
116
|
+
yield out;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Stateful transform: feed it the source (with a trailing `null` to signal EOF).
|
|
120
|
+
async function* withFlush() {
|
|
121
|
+
for await (const chunks of source)
|
|
122
|
+
yield chunks;
|
|
123
|
+
yield null;
|
|
124
|
+
}
|
|
125
|
+
for await (const result of transform.transform(withFlush(), options)) {
|
|
126
|
+
signal?.throwIfAborted();
|
|
127
|
+
const out = [];
|
|
128
|
+
for await (const chunk of normalizeTransformResult(result))
|
|
129
|
+
out.push(chunk);
|
|
130
|
+
if (out.length)
|
|
131
|
+
yield out;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
72
134
|
export class FileHandle {
|
|
73
135
|
context;
|
|
74
136
|
fd;
|
|
@@ -332,6 +394,122 @@ export class FileHandle {
|
|
|
332
394
|
throw UV('EROFS', 'createWriteStream', this.vfs.path);
|
|
333
395
|
return new WriteStream(options, this);
|
|
334
396
|
}
|
|
397
|
+
pull(...args) {
|
|
398
|
+
if (this.vfs.isClosed || this.vfs.flag & constants.O_WRONLY)
|
|
399
|
+
throw UV('EBADF', 'read', this.vfs.path);
|
|
400
|
+
// A trailing plain object that is neither a transform function nor a stateful transform is the options bag.
|
|
401
|
+
const last = args.at(-1);
|
|
402
|
+
const hasOptions = last != null && typeof last == 'object' && typeof last.transform != 'function';
|
|
403
|
+
const options = hasOptions ? args.pop() : {};
|
|
404
|
+
const transforms = args;
|
|
405
|
+
const { autoClose, start, limit, chunkSize = 131072, signal } = options;
|
|
406
|
+
const handle = this;
|
|
407
|
+
async function* source() {
|
|
408
|
+
let position = typeof start == 'number' ? start : 0;
|
|
409
|
+
let remaining = typeof limit == 'number' ? limit : Infinity;
|
|
410
|
+
try {
|
|
411
|
+
while (remaining > 0) {
|
|
412
|
+
signal?.throwIfAborted();
|
|
413
|
+
const size = Math.min(chunkSize, remaining);
|
|
414
|
+
const buffer = new Uint8Array(size);
|
|
415
|
+
const bytesRead = await handle.vfs.read(buffer, 0, size, position);
|
|
416
|
+
if (bytesRead <= 0)
|
|
417
|
+
break;
|
|
418
|
+
position += bytesRead;
|
|
419
|
+
remaining -= bytesRead;
|
|
420
|
+
yield [buffer.subarray(0, bytesRead)];
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
finally {
|
|
424
|
+
if (autoClose)
|
|
425
|
+
await handle.close();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
let stream = source();
|
|
429
|
+
for (const transform of transforms)
|
|
430
|
+
stream = applyTransform(stream, transform, signal);
|
|
431
|
+
return stream;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Return a `node:stream/iter` writer backed by this file handle.
|
|
435
|
+
* @experimental
|
|
436
|
+
*/
|
|
437
|
+
writer(options = {}) {
|
|
438
|
+
if (this.vfs.isClosed)
|
|
439
|
+
throw UV('EBADF', 'write', this.vfs.path);
|
|
440
|
+
if (this.vfs.inode.flags & InodeFlags.Immutable)
|
|
441
|
+
throw UV('EPERM', 'write', this.vfs.path);
|
|
442
|
+
if (this.vfs.fs.attributes.has('readonly'))
|
|
443
|
+
throw UV('EROFS', 'write', this.vfs.path);
|
|
444
|
+
const { autoClose, start, limit, chunkSize = 131072 } = options;
|
|
445
|
+
const handle = this;
|
|
446
|
+
let position = typeof start == 'number' ? start : handle.vfs.position;
|
|
447
|
+
let written = 0;
|
|
448
|
+
let failed = false;
|
|
449
|
+
// Tracks whether the writer has been ended or failed, so disposal and `autoClose` don't double-close.
|
|
450
|
+
let done = false;
|
|
451
|
+
async function finish() {
|
|
452
|
+
if (done)
|
|
453
|
+
return;
|
|
454
|
+
done = true;
|
|
455
|
+
if (autoClose)
|
|
456
|
+
await handle.close();
|
|
457
|
+
}
|
|
458
|
+
const toBuffer = (chunk) => typeof chunk == 'string' ? Buffer.from(chunk, 'utf8') : new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
459
|
+
async function writeChunk(chunk) {
|
|
460
|
+
const buffer = toBuffer(chunk);
|
|
461
|
+
if (typeof limit == 'number' && written + buffer.byteLength > limit) {
|
|
462
|
+
throw new RangeError(`Writer limit of ${limit} bytes exceeded`);
|
|
463
|
+
}
|
|
464
|
+
const bytesWritten = await handle.vfs.write(buffer, 0, buffer.byteLength, position);
|
|
465
|
+
position += bytesWritten;
|
|
466
|
+
written += bytesWritten;
|
|
467
|
+
handle._emitChange();
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
get desiredSize() {
|
|
471
|
+
if (failed)
|
|
472
|
+
return null;
|
|
473
|
+
return typeof limit == 'number' ? Math.max(0, limit - written) : chunkSize;
|
|
474
|
+
},
|
|
475
|
+
async write(chunk, opts) {
|
|
476
|
+
opts?.signal?.throwIfAborted();
|
|
477
|
+
await writeChunk(chunk);
|
|
478
|
+
},
|
|
479
|
+
async writev(chunks, opts) {
|
|
480
|
+
opts?.signal?.throwIfAborted();
|
|
481
|
+
for (const chunk of chunks)
|
|
482
|
+
await writeChunk(chunk);
|
|
483
|
+
},
|
|
484
|
+
writeSync() {
|
|
485
|
+
// Synchronous writes are not supported by the async handle.
|
|
486
|
+
throw UV('ENOSYS', 'writeSync', handle.vfs.path);
|
|
487
|
+
},
|
|
488
|
+
writevSync() {
|
|
489
|
+
throw UV('ENOSYS', 'writevSync', handle.vfs.path);
|
|
490
|
+
},
|
|
491
|
+
async end() {
|
|
492
|
+
await finish();
|
|
493
|
+
return written;
|
|
494
|
+
},
|
|
495
|
+
endSync() {
|
|
496
|
+
throw UV('ENOSYS', 'endSync', handle.vfs.path);
|
|
497
|
+
},
|
|
498
|
+
fail() {
|
|
499
|
+
failed = true;
|
|
500
|
+
void finish();
|
|
501
|
+
},
|
|
502
|
+
[Symbol.dispose]() {
|
|
503
|
+
// `using w = fh.writer()` fails the writer unconditionally.
|
|
504
|
+
failed = true;
|
|
505
|
+
void finish();
|
|
506
|
+
},
|
|
507
|
+
async [Symbol.asyncDispose]() {
|
|
508
|
+
// `await using w = fh.writer()` only finishes if still open.
|
|
509
|
+
await finish();
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
}
|
|
335
513
|
}
|
|
336
514
|
export async function rename(oldPath, newPath) {
|
|
337
515
|
await _async.rename.call(this, oldPath, newPath);
|
|
@@ -463,6 +641,8 @@ export async function appendFile(path, data, _options) {
|
|
|
463
641
|
const $ex = { syscall: 'write', path: path instanceof FileHandle ? path['vfs'].path : path.toString() };
|
|
464
642
|
if (!(flag & constants.O_APPEND))
|
|
465
643
|
throw UV('EBADF', $ex);
|
|
644
|
+
if (typeof data != 'string' && !ArrayBuffer.isView(data))
|
|
645
|
+
throw new TypeError('The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received ' + typeof data);
|
|
466
646
|
const encodedData = typeof data == 'string' ? Buffer.from(data, options.encoding) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
467
647
|
const handle = __addDisposableResource(env_4, typeof path == 'object' && 'fd' in path ? path : await open.call(this, path, options.flag, options.mode), true);
|
|
468
648
|
await handle.appendFile(encodedData, options);
|
package/dist/node/readline.d.ts
CHANGED
|
@@ -131,5 +131,5 @@ export declare function moveCursor(stream: NodeJS.WritableStream, dx: number, dy
|
|
|
131
131
|
*
|
|
132
132
|
* This is automatically called by any readline instance on its `input` if the `input` is a terminal. Closing the `readline` instance does not stop the `input` from emitting `'keypress'` events.
|
|
133
133
|
*/
|
|
134
|
-
export declare function emitKeypressEvents(stream: NodeJS.ReadableStream, readlineInterface?:
|
|
134
|
+
export declare function emitKeypressEvents(stream: NodeJS.ReadableStream, readlineInterface?: readline.Interface): void;
|
|
135
135
|
export {};
|
package/dist/vfs/watchers.js
CHANGED
|
@@ -150,13 +150,15 @@ export function emitChange(context, eventType, filename) {
|
|
|
150
150
|
if ($)
|
|
151
151
|
filename = join($.root ?? '/', filename);
|
|
152
152
|
filename = normalizePath(filename);
|
|
153
|
-
// Notify watchers, including ones on parent directories if they are watching recursively
|
|
154
|
-
for (let path = filename
|
|
153
|
+
// Notify watchers, including ones on parent directories if they are watching recursively.
|
|
154
|
+
for (let path = filename;; path = dirname(path)) {
|
|
155
155
|
const watchersForPath = watchers.get(path);
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
156
|
+
if (watchersForPath) {
|
|
157
|
+
for (const watcher of watchersForPath) {
|
|
158
|
+
watcher.emit('change', eventType, relative.call(watcher._context, path, filename) || basename(filename));
|
|
159
|
+
}
|
|
160
160
|
}
|
|
161
|
+
if (path === '/')
|
|
162
|
+
break;
|
|
161
163
|
}
|
|
162
164
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenfs/core",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.8",
|
|
4
4
|
"description": "A filesystem, anywhere",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -80,14 +80,18 @@
|
|
|
80
80
|
"utilium": "^3.0.0"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
|
+
"@eslint/js": "^10.0.1",
|
|
83
84
|
"@octokit/action": "^8.0.4",
|
|
84
|
-
"c8": "^
|
|
85
|
-
"eslint": "^
|
|
85
|
+
"c8": "^11.0.0",
|
|
86
|
+
"eslint": "^10.1.0",
|
|
86
87
|
"globals": "^17.3.0",
|
|
87
88
|
"prettier": "^3.2.5",
|
|
88
89
|
"tsx": "^4.19.1",
|
|
89
90
|
"typedoc": "^0.28.18",
|
|
90
91
|
"typescript": "^6.0.0",
|
|
91
92
|
"typescript-eslint": "^8.58.0"
|
|
93
|
+
},
|
|
94
|
+
"allowScripts": {
|
|
95
|
+
"esbuild": true
|
|
92
96
|
}
|
|
93
97
|
}
|
package/scripts/test.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
import { promises } from '@zenfs/core';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import type { FileHandle } from 'node:fs/promises';
|
|
5
|
+
import { suite, test } from 'node:test';
|
|
6
|
+
|
|
7
|
+
const content = 'hello';
|
|
8
|
+
|
|
9
|
+
/** Decode a `ByteReadableStream` (`AsyncIterable<Uint8Array[]>`) into a string. */
|
|
10
|
+
async function text(source: AsyncIterable<Uint8Array[]>): Promise<string> {
|
|
11
|
+
const decoder = new TextDecoder();
|
|
12
|
+
let out = '';
|
|
13
|
+
for await (const chunks of source) {
|
|
14
|
+
for (const chunk of chunks) out += decoder.decode(chunk, { stream: true });
|
|
15
|
+
}
|
|
16
|
+
return out + decoder.decode();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// The `asciiUpper` transform from the Node.js `node:stream/iter` docs.
|
|
20
|
+
const asciiUpper = (chunks: Uint8Array[] | null): Uint8Array[] | null => {
|
|
21
|
+
if (chunks === null) return null;
|
|
22
|
+
return chunks.map(c => {
|
|
23
|
+
for (let i = 0; i < c.length; i++) {
|
|
24
|
+
c[i] -= (c[i] >= 97 && c[i] <= 122 ? 1 : 0) * 32;
|
|
25
|
+
}
|
|
26
|
+
return c;
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const handle: FileHandle = await promises.open('./pull.txt', 'w+');
|
|
31
|
+
await handle.writeFile(content);
|
|
32
|
+
|
|
33
|
+
suite('FileHandle.pull', () => {
|
|
34
|
+
test('reads file contents', async () => {
|
|
35
|
+
assert.equal(await text(handle.pull()), content);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('reads a range with start and limit', async () => {
|
|
39
|
+
assert.equal(await text(handle.pull({ start: 1, limit: 3 })), content.slice(1, 4));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('respects a small chunkSize', async () => {
|
|
43
|
+
const chunks: number[] = [];
|
|
44
|
+
for await (const part of handle.pull({ chunkSize: 2 })) {
|
|
45
|
+
for (const c of part) chunks.push(c.length);
|
|
46
|
+
}
|
|
47
|
+
assert.deepEqual(chunks, [2, 2, 1]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('applies the asciiUpper transform from the Node.js docs', async () => {
|
|
51
|
+
assert.equal(await text(handle.pull(asciiUpper)), content.toUpperCase()); // 'HELLO'
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('autoClose closes the handle', async () => {
|
|
55
|
+
const fh = await promises.open('./pull-autoclose.txt', 'w+');
|
|
56
|
+
await fh.writeFile(content);
|
|
57
|
+
assert.equal(await text(fh.pull({ autoClose: true })), content);
|
|
58
|
+
await assert.rejects(fh.readFile('utf8'));
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
suite('FileHandle.writer', () => {
|
|
63
|
+
test('writes data backed by the handle', async () => {
|
|
64
|
+
const fh = await promises.open('./writer.txt', 'w+');
|
|
65
|
+
const w = fh.writer();
|
|
66
|
+
await w.write('Hello!');
|
|
67
|
+
assert.equal(await w.end(), 6);
|
|
68
|
+
assert.equal(await fh.readFile('utf8'), 'Hello!');
|
|
69
|
+
await fh.close();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('limit rejects oversized writes', async () => {
|
|
73
|
+
const fh = await promises.open('./writer-limit.txt', 'w+');
|
|
74
|
+
const w = fh.writer({ limit: 4 });
|
|
75
|
+
await assert.rejects(w.write('too long'), RangeError);
|
|
76
|
+
await fh.close();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('autoClose closes the handle on end', async () => {
|
|
80
|
+
const fh = await promises.open('./writer-autoclose.txt', 'w+');
|
|
81
|
+
using w = fh.writer({ autoClose: true });
|
|
82
|
+
await w.write('bye');
|
|
83
|
+
await w.end();
|
|
84
|
+
await assert.rejects(fh.readFile('utf8'));
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -17,4 +17,12 @@ suite('Inode manipulation', () => {
|
|
|
17
17
|
const newInode = Object.assign(new Inode(), inode);
|
|
18
18
|
assert.equal(newInode.mode, inode.mode);
|
|
19
19
|
});
|
|
20
|
+
|
|
21
|
+
test('Copy to new Inode using the constructor does not alias', () => {
|
|
22
|
+
const newInode = new Inode(inode);
|
|
23
|
+
assert.equal(newInode.mode, inode.mode);
|
|
24
|
+
|
|
25
|
+
newInode.mode = 0o40700;
|
|
26
|
+
assert.equal(inode.mode, 0o40755);
|
|
27
|
+
});
|
|
20
28
|
});
|
package/tests/fs/watch.test.ts
CHANGED
|
@@ -156,4 +156,23 @@ suite('Watch', () => {
|
|
|
156
156
|
await watcher.return!();
|
|
157
157
|
await promise;
|
|
158
158
|
});
|
|
159
|
+
|
|
160
|
+
test('watch("/") receives events for files under root #293', async () => {
|
|
161
|
+
// Regression: emitChange previously exited its parent-walk before
|
|
162
|
+
// checking watchers.get('/'), so a watcher registered on '/' never fired.
|
|
163
|
+
const { promise, resolve } = Promise.withResolvers<[string, string]>();
|
|
164
|
+
|
|
165
|
+
using watcher = fs.watch('/', (eventType, filename) => {
|
|
166
|
+
resolve([eventType, filename]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await fs.promises.writeFile('/watch-root-file.txt', 'x');
|
|
170
|
+
|
|
171
|
+
const [eventType, filename] = await Promise.race([
|
|
172
|
+
promise,
|
|
173
|
+
new Promise<[string, string]>((_, reject) => setTimeout(() => reject(new Error('watch("/") timed out')), 1000)),
|
|
174
|
+
]);
|
|
175
|
+
assert.equal(eventType, 'change');
|
|
176
|
+
assert.equal(filename, 'watch-root-file.txt');
|
|
177
|
+
});
|
|
159
178
|
});
|