@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.
- package/CHANGELOG.md +190 -0
- package/README.md +136 -549
- package/index.js +80 -35
- package/package.json +160 -33
- package/src/Arr.js +887 -419
- package/src/Async.js +585 -0
- package/src/Cache.js +329 -0
- package/src/Checker.js +971 -539
- package/src/Cli.js +1034 -0
- package/src/Color.js +415 -0
- package/src/Crypto.js +707 -0
- package/src/Emitter.js +289 -0
- package/src/Env.js +555 -0
- package/src/FS.js +1483 -894
- package/src/Func.js +670 -353
- package/src/Http.js +538 -0
- package/src/Id.js +384 -201
- package/src/Logger.js +739 -378
- package/src/Num.js +844 -361
- package/src/Obj.js +855 -372
- package/src/SQLite.js +1308 -1005
- package/src/Schema.js +1586 -0
- package/src/Str.js +983 -436
- package/src/Sys.js +511 -0
- package/src/Time.js +1260 -422
- package/src/Utils.js +321 -325
- package/src/errors.js +292 -0
- package/types/index.d.ts +693 -526
- package/types/src/Arr.d.ts +455 -145
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +321 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +195 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +501 -235
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +582 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +294 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +534 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +170 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +382 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +686 -238
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +380 -135
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +374 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +149 -68
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +363 -46
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +473 -137
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +390 -104
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +646 -309
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +940 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +569 -166
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +269 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +663 -204
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +154 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +292 -0
- package/types/src/errors.d.ts.map +1 -0
- package/types/index.d.ts.map +0 -1
package/src/FS.js
CHANGED
|
@@ -1,894 +1,1483 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
if (
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
for (const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
const
|
|
731
|
-
if (
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
if (
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
+
};
|