@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
package/src/sync.ts CHANGED
@@ -1,23 +1,20 @@
1
+ // Reference: Node.js lib/fs.js (sync API)
2
+ // Reimplemented for GJS using Gio.File synchronous operations
3
+
1
4
  import GLib from '@girs/glib-2.0';
2
5
  import Gio from '@girs/gio-2.0';
3
6
  import { existsSync } from '@gjsify/utils';
4
- import { Buffer } from 'buffer';
5
- import { join } from 'path';
7
+ import { Buffer } from 'node:buffer';
8
+ import { join } from 'node:path';
6
9
 
7
10
  import FSWatcher from './fs-watcher.js';
8
11
  import { getEncodingFromOptions, encodeUint8Array, decode } from './encoding.js';
9
12
  import { FileHandle } from './file-handle.js';
10
13
  import { Dirent } from './dirent.js';
14
+ import { Stats, BigIntStats, STAT_ATTRIBUTES } from './stats.js';
15
+ import { createNodeError, isNotFoundError } from './errors.js';
11
16
  import { tempDirPath } from './utils.js';
12
17
 
13
- export { realpathSync } from '@gjsify/deno_std/node/_fs/_fs_realpath';
14
- import { readdirSync } from '@gjsify/deno_std/node/_fs/_fs_readdir';
15
- export { symlinkSync } from '@gjsify/deno_std/node/_fs/_fs_symlink';
16
- export { lstatSync } from '@gjsify/deno_std/node/_fs/_fs_lstat';
17
- export { statSync } from '@gjsify/deno_std/node/_fs/_fs_stat';
18
-
19
-
20
-
21
18
  import type { OpenFlags, EncodingOption } from './types/index.js';
22
19
  import type {
23
20
  PathLike,
@@ -26,21 +23,131 @@ import type {
26
23
  BufferEncodingOption,
27
24
  RmOptions,
28
25
  RmDirOptions,
29
- } from 'fs'; // Types from @types/node
26
+ StatSyncOptions,
27
+ } from 'node:fs'; // Types from @types/node
28
+
29
+ export { existsSync }
30
+
31
+ // --- stat / lstat ---
32
+
33
+ export function statSync(path: PathLike, options?: StatSyncOptions): Stats | BigIntStats | undefined {
34
+ try {
35
+ const file = Gio.File.new_for_path(path.toString());
36
+ const info = file.query_info(STAT_ATTRIBUTES, Gio.FileQueryInfoFlags.NONE, null);
37
+ return options?.bigint ? new BigIntStats(info, path) : new Stats(info, path);
38
+ } catch (err: unknown) {
39
+ if (options?.throwIfNoEntry === false && isNotFoundError(err)) return undefined;
40
+ throw createNodeError(err, 'stat', path);
41
+ }
42
+ }
43
+
44
+ export function lstatSync(path: PathLike, options?: StatSyncOptions): Stats | BigIntStats | undefined {
45
+ try {
46
+ const file = Gio.File.new_for_path(path.toString());
47
+ const info = file.query_info(STAT_ATTRIBUTES, Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
48
+ return options?.bigint ? new BigIntStats(info, path) : new Stats(info, path);
49
+ } catch (err: unknown) {
50
+ if (options?.throwIfNoEntry === false && isNotFoundError(err)) return undefined;
51
+ throw createNodeError(err, 'lstat', path);
52
+ }
53
+ }
54
+
55
+ // --- readdir ---
56
+
57
+ export function readdirSync(
58
+ path: PathLike,
59
+ options?: { withFileTypes?: boolean; encoding?: string; recursive?: boolean }
60
+ ): string[] | Dirent[] {
61
+ const pathStr = path.toString();
62
+ const file = Gio.File.new_for_path(pathStr);
63
+ const enumerator = file.enumerate_children(
64
+ 'standard::name,standard::type',
65
+ Gio.FileQueryInfoFlags.NONE,
66
+ null,
67
+ );
68
+
69
+ const result: (string | Dirent)[] = [];
70
+ let info = enumerator.next_file(null);
71
+
72
+ while (info !== null) {
73
+ const childName = info.get_name();
74
+ const childPath = join(pathStr, childName);
75
+
76
+ if (options?.withFileTypes) {
77
+ result.push(new Dirent(childPath, childName));
78
+ } else {
79
+ result.push(childName);
80
+ }
81
+
82
+ if (options?.recursive && info.get_file_type() === Gio.FileType.DIRECTORY) {
83
+ const subEntries = readdirSync(childPath, options);
84
+ for (const entry of subEntries) {
85
+ if (typeof entry === 'string') {
86
+ result.push(join(childName, entry));
87
+ } else {
88
+ result.push(entry);
89
+ }
90
+ }
91
+ }
92
+
93
+ info = enumerator.next_file(null);
94
+ }
95
+
96
+ return result as string[] | Dirent[];
97
+ }
98
+
99
+ // --- realpath ---
100
+
101
+ const MAX_SYMLINK_DEPTH = 40; // matches Linux MAXSYMLINKS
102
+
103
+ export function realpathSync(path: PathLike): string {
104
+ let current = Gio.File.new_for_path(path.toString());
105
+ let depth = 0;
106
+
107
+ while (true) {
108
+ const info = current.query_info(
109
+ 'standard::is-symlink,standard::symlink-target',
110
+ Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
111
+ null,
112
+ );
30
113
 
31
- export { existsSync, readdirSync }
114
+ if (!info.get_is_symlink()) {
115
+ return current.get_path()!;
116
+ }
117
+
118
+ const target = info.get_symlink_target()!;
119
+ const parent = current.get_parent();
120
+ current = parent ? parent.resolve_relative_path(target) : Gio.File.new_for_path(target);
121
+
122
+ if (++depth > MAX_SYMLINK_DEPTH) {
123
+ throw new Error(`ELOOP: too many levels of symbolic links, realpath '${path}'`);
124
+ }
125
+ }
126
+ }
127
+ (realpathSync as unknown as { native: typeof realpathSync }).native = realpathSync;
128
+
129
+ // --- symlink ---
130
+
131
+ export function symlinkSync(target: PathLike, path: PathLike, _type?: 'file' | 'dir' | 'junction'): void {
132
+ const file = Gio.File.new_for_path(path.toString());
133
+ file.make_symbolic_link(target.toString(), null);
134
+ }
32
135
 
33
136
  export function readFileSync(path: string, options = { encoding: null, flag: 'r' }) {
34
137
  const file = Gio.File.new_for_path(path);
35
138
 
36
- const [ok, data] = file.load_contents(null);
139
+ try {
140
+ const [ok, data] = file.load_contents(null);
37
141
 
38
- if (!ok) {
39
- // TODO: throw a better error
40
- throw new Error('failed to read file');
41
- }
142
+ if (!ok) {
143
+ throw createNodeError(new Error('failed to read file'), 'read', path);
144
+ }
42
145
 
43
- return encodeUint8Array(getEncodingFromOptions(options, "buffer"), data);
146
+ return encodeUint8Array(getEncodingFromOptions(options, "buffer"), data);
147
+ } catch (err: unknown) {
148
+ if ((err as { code?: unknown }).code && typeof (err as { code?: unknown }).code === 'string') throw err; // Already a Node error
149
+ throw createNodeError(err, 'read', path);
150
+ }
44
151
  }
45
152
 
46
153
  /**
@@ -104,18 +211,60 @@ export function mkdirSync(path: PathLike, options?: Mode | MakeDirectoryOptions
104
211
  throw new TypeError("mode as string is currently not supported!");
105
212
  }
106
213
 
107
- if (GLib.mkdir_with_parents(path, mode) !== 0) {
108
- // TODO: throw a better error
109
- throw new Error(`failed to make ${path} directory`);
214
+ if (recursive) {
215
+ return mkdirSyncRecursive(path, mode as number);
110
216
  }
111
217
 
112
- if (recursive) {
113
- // TODO: Returns `undefined`, or if `recursive` is`true`, the first directory path created.
114
- return path.split('/')[0];
218
+ // Non-recursive: create a single directory
219
+ const file = Gio.File.new_for_path(path);
220
+ try {
221
+ file.make_directory(null);
222
+ } catch (err: unknown) {
223
+ throw createNodeError(err, 'mkdir', path);
115
224
  }
116
225
  return undefined;
117
226
  }
118
227
 
228
+ /**
229
+ * Recursively creates directories, similar to `mkdir -p`.
230
+ * Returns the first directory path created, or undefined if all directories already existed.
231
+ */
232
+ function mkdirSyncRecursive(pathStr: string, mode: number): string | undefined {
233
+ const file = Gio.File.new_for_path(pathStr);
234
+
235
+ // Try to create the directory directly
236
+ try {
237
+ file.make_directory(null);
238
+ // This directory was created successfully — it's a candidate for "first created"
239
+ return pathStr;
240
+ } catch (err: unknown) {
241
+ const gErr = err as { code?: number };
242
+ // If it already exists, nothing to create
243
+ if (gErr.code === Gio.IOErrorEnum.EXISTS) {
244
+ return undefined;
245
+ }
246
+ // If parent doesn't exist, create parent first then retry
247
+ if (gErr.code === Gio.IOErrorEnum.NOT_FOUND) {
248
+ const parentPath = join(pathStr, '..');
249
+ const resolvedParent = Gio.File.new_for_path(parentPath).get_path()!;
250
+ if (resolvedParent === pathStr) {
251
+ // Reached root, cannot go further
252
+ throw createNodeError(err, 'mkdir', pathStr);
253
+ }
254
+ const firstCreated = mkdirSyncRecursive(resolvedParent, mode);
255
+ // Now create this directory
256
+ const retryFile = Gio.File.new_for_path(pathStr);
257
+ try {
258
+ retryFile.make_directory(null);
259
+ } catch (retryErr: unknown) {
260
+ throw createNodeError(retryErr, 'mkdir', pathStr);
261
+ }
262
+ return firstCreated ?? pathStr;
263
+ }
264
+ throw createNodeError(err, 'mkdir', pathStr);
265
+ }
266
+ }
267
+
119
268
  /**
120
269
  * Synchronous [`rmdir(2)`](http://man7.org/linux/man-pages/man2/rmdir.2.html). Returns `undefined`.
121
270
  *
@@ -125,41 +274,191 @@ export function mkdirSync(path: PathLike, options?: Mode | MakeDirectoryOptions
125
274
  * To get a behavior similar to the `rm -rf` Unix command, use {@link rmSync} with options `{ recursive: true, force: true }`.
126
275
  * @since v0.1.21
127
276
  */
128
- export function rmdirSync(path: PathLike, options?: RmDirOptions): void {
277
+ export function rmdirSync(path: PathLike, _options?: RmDirOptions): void {
278
+ const file = Gio.File.new_for_path(path.toString());
279
+ try {
280
+ // Check if it's a directory
281
+ const info = file.query_info('standard::type', Gio.FileQueryInfoFlags.NONE, null);
282
+ if (info.get_file_type() !== Gio.FileType.DIRECTORY) {
283
+ const err = Object.assign(new Error(), { code: 4 }); // Gio.IOErrorEnum.NOT_DIRECTORY
284
+ throw createNodeError(err, 'rmdir', path);
285
+ }
286
+ // Check if empty — rmdir only removes empty directories (use rmSync for recursive)
287
+ const enumerator = file.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
288
+ if (enumerator.next_file(null) !== null) {
289
+ const err = Object.assign(new Error(), { code: 5 }); // Gio.IOErrorEnum.NOT_EMPTY
290
+ throw createNodeError(err, 'rmdir', path);
291
+ }
292
+ file.delete(null);
293
+ } catch (err: unknown) {
294
+ if ((err as { code?: unknown }).code && typeof (err as { code?: unknown }).code === 'string') throw err; // Already a Node error
295
+ throw createNodeError(err, 'rmdir', path);
296
+ }
297
+ }
129
298
 
130
- const recursive = options?.recursive || false;
299
+ export function unlinkSync(path: PathLike): void {
300
+ const file = Gio.File.new_for_path(path.toString());
301
+ try {
302
+ file.delete(null);
303
+ } catch (err: unknown) {
304
+ throw createNodeError(err, 'unlink', path);
305
+ }
306
+ }
307
+
308
+ export function writeFileSync(path: string, data: string | Uint8Array) {
309
+ GLib.file_set_contents(path, data);
310
+ }
311
+
312
+ // --- rename ---
313
+
314
+ export function renameSync(oldPath: PathLike, newPath: PathLike): void {
315
+ const src = Gio.File.new_for_path(oldPath.toString());
316
+ const dest = Gio.File.new_for_path(newPath.toString());
317
+ try {
318
+ src.move(dest, Gio.FileCopyFlags.OVERWRITE, null, null);
319
+ } catch (err: unknown) {
320
+ throw createNodeError(err, 'rename', oldPath, newPath);
321
+ }
322
+ }
323
+
324
+ // --- copyFile ---
325
+
326
+ export function copyFileSync(src: PathLike, dest: PathLike, mode?: number): void {
327
+ const srcFile = Gio.File.new_for_path(src.toString());
328
+ const destFile = Gio.File.new_for_path(dest.toString());
329
+ let flags = Gio.FileCopyFlags.NONE;
330
+ // mode 0 = default (overwrite), COPYFILE_EXCL (1) = no overwrite
331
+ if (mode && (mode & 1) === 0) {
332
+ flags = Gio.FileCopyFlags.OVERWRITE;
333
+ } else if (!mode) {
334
+ flags = Gio.FileCopyFlags.OVERWRITE;
335
+ }
336
+ try {
337
+ srcFile.copy(destFile, flags, null, null);
338
+ } catch (err: unknown) {
339
+ throw createNodeError(err, 'copyfile', src, dest);
340
+ }
341
+ }
342
+
343
+ // --- access ---
344
+
345
+ export function accessSync(path: PathLike, mode?: number): void {
346
+ const file = Gio.File.new_for_path(path.toString());
347
+ try {
348
+ const info = file.query_info('access::*', Gio.FileQueryInfoFlags.NONE, null);
349
+ // mode: F_OK=0, R_OK=4, W_OK=2, X_OK=1
350
+ if (mode !== undefined && mode !== 0) {
351
+ // Gio.IOErrorEnum.PERMISSION_DENIED = 14 → maps to EACCES via createNodeError
352
+ const permErr = { code: 14, message: `permission denied, access '${path}'` };
353
+ if ((mode & 4) && !info.get_attribute_boolean('access::can-read')) {
354
+ throw createNodeError(permErr, 'access', path);
355
+ }
356
+ if ((mode & 2) && !info.get_attribute_boolean('access::can-write')) {
357
+ throw createNodeError(permErr, 'access', path);
358
+ }
359
+ if ((mode & 1) && !info.get_attribute_boolean('access::can-execute')) {
360
+ throw createNodeError(permErr, 'access', path);
361
+ }
362
+ }
363
+ } catch (err: unknown) {
364
+ if ((err as { code?: unknown }).code && typeof (err as { code?: unknown }).code === 'string') throw err; // Already a Node-style error
365
+ throw createNodeError(err, 'access', path);
366
+ }
367
+ }
131
368
 
132
- const childFiles = readdirSync(path, { withFileTypes: true });
369
+ // --- appendFile ---
133
370
 
134
- if (!recursive && childFiles.length) {
135
- throw new Error('Dir is not empty!');
371
+ export function appendFileSync(path: PathLike, data: string | Uint8Array, options?: { encoding?: string; mode?: number; flag?: string } | string): void {
372
+ const file = Gio.File.new_for_path(path.toString());
373
+ let bytes: Uint8Array;
374
+ if (typeof data === 'string') {
375
+ bytes = new TextEncoder().encode(data);
376
+ } else {
377
+ bytes = data;
136
378
  }
137
379
 
138
- for (const childFile of childFiles) {
139
- if (childFile.isDirectory()) {
140
- rmdirSync(join(path.toString(), childFile.name));
141
- } else if (childFile.isFile()) {
142
- rmSync(join(path.toString(), childFile.name));
380
+ try {
381
+ const stream = file.append_to(Gio.FileCreateFlags.NONE, null);
382
+ if (bytes.length > 0) {
383
+ stream.write_bytes(new GLib.Bytes(bytes), null);
143
384
  }
385
+ stream.close(null);
386
+ } catch (err: unknown) {
387
+ throw createNodeError(err, 'appendfile', path);
144
388
  }
389
+ }
145
390
 
146
- const result = GLib.rmdir(path.toString());
391
+ // --- readlink ---
147
392
 
148
- if (result !== 0) {
149
- // TODO: throw a better error
150
- throw new Error(`Failed to remove ${path} directory`);
393
+ export function readlinkSync(path: PathLike, options?: { encoding?: string } | string): string | Buffer {
394
+ const file = Gio.File.new_for_path(path.toString());
395
+ try {
396
+ const info = file.query_info('standard::symlink-target', Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
397
+ const target = info.get_symlink_target();
398
+ if (!target) {
399
+ throw Object.assign(new Error(`EINVAL: invalid argument, readlink '${path}'`), { code: 'EINVAL', errno: -22, syscall: 'readlink', path: path.toString() });
400
+ }
401
+ const encoding = typeof options === 'string' ? options : options?.encoding;
402
+ if (encoding === 'buffer') {
403
+ return Buffer.from(target);
404
+ }
405
+ return target;
406
+ } catch (err: unknown) {
407
+ if (typeof (err as { code?: unknown }).code === 'string') throw err;
408
+ throw createNodeError(err, 'readlink', path);
151
409
  }
152
410
  }
153
411
 
154
- export function unlinkSync(path: string) {
155
- GLib.unlink(path);
412
+ // --- link ---
413
+
414
+ export function linkSync(existingPath: PathLike, newPath: PathLike): void {
415
+ // Gio doesn't have a direct hard link API, use GLib
416
+ const result = GLib.spawn_command_line_sync(`ln ${existingPath.toString()} ${newPath.toString()}`);
417
+ if (!result[0]) {
418
+ throw Object.assign(new Error(`EPERM: operation not permitted, link '${existingPath}' -> '${newPath}'`), { code: 'EPERM', errno: -1, syscall: 'link', path: existingPath.toString(), dest: newPath.toString() });
419
+ }
156
420
  }
157
421
 
158
- export function writeFileSync(path: string, data: any) {
159
- GLib.file_set_contents(path, data);
422
+ // --- truncate ---
423
+
424
+ export function truncateSync(path: PathLike, len?: number): void {
425
+ const file = Gio.File.new_for_path(path.toString());
426
+ try {
427
+ const stream = file.replace(null, false, Gio.FileCreateFlags.NONE, null);
428
+ if (len && len > 0) {
429
+ // Read existing content, truncate to len
430
+ const [, data] = file.load_contents(null);
431
+ const truncated = data.slice(0, len);
432
+ if (truncated.length > 0) {
433
+ stream.write_bytes(new GLib.Bytes(truncated), null);
434
+ }
435
+ }
436
+ stream.close(null);
437
+ } catch (err: unknown) {
438
+ throw createNodeError(err, 'truncate', path);
439
+ }
160
440
  }
161
441
 
162
- export function watch(filename: string, options, listener) {
442
+ // --- chmodSync ---
443
+
444
+ export function chmodSync(path: PathLike, mode: Mode): void {
445
+ const modeNum = typeof mode === 'string' ? parseInt(mode, 8) : mode;
446
+ const result = GLib.spawn_command_line_sync(`chmod ${modeNum.toString(8)} ${path.toString()}`);
447
+ if (!result[0]) {
448
+ throw Object.assign(new Error(`EPERM: operation not permitted, chmod '${path}'`), { code: 'EPERM', errno: -1, syscall: 'chmod', path: path.toString() });
449
+ }
450
+ }
451
+
452
+ // --- chownSync ---
453
+
454
+ export function chownSync(path: PathLike, uid: number, gid: number): void {
455
+ const result = GLib.spawn_command_line_sync(`chown ${uid}:${gid} ${path.toString()}`);
456
+ if (!result[0]) {
457
+ throw Object.assign(new Error(`EPERM: operation not permitted, chown '${path}'`), { code: 'EPERM', errno: -1, syscall: 'chown', path: path.toString() });
458
+ }
459
+ }
460
+
461
+ export function watch(filename: string, options: { persistent?: boolean; recursive?: boolean; encoding?: string } | undefined, listener: ((eventType: string, filename: string | null) => void) | undefined) {
163
462
  return new FSWatcher(filename, options, listener);
164
463
  }
165
464
 
@@ -208,32 +507,38 @@ export function mkdtempSync(prefix: string, options?: EncodingOption | BufferEnc
208
507
  * @since v14.14.0
209
508
  */
210
509
  export function rmSync(path: PathLike, options?: RmOptions): void {
211
- const file = Gio.File.new_for_path(path.toString());
510
+ const pathStr = path.toString();
511
+ const file = Gio.File.new_for_path(pathStr);
212
512
  const recursive = options?.recursive || false;
213
-
214
- const dirent = new Dirent(path.toString());
513
+ const force = options?.force || false;
514
+
515
+ let dirent: Dirent;
516
+ try {
517
+ dirent = new Dirent(pathStr);
518
+ } catch (err: unknown) {
519
+ if (force && isNotFoundError(err)) return;
520
+ throw createNodeError(err, 'rm', path);
521
+ }
215
522
 
216
523
  if (dirent.isDirectory()) {
217
524
  const childFiles = readdirSync(path, { withFileTypes: true });
218
525
 
219
526
  if (!recursive && childFiles.length) {
220
- throw new Error('Dir is not empty!');
527
+ const err = Object.assign(new Error(), { code: 5 }); // Gio.IOErrorEnum.NOT_EMPTY
528
+ throw createNodeError(err, 'rm', path);
221
529
  }
222
-
530
+
223
531
  for (const childFile of childFiles) {
224
- if (childFile.isDirectory()) {
225
- rmdirSync(join(path.toString(), childFile.name), options);
226
- } else if (childFile.isFile()) {
227
- rmSync(join(path.toString(), childFile.name), options);
532
+ if (typeof childFile !== 'string') {
533
+ rmSync(join(pathStr, childFile.name), options);
228
534
  }
229
535
  }
230
536
  }
231
537
 
232
- const ok = file.delete(null);
233
-
234
- if (!ok) {
235
- // TODO: throw a better error
236
- const err = new Error('failed to remove file ' + path);
237
- throw err;
538
+ try {
539
+ file.delete(null);
540
+ } catch (err: unknown) {
541
+ if (force && isNotFoundError(err)) return;
542
+ throw createNodeError(err, 'rm', path);
238
543
  }
239
544
  }
package/src/test.mts CHANGED
@@ -1,4 +1,4 @@
1
-
1
+ import '@gjsify/node-globals';
2
2
  import { run } from '@gjsify/unit';
3
3
 
4
4
  import testSuiteCallback from './callback.spec.js';
@@ -7,5 +7,10 @@ import testSuitePromise from './promises.spec.js';
7
7
  import testSuiteSync from './sync.spec.js';
8
8
  import testSuiteSymlink from './symlink.spec.js';
9
9
  import testSuiteStat from './stat.spec.js';
10
+ import testSuiteNewApis from './new-apis.spec.js';
11
+ import testSuiteExtended from './extended.spec.js';
12
+
13
+ import testSuiteErrors from './errors.spec.js';
14
+ import testSuiteStreams from './streams.spec.js';
10
15
 
11
- run({testSuiteCallback, testSuiteFileHandle, testSuitePromise, testSuiteSync, testSuiteSymlink, testSuiteStat});
16
+ run({testSuiteCallback, testSuiteFileHandle, testSuitePromise, testSuiteSync, testSuiteSymlink, testSuiteStat, testSuiteNewApis, testSuiteExtended, testSuiteErrors, testSuiteStreams});
@@ -1,3 +1,3 @@
1
- import type { ObjectEncodingOptions } from 'fs'; // Types from @types/node
1
+ import type { ObjectEncodingOptions } from 'node:fs'; // Types from @types/node
2
2
 
3
3
  export type EncodingOption = ObjectEncodingOptions | BufferEncoding | undefined | null;
@@ -1,4 +1,4 @@
1
- import type { Mode, OpenMode } from 'fs';
1
+ import type { Mode, OpenMode } from 'node:fs';
2
2
 
3
3
  export interface FlagAndOpenMode {
4
4
  mode?: Mode | undefined;
@@ -1,5 +1,5 @@
1
- import type { Abortable } from 'events';
2
- import type { ObjectEncodingOptions, OpenMode } from 'fs'; // Types from @types/node
1
+ import type { Abortable } from 'node:events';
2
+ import type { ObjectEncodingOptions, OpenMode } from 'node:fs'; // Types from @types/node
3
3
 
4
4
  export type ReadOptions =
5
5
  | (ObjectEncodingOptions & Abortable & {
package/src/utils.ts CHANGED
@@ -1,3 +1,5 @@
1
+ // Shared filesystem utilities for GJS — original implementation using Gio
2
+
1
3
  import { existsSync } from './sync.js';
2
4
 
3
5
  const CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
@@ -1,12 +1,16 @@
1
- import { Writable } from "stream";
2
- import { fileURLToPath, URL } from "url";
1
+ // SPDX-License-Identifier: MIT
2
+ // Adapted from Deno (refs/deno/ext/node/polyfills/internal/fs/streams.ts)
3
+ // Copyright (c) 2018-2026 the Deno authors. MIT license.
4
+ // Modifications: Rewritten to use Gio.File for GJS
5
+
6
+ import { Writable } from "node:stream";
7
+ import { fileURLToPath, URL } from "node:url";
3
8
  import { open, write, close } from "./callback.js";
4
9
 
5
10
  import type { OpenFlags } from './types/index.js';
6
- import type { PathLike, WriteStream as IWriteStream } from 'fs';
7
- import type { CreateWriteStreamOptions } from 'fs/promises'; // Types from @types/node
11
+ import type { PathLike, WriteStream as IWriteStream } from 'node:fs';
12
+ import type { CreateWriteStreamOptions } from 'node:fs/promises'; // Types from @types/node
8
13
 
9
- // From Deno
10
14
  const kIsPerformingIO = Symbol("kIsPerformingIO");
11
15
  const kIoDone = Symbol("kIoDone");
12
16
 
@@ -18,8 +22,6 @@ export function toPathIfFileURL(
18
22
  }
19
23
  return fileURLToPath(fileURLOrPath);
20
24
  }
21
-
22
- // Credits https://github.com/denoland/deno_std/blob/main/node/internal/fs/streams.ts
23
25
  export class WriteStream extends Writable implements IWriteStream {
24
26
 
25
27
  /**
package/tsconfig.json CHANGED
@@ -1,18 +1,31 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "module": "ESNext",
4
- "types": ["node"],
5
4
  "target": "ESNext",
6
- "moduleResolution": "NodeNext",
5
+ "moduleResolution": "bundler",
6
+ "types": [
7
+ "node"
8
+ ],
7
9
  "experimentalDecorators": true,
10
+ "emitDeclarationOnly": true,
11
+ "declaration": true,
12
+ "allowImportingTsExtensions": true,
8
13
  "outDir": "lib",
9
14
  "rootDir": "src",
10
- "composite": true
15
+ "declarationDir": "lib/types",
16
+ "composite": true,
17
+ "skipLibCheck": true,
18
+ "allowJs": true,
19
+ "checkJs": false,
20
+ "strict": false
11
21
  },
12
- "skipLibCheck": true,
13
- "allowJs": true,
14
- "checkJs": false,
15
- "reflection": false,
16
- "include": ["src/**/*.ts"],
17
- "exclude": ["src/test.mts", "src/**/*.spec.ts"]
18
- }
22
+ "include": [
23
+ "src/**/*.ts"
24
+ ],
25
+ "exclude": [
26
+ "src/test.ts",
27
+ "src/test.mts",
28
+ "src/**/*.spec.ts",
29
+ "src/**/*.spec.mts"
30
+ ]
31
+ }