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