@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.
Files changed (91) hide show
  1. package/README.md +31 -2
  2. package/lib/esm/callback.js +251 -15
  3. package/lib/esm/dirent.js +47 -6
  4. package/lib/esm/encoding.js +2 -3
  5. package/lib/esm/errors.js +13 -0
  6. package/lib/esm/file-handle.js +108 -66
  7. package/lib/esm/fs-watcher.js +44 -7
  8. package/lib/esm/index.js +140 -5
  9. package/lib/esm/promises.js +290 -69
  10. package/lib/esm/read-stream.js +82 -57
  11. package/lib/esm/stats.js +138 -18
  12. package/lib/esm/sync.js +293 -44
  13. package/lib/esm/write-stream.js +4 -4
  14. package/lib/types/callback.d.ts +233 -0
  15. package/lib/types/dirent.d.ts +77 -0
  16. package/lib/types/encoding.d.ts +6 -0
  17. package/lib/types/errors.d.ts +7 -0
  18. package/lib/types/file-handle.d.ts +367 -0
  19. package/lib/types/fs-watcher.d.ts +17 -0
  20. package/lib/types/index.d.ts +149 -0
  21. package/lib/types/promises.d.ts +158 -0
  22. package/lib/types/read-stream.d.ts +21 -0
  23. package/lib/types/stats.d.ts +67 -0
  24. package/lib/types/sync.d.ts +109 -0
  25. package/lib/types/types/encoding-option.d.ts +2 -0
  26. package/lib/types/types/file-read-options.d.ts +15 -0
  27. package/lib/types/types/file-read-result.d.ts +4 -0
  28. package/lib/types/types/flag-and-open-mode.d.ts +5 -0
  29. package/lib/types/types/index.d.ts +6 -0
  30. package/lib/types/types/open-flags.d.ts +1 -0
  31. package/lib/types/types/read-options.d.ts +5 -0
  32. package/lib/types/utils.d.ts +2 -0
  33. package/lib/types/write-stream.d.ts +45 -0
  34. package/package.json +22 -35
  35. package/src/callback.spec.ts +284 -30
  36. package/src/callback.ts +352 -39
  37. package/src/dirent.ts +56 -8
  38. package/src/encoding.ts +7 -2
  39. package/src/errors.spec.ts +389 -0
  40. package/src/errors.ts +19 -0
  41. package/src/extended.spec.ts +706 -0
  42. package/src/file-handle.spec.ts +104 -23
  43. package/src/file-handle.ts +147 -79
  44. package/src/fs-watcher.ts +55 -8
  45. package/src/index.ts +146 -2
  46. package/src/new-apis.spec.ts +505 -0
  47. package/src/promises.spec.ts +651 -11
  48. package/src/promises.ts +353 -81
  49. package/src/read-stream.ts +98 -74
  50. package/src/stat.spec.ts +22 -14
  51. package/src/stats.ts +176 -75
  52. package/src/streams.spec.ts +455 -0
  53. package/src/symlink.spec.ts +176 -26
  54. package/src/sync.spec.ts +204 -32
  55. package/src/sync.ts +363 -58
  56. package/src/test.mts +7 -2
  57. package/src/types/encoding-option.ts +1 -1
  58. package/src/types/flag-and-open-mode.ts +1 -1
  59. package/src/types/read-options.ts +2 -2
  60. package/src/utils.ts +2 -0
  61. package/src/write-stream.ts +9 -7
  62. package/tsconfig.json +23 -10
  63. package/tsconfig.tsbuildinfo +1 -0
  64. package/lib/cjs/callback.js +0 -112
  65. package/lib/cjs/dirent.js +0 -98
  66. package/lib/cjs/encoding.js +0 -34
  67. package/lib/cjs/file-handle.js +0 -444
  68. package/lib/cjs/fs-watcher.js +0 -50
  69. package/lib/cjs/index.js +0 -95
  70. package/lib/cjs/promises.js +0 -160
  71. package/lib/cjs/read-stream.js +0 -78
  72. package/lib/cjs/stats.js +0 -45
  73. package/lib/cjs/sync.js +0 -126
  74. package/lib/cjs/types/encoding-option.js +0 -0
  75. package/lib/cjs/types/file-read-options.js +0 -0
  76. package/lib/cjs/types/file-read-result.js +0 -0
  77. package/lib/cjs/types/flag-and-open-mode.js +0 -0
  78. package/lib/cjs/types/index.js +0 -6
  79. package/lib/cjs/types/open-flags.js +0 -0
  80. package/lib/cjs/types/read-options.js +0 -0
  81. package/lib/cjs/utils.js +0 -18
  82. package/lib/cjs/write-stream.js +0 -116
  83. package/test/watch.js +0 -1
  84. package/test.gjs.js +0 -35359
  85. package/test.gjs.js.map +0 -7
  86. package/test.gjs.mjs +0 -40534
  87. package/test.gjs.mjs.meta.json +0 -1
  88. package/test.node.js +0 -1479
  89. package/test.node.js.map +0 -7
  90. package/test.node.mjs +0 -710
  91. 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
+ };
@@ -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
- // import { join, dirname } from 'path';
3
- // import { fileURLToPath } from "url";
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
- // const __filename = fileURLToPath(import.meta.url)
6
- // const __dirname = dirname(__filename)
19
+ export default async () => {
20
+ await describe('fs.symlink (callback)', async () => {
7
21
 
8
- import { symlink as symlinkCb, mkdtempSync, lstatSync } from 'fs';
9
- import { symlink, rmdir, unlink } from 'fs/promises';
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
- export default async () => {
12
- await describe('fs.symlink', async () => {
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
- await it('ASYNC: no callback function results in Error', () => {
15
- expect(() => {
16
- // @ts-ignore
17
- symlinkCb("some/path", "some/other/path", "dir");
18
- }).toThrow(Error)
19
- });
133
+ await unlink(link);
134
+ await unlink(target);
135
+ await rmdir(dir);
136
+ });
20
137
 
21
- // TODO FIXME
22
- // await it('ASYNC: create symlink point to a dir', async () => {
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
- // const tmpDir = mkdtempSync('test_tmp_');
25
- // const linkedTmpDir = tmpDir + ".link";
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
- // await symlink(tmpDir, linkedTmpDir)
28
- // const stat = lstatSync(linkedTmpDir);
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
- // expect(stat.isSymbolicLink).toBeTruthy();
168
+ unlinkSync(target);
169
+ rmdirSync(dir);
170
+ });
31
171
 
32
- // await rmdir(tmpDir);
33
- // await unlink(linkedTmpDir);
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
+ };