@ix-xs/node-comfort 1.2.0 → 2.1.0

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 (78) hide show
  1. package/CHANGELOG.md +340 -0
  2. package/README.md +137 -548
  3. package/index.js +80 -35
  4. package/package.json +161 -33
  5. package/src/Arr.js +1050 -418
  6. package/src/Async.js +817 -0
  7. package/src/Cache.js +433 -0
  8. package/src/Checker.js +1202 -539
  9. package/src/Cli.js +1293 -0
  10. package/src/Color.js +556 -0
  11. package/src/Crypto.js +1082 -0
  12. package/src/Emitter.js +356 -0
  13. package/src/Env.js +689 -0
  14. package/src/FS.js +1626 -894
  15. package/src/Func.js +823 -356
  16. package/src/Http.js +700 -0
  17. package/src/Id.js +568 -200
  18. package/src/Logger.js +963 -378
  19. package/src/Num.js +965 -360
  20. package/src/Obj.js +1077 -379
  21. package/src/SQLite.js +1629 -1005
  22. package/src/Schema.js +2101 -0
  23. package/src/Str.js +1209 -432
  24. package/src/Sys.js +654 -0
  25. package/src/Time.js +1533 -440
  26. package/src/Utils.js +342 -325
  27. package/src/errors.js +335 -0
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +735 -526
  30. package/types/src/Arr.d.ts +533 -145
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +372 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +221 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +651 -232
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +664 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +300 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +620 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +189 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +412 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +717 -238
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +508 -135
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +444 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +199 -66
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +414 -46
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +514 -137
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +453 -104
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +754 -308
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +1189 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +617 -166
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +314 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +759 -204
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +158 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +316 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/types/src/internal/shared.d.ts +123 -0
  77. package/types/src/internal/shared.d.ts.map +1 -0
  78. package/types/index.d.ts.map +0 -1
package/src/FS.js CHANGED
@@ -1,894 +1,1626 @@
1
- const { readdirSync, existsSync, mkdirSync, rmSync, writeFileSync, unlinkSync, copyFileSync, readFileSync, appendFileSync, statSync, watch } = require("node:fs");
2
- const { resolve, dirname, sep, isAbsolute, join, relative, basename } = require("node:path");
3
- const { createHash } = require("node:crypto");
4
-
5
- /**
6
- * Normalizes a path to the current platform separator.
7
- * @private
8
- * @param {string} path - The path to normalize.
9
- * @returns {string} The normalized path.
10
- */
11
- const _format = (path) => {
12
- return path.replace(/\//g, sep).replace(/\\/g, sep);
13
- };
14
-
15
- /**
16
- * Returns the calling file path, excluding internal and node_modules frames.
17
- * @private
18
- * @returns {string|undefined} The caller file path, or undefined if not found.
19
- */
20
- const _caller = () => {
21
- const opst = Error.prepareStackTrace;
22
- Error.prepareStackTrace = (_, stack) => stack;
23
- const e = new Error();
24
- const s = e.stack;
25
- Error.prepareStackTrace = opst;
26
-
27
- if (s) {
28
- for (const cs of s) {
29
- const name = cs.getFileName();
30
- if (
31
- name &&
32
- !name.startsWith("node:internal") &&
33
- !name.includes("node_modules") &&
34
- name !== s[0].getFileName()
35
- ) {
36
- return _format(name);
37
- }
38
- }
39
- }
40
- return undefined;
41
- };
42
-
43
- /**
44
- * Recursively walks folders or files from a path.
45
- * @private
46
- * @param {string} [path=process.cwd()] - The base path to scan.
47
- * @param {boolean} [recursive=true] - Whether to recurse into subfolders.
48
- * @param {"folder"|"file"} [type="folder"] - The entry type to collect.
49
- * @returns {string[]} A list of matching absolute paths.
50
- */
51
- const _walk = (path = process.cwd(), recursive = true, type = "folder") => {
52
- let result = [];
53
- try {
54
- for (const entry of readdirSync(path, { withFileTypes: true })) {
55
- if (entry.name.includes("node_modules")) continue;
56
- const _ = resolve(path, entry.name);
57
-
58
- if (type === "folder" && entry.isDirectory()) {
59
- result.push(_);
60
- if (recursive) result = result.concat(_walk(_, recursive, type));
61
- } else if (type === "file" && entry.isFile()) {
62
- result.push(_);
63
- } else if (type === "file" && entry.isDirectory() && recursive) {
64
- result = result.concat(_walk(_, recursive, type));
65
- }
66
- }
67
- return result;
68
- } catch {
69
- return result;
70
- }
71
- };
72
-
73
- /**
74
- * Finds the first folder matching a given name.
75
- * @private
76
- * @param {string} path - The folder to search from.
77
- * @param {string} name - The folder name to find.
78
- * @returns {string|undefined} The matching folder path, or undefined if not found.
79
- */
80
- const _firstFolder = (path, name) => {
81
- try {
82
- const entries = readdirSync(path, { withFileTypes: true });
83
- for (const entry of entries) {
84
- if (entry.name === name && entry.isDirectory()) {
85
- return resolve(path, name);
86
- }
87
- }
88
- for (const entry of entries) {
89
- if (entry.isDirectory() && !entry.name.includes("node_modules")) {
90
- const result = _firstFolder(resolve(path, entry.name), name);
91
- if (result) return result;
92
- }
93
- }
94
- } catch {}
95
- return undefined;
96
- };
97
-
98
- /**
99
- * Finds the first file matching a given name.
100
- * @private
101
- * @param {string} path - The folder to search from.
102
- * @param {string} name - The file name to find.
103
- * @returns {string|undefined} The matching file path, or undefined if not found.
104
- */
105
- const _firstFile = (path, name) => {
106
- try {
107
- const entries = readdirSync(path, { withFileTypes: true });
108
- for (const entry of entries) {
109
- if (entry.name === name && entry.isFile()) {
110
- return resolve(path, name);
111
- }
112
- }
113
- for (const entry of entries) {
114
- if (entry.isDirectory() && !entry.name.includes("node_modules")) {
115
- const result = _firstFile(resolve(path, entry.name), name);
116
- if (result) return result;
117
- }
118
- }
119
- } catch {}
120
- return undefined;
121
- };
122
-
123
- /**
124
- * Resolves a user path into an existing absolute path when possible.
125
- * @private
126
- * @param {string} [path=process.cwd()] - The path to resolve.
127
- * @param {"folder"|"file"} [type="folder"] - The expected target type.
128
- * @returns {string|undefined} The resolved absolute path, or undefined if it does not exist.
129
- */
130
- const _target = (path = process.cwd(), type = "folder") => {
131
- if (path === process.cwd()) return path;
132
-
133
- let absolute;
134
- const caller = dirname(_caller() || process.cwd());
135
-
136
- if (
137
- _format(path).startsWith(_format("./")) ||
138
- _format(path).startsWith(_format("../"))
139
- ) {
140
- absolute = resolve(caller, path);
141
- } else if (isAbsolute(path)) {
142
- absolute = _format(path);
143
- } else {
144
- absolute = resolve(process.cwd(), _format(path));
145
- if (!existsSync(absolute)) {
146
- absolute =
147
- type === "folder"
148
- ? _firstFolder(process.cwd(), _format(path)) ?? undefined
149
- : _firstFile(process.cwd(), _format(path)) ?? undefined;
150
- }
151
- }
152
-
153
- if (absolute && existsSync(absolute)) return _format(absolute);
154
- return undefined;
155
- };
156
-
157
- try {
158
- process.loadEnvFile();
159
- } catch {}
160
-
161
- /**
162
- * File-system helper with safe path resolution and high-level operations.
163
- */
164
- module.exports = {
165
- /**
166
- * Reads an environment variable from process.env.
167
- * @param {string} value - The environment variable name.
168
- * @param {string} [envFilePath] - Reserved for future use.
169
- * @returns {string|undefined} The environment value, or undefined if not set.
170
- */
171
- getEnv(value, envFilePath) {
172
- return process.env[value];
173
- },
174
-
175
- /**
176
- * Resolves a path even when it does not already exist.
177
- * @param {string} [path=process.cwd()] - The path to resolve.
178
- * @returns {string} The resolved path.
179
- */
180
- createPath(path = process.cwd()) {
181
- const target = _target(path);
182
- return target
183
- ? target
184
- : isAbsolute(path)
185
- ? path
186
- : path.startsWith("./") || path.startsWith("../")
187
- ? resolve(dirname(_caller()), path)
188
- : resolve(process.cwd(), path);
189
- },
190
-
191
- /**
192
- * Returns folders found inside a path.
193
- * @param {string} [path=process.cwd()] - The base path to inspect.
194
- * @param {boolean} [recursive=true] - Whether to scan recursively.
195
- * @returns {string[]|undefined} Matching folders, or undefined if the path cannot be resolved.
196
- */
197
- getFoldersIn(path = process.cwd(), recursive = true) {
198
- const target = _target(path ?? process.cwd());
199
- return target ? _walk(target, recursive) : undefined;
200
- },
201
-
202
- /**
203
- * Returns the resolved folder path.
204
- * @param {string} [path=process.cwd()] - The folder path to resolve.
205
- * @returns {string|undefined} The resolved folder path, or undefined if not found.
206
- */
207
- getFolder(path = process.cwd()) {
208
- return _target(path ?? process.cwd());
209
- },
210
-
211
- /**
212
- * Returns files found inside a path.
213
- * @param {string} [path=process.cwd()] - The base path to inspect.
214
- * @param {boolean} [recursive=true] - Whether to scan recursively.
215
- * @returns {string[]|undefined} Matching files, or undefined if the path cannot be resolved.
216
- */
217
- getFilesIn(path = process.cwd(), recursive = true) {
218
- const target = _target(path ?? process.cwd());
219
- return target ? _walk(target, recursive, "file") : undefined;
220
- },
221
-
222
- /**
223
- * Returns the resolved file path.
224
- * @param {string} path - The file path to resolve.
225
- * @returns {string|undefined} The resolved file path, or undefined if not found.
226
- */
227
- getFile(path) {
228
- return path ? _target(path, "file") : path;
229
- },
230
-
231
- /**
232
- * Creates a folder.
233
- * @param {string} path - The folder path to create.
234
- * @param {boolean} [force=false] - Whether to recreate the folder if it already exists.
235
- * @returns {boolean|undefined} True on success, false on failure, undefined if no path was provided.
236
- */
237
- createFolder(path, force = false) {
238
- if (!path) return undefined;
239
- const target = this.createPath(path);
240
-
241
- try {
242
- if (this.getFolder(target)) {
243
- if (force) {
244
- this.deleteFolder(target);
245
- mkdirSync(target, { recursive: true });
246
- return true;
247
- }
248
- return false;
249
- }
250
- mkdirSync(target, { recursive: true });
251
- return true;
252
- } catch {
253
- return false;
254
- }
255
- },
256
-
257
- /**
258
- * Creates a file.
259
- * @param {string} path - The file path to create.
260
- * @param {boolean} [force=false] - Whether to recreate the file if it already exists.
261
- * @param {string|object|null|undefined} [data] - File content to write.
262
- * @returns {boolean|undefined} True on success, false on failure, undefined if no path was provided.
263
- */
264
- createFile(path, force = false, data) {
265
- if (!path) return undefined;
266
- const target = this.createPath(path);
267
- const content =
268
- data === null || typeof data === "undefined"
269
- ? ""
270
- : typeof data === "string"
271
- ? data
272
- : JSON.stringify(data, null, 2);
273
-
274
- try {
275
- if (this.getFile(target)) {
276
- if (force) {
277
- this.deleteFile(target);
278
- writeFileSync(target, content, "utf8");
279
- return true;
280
- }
281
- return false;
282
- }
283
-
284
- if (!this.getFolder(dirname(target))) {
285
- this.createFolder(dirname(target));
286
- }
287
-
288
- writeFileSync(target, content, "utf8");
289
- return true;
290
- } catch {
291
- return false;
292
- }
293
- },
294
-
295
- /**
296
- * Deletes folders found inside a path.
297
- * @param {string} [path=process.cwd()] - The base path to inspect.
298
- * @param {(folder: string) => boolean} [filter=() => true] - Filter applied to each folder path.
299
- * @returns {number|undefined} Number of deleted folders, or undefined if the path cannot be resolved.
300
- */
301
- deleteFoldersIn(path = process.cwd(), filter = () => true) {
302
- const target = _target(path);
303
- if (!target) return undefined;
304
-
305
- const folders = this.getFoldersIn(target);
306
- let deleted = 0;
307
-
308
- for (const folder of folders.map(_format)) {
309
- if (!filter(folder)) continue;
310
- this.deleteFolder(folder);
311
- deleted++;
312
- }
313
- return deleted;
314
- },
315
-
316
- /**
317
- * Deletes a folder.
318
- * @param {string} path - The folder path to delete.
319
- * @returns {boolean|undefined} True on success, false on failure, undefined if the path is invalid.
320
- */
321
- deleteFolder(path) {
322
- const target = path ? _target(path) : path;
323
- if (!target || !this.getFolder(target)) return undefined;
324
-
325
- try {
326
- rmSync(target, { recursive: true, force: true });
327
- return true;
328
- } catch {
329
- return false;
330
- }
331
- },
332
-
333
- /**
334
- * Deletes files found inside a path.
335
- * @param {string} [path=process.cwd()] - The base path to inspect.
336
- * @param {boolean} [recursive=false] - Whether to scan recursively.
337
- * @param {(file: string) => boolean} [filter=() => true] - Filter applied to each file path.
338
- * @returns {number|undefined} Number of deleted files, or undefined if the path cannot be resolved.
339
- */
340
- deleteFilesIn(path = process.cwd(), recursive = false, filter = () => true) {
341
- const target = _target(path);
342
- if (!target) return undefined;
343
-
344
- const files = this.getFilesIn(target, recursive);
345
- let deleted = 0;
346
-
347
- for (const file of files.map(_format)) {
348
- if (!filter(file)) continue;
349
- this.deleteFile(file);
350
- deleted++;
351
- }
352
- return deleted;
353
- },
354
-
355
- /**
356
- * Deletes a file.
357
- * @param {string} path - The file path to delete.
358
- * @returns {boolean|undefined} True on success, false on failure, undefined if the path is invalid.
359
- */
360
- deleteFile(path) {
361
- const target = path ? _target(path, "file") : path;
362
- if (!target || !this.getFile(target)) return undefined;
363
-
364
- try {
365
- unlinkSync(target);
366
- return true;
367
- } catch {
368
- return false;
369
- }
370
- },
371
-
372
- /**
373
- * Copies folders from a source path into a destination folder.
374
- * @param {Object} options - Copy options.
375
- * @param {string} options.dest - Destination path.
376
- * @param {string} [options.path] - Source path.
377
- * @param {boolean} [options.recursive=true] - Whether to recurse into subfolders.
378
- * @param {boolean} [options.withFiles=false] - Whether to copy files as well.
379
- * @param {boolean} [options.force=false] - Whether to overwrite existing folders.
380
- * @param {(folder: string) => boolean} [options.filter=() => true] - Filter applied to each folder.
381
- * @returns {number|undefined} Number of copied folders, or undefined if invalid input.
382
- */
383
- copyFoldersIn(options) {
384
- const target = _target(options.path ?? process.cwd());
385
- if (!target || !options.dest) return undefined;
386
-
387
- const folders = this.getFoldersIn(target, options.recursive ?? true) ?? [];
388
- options.filter =
389
- typeof options.filter === "function" ? options.filter : () => true;
390
- let copied = 0;
391
-
392
- for (const folder of folders.map(_format)) {
393
- if (!options.filter(folder)) continue;
394
-
395
- const destFolder = join(
396
- this.createPath(options.dest),
397
- relative(target, folder),
398
- );
399
-
400
- if (options.force && this.getFolder(destFolder)) {
401
- this.deleteFolder(destFolder);
402
- }
403
-
404
- this.createFolder(destFolder);
405
- copied++;
406
-
407
- if (options.withFiles) {
408
- if (options.recursive) {
409
- for (const file of this.getFilesIn(folder, true) ?? []) {
410
- const destFile = join(destFolder, relative(folder, file));
411
- this.createFolder(dirname(destFile));
412
- copyFileSync(file, destFile);
413
- }
414
- } else {
415
- for (const file of this.getFilesIn(folder, false) ?? []) {
416
- const destFile = join(destFolder, basename(file));
417
- copyFileSync(file, destFile);
418
- }
419
- }
420
- }
421
- }
422
- return copied;
423
- },
424
-
425
- /**
426
- * Copies a folder to a destination path.
427
- * @param {Object} options - Copy options.
428
- * @param {string} options.dest - Destination path.
429
- * @param {string} [options.path] - Source path.
430
- * @param {boolean} [options.recursive=false] - Whether to copy subfolders.
431
- * @param {boolean} [options.withFiles=false] - Whether to copy files.
432
- * @param {boolean} [options.force=false] - Whether to overwrite the destination.
433
- * @returns {boolean|undefined} True on success, false on failure, undefined if invalid input.
434
- */
435
- copyFolder(options) {
436
- const target = _target(options.path ?? process.cwd());
437
- const dest = this.createPath(options.dest);
438
-
439
- if (!target || !dest) return undefined;
440
-
441
- if (options.force && this.getFolder(dest)) {
442
- this.deleteFolder(dest);
443
- }
444
-
445
- this.createFolder(dest);
446
-
447
- if (options.recursive) {
448
- for (const folder of this.getFoldersIn(target, true) ?? []) {
449
- const destFolder = join(dest, relative(target, folder));
450
- this.createFolder(destFolder);
451
-
452
- if (options.withFiles) {
453
- for (const file of this.getFilesIn(folder, false) ?? []) {
454
- const destFile = join(destFolder, basename(file));
455
- copyFileSync(file, destFile);
456
- }
457
- }
458
- }
459
-
460
- if (options.withFiles) {
461
- for (const file of this.getFilesIn(target, false) ?? []) {
462
- const destFile = join(dest, basename(file));
463
- copyFileSync(file, destFile);
464
- }
465
- }
466
- } else {
467
- if (options.withFiles) {
468
- for (const file of this.getFilesIn(target, false) ?? []) {
469
- const destFile = join(dest, basename(file));
470
- copyFileSync(file, destFile);
471
- }
472
- }
473
- }
474
- return true;
475
- },
476
-
477
- /**
478
- * Copies files from a source path into a destination folder.
479
- * @param {Object} options - Copy options.
480
- * @param {string} options.dest - Destination path.
481
- * @param {string} [options.path] - Source path.
482
- * @param {boolean} [options.recursive=true] - Whether to recurse into subfolders.
483
- * @param {boolean} [options.force=false] - Whether to overwrite existing files.
484
- * @param {(file: string) => boolean} [options.filter=() => true] - Filter applied to each file.
485
- * @returns {number|undefined} Number of copied files, or undefined if invalid input.
486
- */
487
- copyFilesIn(options) {
488
- const target = _target(options.path ?? process.cwd());
489
- const destBase = this.createPath(options.dest);
490
-
491
- if (!target || !options.dest) return undefined;
492
-
493
- const recursive = options.recursive ?? true;
494
- const files = this.getFilesIn(target, recursive) ?? [];
495
- const filter =
496
- typeof options.filter === "function" ? options.filter : () => true;
497
- let copied = 0;
498
-
499
- for (const file of files.map(_format)) {
500
- if (!filter(file)) continue;
501
-
502
- const destFile = join(
503
- destBase,
504
- recursive ? relative(target, file) : basename(file),
505
- );
506
-
507
- if (options.force && this.getFile(destFile)) {
508
- this.deleteFile(destFile);
509
- }
510
-
511
- this.createFolder(dirname(destFile));
512
- copyFileSync(file, destFile);
513
- copied++;
514
- }
515
- return copied;
516
- },
517
-
518
- /**
519
- * Copies a file to a destination path.
520
- * @param {Object} options - Copy options.
521
- * @param {string} options.path - Source file path.
522
- * @param {string} options.dest - Destination path.
523
- * @param {boolean} [options.force=false] - Whether to overwrite the destination file.
524
- * @returns {boolean|undefined} True on success, false on failure, undefined if invalid input.
525
- */
526
- copyFile(options) {
527
- const target = _target(options.path, "file");
528
- if (!target || !options.dest) return undefined;
529
-
530
- let destFile;
531
- if (
532
- !basename(options.dest) ||
533
- options.dest.endsWith("/") ||
534
- options.dest.endsWith("\\")
535
- ) {
536
- destFile = join(this.createPath(options.dest), basename(target));
537
- } else {
538
- destFile = this.createPath(options.dest);
539
- }
540
-
541
- if (options.force && this.getFile(destFile)) {
542
- this.deleteFile(destFile);
543
- }
544
-
545
- this.createFolder(dirname(destFile));
546
- copyFileSync(target, destFile);
547
- return true;
548
- },
549
-
550
- /**
551
- * Moves folders from a source path into a destination folder.
552
- * @param {Object} options - Move options.
553
- * @param {string} options.dest - Destination path.
554
- * @param {string} [options.path] - Source path.
555
- * @param {boolean} [options.recursive=true] - Whether to recurse into subfolders.
556
- * @param {boolean} [options.withFiles=false] - Whether to move files as well.
557
- * @param {boolean} [options.force=false] - Whether to overwrite existing folders.
558
- * @param {(folder: string) => boolean} [options.filter=() => true] - Filter applied to each folder.
559
- * @returns {number|undefined} Number of copied folders, or undefined if invalid input.
560
- */
561
- moveFoldersIn(options) {
562
- const copied = this.copyFoldersIn(options);
563
- if (copied === undefined || !options.path) return copied;
564
-
565
- const target = _target(options.path);
566
- if (!target) return copied;
567
-
568
- const folders = this.getFoldersIn(target, options.recursive ?? true) ?? [];
569
- options.filter =
570
- typeof options.filter === "function" ? options.filter : () => true;
571
-
572
- for (const folder of folders.map(_format)) {
573
- if (!options.filter(folder)) continue;
574
- this.deleteFolder(folder);
575
- }
576
- return copied;
577
- },
578
-
579
- /**
580
- * Moves a folder to a destination path.
581
- * @param {Object} options - Move options.
582
- * @param {string} options.path - Source folder path.
583
- * @param {string} options.dest - Destination path.
584
- * @param {boolean} [options.recursive=false] - Whether to move subfolders.
585
- * @param {boolean} [options.withFiles=false] - Whether to move files.
586
- * @param {boolean} [options.force=false] - Whether to overwrite the destination.
587
- * @returns {boolean|undefined} True on success, false on failure, undefined if invalid input.
588
- */
589
- moveFolder(options) {
590
- const copied = this.copyFolder(options);
591
- const target = _target(options.path);
592
- if (!target) return copied;
593
-
594
- this.deleteFolder(target);
595
- return copied;
596
- },
597
-
598
- /**
599
- * Moves files from a source path into a destination folder.
600
- * @param {Object} options - Move options.
601
- * @param {string} options.dest - Destination path.
602
- * @param {string} [options.path] - Source path.
603
- * @param {boolean} [options.recursive=true] - Whether to recurse into subfolders.
604
- * @param {boolean} [options.force=false] - Whether to overwrite existing files.
605
- * @param {(file: string) => boolean} [options.filter=() => true] - Filter applied to each file.
606
- * @returns {number|undefined} Number of copied files, or undefined if invalid input.
607
- */
608
- moveFilesIn(options) {
609
- const copied = this.copyFilesIn(options);
610
- if (copied === undefined || !options.path) return copied;
611
-
612
- const target = _target(options.path ?? process.cwd());
613
- if (!target) return copied;
614
-
615
- const files = this.getFilesIn(target, options.recursive ?? true) ?? [];
616
- const filter =
617
- typeof options.filter === "function" ? options.filter : () => true;
618
-
619
- for (const file of files.map(_format)) {
620
- if (!filter(file)) continue;
621
- this.deleteFile(file);
622
- }
623
- return copied;
624
- },
625
-
626
- /**
627
- * Moves a file to a destination path.
628
- * @param {Object} options - Move options.
629
- * @param {string} options.path - Source file path.
630
- * @param {string} options.dest - Destination path.
631
- * @param {boolean} [options.force=false] - Whether to overwrite the destination file.
632
- * @returns {boolean|undefined} True on success, false on failure, undefined if invalid input.
633
- */
634
- moveFile(options) {
635
- const moved = this.copyFile(options);
636
- if (moved) {
637
- const target = _target(options.path, "file");
638
- if (target) this.deleteFile(target);
639
- }
640
- return moved;
641
- },
642
-
643
- /**
644
- * Reads a file as UTF-8 text.
645
- * @param {string} path - The file path to read.
646
- * @returns {string|undefined} File content, or undefined if the file cannot be read.
647
- */
648
- readFile(path) {
649
- const target = _target(path, "file");
650
- if (!target) return undefined;
651
-
652
- try {
653
- return readFileSync(target, "utf8");
654
- } catch {
655
- return undefined;
656
- }
657
- },
658
-
659
- /**
660
- * Reads and parses a JSON file.
661
- * @template T
662
- * @param {string} path - The JSON file path.
663
- * @param {T} [fallback] - Value returned when the file is missing or invalid.
664
- * @returns {T|any|undefined} The parsed value, or the fallback.
665
- */
666
- readJSON(path, fallback) {
667
- const content = this.readFile(path);
668
- if (content === undefined) return fallback;
669
- try {
670
- return JSON.parse(content);
671
- } catch {
672
- return fallback;
673
- }
674
- },
675
-
676
- /**
677
- * Serializes a value to JSON and writes it to a file (creating folders as needed).
678
- * @param {string} path - The destination file path.
679
- * @param {*} data - The value to serialize.
680
- * @param {{ spaces?: number, force?: boolean }} [options] - Formatting options (`spaces` default 2).
681
- * @returns {boolean} True on success, false on failure.
682
- */
683
- writeJSON(path, data, options = {}) {
684
- const spaces = options.spaces ?? 2;
685
- try {
686
- const target = this.createPath(path);
687
- if (!this.getFolder(dirname(target))) this.createFolder(dirname(target));
688
- writeFileSync(target, JSON.stringify(data, null, spaces), "utf8");
689
- return true;
690
- } catch {
691
- return false;
692
- }
693
- },
694
-
695
- /**
696
- * Appends text to a file, creating it (and its folders) if necessary.
697
- * @param {string} path - The file path.
698
- * @param {string} data - The text to append.
699
- * @returns {boolean} True on success, false on failure.
700
- */
701
- appendFile(path, data) {
702
- try {
703
- const target = _target(path, "file") ?? this.createPath(path);
704
- if (!this.getFolder(dirname(target))) this.createFolder(dirname(target));
705
- appendFileSync(target, typeof data === "string" ? data : String(data), "utf8");
706
- return true;
707
- } catch {
708
- return false;
709
- }
710
- },
711
-
712
- /**
713
- * Checks whether a path (file or folder) exists.
714
- * @param {string} path - The path to check.
715
- * @returns {boolean} True if the path exists.
716
- */
717
- exists(path) {
718
- if (!path) return false;
719
- const direct = this.createPath(path);
720
- if (existsSync(direct)) return true;
721
- return Boolean(_target(path, "file") || _target(path, "folder"));
722
- },
723
-
724
- /**
725
- * Returns filesystem stats for a path, or undefined if it does not exist.
726
- * @param {string} path - The path to stat.
727
- * @returns {import("node:fs").Stats|undefined} The stats object, or undefined.
728
- */
729
- stat(path) {
730
- const target = _target(path, "file") ?? _target(path, "folder");
731
- if (!target) return undefined;
732
- try {
733
- return statSync(target);
734
- } catch {
735
- return undefined;
736
- }
737
- },
738
-
739
- /**
740
- * Returns the size of a file in bytes.
741
- * @param {string} path - The file path.
742
- * @returns {number|undefined} Size in bytes, or undefined if not found.
743
- */
744
- fileSize(path) {
745
- const stats = this.stat(path);
746
- return stats ? stats.size : undefined;
747
- },
748
-
749
- /**
750
- * Computes a hash digest of a file's contents.
751
- * @param {string} path - The file path.
752
- * @param {{ algorithm?: string, encoding?: "hex"|"base64"|"base64url" }} [options] - Hash options.
753
- * @returns {string|undefined} The digest, or undefined if the file cannot be read.
754
- */
755
- hashFile(path, options = {}) {
756
- const target = _target(path, "file");
757
- if (!target) return undefined;
758
- try {
759
- const buffer = readFileSync(target);
760
- return createHash(options.algorithm ?? "sha256").update(buffer).digest(options.encoding ?? "hex");
761
- } catch {
762
- return undefined;
763
- }
764
- },
765
-
766
- /**
767
- * Empties a folder's contents while keeping the folder itself.
768
- * @param {string} path - The folder to empty.
769
- * @returns {boolean|undefined} True on success, false on failure, undefined if not found.
770
- */
771
- emptyFolder(path) {
772
- const target = _target(path);
773
- if (!target) return undefined;
774
- try {
775
- for (const entry of readdirSync(target)) {
776
- rmSync(resolve(target, entry), { recursive: true, force: true });
777
- }
778
- return true;
779
- } catch {
780
- return false;
781
- }
782
- },
783
-
784
- /**
785
- * Watches a path and exposes event listeners.
786
- * @param {Object} [options] - Watch options.
787
- * @param {string} [options.path=process.cwd()] - Path to watch.
788
- * @param {boolean} [options.recursive=false] - Whether to watch recursively.
789
- * @param {(event: "rename"|"change", file: string) => boolean} [options.filter] - Filter applied before dispatch.
790
- * @returns {{
791
- * on: <E extends "change"|"rename"|"all">(event: E, callback: E extends "all" ? (event: "change"|"rename", file: string) => void|Promise<void> : (file: string) => void|Promise<void>) => any,
792
- * stop: () => void,
793
- * pause: () => any,
794
- * resume: () => any,
795
- * }|undefined} Watch controller, or undefined if the path cannot be resolved.
796
- */
797
- watch(options = {}) {
798
- const target = _target(options.path ?? process.cwd());
799
- if (!target) return undefined;
800
-
801
- let paused = false;
802
- const listeners = {};
803
-
804
- /**
805
- * Dispatches an event to registered listeners.
806
- * @private
807
- * @param {"rename"|"change"|"all"} event - The event type.
808
- * @param {string} file - The affected file path.
809
- */
810
- const handler = (event, file) => {
811
- if (paused) return;
812
- if (
813
- typeof options.filter === "function" &&
814
- !options.filter(event, file)
815
- ) {
816
- return;
817
- }
818
-
819
- if (listeners[event]) {
820
- for (const cb of listeners[event]) cb(file);
821
- }
822
- if (listeners["all"]) {
823
- for (const cb of listeners["all"]) cb(event, file);
824
- }
825
- };
826
-
827
- /**
828
- * Registers a listener for an event.
829
- * @private
830
- * @param {"change"|"rename"|"all"} type - The event type.
831
- * @param {(file: string) => void|Promise<void> | (event: "change"|"rename", file: string) => void|Promise<void>} callback - The callback to register.
832
- */
833
- const addListener = (type, callback) => {
834
- listeners[type] = listeners[type] ?? [];
835
- listeners[type].push(callback);
836
- };
837
-
838
- const watcher = watch(
839
- target,
840
- { recursive: options.recursive ?? false },
841
- (eventType, fileName) => {
842
- handler(eventType, fileName ? join(target, fileName) : target);
843
- },
844
- );
845
-
846
- /**
847
- * @typedef {"change"|"rename"|"all"} EventNames
848
- * @typedef {{
849
- * change: (file: string) => void|Promise<void>,
850
- * rename: (file: string) => void|Promise<void>,
851
- * all: (event: EventNames, file: string) => void|Promise<void>,
852
- * }} EventHandlers
853
- */
854
- return {
855
- /**
856
- * Registers a callback for a watch event.
857
- * @template {EventNames} E
858
- * @param {E} event - The event name.
859
- * @param {EventHandlers[E]} callback - The callback to execute.
860
- * @returns {this} The same watch controller.
861
- */
862
- on(event, callback) {
863
- addListener(event, callback);
864
- return this;
865
- },
866
- /**
867
- * Stops the underlying file watcher.
868
- */
869
- stop() {
870
- if (watcher) {
871
- try {
872
- watcher.close();
873
- } catch {}
874
- }
875
- },
876
- /**
877
- * Pauses event dispatching without closing the watcher.
878
- * @returns {this} The same watch controller.
879
- */
880
- pause() {
881
- paused = true;
882
- return this;
883
- },
884
- /**
885
- * Resumes event dispatching after a pause.
886
- * @returns {this} The same watch controller.
887
- */
888
- resume() {
889
- paused = false;
890
- return this;
891
- },
892
- };
893
- },
894
- };
1
+ "use strict";
2
+
3
+ /**
4
+ * Files and folders without try/catch: read and write text or JSON
5
+ * (atomically), copy, move, delete, list, glob, watch, hash.
6
+ *
7
+ * Paths: absolute paths are used as they are. Paths starting with `./` or
8
+ * `../` start from the file that calls the function, so a script works
9
+ * wherever it's run from. Other relative paths start from the working
10
+ * directory, like Node's `fs`.
11
+ *
12
+ * Everyday failures don't throw: you get `undefined` when something doesn't
13
+ * exist and `false` when an operation fails.
14
+ *
15
+ * @example
16
+ * nc.writeJSON("./data/settings.json", { theme: "dark" });
17
+ * const settings = nc.readJSON("./data/settings.json", {});
18
+ * const sources = nc.glob("src/**\/*.{js,ts}");
19
+ */
20
+
21
+ const fs = require("node:fs");
22
+ const os = require("node:os");
23
+ const nodePath = require("node:path");
24
+ const util = require("node:util");
25
+ const { fileURLToPath } = require("node:url");
26
+ const { sleepSync, toMs } = require("./internal/shared.js");
27
+
28
+ /**
29
+ * Options for `copyFoldersIn()` and `moveFoldersIn()`.
30
+ * @typedef {object} CopyFoldersOptions
31
+ * @property {string} dest Destination folder.
32
+ * @property {string} [path] Source folder. Defaults to the working directory.
33
+ * @property {boolean} [recursive] Include nested folders. Defaults to `true`.
34
+ * @property {boolean} [withFiles] Copy the files in each folder too.
35
+ * @property {boolean} [force] Delete an existing destination folder first.
36
+ * @property {(folder: string) => boolean} [filter] Keep a folder when it returns `true`. Gets absolute paths.
37
+ */
38
+
39
+ /**
40
+ * Options for `copyFolder()` and `moveFolder()`.
41
+ * @typedef {object} CopyFolderOptions
42
+ * @property {string} dest Destination folder.
43
+ * @property {string} [path] Source folder. Defaults to the working directory.
44
+ * @property {boolean} [recursive] Copy nested folders too.
45
+ * @property {boolean} [withFiles] Copy files too. `copy()` does all of this in one call.
46
+ * @property {boolean} [force] Delete an existing destination first.
47
+ */
48
+
49
+ /**
50
+ * Options for `copyFilesIn()` and `moveFilesIn()`.
51
+ * @typedef {object} CopyFilesOptions
52
+ * @property {string} dest Destination folder.
53
+ * @property {string} [path] Source folder. Defaults to the working directory.
54
+ * @property {boolean} [recursive] Include files from nested folders, keeping the tree. Defaults to `true`.
55
+ * @property {boolean} [force] Delete existing destination files first.
56
+ * @property {(file: string) => boolean} [filter] Keep a file when it returns `true`. Gets absolute paths.
57
+ */
58
+
59
+ /**
60
+ * Options for `copyFile()` and `moveFile()`.
61
+ * @typedef {object} CopyFileOptions
62
+ * @property {string} path Source file.
63
+ * @property {string} dest Destination file, or a folder if it ends with `/`.
64
+ * @property {boolean} [force] Delete the destination first.
65
+ */
66
+
67
+ /**
68
+ * Options for `copy()`.
69
+ * @typedef {object} CopyOptions
70
+ * @property {boolean} [overwrite] Replace existing files. Defaults to `true`.
71
+ * @property {(source: string, destination: string) => boolean} [filter] Return `false` to skip a file or folder.
72
+ * @property {boolean} [preserveTimestamps] Keep modification times.
73
+ */
74
+
75
+ /**
76
+ * Options for `writeFile()`.
77
+ * @typedef {object} WriteFileOptions
78
+ * @property {boolean} [atomic] Write to a temporary file and rename it, so nobody reads a half-written file and a crash can't corrupt it. Defaults to `true`.
79
+ * @property {BufferEncoding} [encoding] Defaults to `"utf8"`.
80
+ * @property {number} [mode] Permissions, like `0o600` for secrets. A file that already exists keeps the ones it has.
81
+ * @property {boolean} [fsync] Wait until the data is on the disk before returning. Slower, for files that must survive a power cut.
82
+ * @property {number} [spaces] Indentation when writing an object as JSON. Defaults to `2`.
83
+ */
84
+
85
+ /**
86
+ * Options for `writeJSON()`.
87
+ * @typedef {object} WriteJSONOptions
88
+ * @property {number} [spaces] Defaults to `2`.
89
+ * @property {boolean} [atomic] Defaults to `true`.
90
+ * @property {(key: string, value: unknown) => unknown} [replacer] Passed to `JSON.stringify`.
91
+ * @property {number} [mode] Permissions, like `0o600` for secrets. A file that already exists keeps the ones it has.
92
+ * @property {boolean} [fsync] Wait until the data is on the disk before returning.
93
+ */
94
+
95
+ /**
96
+ * Options for `readJSON()`.
97
+ * @typedef {object} ReadJSONOptions
98
+ * @property {boolean} [comments] Allow comments and trailing commas, like in `tsconfig.json`.
99
+ * @property {(key: string, value: any) => any} [reviver] Passed to `JSON.parse`.
100
+ */
101
+
102
+ /**
103
+ * Options for `getFilesIn()` and `getFoldersIn()`.
104
+ * @typedef {object} ListOptions
105
+ * @property {boolean} [recursive] Go into nested folders. Defaults to `true`.
106
+ * @property {boolean} [hidden] Include names starting with a dot. Defaults to `true`.
107
+ * @property {string[]} [ignore] Folder names to skip. Defaults to `["node_modules"]`.
108
+ */
109
+
110
+ /**
111
+ * Options for `glob()`.
112
+ * @typedef {object} GlobOptions
113
+ * @property {string} [cwd] Where the pattern starts. Defaults to the working directory.
114
+ * @property {boolean} [absolute] Return absolute paths.
115
+ * @property {boolean} [dot] Let `*` and `**` match names starting with a dot.
116
+ * @property {string[]} [ignore] Patterns to leave out. Defaults to `node_modules` and `.git`.
117
+ * @property {"files" | "folders" | "all"} [type] Defaults to `"files"`.
118
+ */
119
+
120
+ /**
121
+ * Options for `find()`.
122
+ * @typedef {object} FindOptions
123
+ * @property {string} [cwd] Where to start. Defaults to the working directory.
124
+ * @property {"file" | "folder" | "any"} [type] Defaults to `"file"`.
125
+ * @property {string[]} [ignore] Folder names to skip. Defaults to `node_modules` and `.git`.
126
+ */
127
+
128
+ /**
129
+ * Options for `watch()`.
130
+ * @typedef {object} WatchOptions
131
+ * @property {string} [path] What to watch. Defaults to the working directory.
132
+ * @property {boolean} [recursive] Watch nested folders too.
133
+ * @property {(event: "rename" | "change", file: string) => boolean} [filter] Return `false` to ignore an event.
134
+ * @property {number | string} [debounce] Merge events on the same file within this long, in ms or like `"100ms"`. Editors often save in several steps.
135
+ */
136
+
137
+ /**
138
+ * Returned by `watch()`.
139
+ * @typedef {object} Watcher
140
+ * @property {<E extends "change" | "rename" | "all" | "error">(event: E, callback: E extends "all" ? (event: "change" | "rename", file: string) => void : E extends "error" ? (error: Error) => void : (file: string) => void) => Watcher} on Listens for changes. `"all"` also gets the event name. `"error"` tells you the watch broke, for instance because the folder was deleted.
141
+ * @property {() => void} stop Stops watching.
142
+ * @property {() => Watcher} pause Ignores events until `resume()`.
143
+ * @property {() => Watcher} resume
144
+ */
145
+
146
+ const SRC_DIR = __dirname;
147
+ const INDEX_FILE = nodePath.join(__dirname, "..", "index.js");
148
+
149
+ /**
150
+ * The folder of the first caller outside node-comfort and node_modules.
151
+ * @returns {string}
152
+ */
153
+ const _callerDir = () => {
154
+ for (const frame of util.getCallSites(50)) {
155
+ let file = frame.scriptName;
156
+ if (!file) continue;
157
+ if (file.startsWith("file://")) file = fileURLToPath(file);
158
+ if (file.startsWith("node:") || file.includes("node_modules")) continue;
159
+ const resolved = nodePath.resolve(file);
160
+ if (resolved.startsWith(SRC_DIR + nodePath.sep) || resolved === INDEX_FILE) continue;
161
+ return nodePath.dirname(resolved);
162
+ }
163
+ return process.cwd();
164
+ };
165
+
166
+ /**
167
+ * @param {string | undefined} input
168
+ * @returns {string}
169
+ */
170
+ const _abs = (input) => {
171
+ if (input === undefined || input === "") return process.cwd();
172
+ const p = String(input);
173
+ if (nodePath.isAbsolute(p)) return nodePath.normalize(p);
174
+ if (/^\.\.?([\\/]|$)/.test(p)) return nodePath.resolve(_callerDir(), p);
175
+ return nodePath.resolve(process.cwd(), p);
176
+ };
177
+
178
+ /**
179
+ * The absolute path, if it exists and has the right type.
180
+ * @param {string | undefined} input
181
+ * @param {"file" | "folder" | "any"} type
182
+ * @returns {string | undefined}
183
+ */
184
+ const _existing = (input, type) => {
185
+ const p = _abs(input);
186
+ try {
187
+ const stats = fs.statSync(p);
188
+ if (type === "file" && !stats.isFile()) return undefined;
189
+ if (type === "folder" && !stats.isDirectory()) return undefined;
190
+ return p;
191
+ } catch {
192
+ return undefined;
193
+ }
194
+ };
195
+
196
+ /**
197
+ * @param {string} root
198
+ * @param {"file" | "folder"} type
199
+ * @param {Required<ListOptions>} options
200
+ * @returns {string[]}
201
+ */
202
+ const _walk = (root, type, options) => {
203
+ /** @type {string[]} */
204
+ const out = [];
205
+ _walkInto(root, type, options, new Set(options.ignore), out);
206
+ return out;
207
+ };
208
+
209
+ /**
210
+ * @param {string} dir
211
+ * @param {"file" | "folder"} type
212
+ * @param {Required<ListOptions>} options
213
+ * @param {Set<string>} ignore
214
+ * @param {string[]} out Filled in place: no array is built and thrown away at each level.
215
+ */
216
+ const _walkInto = (dir, type, options, ignore, out) => {
217
+ /** @type {fs.Dirent[]} */
218
+ let entries;
219
+ try {
220
+ entries = fs.readdirSync(dir, { withFileTypes: true });
221
+ } catch {
222
+ return;
223
+ }
224
+ for (const entry of entries) {
225
+ if (!options.hidden && entry.name.startsWith(".")) continue;
226
+ const full = nodePath.join(dir, entry.name);
227
+ if (entry.isDirectory()) {
228
+ if (ignore.has(entry.name)) continue;
229
+ if (type === "folder") out.push(full);
230
+ if (options.recursive) _walkInto(full, type, options, ignore, out);
231
+ } else if (type === "file" && (entry.isFile() || entry.isSymbolicLink())) {
232
+ out.push(full);
233
+ }
234
+ }
235
+ };
236
+
237
+ /**
238
+ * @param {boolean | ListOptions | undefined} recursiveOrOptions
239
+ * @param {boolean} defaultRecursive
240
+ * @returns {Required<ListOptions>}
241
+ */
242
+ const _listOptions = (recursiveOrOptions, defaultRecursive) => {
243
+ const opts = typeof recursiveOrOptions === "object" ? recursiveOrOptions : { recursive: recursiveOrOptions };
244
+ return { recursive: opts.recursive ?? defaultRecursive, hidden: opts.hidden ?? true, ignore: opts.ignore ?? ["node_modules"] };
245
+ };
246
+
247
+ /**
248
+ * @param {unknown} data
249
+ * @param {number} spaces
250
+ * @returns {string | Buffer}
251
+ */
252
+ const _content = (data, spaces) => {
253
+ if (data === null || data === undefined) return "";
254
+ if (typeof data === "string" || Buffer.isBuffer(data)) return data;
255
+ if (data instanceof Uint8Array) return Buffer.from(data);
256
+ return JSON.stringify(data, null, spaces);
257
+ };
258
+
259
+ const _RETRY_CODES = new Set(["EPERM", "EBUSY", "EACCES"]);
260
+
261
+ /**
262
+ * Renames a file. On Windows, an antivirus or an indexer can hold the
263
+ * target for a few milliseconds: a refused rename is tried again.
264
+ * @param {string} from
265
+ * @param {string} to
266
+ */
267
+ const _rename = (from, to) => {
268
+ for (let attempt = 0; ; attempt++) {
269
+ try {
270
+ fs.renameSync(from, to);
271
+ return;
272
+ } catch (error) {
273
+ if (process.platform !== "win32" || attempt >= 8 || !_RETRY_CODES.has(/** @type {NodeJS.ErrnoException} */ (error).code ?? "")) throw error;
274
+ sleepSync(5 * (attempt + 1));
275
+ }
276
+ }
277
+ };
278
+
279
+ /**
280
+ * @param {string} target
281
+ * @param {string | Buffer} content
282
+ * @param {WriteFileOptions} options
283
+ */
284
+ const _write = (target, content, options) => {
285
+ fs.mkdirSync(nodePath.dirname(target), { recursive: true });
286
+ const encoding = options.encoding ?? "utf8";
287
+ if (options.atomic === false) {
288
+ /** @type {fs.WriteFileOptions} */
289
+ const direct = { encoding, flush: options.fsync ?? false };
290
+ if (options.mode !== undefined) direct.mode = options.mode;
291
+ fs.writeFileSync(target, content, direct);
292
+ return;
293
+ }
294
+ // The new file takes the place of the old one, so it must take its permissions too:
295
+ // a secret readable by its owner only stays that way.
296
+ let mode = options.mode;
297
+ if (mode === undefined) {
298
+ try {
299
+ mode = fs.statSync(target).mode & 0o777;
300
+ } catch {
301
+ mode = undefined;
302
+ }
303
+ }
304
+ const temp = `${target}.${process.pid}.${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}.tmp`;
305
+ try {
306
+ /** @type {fs.WriteFileOptions} */
307
+ const writeOptions = { encoding, flush: options.fsync ?? false };
308
+ if (mode !== undefined) writeOptions.mode = mode;
309
+ fs.writeFileSync(temp, content, writeOptions);
310
+ _rename(temp, target);
311
+ } catch (error) {
312
+ fs.rmSync(temp, { force: true });
313
+ throw error;
314
+ }
315
+ };
316
+
317
+ /**
318
+ * Removes comments and trailing commas from JSONC.
319
+ * @param {string} text
320
+ * @returns {string}
321
+ */
322
+ const _stripJSONComments = (text) => {
323
+ let out = "";
324
+ let inString = false;
325
+ for (let i = 0; i < text.length; i++) {
326
+ const char = text[i];
327
+ const next = text[i + 1];
328
+ if (inString) {
329
+ out += char;
330
+ if (char === "\\") out += text[++i] ?? "";
331
+ else if (char === '"') inString = false;
332
+ continue;
333
+ }
334
+ if (char === '"') {
335
+ inString = true;
336
+ out += char;
337
+ } else if (char === "/" && next === "/") {
338
+ while (i < text.length && text[i] !== "\n") i++;
339
+ out += "\n";
340
+ } else if (char === "/" && next === "*") {
341
+ i += 2;
342
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
343
+ i++;
344
+ } else {
345
+ out += char;
346
+ }
347
+ }
348
+ return out.replace(/,(\s*[}\]])/g, "$1");
349
+ };
350
+
351
+ /**
352
+ * @param {string} pattern
353
+ * @param {boolean} dot
354
+ * @returns {RegExp}
355
+ */
356
+ const _compileGlob = (pattern, dot) => {
357
+ const noDot = dot ? "" : "(?!\\.)";
358
+ let re = "";
359
+ let i = 0;
360
+ let braces = 0;
361
+ let atSegmentStart = true;
362
+ while (i < pattern.length) {
363
+ const char = pattern[i];
364
+ if (char === "*" && pattern[i + 1] === "*") {
365
+ const prevSlash = i === 0 || pattern[i - 1] === "/";
366
+ const nextSlash = pattern[i + 2] === "/" || i + 2 === pattern.length;
367
+ if (prevSlash && nextSlash) {
368
+ if (pattern[i + 2] === "/") {
369
+ re += `(?:${noDot}[^/]*/)*`;
370
+ i += 3;
371
+ } else {
372
+ re += `(?:${noDot}[^/]*(?:/${noDot}[^/]*)*)?`;
373
+ i += 2;
374
+ }
375
+ atSegmentStart = true;
376
+ continue;
377
+ }
378
+ re += `${atSegmentStart ? noDot : ""}[^/]*`;
379
+ i += 2;
380
+ atSegmentStart = false;
381
+ continue;
382
+ }
383
+ if (char === "*") {
384
+ re += `${atSegmentStart ? noDot : ""}[^/]*`;
385
+ } else if (char === "?") {
386
+ re += `${atSegmentStart ? noDot : ""}[^/]`;
387
+ } else if (char === "[") {
388
+ const end = pattern.indexOf("]", i + 1);
389
+ if (end === -1) re += "\\[";
390
+ else {
391
+ let cls = pattern.slice(i + 1, end).replace(/\\/g, "\\\\");
392
+ if (cls.startsWith("!")) cls = `^${cls.slice(1)}`;
393
+ re += `[${cls}]`;
394
+ i = end;
395
+ }
396
+ } else if (char === "{") {
397
+ braces++;
398
+ re += "(?:";
399
+ } else if (char === "}" && braces > 0) {
400
+ braces--;
401
+ re += ")";
402
+ } else if (char === "," && braces > 0) {
403
+ re += "|";
404
+ } else if (char === "/") {
405
+ re += "/";
406
+ atSegmentStart = true;
407
+ i++;
408
+ continue;
409
+ } else {
410
+ re += char.replace(/[.+^${}()|\\]/g, "\\$&");
411
+ }
412
+ atSegmentStart = false;
413
+ i++;
414
+ }
415
+ return new RegExp(`^${re}$`);
416
+ };
417
+
418
+ /** @type {Map<string, RegExp>} */
419
+ const _globs = new Map();
420
+
421
+ /**
422
+ * A glob pattern as a regular expression, compiled once.
423
+ * @param {string} pattern Forward slashes only.
424
+ * @param {boolean} dot Whether `*` matches names starting with a dot.
425
+ * @returns {RegExp}
426
+ */
427
+ const _globToRegExp = (pattern, dot) => {
428
+ const key = dot ? `.${pattern}` : `/${pattern}`;
429
+ let regex = _globs.get(key);
430
+ if (!regex) {
431
+ regex = _compileGlob(pattern, dot);
432
+ if (_globs.size >= 500) _globs.clear();
433
+ _globs.set(key, regex);
434
+ }
435
+ return regex;
436
+ };
437
+
438
+ /**
439
+ * @param {string} path
440
+ * @returns {string} The real path, symlinks and short names resolved.
441
+ */
442
+ const _realPath = (path) => {
443
+ try {
444
+ return fs.realpathSync.native(path);
445
+ } catch {
446
+ try {
447
+ return fs.realpathSync(path);
448
+ } catch {
449
+ return path;
450
+ }
451
+ }
452
+ };
453
+
454
+ /**
455
+ * @param {string} pattern Normalized with `/`.
456
+ * @returns {boolean}
457
+ */
458
+ const _isAbsolutePattern = (pattern) => pattern.startsWith("/") || /^[A-Za-z]:\//.test(pattern);
459
+
460
+ /**
461
+ * The folder an absolute pattern starts from, before its first wildcard.
462
+ * @param {string} pattern
463
+ * @returns {string}
464
+ */
465
+ const _globRoot = (pattern) => {
466
+ const segments = pattern.split("/").slice(0, -1);
467
+ const root = [];
468
+ for (const segment of segments) {
469
+ if (/[*?[{]/.test(segment)) break;
470
+ root.push(segment);
471
+ }
472
+ const joined = root.join("/");
473
+ return joined === "" || /^[A-Za-z]:$/.test(joined) ? `${joined}/` : joined;
474
+ };
475
+
476
+ /**
477
+ * Reads an environment variable. For typed and checked values, see `nc.env`.
478
+ *
479
+ * @example
480
+ * nc.getEnv("REGION", "eu-west-1");
481
+ *
482
+ * @param {string} name
483
+ * @param {string} [fallback]
484
+ * @returns {string | undefined}
485
+ */
486
+ function getEnv(name, fallback) {
487
+ return process.env[name] ?? fallback;
488
+ }
489
+
490
+ /**
491
+ * The absolute version of a path, whether it exists or not.
492
+ *
493
+ * @example
494
+ * nc.createPath("./config/app.json"); // next to the calling file
495
+ * nc.createPath("logs/app.log"); // from the working directory
496
+ *
497
+ * @param {string} [path] Defaults to the working directory.
498
+ * @returns {string}
499
+ */
500
+ function createPath(path) {
501
+ return _abs(path);
502
+ }
503
+
504
+ /**
505
+ * The absolute path of a folder, or `undefined` if it doesn't exist.
506
+ *
507
+ * @example
508
+ * if (!nc.getFolder("./uploads")) nc.createFolder("./uploads");
509
+ *
510
+ * @param {string} [path] Defaults to the working directory.
511
+ * @returns {string | undefined}
512
+ */
513
+ function getFolder(path) {
514
+ return _existing(path, "folder");
515
+ }
516
+
517
+ /**
518
+ * The absolute path of a file, or `undefined` if it doesn't exist.
519
+ *
520
+ * @param {string} path
521
+ * @returns {string | undefined}
522
+ */
523
+ function getFile(path) {
524
+ return path ? _existing(path, "file") : undefined;
525
+ }
526
+
527
+ /**
528
+ * The folders inside a folder, nested ones included by default.
529
+ * `node_modules` is skipped.
530
+ *
531
+ * @example
532
+ * nc.getFoldersIn("./src"); // everything
533
+ * nc.getFoldersIn("./src", false); // direct children only
534
+ *
535
+ * @param {string} [path] Defaults to the working directory.
536
+ * @param {boolean | ListOptions} [recursive=true]
537
+ * @returns {string[] | undefined} Absolute paths, or `undefined` if the folder doesn't exist.
538
+ */
539
+ function getFoldersIn(path, recursive = true) {
540
+ const root = _existing(path, "folder");
541
+ return root ? _walk(root, "folder", _listOptions(recursive, true)) : undefined;
542
+ }
543
+
544
+ /**
545
+ * The files inside a folder, nested ones included by default.
546
+ * `node_modules` is skipped.
547
+ *
548
+ * @example
549
+ * nc.getFilesIn("./src").filter((file) => file.endsWith(".js"));
550
+ *
551
+ * @param {string} [path] Defaults to the working directory.
552
+ * @param {boolean | ListOptions} [recursive=true]
553
+ * @returns {string[] | undefined} Absolute paths, or `undefined` if the folder doesn't exist.
554
+ */
555
+ function getFilesIn(path, recursive = true) {
556
+ const root = _existing(path, "folder");
557
+ return root ? _walk(root, "file", _listOptions(recursive, true)) : undefined;
558
+ }
559
+
560
+ /**
561
+ * Finds paths matching a glob pattern: `*` (anything but `/`), `**` (any
562
+ * depth), `?`, `[abc]` and `{js,ts}`.
563
+ *
564
+ * A pattern starting from the root of the disk searches there, and its
565
+ * results are absolute.
566
+ *
567
+ * @example
568
+ * nc.glob("src/**\/*.js"); // ["src/index.js", "src/lib/a.js"]
569
+ * nc.glob("**\/*.{png,jpg}", { cwd: "./assets", absolute: true });
570
+ * nc.glob("packages/*", { type: "folders" });
571
+ * nc.glob(`${folder}/**\/*.json`); // absolute pattern, absolute results
572
+ *
573
+ * @param {string | string[]} pattern One or more patterns.
574
+ * @param {GlobOptions} [options]
575
+ * @returns {string[]} Sorted, relative to `cwd` with `/` separators, unless the pattern is absolute or `absolute` is set.
576
+ */
577
+ function glob(pattern, options = {}) {
578
+ const patterns = (Array.isArray(pattern) ? pattern : [pattern]).map((p) => p.replace(/\\/g, "/").replace(/^\.\//, ""));
579
+ const rooted = patterns.filter(_isAbsolutePattern);
580
+ if (!rooted.length) return _globIn(_abs(options.cwd), patterns, options, options.absolute ?? false);
581
+
582
+ const relative = patterns.filter((p) => !_isAbsolutePattern(p));
583
+ const found = relative.length ? _globIn(_abs(options.cwd), relative, options, options.absolute ?? false) : [];
584
+ /** @type {Map<string, string[]>} */
585
+ const byRoot = new Map();
586
+ for (const p of rooted) {
587
+ const root = _globRoot(p);
588
+ byRoot.set(root, [...(byRoot.get(root) ?? []), p.slice(root.endsWith("/") ? root.length : root.length + 1)]);
589
+ }
590
+ for (const [root, rest] of byRoot) found.push(..._globIn(root, rest, options, options.absolute ?? true));
591
+ return found.sort();
592
+ }
593
+
594
+ /**
595
+ * @param {string} cwd
596
+ * @param {string[]} patterns Normalized, relative to `cwd`.
597
+ * @param {GlobOptions} options
598
+ * @param {boolean} absolute
599
+ * @returns {string[]}
600
+ */
601
+ const _globIn = (cwd, patterns, options, absolute) => {
602
+ const matchers = patterns.map((p) => _globToRegExp(p, options.dot ?? false));
603
+ const ignores = (options.ignore ?? ["**/node_modules/**", "**/.git/**"]).map((p) => _globToRegExp(p.replace(/\\/g, "/"), true));
604
+ const type = options.type ?? "files";
605
+ // Without "**", a pattern can't match deeper than it has parts: no need to read further down.
606
+ let maxDepth = 0;
607
+ const bases = patterns.map((p) => {
608
+ const segments = p.split("/");
609
+ maxDepth = p.includes("**") ? Infinity : Math.max(maxDepth, segments.length);
610
+ const staticParts = [];
611
+ for (const segment of segments.slice(0, -1)) {
612
+ if (/[*?[{]/.test(segment)) break;
613
+ staticParts.push(segment);
614
+ }
615
+ return staticParts.join("/");
616
+ });
617
+ /** @type {Set<string>} */
618
+ const results = new Set();
619
+ /** @type {Set<string>} */
620
+ const visited = new Set();
621
+
622
+ /**
623
+ * @param {string} relDir
624
+ * @param {number} depth How many parts `relDir` has.
625
+ */
626
+ const visit = (relDir, depth) => {
627
+ if (visited.has(relDir)) return;
628
+ visited.add(relDir);
629
+ /** @type {fs.Dirent[]} */
630
+ let entries;
631
+ try {
632
+ entries = fs.readdirSync(nodePath.join(cwd, relDir), { withFileTypes: true });
633
+ } catch {
634
+ return;
635
+ }
636
+ for (const entry of entries) {
637
+ const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
638
+ const isDir = entry.isDirectory();
639
+ if (ignores.some((re) => re.test(rel) || (isDir && re.test(`${rel}/`)))) continue;
640
+ const wanted = type === "all" || (type === "folders" ? isDir : !isDir);
641
+ if (wanted && matchers.some((re) => re.test(rel))) results.add(rel);
642
+ if (isDir && depth + 1 < maxDepth) visit(rel, depth + 1);
643
+ }
644
+ };
645
+ for (const base of new Set(bases)) visit(base, base ? base.split("/").length : 0);
646
+ return [...results].sort().map((rel) => (absolute ? nodePath.join(cwd, rel) : rel));
647
+ };
648
+
649
+ /**
650
+ * Searches a folder tree for the first file or folder with this name,
651
+ * closest first.
652
+ *
653
+ * @example
654
+ * nc.find("config.json");
655
+ * nc.find("fixtures", { type: "folder", cwd: "./test" });
656
+ *
657
+ * @param {string} name
658
+ * @param {FindOptions} [options]
659
+ * @returns {string | undefined} The absolute path.
660
+ */
661
+ function find(name, options = {}) {
662
+ const type = options.type ?? "file";
663
+ const ignore = new Set(options.ignore ?? ["node_modules", ".git"]);
664
+ const queue = [_abs(options.cwd)];
665
+ for (let next = 0; next < queue.length; next++) {
666
+ const dir = queue[next];
667
+ /** @type {fs.Dirent[]} */
668
+ let entries;
669
+ try {
670
+ entries = fs.readdirSync(dir, { withFileTypes: true });
671
+ } catch {
672
+ continue;
673
+ }
674
+ for (const entry of entries) {
675
+ const isDir = entry.isDirectory();
676
+ if (entry.name === name && (type === "any" || (type === "folder" ? isDir : !isDir))) return nodePath.join(dir, entry.name);
677
+ }
678
+ for (const entry of entries) if (entry.isDirectory() && !ignore.has(entry.name)) queue.push(nodePath.join(dir, entry.name));
679
+ }
680
+ return undefined;
681
+ }
682
+
683
+ /**
684
+ * Looks for a file or folder in a folder, then in its parent, and so on up
685
+ * to the root. It's how you find the `package.json` or the `.git` a file
686
+ * belongs to.
687
+ *
688
+ * @example
689
+ * nc.findUp("package.json"); // the closest one above the working directory
690
+ * nc.findUp(".git", { type: "folder", cwd: __dirname });
691
+ * nc.findUp(["pnpm-lock.yaml", "yarn.lock", "package-lock.json"]);
692
+ *
693
+ * @param {string | string[]} name One name, or several to try in each folder, in this order.
694
+ * @param {{ cwd?: string, type?: "file" | "folder" | "any", stopAt?: string }} [options] Where to start (the working directory by default), what to look for (`"any"` by default), and a folder not to go above.
695
+ * @returns {string | undefined} The absolute path.
696
+ */
697
+ function findUp(name, options = {}) {
698
+ const names = Array.isArray(name) ? name : [name];
699
+ const type = options.type ?? "any";
700
+ const stopAt = options.stopAt === undefined ? undefined : _abs(options.stopAt);
701
+ let dir = _abs(options.cwd);
702
+ for (;;) {
703
+ for (const candidate of names) {
704
+ const found = _existing(nodePath.join(dir, candidate), type);
705
+ if (found) return found;
706
+ }
707
+ const parent = nodePath.dirname(dir);
708
+ if (parent === dir || dir === stopAt) return undefined;
709
+ dir = parent;
710
+ }
711
+ }
712
+
713
+ /**
714
+ * Reads a file as text, or as a Buffer with `"buffer"`.
715
+ *
716
+ * @example
717
+ * const text = nc.readFile("./README.md");
718
+ * const bytes = nc.readFile("./logo.png", "buffer");
719
+ *
720
+ * @overload
721
+ * @param {string} path
722
+ * @param {"buffer"} encoding
723
+ * @returns {Buffer | undefined} `undefined` if it can't be read.
724
+ */
725
+ /**
726
+ * Reads a file as text.
727
+ *
728
+ * @overload
729
+ * @param {string} path
730
+ * @param {BufferEncoding} [encoding="utf8"]
731
+ * @returns {string | undefined} `undefined` if it can't be read.
732
+ */
733
+ /**
734
+ * @param {string} path
735
+ * @param {BufferEncoding | "buffer"} [encoding="utf8"]
736
+ * @returns {string | Buffer | undefined}
737
+ */
738
+ function readFile(path, encoding = "utf8") {
739
+ if (!path) return undefined;
740
+ try {
741
+ return encoding === "buffer" ? fs.readFileSync(_abs(path)) : fs.readFileSync(_abs(path), encoding);
742
+ } catch {
743
+ // missing, a folder, or unreadable
744
+ return undefined;
745
+ }
746
+ }
747
+
748
+ /**
749
+ * Reads a text file as lines. A final newline doesn't add an empty line.
750
+ *
751
+ * @example
752
+ * for (const url of nc.readLines("./urls.txt") ?? []) await check(url);
753
+ *
754
+ * @param {string} path
755
+ * @returns {string[] | undefined} `undefined` if it can't be read.
756
+ */
757
+ function readLines(path) {
758
+ const text = readFile(path);
759
+ if (text === undefined) return undefined;
760
+ if (text === "") return [];
761
+ return text.replace(/(\r\n|\r|\n)$/, "").split(/\r\n|\r|\n/);
762
+ }
763
+
764
+ /**
765
+ * Reads a JSON file. If it's missing or invalid you get `fallback`, never
766
+ * an error.
767
+ *
768
+ * @example
769
+ * const settings = nc.readJSON("./settings.json", { theme: "light" });
770
+ * const tsconfig = nc.readJSON("./tsconfig.json", {}, { comments: true });
771
+ *
772
+ * @template [T=any]
773
+ * @param {string} path
774
+ * @param {T} [fallback]
775
+ * @param {ReadJSONOptions} [options]
776
+ * @returns {T}
777
+ */
778
+ function readJSON(path, fallback, options = {}) {
779
+ const text = readFile(path);
780
+ if (text === undefined) return /** @type {T} */ (fallback);
781
+ try {
782
+ const clean = text.replace(/^\uFEFF/, "");
783
+ return JSON.parse(options.comments ? _stripJSONComments(clean) : clean, options.reviver);
784
+ } catch {
785
+ return /** @type {T} */ (fallback);
786
+ }
787
+ }
788
+
789
+ /**
790
+ * Writes a file atomically, creating its folders. Objects are written as
791
+ * JSON.
792
+ *
793
+ * @example
794
+ * nc.writeFile("./dist/index.html", html);
795
+ * nc.writeFile("./.secrets/token", token, { mode: 0o600 });
796
+ *
797
+ * @param {string} path
798
+ * @param {string | Buffer | Uint8Array | object} data
799
+ * @param {WriteFileOptions} [options]
800
+ * @returns {boolean} `false` if it failed.
801
+ */
802
+ function writeFile(path, data, options = {}) {
803
+ try {
804
+ _write(_abs(path), _content(data, options.spaces ?? 2), options);
805
+ return true;
806
+ } catch {
807
+ return false;
808
+ }
809
+ }
810
+
811
+ /**
812
+ * Writes a value as JSON, atomically, creating folders.
813
+ *
814
+ * @example
815
+ * nc.writeJSON("./data/users.json", users);
816
+ *
817
+ * @param {string} path
818
+ * @param {unknown} data
819
+ * @param {WriteJSONOptions} [options]
820
+ * @returns {boolean} `false` if it failed, for example on a circular structure.
821
+ */
822
+ function writeJSON(path, data, options = {}) {
823
+ try {
824
+ const text = JSON.stringify(data, /** @type {any} */ (options.replacer), options.spaces ?? 2);
825
+ if (text === undefined) return false;
826
+ _write(_abs(path), `${text}\n`, { atomic: options.atomic, mode: options.mode, fsync: options.fsync });
827
+ return true;
828
+ } catch {
829
+ return false;
830
+ }
831
+ }
832
+
833
+ /**
834
+ * Appends to a file, creating it and its folders if needed.
835
+ *
836
+ * @example
837
+ * nc.appendFile("./logs/audit.log", `${new Date().toISOString()} login ${user}\n`);
838
+ *
839
+ * @param {string} path
840
+ * @param {string | Buffer} data
841
+ * @returns {boolean}
842
+ */
843
+ function appendFile(path, data) {
844
+ try {
845
+ const target = _abs(path);
846
+ const content = Buffer.isBuffer(data) ? data : String(data);
847
+ try {
848
+ fs.appendFileSync(target, content);
849
+ } catch (error) {
850
+ if (/** @type {NodeJS.ErrnoException} */ (error).code !== "ENOENT") throw error;
851
+ fs.mkdirSync(nodePath.dirname(target), { recursive: true });
852
+ fs.appendFileSync(target, content);
853
+ }
854
+ return true;
855
+ } catch {
856
+ return false;
857
+ }
858
+ }
859
+
860
+ /**
861
+ * Creates a file and its folders.
862
+ *
863
+ * @example
864
+ * nc.createFile("./notes.txt", false, "hello"); // only if it doesn't exist
865
+ * nc.createFile("./data.json", true, { ok: true }); // replace it
866
+ *
867
+ * @param {string} path
868
+ * @param {boolean} [force=false] Replace the file if it exists.
869
+ * @param {string | Buffer | object | null} [data] Objects are written as JSON.
870
+ * @returns {boolean | undefined} `false` if it already existed or failed.
871
+ */
872
+ function createFile(path, force = false, data) {
873
+ if (!path) return undefined;
874
+ const target = _abs(path);
875
+ if (!force && fs.existsSync(target)) return false;
876
+ try {
877
+ _write(target, _content(data, 2), { atomic: false });
878
+ return true;
879
+ } catch {
880
+ return false;
881
+ }
882
+ }
883
+
884
+ /**
885
+ * Creates a folder and its parents.
886
+ *
887
+ * @example
888
+ * nc.createFolder("./uploads");
889
+ * nc.createFolder("./cache", true); // start from an empty folder
890
+ *
891
+ * @param {string} path
892
+ * @param {boolean} [force=false] Delete and recreate it if it exists.
893
+ * @returns {boolean | undefined} `false` if it already existed or failed.
894
+ */
895
+ function createFolder(path, force = false) {
896
+ if (!path) return undefined;
897
+ const target = _abs(path);
898
+ try {
899
+ if (fs.existsSync(target)) {
900
+ if (!force) return false;
901
+ fs.rmSync(target, { recursive: true, force: true });
902
+ }
903
+ fs.mkdirSync(target, { recursive: true });
904
+ return true;
905
+ } catch {
906
+ return false;
907
+ }
908
+ }
909
+
910
+ /**
911
+ * Makes sure a folder exists and returns its absolute path.
912
+ *
913
+ * @example
914
+ * const dir = nc.ensureFolder("./storage/uploads");
915
+ *
916
+ * @param {string} path
917
+ * @returns {string}
918
+ * @throws {Error} If it can't be created.
919
+ */
920
+ function ensureFolder(path) {
921
+ const target = _abs(path);
922
+ fs.mkdirSync(target, { recursive: true });
923
+ return target;
924
+ }
925
+
926
+ /**
927
+ * Makes sure a file exists (an empty one if needed) and returns its
928
+ * absolute path. An existing file is left as is.
929
+ *
930
+ * @param {string} path
931
+ * @returns {string}
932
+ * @throws {Error} If it can't be created.
933
+ */
934
+ function ensureFile(path) {
935
+ const target = _abs(path);
936
+ if (!fs.existsSync(target)) {
937
+ fs.mkdirSync(nodePath.dirname(target), { recursive: true });
938
+ fs.writeFileSync(target, "", { flag: "a" });
939
+ }
940
+ return target;
941
+ }
942
+
943
+ /**
944
+ * Updates a file's modification time, creating the file if needed, like
945
+ * the Unix `touch` command.
946
+ *
947
+ * @param {string} path
948
+ * @returns {boolean}
949
+ */
950
+ function touch(path) {
951
+ try {
952
+ const target = ensureFile(path);
953
+ const now = new Date();
954
+ fs.utimesSync(target, now, now);
955
+ return true;
956
+ } catch {
957
+ return false;
958
+ }
959
+ }
960
+
961
+ /**
962
+ * Deletes a folder and everything in it.
963
+ *
964
+ * @param {string} path
965
+ * @returns {boolean | undefined} `undefined` if it doesn't exist.
966
+ */
967
+ function deleteFolder(path) {
968
+ const target = path ? _existing(path, "folder") : undefined;
969
+ if (!target) return undefined;
970
+ try {
971
+ fs.rmSync(target, { recursive: true, force: true });
972
+ return true;
973
+ } catch {
974
+ return false;
975
+ }
976
+ }
977
+
978
+ /**
979
+ * Deletes a file.
980
+ *
981
+ * @param {string} path
982
+ * @returns {boolean | undefined} `undefined` if it doesn't exist.
983
+ */
984
+ function deleteFile(path) {
985
+ const target = path ? _existing(path, "file") : undefined;
986
+ if (!target) return undefined;
987
+ try {
988
+ fs.unlinkSync(target);
989
+ return true;
990
+ } catch {
991
+ return false;
992
+ }
993
+ }
994
+
995
+ /**
996
+ * Deletes a file or a folder, whichever it is. Nothing happens if the path
997
+ * doesn't exist.
998
+ *
999
+ * @example
1000
+ * nc.remove("./dist");
1001
+ *
1002
+ * @param {string} path
1003
+ * @returns {boolean} `true` if something was deleted.
1004
+ */
1005
+ function remove(path) {
1006
+ const target = _existing(path, "any");
1007
+ if (!target) return false;
1008
+ try {
1009
+ fs.rmSync(target, { recursive: true, force: true });
1010
+ return true;
1011
+ } catch {
1012
+ return false;
1013
+ }
1014
+ }
1015
+
1016
+ /**
1017
+ * Deletes the folders inside a folder that pass a test.
1018
+ *
1019
+ * @example
1020
+ * nc.deleteFoldersIn("./packages", (folder) => folder.endsWith("dist"));
1021
+ *
1022
+ * @param {string} [path] Defaults to the working directory.
1023
+ * @param {(folder: string) => boolean} [filter] Gets absolute paths. Defaults to every folder.
1024
+ * @returns {number | undefined} How many were deleted.
1025
+ */
1026
+ function deleteFoldersIn(path, filter = () => true) {
1027
+ const folders = getFoldersIn(path);
1028
+ if (!folders) return undefined;
1029
+ let deleted = 0;
1030
+ for (const folder of folders) {
1031
+ if (!fs.existsSync(folder) || !filter(folder)) continue;
1032
+ fs.rmSync(folder, { recursive: true, force: true });
1033
+ deleted++;
1034
+ }
1035
+ return deleted;
1036
+ }
1037
+
1038
+ /**
1039
+ * Deletes the files inside a folder that pass a test.
1040
+ *
1041
+ * @example
1042
+ * nc.deleteFilesIn("./logs", false, (file) => file.endsWith(".log"));
1043
+ *
1044
+ * @param {string} [path] Defaults to the working directory.
1045
+ * @param {boolean} [recursive=false]
1046
+ * @param {(file: string) => boolean} [filter] Gets absolute paths. Defaults to every file.
1047
+ * @returns {number | undefined} How many were deleted.
1048
+ */
1049
+ function deleteFilesIn(path, recursive = false, filter = () => true) {
1050
+ const files = getFilesIn(path, recursive);
1051
+ if (!files) return undefined;
1052
+ let deleted = 0;
1053
+ for (const file of files) {
1054
+ if (!filter(file)) continue;
1055
+ try {
1056
+ fs.unlinkSync(file);
1057
+ deleted++;
1058
+ } catch {
1059
+ // keep going
1060
+ }
1061
+ }
1062
+ return deleted;
1063
+ }
1064
+
1065
+ /**
1066
+ * Deletes everything inside a folder, keeping the folder.
1067
+ *
1068
+ * @param {string} path
1069
+ * @returns {boolean | undefined} `undefined` if it doesn't exist.
1070
+ */
1071
+ function emptyFolder(path) {
1072
+ const target = _existing(path, "folder");
1073
+ if (!target) return undefined;
1074
+ try {
1075
+ for (const entry of fs.readdirSync(target)) fs.rmSync(nodePath.join(target, entry), { recursive: true, force: true });
1076
+ return true;
1077
+ } catch {
1078
+ return false;
1079
+ }
1080
+ }
1081
+
1082
+ /**
1083
+ * Copies a file or a whole folder, like `cp -r`.
1084
+ *
1085
+ * @example
1086
+ * nc.copy("./templates", "./dist/templates");
1087
+ * nc.copy("./.env.example", "./.env", { overwrite: false });
1088
+ *
1089
+ * @param {string} source
1090
+ * @param {string} destination
1091
+ * @param {CopyOptions} [options]
1092
+ * @returns {boolean} `false` if the source is missing or the copy failed.
1093
+ */
1094
+ function copy(source, destination, options = {}) {
1095
+ const src = _existing(source, "any");
1096
+ if (!src) return false;
1097
+ try {
1098
+ const dest = _abs(destination);
1099
+ fs.mkdirSync(nodePath.dirname(dest), { recursive: true });
1100
+ fs.cpSync(src, dest, {
1101
+ recursive: true,
1102
+ force: options.overwrite ?? true,
1103
+ errorOnExist: false,
1104
+ preserveTimestamps: options.preserveTimestamps ?? false,
1105
+ filter: options.filter ? (s, d) => /** @type {Function} */ (options.filter)(s, d) : undefined,
1106
+ });
1107
+ return true;
1108
+ } catch {
1109
+ return false;
1110
+ }
1111
+ }
1112
+
1113
+ /**
1114
+ * Moves or renames a file or folder, across drives too.
1115
+ *
1116
+ * @example
1117
+ * nc.move("./report.pdf", "./archive/2024/report.pdf");
1118
+ *
1119
+ * @param {string} source
1120
+ * @param {string} destination
1121
+ * @param {{ overwrite?: boolean }} [options] Replace an existing destination. Defaults to `true`.
1122
+ * @returns {boolean}
1123
+ */
1124
+ function move(source, destination, options = {}) {
1125
+ const src = _existing(source, "any");
1126
+ if (!src) return false;
1127
+ const dest = _abs(destination);
1128
+ try {
1129
+ if (options.overwrite === false && fs.existsSync(dest)) return false;
1130
+ fs.mkdirSync(nodePath.dirname(dest), { recursive: true });
1131
+ try {
1132
+ // A rename replaces a file in one step: the destination is never missing, not even for an instant.
1133
+ fs.renameSync(src, dest);
1134
+ } catch (error) {
1135
+ const code = /** @type {NodeJS.ErrnoException} */ (error).code;
1136
+ if (code === "EXDEV") {
1137
+ // another drive: copy, then delete
1138
+ fs.rmSync(dest, { recursive: true, force: true });
1139
+ fs.cpSync(src, dest, { recursive: true });
1140
+ fs.rmSync(src, { recursive: true, force: true });
1141
+ } else {
1142
+ // a folder in the way, or a file where a folder goes
1143
+ fs.rmSync(dest, { recursive: true, force: true });
1144
+ _rename(src, dest);
1145
+ }
1146
+ }
1147
+ return true;
1148
+ } catch {
1149
+ return false;
1150
+ }
1151
+ }
1152
+
1153
+ /**
1154
+ * Copies the folders inside a folder into another one. `copy()` is simpler
1155
+ * for most cases.
1156
+ *
1157
+ * @param {CopyFoldersOptions} options
1158
+ * @returns {number | undefined} How many were copied.
1159
+ */
1160
+ function copyFoldersIn(options) {
1161
+ const root = _existing(options.path, "folder");
1162
+ if (!root || !options.dest) return undefined;
1163
+ const dest = _abs(options.dest);
1164
+ const recursive = options.recursive ?? true;
1165
+ const filter = typeof options.filter === "function" ? options.filter : () => true;
1166
+ let copied = 0;
1167
+ for (const folder of getFoldersIn(root, recursive) ?? []) {
1168
+ if (!filter(folder)) continue;
1169
+ const target = nodePath.join(dest, nodePath.relative(root, folder));
1170
+ if (options.force && fs.existsSync(target)) fs.rmSync(target, { recursive: true, force: true });
1171
+ fs.mkdirSync(target, { recursive: true });
1172
+ copied++;
1173
+ if (options.withFiles) {
1174
+ for (const file of getFilesIn(folder, false) ?? []) fs.copyFileSync(file, nodePath.join(target, nodePath.basename(file)));
1175
+ }
1176
+ }
1177
+ return copied;
1178
+ }
1179
+
1180
+ /**
1181
+ * Copies a folder. Without `recursive` or `withFiles`, only the empty
1182
+ * folder is created; `copy()` copies everything in one call.
1183
+ *
1184
+ * @param {CopyFolderOptions} options
1185
+ * @returns {boolean | undefined}
1186
+ */
1187
+ function copyFolder(options) {
1188
+ const root = _existing(options.path, "folder");
1189
+ if (!root || !options.dest) return undefined;
1190
+ const dest = _abs(options.dest);
1191
+ try {
1192
+ if (options.force && fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
1193
+ fs.mkdirSync(dest, { recursive: true });
1194
+ const folders = options.recursive ? [root, ...(getFoldersIn(root, true) ?? [])] : [root];
1195
+ for (const folder of folders) {
1196
+ const target = nodePath.join(dest, nodePath.relative(root, folder));
1197
+ fs.mkdirSync(target, { recursive: true });
1198
+ if (options.withFiles) {
1199
+ for (const file of getFilesIn(folder, false) ?? []) fs.copyFileSync(file, nodePath.join(target, nodePath.basename(file)));
1200
+ }
1201
+ }
1202
+ return true;
1203
+ } catch {
1204
+ return false;
1205
+ }
1206
+ }
1207
+
1208
+ /**
1209
+ * Copies the files inside a folder into another one, keeping the tree.
1210
+ *
1211
+ * @example
1212
+ * nc.copyFilesIn({ path: "./src", dest: "./dist", filter: (file) => file.endsWith(".css") });
1213
+ *
1214
+ * @param {CopyFilesOptions} options
1215
+ * @returns {number | undefined} How many were copied.
1216
+ */
1217
+ function copyFilesIn(options) {
1218
+ const root = _existing(options.path, "folder");
1219
+ if (!root || !options.dest) return undefined;
1220
+ const dest = _abs(options.dest);
1221
+ const recursive = options.recursive ?? true;
1222
+ const filter = typeof options.filter === "function" ? options.filter : () => true;
1223
+ let copied = 0;
1224
+ for (const file of getFilesIn(root, recursive) ?? []) {
1225
+ if (!filter(file)) continue;
1226
+ const target = nodePath.join(dest, recursive ? nodePath.relative(root, file) : nodePath.basename(file));
1227
+ if (options.force && fs.existsSync(target)) fs.rmSync(target, { force: true });
1228
+ fs.mkdirSync(nodePath.dirname(target), { recursive: true });
1229
+ fs.copyFileSync(file, target);
1230
+ copied++;
1231
+ }
1232
+ return copied;
1233
+ }
1234
+
1235
+ /**
1236
+ * Copies one file. If `dest` ends with `/`, the file keeps its name inside
1237
+ * that folder.
1238
+ *
1239
+ * @example
1240
+ * nc.copyFile({ path: "./src/index.js", dest: "./dist/" });
1241
+ *
1242
+ * @param {CopyFileOptions} options
1243
+ * @returns {boolean | undefined}
1244
+ */
1245
+ function copyFile(options) {
1246
+ const src = _existing(options.path, "file");
1247
+ if (!src || !options.dest) return undefined;
1248
+ const intoFolder = /[\\/]$/.test(options.dest) || (fs.existsSync(_abs(options.dest)) && fs.statSync(_abs(options.dest)).isDirectory());
1249
+ const dest = intoFolder ? nodePath.join(_abs(options.dest), nodePath.basename(src)) : _abs(options.dest);
1250
+ try {
1251
+ if (options.force && fs.existsSync(dest)) fs.rmSync(dest, { force: true });
1252
+ fs.mkdirSync(nodePath.dirname(dest), { recursive: true });
1253
+ fs.copyFileSync(src, dest);
1254
+ return true;
1255
+ } catch {
1256
+ return false;
1257
+ }
1258
+ }
1259
+
1260
+ /**
1261
+ * Moves the folders inside a folder.
1262
+ *
1263
+ * @param {CopyFoldersOptions} options
1264
+ * @returns {number | undefined} How many were moved.
1265
+ */
1266
+ function moveFoldersIn(options) {
1267
+ const root = _existing(options.path, "folder");
1268
+ const copied = copyFoldersIn(options);
1269
+ if (copied === undefined || !root) return copied;
1270
+ const filter = typeof options.filter === "function" ? options.filter : () => true;
1271
+ for (const folder of getFoldersIn(root, options.recursive ?? true) ?? []) {
1272
+ if (fs.existsSync(folder) && filter(folder)) fs.rmSync(folder, { recursive: true, force: true });
1273
+ }
1274
+ return copied;
1275
+ }
1276
+
1277
+ /**
1278
+ * Moves a folder.
1279
+ *
1280
+ * @param {CopyFolderOptions & { path: string }} options
1281
+ * @returns {boolean | undefined}
1282
+ */
1283
+ function moveFolder(options) {
1284
+ const root = _existing(options.path, "folder");
1285
+ const copied = copyFolder(options);
1286
+ if (copied && root) fs.rmSync(root, { recursive: true, force: true });
1287
+ return copied;
1288
+ }
1289
+
1290
+ /**
1291
+ * Moves the files inside a folder.
1292
+ *
1293
+ * @param {CopyFilesOptions} options
1294
+ * @returns {number | undefined} How many were moved.
1295
+ */
1296
+ function moveFilesIn(options) {
1297
+ const root = _existing(options.path, "folder");
1298
+ if (!root || !options.dest) return undefined;
1299
+ const filter = typeof options.filter === "function" ? options.filter : () => true;
1300
+ const files = new Set((getFilesIn(root, options.recursive ?? true) ?? []).filter(filter));
1301
+ const copied = copyFilesIn({ ...options, path: root, filter: (file) => files.has(file) });
1302
+ for (const file of files) fs.rmSync(file, { force: true });
1303
+ return copied;
1304
+ }
1305
+
1306
+ /**
1307
+ * Moves one file.
1308
+ *
1309
+ * @example
1310
+ * nc.moveFile({ path: "./logs/app.log", dest: "./logs/archive/" });
1311
+ *
1312
+ * @param {CopyFileOptions} options
1313
+ * @returns {boolean | undefined}
1314
+ */
1315
+ function moveFile(options) {
1316
+ const src = _existing(options.path, "file");
1317
+ const moved = copyFile(options);
1318
+ if (moved && src) fs.rmSync(src, { force: true });
1319
+ return moved;
1320
+ }
1321
+
1322
+ /**
1323
+ * Does a file or folder exist at this path?
1324
+ *
1325
+ * @param {string} path
1326
+ * @returns {boolean}
1327
+ */
1328
+ function exists(path) {
1329
+ return path ? fs.existsSync(_abs(path)) : false;
1330
+ }
1331
+
1332
+ /**
1333
+ * Is it an existing file?
1334
+ *
1335
+ * @param {string} path
1336
+ * @returns {boolean}
1337
+ */
1338
+ function isFile(path) {
1339
+ return _existing(path, "file") !== undefined;
1340
+ }
1341
+
1342
+ /**
1343
+ * Is it an existing folder?
1344
+ *
1345
+ * @param {string} path
1346
+ * @returns {boolean}
1347
+ */
1348
+ function isFolder(path) {
1349
+ return _existing(path, "folder") !== undefined;
1350
+ }
1351
+
1352
+ /**
1353
+ * Size, dates and type of a path.
1354
+ *
1355
+ * @example
1356
+ * nc.stat("./video.mp4")?.mtime;
1357
+ *
1358
+ * @param {string} path
1359
+ * @returns {fs.Stats | undefined} `undefined` if it doesn't exist.
1360
+ */
1361
+ function stat(path) {
1362
+ try {
1363
+ return fs.statSync(_abs(path));
1364
+ } catch {
1365
+ return undefined;
1366
+ }
1367
+ }
1368
+
1369
+ /**
1370
+ * A file's size in bytes.
1371
+ *
1372
+ * @example
1373
+ * nc.num.formatBytes(nc.fileSize("./backup.zip") ?? 0); // "1.2 GB"
1374
+ *
1375
+ * @param {string} path
1376
+ * @returns {number | undefined} `undefined` if it doesn't exist.
1377
+ */
1378
+ function fileSize(path) {
1379
+ if (!path) return undefined;
1380
+ try {
1381
+ const stats = fs.statSync(_abs(path));
1382
+ return stats.isFile() ? stats.size : undefined;
1383
+ } catch {
1384
+ return undefined;
1385
+ }
1386
+ }
1387
+
1388
+ /**
1389
+ * The total size of every file in a folder.
1390
+ *
1391
+ * @param {string} path
1392
+ * @returns {number | undefined} `undefined` if it doesn't exist.
1393
+ */
1394
+ function folderSize(path) {
1395
+ const root = _existing(path, "folder");
1396
+ if (!root) return undefined;
1397
+ let total = 0;
1398
+ for (const file of _walk(root, "file", { recursive: true, hidden: true, ignore: [] })) {
1399
+ try {
1400
+ total += fs.lstatSync(file).size;
1401
+ } catch {
1402
+ // file removed in the meantime
1403
+ }
1404
+ }
1405
+ return total;
1406
+ }
1407
+
1408
+ /**
1409
+ * Hashes a file in chunks, so huge files are fine.
1410
+ *
1411
+ * @example
1412
+ * nc.hashFile("./release.zip"); // sha256, hex
1413
+ * nc.hashFile("./release.zip", { algorithm: "md5" });
1414
+ *
1415
+ * @param {string} path
1416
+ * @param {{ algorithm?: string, encoding?: "hex" | "base64" | "base64url" }} [options] Defaults to sha256 in hex.
1417
+ * @returns {string | undefined} `undefined` if it can't be read.
1418
+ */
1419
+ function hashFile(path, options = {}) {
1420
+ const target = _existing(path, "file");
1421
+ if (!target) return undefined;
1422
+ let fd;
1423
+ try {
1424
+ const hash = require("node:crypto").createHash(options.algorithm ?? "sha256");
1425
+ fd = fs.openSync(target, "r");
1426
+ const buffer = Buffer.allocUnsafe(1 << 20);
1427
+ let read;
1428
+ while ((read = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) hash.update(buffer.subarray(0, read));
1429
+ return hash.digest(options.encoding ?? "hex");
1430
+ } catch {
1431
+ return undefined;
1432
+ } finally {
1433
+ if (fd !== undefined) fs.closeSync(fd);
1434
+ }
1435
+ }
1436
+
1437
+ /**
1438
+ * Creates a new, unique folder in the system's temp directory.
1439
+ *
1440
+ * @example
1441
+ * const dir = nc.tempFolder("export-");
1442
+ * try { await exportTo(dir); } finally { nc.remove(dir); }
1443
+ *
1444
+ * @param {string} [prefix="nc-"]
1445
+ * @returns {string}
1446
+ */
1447
+ function tempFolder(prefix = "nc-") {
1448
+ return fs.mkdtempSync(nodePath.join(os.tmpdir(), prefix));
1449
+ }
1450
+
1451
+ /**
1452
+ * Turns any text into a file name that works on Windows, macOS and Linux:
1453
+ * forbidden characters are replaced, reserved names like `CON` are
1454
+ * prefixed, and the length is capped.
1455
+ *
1456
+ * @example
1457
+ * nc.sanitizeFilename('Report: Q1/Q2 "final"?.pdf'); // "Report_ Q1_Q2 _final__.pdf"
1458
+ * nc.sanitizeFilename("CON.txt"); // "_CON.txt"
1459
+ *
1460
+ * @param {string} name
1461
+ * @param {{ replacement?: string }} [options] Defaults to `"_"`.
1462
+ * @returns {string}
1463
+ */
1464
+ function sanitizeFilename(name, options = {}) {
1465
+ const replacement = options.replacement ?? "_";
1466
+ let clean = String(name).replace(/[<>:"/\\|?*\u0000-\u001f\u007f]/g, replacement).replace(/[. ]+$/, "");
1467
+ if (/^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i.test(clean)) clean = `_${clean}`;
1468
+ if (clean === "." || clean === "..") clean = replacement;
1469
+ if (Buffer.byteLength(clean) > 255) {
1470
+ // File systems count bytes, and a character cut in two would be broken.
1471
+ let bytes = 0;
1472
+ let kept = "";
1473
+ for (const char of clean) {
1474
+ bytes += Buffer.byteLength(char);
1475
+ if (bytes > 255) break;
1476
+ kept += char;
1477
+ }
1478
+ clean = kept.replace(/[. ]+$/, "");
1479
+ }
1480
+ return clean || "_";
1481
+ }
1482
+
1483
+ /**
1484
+ * Watches a file or folder for changes.
1485
+ *
1486
+ * @example
1487
+ * const watcher = nc.watch({ path: "./src", recursive: true, debounce: 100 })
1488
+ * .on("change", (file) => nc.info(`Changed: ${file}`))
1489
+ * .on("rename", (file) => nc.info(`Added or removed: ${file}`));
1490
+ * watcher.stop();
1491
+ *
1492
+ * @param {WatchOptions} [options]
1493
+ * @returns {Watcher | undefined} `undefined` if the path doesn't exist.
1494
+ */
1495
+ function watch(options = {}) {
1496
+ const target = _existing(options.path, "any");
1497
+ if (!target) return undefined;
1498
+ let paused = false;
1499
+ const debounce = options.debounce === undefined ? 0 : toMs(options.debounce, "debounce");
1500
+ /** @type {Record<string, Function[]>} */
1501
+ const listeners = { change: [], rename: [], all: [], error: [] };
1502
+ /** @type {Map<string, NodeJS.Timeout>} */
1503
+ const pending = new Map();
1504
+
1505
+ /**
1506
+ * @param {"change" | "rename"} event
1507
+ * @param {string} file
1508
+ */
1509
+ const dispatch = (event, file) => {
1510
+ if (paused) return;
1511
+ if (typeof options.filter === "function" && !options.filter(event, file)) return;
1512
+ for (const cb of listeners[event]) cb(file);
1513
+ for (const cb of listeners.all) cb(event, file);
1514
+ };
1515
+
1516
+ const isDir = fs.statSync(target).isDirectory();
1517
+ // Watching a short (8.3) or symlinked path crashes libuv on Windows, so
1518
+ // watch the real one and keep reporting paths under the one you passed.
1519
+ const real = _realPath(target);
1520
+ let gone = false;
1521
+ const watcher = fs.watch(real, { recursive: options.recursive ?? false }, (event, fileName) => {
1522
+ if (gone) return;
1523
+ const file = fileName && isDir ? nodePath.join(target, String(fileName)) : target;
1524
+ const kind = event === "rename" ? "rename" : "change";
1525
+ // A folder that was deleted keeps sending events on some systems, without
1526
+ // end and without an error: say it once, then stop listening.
1527
+ if (kind === "rename" && isDir && !fs.existsSync(real)) {
1528
+ gone = true;
1529
+ controller.stop();
1530
+ const error = Object.assign(new Error(`The watched folder no longer exists: ${target}`), { code: "ENOENT", path: target });
1531
+ for (const cb of listeners.error) cb(error);
1532
+ return;
1533
+ }
1534
+ if (!(debounce > 0)) return dispatch(kind, file);
1535
+ const key = `${kind}:${file}`;
1536
+ clearTimeout(pending.get(key));
1537
+ pending.set(key, setTimeout(() => {
1538
+ pending.delete(key);
1539
+ dispatch(kind, file);
1540
+ }, debounce));
1541
+ });
1542
+ watcher.on("error", (error) => {
1543
+ if (gone) return;
1544
+ // Some systems report the deleted folder here: it is the same news, told once.
1545
+ if (isDir && !fs.existsSync(real)) {
1546
+ gone = true;
1547
+ controller.stop();
1548
+ }
1549
+ for (const cb of listeners.error) cb(error);
1550
+ });
1551
+
1552
+ /** @type {Watcher} */
1553
+ const controller = {
1554
+ on(event, callback) {
1555
+ (listeners[event] ??= []).push(callback);
1556
+ return controller;
1557
+ },
1558
+ stop() {
1559
+ for (const timer of pending.values()) clearTimeout(timer);
1560
+ pending.clear();
1561
+ try {
1562
+ watcher.close();
1563
+ } catch {
1564
+ // already closed
1565
+ }
1566
+ },
1567
+ pause() {
1568
+ paused = true;
1569
+ return controller;
1570
+ },
1571
+ resume() {
1572
+ paused = false;
1573
+ return controller;
1574
+ },
1575
+ };
1576
+ return controller;
1577
+ }
1578
+
1579
+ module.exports = {
1580
+ getEnv,
1581
+ createPath,
1582
+ getFolder,
1583
+ getFile,
1584
+ getFoldersIn,
1585
+ getFilesIn,
1586
+ glob,
1587
+ find,
1588
+ findUp,
1589
+ readFile,
1590
+ readLines,
1591
+ readJSON,
1592
+ writeFile,
1593
+ writeJSON,
1594
+ appendFile,
1595
+ createFile,
1596
+ createFolder,
1597
+ ensureFolder,
1598
+ ensureFile,
1599
+ touch,
1600
+ deleteFolder,
1601
+ deleteFile,
1602
+ remove,
1603
+ deleteFoldersIn,
1604
+ deleteFilesIn,
1605
+ emptyFolder,
1606
+ copy,
1607
+ move,
1608
+ copyFoldersIn,
1609
+ copyFolder,
1610
+ copyFilesIn,
1611
+ copyFile,
1612
+ moveFoldersIn,
1613
+ moveFolder,
1614
+ moveFilesIn,
1615
+ moveFile,
1616
+ exists,
1617
+ isFile,
1618
+ isFolder,
1619
+ stat,
1620
+ fileSize,
1621
+ folderSize,
1622
+ hashFile,
1623
+ tempFolder,
1624
+ sanitizeFilename,
1625
+ watch,
1626
+ };