@gjsify/fs 0.0.4 → 0.1.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.
- package/README.md +31 -2
- package/lib/esm/callback.js +251 -15
- package/lib/esm/dirent.js +47 -6
- package/lib/esm/encoding.js +2 -3
- package/lib/esm/errors.js +13 -0
- package/lib/esm/file-handle.js +108 -66
- package/lib/esm/fs-watcher.js +44 -7
- package/lib/esm/index.js +140 -5
- package/lib/esm/promises.js +290 -69
- package/lib/esm/read-stream.js +82 -57
- package/lib/esm/stats.js +138 -18
- package/lib/esm/sync.js +293 -44
- package/lib/esm/write-stream.js +4 -4
- package/lib/types/callback.d.ts +233 -0
- package/lib/types/dirent.d.ts +77 -0
- package/lib/types/encoding.d.ts +6 -0
- package/lib/types/errors.d.ts +7 -0
- package/lib/types/file-handle.d.ts +367 -0
- package/lib/types/fs-watcher.d.ts +17 -0
- package/lib/types/index.d.ts +149 -0
- package/lib/types/promises.d.ts +158 -0
- package/lib/types/read-stream.d.ts +21 -0
- package/lib/types/stats.d.ts +67 -0
- package/lib/types/sync.d.ts +109 -0
- package/lib/types/types/encoding-option.d.ts +2 -0
- package/lib/types/types/file-read-options.d.ts +15 -0
- package/lib/types/types/file-read-result.d.ts +4 -0
- package/lib/types/types/flag-and-open-mode.d.ts +5 -0
- package/lib/types/types/index.d.ts +6 -0
- package/lib/types/types/open-flags.d.ts +1 -0
- package/lib/types/types/read-options.d.ts +5 -0
- package/lib/types/utils.d.ts +2 -0
- package/lib/types/write-stream.d.ts +45 -0
- package/package.json +22 -35
- package/src/callback.spec.ts +284 -30
- package/src/callback.ts +352 -39
- package/src/dirent.ts +56 -8
- package/src/encoding.ts +7 -2
- package/src/errors.spec.ts +389 -0
- package/src/errors.ts +19 -0
- package/src/extended.spec.ts +706 -0
- package/src/file-handle.spec.ts +104 -23
- package/src/file-handle.ts +147 -79
- package/src/fs-watcher.ts +55 -8
- package/src/index.ts +146 -2
- package/src/new-apis.spec.ts +505 -0
- package/src/promises.spec.ts +651 -11
- package/src/promises.ts +353 -81
- package/src/read-stream.ts +98 -74
- package/src/stat.spec.ts +22 -14
- package/src/stats.ts +176 -75
- package/src/streams.spec.ts +455 -0
- package/src/symlink.spec.ts +176 -26
- package/src/sync.spec.ts +204 -32
- package/src/sync.ts +363 -58
- package/src/test.mts +7 -2
- package/src/types/encoding-option.ts +1 -1
- package/src/types/flag-and-open-mode.ts +1 -1
- package/src/types/read-options.ts +2 -2
- package/src/utils.ts +2 -0
- package/src/write-stream.ts +9 -7
- package/tsconfig.json +23 -10
- package/tsconfig.tsbuildinfo +1 -0
- package/lib/cjs/callback.js +0 -112
- package/lib/cjs/dirent.js +0 -98
- package/lib/cjs/encoding.js +0 -34
- package/lib/cjs/file-handle.js +0 -444
- package/lib/cjs/fs-watcher.js +0 -50
- package/lib/cjs/index.js +0 -95
- package/lib/cjs/promises.js +0 -160
- package/lib/cjs/read-stream.js +0 -78
- package/lib/cjs/stats.js +0 -45
- package/lib/cjs/sync.js +0 -126
- package/lib/cjs/types/encoding-option.js +0 -0
- package/lib/cjs/types/file-read-options.js +0 -0
- package/lib/cjs/types/file-read-result.js +0 -0
- package/lib/cjs/types/flag-and-open-mode.js +0 -0
- package/lib/cjs/types/index.js +0 -6
- package/lib/cjs/types/open-flags.js +0 -0
- package/lib/cjs/types/read-options.js +0 -0
- package/lib/cjs/utils.js +0 -18
- package/lib/cjs/write-stream.js +0 -116
- package/test/watch.js +0 -1
- package/test.gjs.js +0 -35359
- package/test.gjs.js.map +0 -7
- package/test.gjs.mjs +0 -40534
- package/test.gjs.mjs.meta.json +0 -1
- package/test.node.js +0 -1479
- package/test.node.js.map +0 -7
- package/test.node.mjs +0 -710
- package/tsconfig.types.json +0 -8
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
// fs stream tests — createReadStream, createWriteStream, pipe integration
|
|
2
|
+
// Reference: refs/node-test/parallel/test-fs-read-stream*.js, test-fs-write-stream*.js
|
|
3
|
+
|
|
4
|
+
import { describe, it, expect } from '@gjsify/unit';
|
|
5
|
+
import { createReadStream, createWriteStream, writeFileSync, readFileSync, mkdtempSync, rmSync, mkdirSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { Buffer } from 'node:buffer';
|
|
9
|
+
import { Transform, PassThrough } from 'node:stream';
|
|
10
|
+
|
|
11
|
+
let tmpDir: string;
|
|
12
|
+
|
|
13
|
+
function setup(): void {
|
|
14
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'gjsify-fs-stream-'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function cleanup(): void {
|
|
18
|
+
try { rmSync(tmpDir, { recursive: true }); } catch {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default async () => {
|
|
22
|
+
// ---- createReadStream ----
|
|
23
|
+
|
|
24
|
+
await describe('fs.createReadStream', async () => {
|
|
25
|
+
await it('should read a file and emit data + end events', async () => {
|
|
26
|
+
setup();
|
|
27
|
+
try {
|
|
28
|
+
const filePath = join(tmpDir, 'read-test.txt');
|
|
29
|
+
writeFileSync(filePath, 'hello world');
|
|
30
|
+
const chunks: string[] = [];
|
|
31
|
+
await new Promise<void>((resolve, reject) => {
|
|
32
|
+
const stream = createReadStream(filePath, { encoding: 'utf8' });
|
|
33
|
+
stream.on('data', (chunk) => chunks.push(chunk as string));
|
|
34
|
+
stream.on('end', () => resolve());
|
|
35
|
+
stream.on('error', reject);
|
|
36
|
+
});
|
|
37
|
+
expect(chunks.join('')).toBe('hello world');
|
|
38
|
+
} finally {
|
|
39
|
+
cleanup();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await it('should read file as Buffer by default', async () => {
|
|
44
|
+
setup();
|
|
45
|
+
try {
|
|
46
|
+
const filePath = join(tmpDir, 'buf-test.txt');
|
|
47
|
+
writeFileSync(filePath, 'buffer data');
|
|
48
|
+
const chunks: Buffer[] = [];
|
|
49
|
+
await new Promise<void>((resolve, reject) => {
|
|
50
|
+
const stream = createReadStream(filePath);
|
|
51
|
+
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk as Uint8Array)));
|
|
52
|
+
stream.on('end', () => resolve());
|
|
53
|
+
stream.on('error', reject);
|
|
54
|
+
});
|
|
55
|
+
const result = Buffer.concat(chunks).toString('utf8');
|
|
56
|
+
expect(result).toBe('buffer data');
|
|
57
|
+
} finally {
|
|
58
|
+
cleanup();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await it('should handle empty file', async () => {
|
|
63
|
+
setup();
|
|
64
|
+
try {
|
|
65
|
+
const filePath = join(tmpDir, 'empty.txt');
|
|
66
|
+
writeFileSync(filePath, '');
|
|
67
|
+
const chunks: string[] = [];
|
|
68
|
+
await new Promise<void>((resolve, reject) => {
|
|
69
|
+
const stream = createReadStream(filePath, { encoding: 'utf8' });
|
|
70
|
+
stream.on('data', (chunk) => chunks.push(chunk as string));
|
|
71
|
+
stream.on('end', () => resolve());
|
|
72
|
+
stream.on('error', reject);
|
|
73
|
+
});
|
|
74
|
+
expect(chunks.join('')).toBe('');
|
|
75
|
+
} finally {
|
|
76
|
+
cleanup();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await it('should emit error for non-existent file', async () => {
|
|
81
|
+
const err = await new Promise<Error>((resolve) => {
|
|
82
|
+
const stream = createReadStream('/nonexistent_gjsify_test_12345');
|
|
83
|
+
stream.on('error', (e) => resolve(e));
|
|
84
|
+
});
|
|
85
|
+
expect(err).toBeDefined();
|
|
86
|
+
// GJS Gio errors have numeric codes; Node.js has string 'ENOENT'
|
|
87
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
88
|
+
expect(code === 'ENOENT' || code === 1 || (err as any).code !== undefined).toBeTruthy();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await it('should read file with start and end options', async () => {
|
|
92
|
+
setup();
|
|
93
|
+
try {
|
|
94
|
+
const filePath = join(tmpDir, 'range-test.txt');
|
|
95
|
+
writeFileSync(filePath, 'abcdefghij');
|
|
96
|
+
const chunks: string[] = [];
|
|
97
|
+
await new Promise<void>((resolve, reject) => {
|
|
98
|
+
const stream = createReadStream(filePath, { encoding: 'utf8', start: 2, end: 6 });
|
|
99
|
+
stream.on('data', (chunk) => chunks.push(chunk as string));
|
|
100
|
+
stream.on('end', () => resolve());
|
|
101
|
+
stream.on('error', reject);
|
|
102
|
+
});
|
|
103
|
+
// start=2 (index 'c'), end=6 (index 'g', inclusive)
|
|
104
|
+
expect(chunks.join('')).toBe('cdefg');
|
|
105
|
+
} finally {
|
|
106
|
+
cleanup();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await it('should read a larger file (64KB)', async () => {
|
|
111
|
+
setup();
|
|
112
|
+
try {
|
|
113
|
+
const filePath = join(tmpDir, 'large.txt');
|
|
114
|
+
const size = 64 * 1024;
|
|
115
|
+
writeFileSync(filePath, 'A'.repeat(size));
|
|
116
|
+
let totalLength = 0;
|
|
117
|
+
await new Promise<void>((resolve, reject) => {
|
|
118
|
+
const stream = createReadStream(filePath);
|
|
119
|
+
stream.on('data', (chunk) => { totalLength += (chunk as Buffer).length; });
|
|
120
|
+
stream.on('end', () => resolve());
|
|
121
|
+
stream.on('error', reject);
|
|
122
|
+
});
|
|
123
|
+
expect(totalLength).toBe(size);
|
|
124
|
+
} finally {
|
|
125
|
+
cleanup();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await it('should track bytesRead', async () => {
|
|
130
|
+
setup();
|
|
131
|
+
try {
|
|
132
|
+
const filePath = join(tmpDir, 'bytes-read.txt');
|
|
133
|
+
const content = 'hello bytes';
|
|
134
|
+
writeFileSync(filePath, content);
|
|
135
|
+
const stream = createReadStream(filePath);
|
|
136
|
+
await new Promise<void>((resolve, reject) => {
|
|
137
|
+
stream.on('data', () => {});
|
|
138
|
+
stream.on('end', () => resolve());
|
|
139
|
+
stream.on('error', reject);
|
|
140
|
+
});
|
|
141
|
+
expect(stream.bytesRead).toBe(Buffer.byteLength(content));
|
|
142
|
+
} finally {
|
|
143
|
+
cleanup();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await it('should have path property', async () => {
|
|
148
|
+
setup();
|
|
149
|
+
try {
|
|
150
|
+
const filePath = join(tmpDir, 'path-prop.txt');
|
|
151
|
+
writeFileSync(filePath, 'test');
|
|
152
|
+
const stream = createReadStream(filePath);
|
|
153
|
+
expect(stream.path).toBe(filePath);
|
|
154
|
+
await new Promise<void>((resolve) => {
|
|
155
|
+
stream.on('data', () => {});
|
|
156
|
+
stream.on('end', () => resolve());
|
|
157
|
+
});
|
|
158
|
+
} finally {
|
|
159
|
+
cleanup();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ---- createWriteStream ----
|
|
165
|
+
|
|
166
|
+
await describe('fs.createWriteStream', async () => {
|
|
167
|
+
await it('should write data to a file', async () => {
|
|
168
|
+
setup();
|
|
169
|
+
try {
|
|
170
|
+
const filePath = join(tmpDir, 'write-test.txt');
|
|
171
|
+
await new Promise<void>((resolve, reject) => {
|
|
172
|
+
const stream = createWriteStream(filePath);
|
|
173
|
+
stream.on('error', reject);
|
|
174
|
+
stream.write('hello ');
|
|
175
|
+
stream.write('world');
|
|
176
|
+
stream.end(() => resolve());
|
|
177
|
+
});
|
|
178
|
+
expect(readFileSync(filePath, 'utf8')).toBe('hello world');
|
|
179
|
+
} finally {
|
|
180
|
+
cleanup();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await it('should emit finish event', async () => {
|
|
185
|
+
setup();
|
|
186
|
+
try {
|
|
187
|
+
const filePath = join(tmpDir, 'finish-test.txt');
|
|
188
|
+
const stream = createWriteStream(filePath);
|
|
189
|
+
const finished = await new Promise<boolean>((resolve) => {
|
|
190
|
+
stream.on('finish', () => resolve(true));
|
|
191
|
+
stream.end('done');
|
|
192
|
+
});
|
|
193
|
+
expect(finished).toBe(true);
|
|
194
|
+
expect(readFileSync(filePath, 'utf8')).toBe('done');
|
|
195
|
+
} finally {
|
|
196
|
+
cleanup();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await it('should write Buffer data', async () => {
|
|
201
|
+
setup();
|
|
202
|
+
try {
|
|
203
|
+
const filePath = join(tmpDir, 'buf-write.txt');
|
|
204
|
+
await new Promise<void>((resolve, reject) => {
|
|
205
|
+
const stream = createWriteStream(filePath);
|
|
206
|
+
stream.on('error', reject);
|
|
207
|
+
stream.write(Buffer.from('buffer '));
|
|
208
|
+
stream.end(Buffer.from('content'));
|
|
209
|
+
stream.on('finish', () => resolve());
|
|
210
|
+
});
|
|
211
|
+
expect(readFileSync(filePath, 'utf8')).toBe('buffer content');
|
|
212
|
+
} finally {
|
|
213
|
+
cleanup();
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await it('should write a large file (64KB)', async () => {
|
|
218
|
+
setup();
|
|
219
|
+
try {
|
|
220
|
+
const filePath = join(tmpDir, 'large-write.txt');
|
|
221
|
+
const chunkSize = 1024;
|
|
222
|
+
const numChunks = 64;
|
|
223
|
+
await new Promise<void>((resolve, reject) => {
|
|
224
|
+
const stream = createWriteStream(filePath);
|
|
225
|
+
stream.on('error', reject);
|
|
226
|
+
for (let i = 0; i < numChunks; i++) {
|
|
227
|
+
stream.write('B'.repeat(chunkSize));
|
|
228
|
+
}
|
|
229
|
+
stream.end(() => resolve());
|
|
230
|
+
});
|
|
231
|
+
const content = readFileSync(filePath, 'utf8');
|
|
232
|
+
expect(content.length).toBe(chunkSize * numChunks);
|
|
233
|
+
} finally {
|
|
234
|
+
cleanup();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
await it('should have path property', async () => {
|
|
239
|
+
setup();
|
|
240
|
+
try {
|
|
241
|
+
const filePath = join(tmpDir, 'path-prop-ws.txt');
|
|
242
|
+
const stream = createWriteStream(filePath);
|
|
243
|
+
expect(stream.path).toBe(filePath);
|
|
244
|
+
await new Promise<void>((resolve, reject) => {
|
|
245
|
+
stream.on('error', reject);
|
|
246
|
+
stream.end('test', () => resolve());
|
|
247
|
+
});
|
|
248
|
+
} finally {
|
|
249
|
+
cleanup();
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await it('should track bytesWritten', async () => {
|
|
254
|
+
setup();
|
|
255
|
+
try {
|
|
256
|
+
const filePath = join(tmpDir, 'bytes-written.txt');
|
|
257
|
+
const stream = createWriteStream(filePath);
|
|
258
|
+
await new Promise<void>((resolve, reject) => {
|
|
259
|
+
stream.on('error', reject);
|
|
260
|
+
stream.write('hello');
|
|
261
|
+
stream.end(' world', () => resolve());
|
|
262
|
+
});
|
|
263
|
+
expect(stream.bytesWritten).toBe(11); // "hello world"
|
|
264
|
+
} finally {
|
|
265
|
+
cleanup();
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ---- pipe: ReadStream → WriteStream ----
|
|
271
|
+
|
|
272
|
+
await describe('fs pipe: createReadStream → createWriteStream', async () => {
|
|
273
|
+
await it('should copy a file via pipe', async () => {
|
|
274
|
+
setup();
|
|
275
|
+
try {
|
|
276
|
+
const srcPath = join(tmpDir, 'src.txt');
|
|
277
|
+
const dstPath = join(tmpDir, 'dst.txt');
|
|
278
|
+
writeFileSync(srcPath, 'pipe copy test');
|
|
279
|
+
await new Promise<void>((resolve, reject) => {
|
|
280
|
+
const src = createReadStream(srcPath);
|
|
281
|
+
const dst = createWriteStream(dstPath);
|
|
282
|
+
src.on('error', reject);
|
|
283
|
+
dst.on('error', reject);
|
|
284
|
+
dst.on('finish', () => resolve());
|
|
285
|
+
src.pipe(dst);
|
|
286
|
+
});
|
|
287
|
+
expect(readFileSync(dstPath, 'utf8')).toBe('pipe copy test');
|
|
288
|
+
} finally {
|
|
289
|
+
cleanup();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await it('should copy a large file via pipe', async () => {
|
|
294
|
+
setup();
|
|
295
|
+
try {
|
|
296
|
+
const srcPath = join(tmpDir, 'large-src.txt');
|
|
297
|
+
const dstPath = join(tmpDir, 'large-dst.txt');
|
|
298
|
+
const size = 128 * 1024; // 128KB
|
|
299
|
+
writeFileSync(srcPath, 'D'.repeat(size));
|
|
300
|
+
await new Promise<void>((resolve, reject) => {
|
|
301
|
+
const src = createReadStream(srcPath);
|
|
302
|
+
const dst = createWriteStream(dstPath);
|
|
303
|
+
src.on('error', reject);
|
|
304
|
+
dst.on('error', reject);
|
|
305
|
+
dst.on('finish', () => resolve());
|
|
306
|
+
src.pipe(dst);
|
|
307
|
+
});
|
|
308
|
+
const result = readFileSync(dstPath, 'utf8');
|
|
309
|
+
expect(result.length).toBe(size);
|
|
310
|
+
} finally {
|
|
311
|
+
cleanup();
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await it('should pipe through a Transform (uppercase)', async () => {
|
|
316
|
+
setup();
|
|
317
|
+
try {
|
|
318
|
+
const srcPath = join(tmpDir, 'transform-src.txt');
|
|
319
|
+
const dstPath = join(tmpDir, 'transform-dst.txt');
|
|
320
|
+
writeFileSync(srcPath, 'hello transform');
|
|
321
|
+
const upper = new Transform({
|
|
322
|
+
transform(chunk, _enc, cb) {
|
|
323
|
+
cb(null, chunk.toString().toUpperCase());
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
await new Promise<void>((resolve, reject) => {
|
|
327
|
+
const src = createReadStream(srcPath, { encoding: 'utf8' });
|
|
328
|
+
const dst = createWriteStream(dstPath);
|
|
329
|
+
dst.on('error', reject);
|
|
330
|
+
dst.on('finish', () => resolve());
|
|
331
|
+
src.pipe(upper).pipe(dst);
|
|
332
|
+
});
|
|
333
|
+
expect(readFileSync(dstPath, 'utf8')).toBe('HELLO TRANSFORM');
|
|
334
|
+
} finally {
|
|
335
|
+
cleanup();
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
await it('should pipe through a PassThrough', async () => {
|
|
340
|
+
setup();
|
|
341
|
+
try {
|
|
342
|
+
const srcPath = join(tmpDir, 'pt-src.txt');
|
|
343
|
+
const dstPath = join(tmpDir, 'pt-dst.txt');
|
|
344
|
+
writeFileSync(srcPath, 'passthrough test');
|
|
345
|
+
const pt = new PassThrough();
|
|
346
|
+
await new Promise<void>((resolve, reject) => {
|
|
347
|
+
const src = createReadStream(srcPath);
|
|
348
|
+
const dst = createWriteStream(dstPath);
|
|
349
|
+
dst.on('error', reject);
|
|
350
|
+
dst.on('finish', () => resolve());
|
|
351
|
+
src.pipe(pt).pipe(dst);
|
|
352
|
+
});
|
|
353
|
+
expect(readFileSync(dstPath, 'utf8')).toBe('passthrough test');
|
|
354
|
+
} finally {
|
|
355
|
+
cleanup();
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// ---- Multiple sequential reads ----
|
|
361
|
+
|
|
362
|
+
await describe('fs streams: sequential operations', async () => {
|
|
363
|
+
await it('should read the same file twice sequentially', async () => {
|
|
364
|
+
setup();
|
|
365
|
+
try {
|
|
366
|
+
const filePath = join(tmpDir, 'multi-read.txt');
|
|
367
|
+
writeFileSync(filePath, 'read me twice');
|
|
368
|
+
const readOnce = (): Promise<string> => new Promise((resolve, reject) => {
|
|
369
|
+
const chunks: string[] = [];
|
|
370
|
+
const stream = createReadStream(filePath, { encoding: 'utf8' });
|
|
371
|
+
stream.on('data', (chunk) => chunks.push(chunk as string));
|
|
372
|
+
stream.on('end', () => resolve(chunks.join('')));
|
|
373
|
+
stream.on('error', reject);
|
|
374
|
+
});
|
|
375
|
+
const r1 = await readOnce();
|
|
376
|
+
const r2 = await readOnce();
|
|
377
|
+
expect(r1).toBe('read me twice');
|
|
378
|
+
expect(r2).toBe('read me twice');
|
|
379
|
+
} finally {
|
|
380
|
+
cleanup();
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
await it('should write then read the same file', async () => {
|
|
385
|
+
setup();
|
|
386
|
+
try {
|
|
387
|
+
const filePath = join(tmpDir, 'write-then-read.txt');
|
|
388
|
+
// Write
|
|
389
|
+
await new Promise<void>((resolve, reject) => {
|
|
390
|
+
const ws = createWriteStream(filePath);
|
|
391
|
+
ws.on('error', reject);
|
|
392
|
+
ws.end('written content', () => resolve());
|
|
393
|
+
});
|
|
394
|
+
// Read
|
|
395
|
+
const chunks: string[] = [];
|
|
396
|
+
await new Promise<void>((resolve, reject) => {
|
|
397
|
+
const rs = createReadStream(filePath, { encoding: 'utf8' });
|
|
398
|
+
rs.on('data', (chunk) => chunks.push(chunk as string));
|
|
399
|
+
rs.on('end', () => resolve());
|
|
400
|
+
rs.on('error', reject);
|
|
401
|
+
});
|
|
402
|
+
expect(chunks.join('')).toBe('written content');
|
|
403
|
+
} finally {
|
|
404
|
+
cleanup();
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// ---- Unicode and special content ----
|
|
410
|
+
|
|
411
|
+
await describe('fs streams: unicode and binary', async () => {
|
|
412
|
+
await it('should handle UTF-8 content with multibyte chars', async () => {
|
|
413
|
+
setup();
|
|
414
|
+
try {
|
|
415
|
+
const filePath = join(tmpDir, 'unicode.txt');
|
|
416
|
+
const content = 'Hallo Welt! 你好世界 🌍🎉';
|
|
417
|
+
writeFileSync(filePath, content);
|
|
418
|
+
const chunks: string[] = [];
|
|
419
|
+
await new Promise<void>((resolve, reject) => {
|
|
420
|
+
const stream = createReadStream(filePath, { encoding: 'utf8' });
|
|
421
|
+
stream.on('data', (chunk) => chunks.push(chunk as string));
|
|
422
|
+
stream.on('end', () => resolve());
|
|
423
|
+
stream.on('error', reject);
|
|
424
|
+
});
|
|
425
|
+
expect(chunks.join('')).toBe(content);
|
|
426
|
+
} finally {
|
|
427
|
+
cleanup();
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
await it('should handle binary data round-trip', async () => {
|
|
432
|
+
setup();
|
|
433
|
+
try {
|
|
434
|
+
const filePath = join(tmpDir, 'binary.dat');
|
|
435
|
+
const data = Buffer.from([0x00, 0x01, 0x80, 0xff, 0xfe, 0x7f, 0x00, 0x42]);
|
|
436
|
+
writeFileSync(filePath, data);
|
|
437
|
+
const chunks: Buffer[] = [];
|
|
438
|
+
await new Promise<void>((resolve, reject) => {
|
|
439
|
+
const stream = createReadStream(filePath);
|
|
440
|
+
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk as Uint8Array)));
|
|
441
|
+
stream.on('end', () => resolve());
|
|
442
|
+
stream.on('error', reject);
|
|
443
|
+
});
|
|
444
|
+
const result = Buffer.concat(chunks);
|
|
445
|
+
expect(result.length).toBe(8);
|
|
446
|
+
expect(result[0]).toBe(0x00);
|
|
447
|
+
expect(result[2]).toBe(0x80);
|
|
448
|
+
expect(result[3]).toBe(0xff);
|
|
449
|
+
expect(result[7]).toBe(0x42);
|
|
450
|
+
} finally {
|
|
451
|
+
cleanup();
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
};
|
package/src/symlink.spec.ts
CHANGED
|
@@ -1,38 +1,188 @@
|
|
|
1
|
+
// Ported from refs/node-test/parallel/test-fs-symlink.js,
|
|
2
|
+
// test-fs-readlink.js, test-fs-realpath.js
|
|
3
|
+
// Original: MIT license, Node.js contributors
|
|
1
4
|
import { describe, it, expect } from '@gjsify/unit';
|
|
2
|
-
|
|
3
|
-
|
|
5
|
+
import {
|
|
6
|
+
symlink as symlinkCb,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
rmdirSync,
|
|
9
|
+
unlinkSync,
|
|
10
|
+
lstatSync,
|
|
11
|
+
readlinkSync,
|
|
12
|
+
realpathSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from 'node:fs';
|
|
15
|
+
import { symlink, readlink, realpath, unlink, rmdir, lstat, writeFile, mkdtemp } from 'node:fs/promises';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
4
18
|
|
|
5
|
-
|
|
6
|
-
|
|
19
|
+
export default async () => {
|
|
20
|
+
await describe('fs.symlink (callback)', async () => {
|
|
7
21
|
|
|
8
|
-
|
|
9
|
-
|
|
22
|
+
await it('should throw when no callback provided', async () => {
|
|
23
|
+
expect(() => {
|
|
24
|
+
// @ts-ignore
|
|
25
|
+
symlinkCb('some/path', 'some/other/path', 'dir');
|
|
26
|
+
}).toThrow(Error);
|
|
27
|
+
});
|
|
10
28
|
|
|
11
|
-
|
|
12
|
-
|
|
29
|
+
await it('should create a symlink pointing to a file', async () => {
|
|
30
|
+
const dir = mkdtempSync(join(tmpdir(), 'fs-sym-cb-'));
|
|
31
|
+
const target = join(dir, 'target.txt');
|
|
32
|
+
const link = join(dir, 'link.txt');
|
|
33
|
+
writeFileSync(target, 'symlink target');
|
|
34
|
+
|
|
35
|
+
await new Promise<void>((resolve, reject) => {
|
|
36
|
+
symlinkCb(target, link, (err) => {
|
|
37
|
+
if (err) return reject(err);
|
|
38
|
+
try {
|
|
39
|
+
const s = lstatSync(link);
|
|
40
|
+
expect(s.isSymbolicLink()).toBe(true);
|
|
41
|
+
resolve();
|
|
42
|
+
} catch (e) {
|
|
43
|
+
reject(e);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
unlinkSync(link);
|
|
49
|
+
unlinkSync(target);
|
|
50
|
+
rmdirSync(dir);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await describe('fs.symlink (promise)', async () => {
|
|
55
|
+
await it('should create a symlink pointing to a file', async () => {
|
|
56
|
+
const dir = await mkdtemp(join(tmpdir(), 'fs-sym-p-'));
|
|
57
|
+
const target = join(dir, 'target.txt');
|
|
58
|
+
const link = join(dir, 'link.txt');
|
|
59
|
+
await writeFile(target, 'symlink target content');
|
|
60
|
+
|
|
61
|
+
await symlink(target, link);
|
|
62
|
+
|
|
63
|
+
const s = lstatSync(link);
|
|
64
|
+
expect(s.isSymbolicLink()).toBe(true);
|
|
65
|
+
|
|
66
|
+
await unlink(link);
|
|
67
|
+
await unlink(target);
|
|
68
|
+
await rmdir(dir);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await it('should throw ENOENT when symlinking to non-existent target', async () => {
|
|
72
|
+
const dir = await mkdtemp(join(tmpdir(), 'fs-sym-err-'));
|
|
73
|
+
const link = join(dir, 'link.txt');
|
|
74
|
+
// Creating a dangling symlink — target doesn't exist
|
|
75
|
+
await symlink('/nonexistent/path/12345abc', link);
|
|
76
|
+
const s = await lstat(link);
|
|
77
|
+
expect(s.isSymbolicLink()).toBe(true);
|
|
78
|
+
await unlink(link);
|
|
79
|
+
await rmdir(dir);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await it('should throw EEXIST when symlink already exists', async () => {
|
|
83
|
+
const dir = await mkdtemp(join(tmpdir(), 'fs-sym-exist-'));
|
|
84
|
+
const target = join(dir, 'target.txt');
|
|
85
|
+
const link = join(dir, 'link.txt');
|
|
86
|
+
await writeFile(target, 'data');
|
|
87
|
+
await symlink(target, link);
|
|
88
|
+
|
|
89
|
+
let threw = false;
|
|
90
|
+
try {
|
|
91
|
+
await symlink(target, link);
|
|
92
|
+
} catch (e: unknown) {
|
|
93
|
+
threw = true;
|
|
94
|
+
expect((e as NodeJS.ErrnoException).code).toBe('EEXIST');
|
|
95
|
+
}
|
|
96
|
+
expect(threw).toBe(true);
|
|
97
|
+
|
|
98
|
+
await unlink(link);
|
|
99
|
+
await unlink(target);
|
|
100
|
+
await rmdir(dir);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await describe('fs.readlink', async () => {
|
|
105
|
+
await it('should read the symlink target synchronously', async () => {
|
|
106
|
+
const dir = mkdtempSync(join(tmpdir(), 'fs-rl-'));
|
|
107
|
+
const target = join(dir, 'target.txt');
|
|
108
|
+
const link2 = join(dir, 'link2.txt');
|
|
109
|
+
writeFileSync(target, 'data');
|
|
110
|
+
|
|
111
|
+
await new Promise<void>(resolve => {
|
|
112
|
+
symlinkCb(target, link2, () => {
|
|
113
|
+
const dest = readlinkSync(link2);
|
|
114
|
+
expect(dest).toBe(target);
|
|
115
|
+
unlinkSync(link2);
|
|
116
|
+
unlinkSync(target);
|
|
117
|
+
rmdirSync(dir);
|
|
118
|
+
resolve();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await it('should read the symlink target via promise', async () => {
|
|
124
|
+
const dir = await mkdtemp(join(tmpdir(), 'fs-rl-p-'));
|
|
125
|
+
const target = join(dir, 'target.txt');
|
|
126
|
+
const link = join(dir, 'link.txt');
|
|
127
|
+
await writeFile(target, 'data');
|
|
128
|
+
await symlink(target, link);
|
|
129
|
+
|
|
130
|
+
const dest = await readlink(link);
|
|
131
|
+
expect(dest).toBe(target);
|
|
13
132
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}).toThrow(Error)
|
|
19
|
-
});
|
|
133
|
+
await unlink(link);
|
|
134
|
+
await unlink(target);
|
|
135
|
+
await rmdir(dir);
|
|
136
|
+
});
|
|
20
137
|
|
|
21
|
-
|
|
22
|
-
|
|
138
|
+
await it('should throw ENOENT when reading non-existent symlink', async () => {
|
|
139
|
+
let threw = false;
|
|
140
|
+
try {
|
|
141
|
+
await readlink('/nonexistent/path/no-link-xyz');
|
|
142
|
+
} catch (e: unknown) {
|
|
143
|
+
threw = true;
|
|
144
|
+
expect((e as NodeJS.ErrnoException).code).toBe('ENOENT');
|
|
145
|
+
}
|
|
146
|
+
expect(threw).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
23
149
|
|
|
24
|
-
|
|
25
|
-
|
|
150
|
+
await describe('fs.realpath', async () => {
|
|
151
|
+
await it('should resolve a symlink to real path (sync)', async () => {
|
|
152
|
+
const dir = mkdtempSync(join(tmpdir(), 'fs-rp-'));
|
|
153
|
+
const target = join(dir, 'realfile.txt');
|
|
154
|
+
const link = join(dir, 'link.txt');
|
|
155
|
+
writeFileSync(target, 'realpath test');
|
|
26
156
|
|
|
27
|
-
|
|
28
|
-
|
|
157
|
+
await new Promise<void>(resolve => {
|
|
158
|
+
symlinkCb(target, link, () => {
|
|
159
|
+
const real = realpathSync(link);
|
|
160
|
+
// realpath resolves symlinks to actual path
|
|
161
|
+
expect(typeof real).toBe('string');
|
|
162
|
+
expect(real.length).toBeGreaterThan(0);
|
|
163
|
+
unlinkSync(link);
|
|
164
|
+
resolve();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
29
167
|
|
|
30
|
-
|
|
168
|
+
unlinkSync(target);
|
|
169
|
+
rmdirSync(dir);
|
|
170
|
+
});
|
|
31
171
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
172
|
+
await it('should resolve a symlink to real path (promise)', async () => {
|
|
173
|
+
const dir = await mkdtemp(join(tmpdir(), 'fs-rp-p-'));
|
|
174
|
+
const target = join(dir, 'realfile.txt');
|
|
175
|
+
const link = join(dir, 'link.txt');
|
|
176
|
+
await writeFile(target, 'realpath test');
|
|
177
|
+
await symlink(target, link);
|
|
36
178
|
|
|
179
|
+
const real = await realpath(link);
|
|
180
|
+
expect(typeof real).toBe('string');
|
|
181
|
+
expect(real.length).toBeGreaterThan(0);
|
|
37
182
|
|
|
38
|
-
|
|
183
|
+
await unlink(link);
|
|
184
|
+
await unlink(target);
|
|
185
|
+
await rmdir(dir);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
};
|