@ix-xs/node-comfort 1.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/core/FS.js ADDED
@@ -0,0 +1,765 @@
1
+ const dotenv = require("@dotenvx/dotenvx");
2
+ const {
3
+ readdirSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ rmSync,
7
+ writeFileSync,
8
+ unlinkSync,
9
+ copyFileSync,
10
+ readFileSync,
11
+ watch,
12
+ } = require("node:fs");
13
+ const {
14
+ resolve,
15
+ dirname,
16
+ sep,
17
+ isAbsolute,
18
+ join,
19
+ relative,
20
+ basename,
21
+ } = require("node:path");
22
+
23
+ /**
24
+ * Normalizes path separators for cross-platform compatibility.
25
+ * @private
26
+ * @param {string} path - Path to normalize.
27
+ * @returns {string} Normalized path.
28
+ */
29
+ const _format = (path) => {
30
+ return path.replace(/\//g, sep).replace(/\\/g, sep);
31
+ };
32
+
33
+ /**
34
+ * Detects the caller's file path from stack trace (excludes node_modules/internal).
35
+ * @private
36
+ * @returns {string|undefined} Caller file path or undefined.
37
+ */
38
+ const _caller = () => {
39
+ const opst = Error.prepareStackTrace;
40
+ Error.prepareStackTrace = (_, stack) => stack;
41
+ const e = new Error();
42
+ const s = e.stack;
43
+ Error.prepareStackTrace = opst;
44
+
45
+ if (s) {
46
+ for (const cs of s) {
47
+ const name = cs.getFileName();
48
+ if (
49
+ name &&
50
+ !name.startsWith("node:internal") &&
51
+ !name.includes("node_modules") &&
52
+ name !== s[0].getFileName()
53
+ ) {
54
+ return _format(name);
55
+ }
56
+ }
57
+ }
58
+ return undefined;
59
+ };
60
+
61
+ /**
62
+ * Recursively walks directory returning files or folders.
63
+ * @private
64
+ * @param {string} path - Directory path.
65
+ * @param {boolean} [recursive=true] - Include subdirectories.
66
+ * @param {"folder"|"file"} [type="folder"] - Filter type.
67
+ * @returns {string[]} Array of matching paths.
68
+ */
69
+ const _walk = (path = process.cwd(), recursive = true, type = "folder") => {
70
+ let result = [];
71
+ try {
72
+ for (const entry of readdirSync(path, { withFileTypes: true })) {
73
+ if (entry.name.includes("node_modules")) continue;
74
+ const _ = resolve(path, entry.name);
75
+
76
+ if (type === "folder" && entry.isDirectory()) {
77
+ result.push(_);
78
+ if (recursive) result = result.concat(_walk(_, recursive, type));
79
+ } else if (type === "file" && entry.isFile()) {
80
+ result.push(_);
81
+ } else if (type === "file" && entry.isDirectory() && recursive) {
82
+ result = result.concat(_walk(_, recursive, type));
83
+ }
84
+ }
85
+ return result;
86
+ } catch (error) {
87
+ return result;
88
+ }
89
+ };
90
+
91
+ /**
92
+ * Finds first matching folder recursively from root.
93
+ * @private
94
+ * @param {string} path - Starting path.
95
+ * @param {string} name - Folder name to find.
96
+ * @returns {string|undefined} First matching folder path.
97
+ */
98
+ const _firstFolder = (path, name) => {
99
+ try {
100
+ const entries = readdirSync(path, { withFileTypes: true });
101
+ for (const entry of entries) {
102
+ if (entry.name === name && entry.isDirectory()) {
103
+ return resolve(path, name);
104
+ }
105
+ }
106
+ for (const entry of entries) {
107
+ if (entry.isDirectory() && !entry.name.includes("node_modules")) {
108
+ const result = _firstFolder(resolve(path, entry.name), name);
109
+ if (result) return result;
110
+ }
111
+ }
112
+ } catch {}
113
+ return undefined;
114
+ };
115
+
116
+ /**
117
+ * Finds first matching file recursively from root.
118
+ * @private
119
+ * @param {string} path - Starting path.
120
+ * @param {string} name - File name to find.
121
+ * @returns {string|undefined} First matching file path.
122
+ */
123
+ const _firstFile = (path, name) => {
124
+ try {
125
+ const entries = readdirSync(path, { withFileTypes: true });
126
+ for (const entry of entries) {
127
+ if (entry.name === name && entry.isFile()) {
128
+ return resolve(path, name);
129
+ }
130
+ }
131
+ for (const entry of entries) {
132
+ if (entry.isDirectory() && !entry.name.includes("node_modules")) {
133
+ const result = _firstFile(resolve(path, entry.name), name);
134
+ if (result) return result;
135
+ }
136
+ }
137
+ } catch {}
138
+ return undefined;
139
+ };
140
+
141
+ /**
142
+ * Resolves target path with smart relative/absolute/caller context detection.
143
+ * @private
144
+ * @param {string} [path=process.cwd()] - Path to resolve.
145
+ * @param {"folder"|"file"} [type="folder"] - Resolution type.
146
+ * @returns {string|undefined} Resolved absolute path or undefined.
147
+ */
148
+ const _target = (path = process.cwd(), type = "folder") => {
149
+ if (path === process.cwd()) return path;
150
+
151
+ let absolute;
152
+ const caller = dirname(_caller() || process.cwd());
153
+
154
+ if (
155
+ _format(path).startsWith(_format("./")) ||
156
+ _format(path).startsWith(_format("../"))
157
+ ) {
158
+ absolute = resolve(caller, path);
159
+ } else if (isAbsolute(path)) {
160
+ absolute = _format(path);
161
+ } else {
162
+ absolute = resolve(process.cwd(), _format(path));
163
+ if (!existsSync(absolute)) {
164
+ absolute =
165
+ type === "folder"
166
+ ? (_firstFolder(process.cwd(), _format(path)) ?? undefined)
167
+ : (_firstFile(process.cwd(), _format(path)) ?? undefined);
168
+ }
169
+ }
170
+
171
+ if (absolute && existsSync(absolute)) return _format(absolute);
172
+ return undefined;
173
+ };
174
+
175
+ /**
176
+ * @module FS
177
+ * @description
178
+ * Comprehensive synchronous file system utilities with intelligent path resolution,
179
+ * caller context awareness, and cross-platform support.
180
+ *
181
+ * Handles relative/absolute paths relative to caller file, recursive operations,
182
+ * file/folder watching, and bulk copy/move operations. Automatically skips `node_modules`.
183
+ *
184
+ * @example
185
+ * const nodeComfort = require("@ix-xs/node-comfort");
186
+ *
187
+ * // Create folder structure
188
+ * nodeComfort.createFolder("./data/users");
189
+ *
190
+ * // Write JSON file
191
+ * nodeComfort.createFile("./data/users.json", false, { users: [] });
192
+ *
193
+ * // List all files recursively
194
+ * console.log(nodeComfort.getFilesIn("./data"));
195
+ *
196
+ * // Watch for changes
197
+ * const watcher = nodeComfort.watch({ path: "./data", recursive: true });
198
+ * watcher.on("change", (file) => console.log("File changed:", file));
199
+ */
200
+ module.exports = {
201
+ /**
202
+ * Safely read an environment variable.
203
+ *
204
+ * Loads variables from the default `.env` file (if present) and then
205
+ * returns the value for the given key. When `options.envFile` is provided,
206
+ * the function expects `process.env[options.envFile]` to be an object
207
+ * containing namespaced environment variables (e.g. per environment).
208
+ *
209
+ * @param {string} env - Environment variable name (e.g. "PORT").
210
+ * @param {object} [options] - Optional configuration object.
211
+ * @param {string} [options.envFile] - Name of a namespaced env container under `process.env`.
212
+ * @returns {string|undefined} The environment variable value, or `undefined` if not found.
213
+ */
214
+ getEnv(env, options) {
215
+ dotenv.config({ quiet: true });
216
+ return options?.envFile
217
+ ? process.env[options?.envFile][env]
218
+ : process.env[env];
219
+ },
220
+ /**
221
+ * Resolves path relative to caller file or current directory.
222
+ * Supports `./`, `../`, absolute paths, and searches for existing paths.
223
+ * @param {string} [path=process.cwd()] - Path to resolve.
224
+ * @returns {string} Resolved absolute path.
225
+ * @example
226
+ * nodeComfort.createPath("./data/users"); // Resolves from caller file
227
+ */
228
+ createPath(path = process.cwd()) {
229
+ const target = _target(path);
230
+ return target
231
+ ? target
232
+ : isAbsolute(path)
233
+ ? path
234
+ : path.startsWith("./") || path.startsWith("../")
235
+ ? resolve(dirname(_caller()), path)
236
+ : resolve(process.cwd(), path);
237
+ },
238
+
239
+ /**
240
+ * Returns all folders in path (recursive option).
241
+ * @param {string} [path=process.cwd()] - Starting directory.
242
+ * @param {boolean} [recursive=true] - Include subfolders.
243
+ * @returns {string[]|undefined} Array of folder paths.
244
+ */
245
+ getFoldersIn(path = process.cwd(), recursive = true) {
246
+ const target = _target(path ?? process.cwd());
247
+ return target ? _walk(target, recursive) : undefined;
248
+ },
249
+
250
+ /**
251
+ * Resolves and returns existing folder path.
252
+ * @param {string} [path=process.cwd()] - Folder path.
253
+ * @returns {string|undefined} Absolute folder path.
254
+ */
255
+ getFolder(path = process.cwd()) {
256
+ return _target(path ?? process.cwd());
257
+ },
258
+
259
+ /**
260
+ * Returns all files in path (recursive option).
261
+ * @param {string} [path=process.cwd()] - Starting directory.
262
+ * @param {boolean} [recursive=true] - Include subdirectories.
263
+ * @returns {string[]|undefined} Array of file paths.
264
+ */
265
+ getFilesIn(path = process.cwd(), recursive = true) {
266
+ const target = _target(path ?? process.cwd());
267
+ return target ? _walk(target, recursive, "file") : undefined;
268
+ },
269
+
270
+ /**
271
+ * Resolves and returns existing file path.
272
+ * @param {string} path - File path.
273
+ * @returns {string|undefined} Absolute file path.
274
+ */
275
+ getFile(path) {
276
+ return path ? _target(path, "file") : path;
277
+ },
278
+
279
+ /**
280
+ * Creates folder (recursive) with force overwrite option.
281
+ * @param {string} path - Folder path to create.
282
+ * @param {boolean} [force=false] - Overwrite if exists.
283
+ * @returns {boolean|undefined} Success status.
284
+ */
285
+ createFolder(path, force = false) {
286
+ if (!path) return undefined;
287
+ const target = this.createPath(path);
288
+
289
+ try {
290
+ if (this.getFolder(target)) {
291
+ if (force) {
292
+ this.deleteFolder(target);
293
+ mkdirSync(target, { recursive: true });
294
+ return true;
295
+ }
296
+ return false;
297
+ }
298
+ mkdirSync(target, { recursive: true });
299
+ return true;
300
+ } catch {
301
+ return false;
302
+ }
303
+ },
304
+
305
+ /**
306
+ * Creates file with auto-directory creation and content serialization.
307
+ * Supports string, object (→ JSON), null/undefined (→ empty).
308
+ * @param {string} path - File path.
309
+ * @param {boolean} [force=false] - Overwrite if exists.
310
+ * @param {string|object|null|undefined} [data] - File content.
311
+ * @returns {boolean|undefined} Success status.
312
+ * @example
313
+ * nodeComfort.createFile("config.json", false, { apiKey: "secret" });
314
+ */
315
+ createFile(path, force = false, data) {
316
+ if (!path) return undefined;
317
+ const target = this.createPath(path);
318
+ const content =
319
+ data === null || typeof data === "undefined"
320
+ ? ""
321
+ : typeof data === "string"
322
+ ? data
323
+ : JSON.stringify(data, null, 2);
324
+
325
+ try {
326
+ if (this.getFile(target)) {
327
+ if (force) {
328
+ this.deleteFile(target);
329
+ writeFileSync(target, content, "utf8");
330
+ return true;
331
+ }
332
+ return false;
333
+ }
334
+
335
+ if (!this.getFolder(dirname(target))) {
336
+ this.createFolder(dirname(target));
337
+ }
338
+
339
+ writeFileSync(target, content, "utf8");
340
+ return true;
341
+ } catch {
342
+ return false;
343
+ }
344
+ },
345
+
346
+ /**
347
+ * Deletes multiple folders matching filter.
348
+ * @param {string} [path=process.cwd()] - Starting directory.
349
+ * @param {(folder: string) => boolean} [filter] - Folder filter function.
350
+ * @returns {number|undefined} Count of deleted folders.
351
+ */
352
+ deleteFoldersIn(path = process.cwd(), filter = () => true) {
353
+ const target = _target(path);
354
+ if (!target) return undefined;
355
+
356
+ const folders = this.getFoldersIn(target);
357
+ let deleted = 0;
358
+
359
+ for (const folder of folders.map(_format)) {
360
+ if (!filter(folder)) continue;
361
+ this.deleteFolder(folder);
362
+ deleted++;
363
+ }
364
+ return deleted;
365
+ },
366
+
367
+ /**
368
+ * Deletes folder and all contents recursively.
369
+ * @param {string} path - Folder path.
370
+ * @returns {boolean|undefined} Success status.
371
+ */
372
+ deleteFolder(path) {
373
+ const target = path ? _target(path) : path;
374
+ if (!target || !this.getFolder(target)) return undefined;
375
+
376
+ try {
377
+ rmSync(target, { recursive: true, force: true });
378
+ return true;
379
+ } catch {
380
+ return false;
381
+ }
382
+ },
383
+
384
+ /**
385
+ * Deletes multiple files matching filter.
386
+ * @param {string} [path=process.cwd()] - Starting directory.
387
+ * @param {boolean} [recursive=false] - Search subdirectories.
388
+ * @param {(file: string) => boolean} [filter] - File filter function.
389
+ * @returns {number|undefined} Count of deleted files.
390
+ */
391
+ deleteFilesIn(path = process.cwd(), recursive = false, filter = () => true) {
392
+ const target = _target(path);
393
+ if (!target) return undefined;
394
+
395
+ const files = this.getFilesIn(target, recursive);
396
+ let deleted = 0;
397
+
398
+ for (const file of files.map(_format)) {
399
+ if (!filter(file)) continue;
400
+ this.deleteFile(file);
401
+ deleted++;
402
+ }
403
+ return deleted;
404
+ },
405
+
406
+ /**
407
+ * Deletes single file.
408
+ * @param {string} path - File path.
409
+ * @returns {boolean|undefined} Success status.
410
+ */
411
+ deleteFile(path) {
412
+ const target = path ? _target(path, "file") : path;
413
+ if (!target || !this.getFile(target)) return undefined;
414
+
415
+ try {
416
+ unlinkSync(target);
417
+ return true;
418
+ } catch {
419
+ return false;
420
+ }
421
+ },
422
+
423
+ /**
424
+ * Copies multiple folders with optional files and recursive support.
425
+ * @param {Object} options - Copy options.
426
+ * @param {string} options.dest - Destination base path.
427
+ * @param {string} [options.path] - Source path.
428
+ * @param {boolean} [options.recursive=true] - Copy subfolders.
429
+ * @param {boolean} [options.withFiles=false] - Include files.
430
+ * @param {boolean} [options.force=false] - Overwrite existing.
431
+ * @param {(folder: string) => boolean} [options.filter] - Folder filter.
432
+ * @returns {number|undefined} Count of copied folders.
433
+ */
434
+ copyFoldersIn(options) {
435
+ const target = _target(options.path ?? process.cwd());
436
+ if (!target || !options.dest) return undefined;
437
+
438
+ const folders = this.getFoldersIn(target, options.recursive ?? true) ?? [];
439
+ options.filter =
440
+ typeof options.filter === "function" ? options.filter : () => true;
441
+ let copied = 0;
442
+
443
+ for (const folder of folders.map(_format)) {
444
+ if (!options.filter(folder)) continue;
445
+
446
+ const destFolder = join(
447
+ this.createPath(options.dest),
448
+ relative(target, folder),
449
+ );
450
+
451
+ if (options.force && this.getFolder(destFolder)) {
452
+ this.deleteFolder(destFolder);
453
+ }
454
+
455
+ this.createFolder(destFolder);
456
+ copied++;
457
+
458
+ if (options.withFiles) {
459
+ if (options.recursive) {
460
+ for (const file of this.getFilesIn(folder, true) ?? []) {
461
+ const destFile = join(destFolder, relative(folder, file));
462
+ this.createFolder(dirname(destFile));
463
+ copyFileSync(file, destFile);
464
+ }
465
+ } else {
466
+ for (const file of this.getFilesIn(folder, false) ?? []) {
467
+ const destFile = join(destFolder, basename(file));
468
+ copyFileSync(file, destFile);
469
+ }
470
+ }
471
+ }
472
+ }
473
+ return copied;
474
+ },
475
+
476
+ /**
477
+ * Copies single folder with optional contents.
478
+ * @param {Object} options - Copy options.
479
+ * @param {string} options.dest - Destination path.
480
+ * @param {string} [options.path] - Source path.
481
+ * @param {boolean} [options.recursive=false] - Copy subfolders.
482
+ * @param {boolean} [options.withFiles=false] - Include files.
483
+ * @param {boolean} [options.force=false] - Overwrite existing.
484
+ * @returns {boolean|undefined} Success status.
485
+ */
486
+ copyFolder(options) {
487
+ const target = _target(options.path ?? process.cwd());
488
+ const dest = this.createPath(options.dest);
489
+
490
+ if (!target || !dest) return undefined;
491
+
492
+ if (options.force && this.getFolder(dest)) {
493
+ this.deleteFolder(dest);
494
+ }
495
+
496
+ this.createFolder(dest);
497
+
498
+ if (options.recursive) {
499
+ for (const folder of this.getFoldersIn(target, true) ?? []) {
500
+ const destFolder = join(dest, relative(target, folder));
501
+ this.createFolder(destFolder);
502
+
503
+ if (options.withFiles) {
504
+ for (const file of this.getFilesIn(folder, false) ?? []) {
505
+ const destFile = join(destFolder, basename(file));
506
+ copyFileSync(file, destFile);
507
+ }
508
+ }
509
+ }
510
+
511
+ if (options.withFiles) {
512
+ for (const file of this.getFilesIn(target, false) ?? []) {
513
+ const destFile = join(dest, basename(file));
514
+ copyFileSync(file, destFile);
515
+ }
516
+ }
517
+ } else {
518
+ if (options.withFiles) {
519
+ for (const file of this.getFilesIn(target, false) ?? []) {
520
+ const destFile = join(dest, basename(file));
521
+ copyFileSync(file, destFile);
522
+ }
523
+ }
524
+ }
525
+ return true;
526
+ },
527
+
528
+ /**
529
+ * Copies multiple files with optional recursive structure preservation.
530
+ * @param {Object} options - Copy options.
531
+ * @param {string} options.dest - Destination base path.
532
+ * @param {string} [options.path] - Source path.
533
+ * @param {boolean} [options.recursive=true] - Preserve directory structure.
534
+ * @param {boolean} [options.force=false] - Overwrite existing.
535
+ * @param {(file: string) => boolean} [options.filter] - File filter.
536
+ * @returns {number|undefined} Count of copied files.
537
+ */
538
+ copyFilesIn(options) {
539
+ const target = _target(options.path ?? process.cwd());
540
+ const destBase = this.createPath(options.dest);
541
+
542
+ if (!target || !options.dest) return undefined;
543
+
544
+ const recursive = options.recursive ?? true;
545
+ const files = this.getFilesIn(target, recursive) ?? [];
546
+ const filter =
547
+ typeof options.filter === "function" ? options.filter : () => true;
548
+ let copied = 0;
549
+
550
+ for (const file of files.map(_format)) {
551
+ if (!filter(file)) continue;
552
+
553
+ const destFile = join(
554
+ destBase,
555
+ recursive ? relative(target, file) : basename(file),
556
+ );
557
+
558
+ if (options.force && this.getFile(destFile)) {
559
+ this.deleteFile(destFile);
560
+ }
561
+
562
+ this.createFolder(dirname(destFile));
563
+ copyFileSync(file, destFile);
564
+ copied++;
565
+ }
566
+ return copied;
567
+ },
568
+
569
+ /**
570
+ * Copies single file to destination (auto-creates parent folders).
571
+ * @param {Object} options - Copy options.
572
+ * @param {string} options.path - Source file path.
573
+ * @param {string} options.dest - Destination path or folder.
574
+ * @param {boolean} [options.force=false] - Overwrite existing.
575
+ * @returns {boolean|undefined} Success status.
576
+ */
577
+ copyFile(options) {
578
+ const target = _target(options.path, "file");
579
+ if (!target || !options.dest) return undefined;
580
+
581
+ let destFile;
582
+ if (
583
+ !basename(options.dest) ||
584
+ options.dest.endsWith("/") ||
585
+ options.dest.endsWith("\\")
586
+ ) {
587
+ destFile = join(this.createPath(options.dest), basename(target));
588
+ } else {
589
+ destFile = this.createPath(options.dest);
590
+ }
591
+
592
+ if (options.force && this.getFile(destFile)) {
593
+ this.deleteFile(destFile);
594
+ }
595
+
596
+ this.createFolder(dirname(destFile));
597
+ copyFileSync(target, destFile);
598
+ return true;
599
+ },
600
+
601
+ /**
602
+ * Moves multiple folders (copy + delete source).
603
+ * @param {Object} options - Move options (same as copyFoldersIn).
604
+ * @returns {number|undefined} Count of moved folders.
605
+ */
606
+ moveFoldersIn(options) {
607
+ const copied = this.copyFoldersIn(options);
608
+ if (copied === undefined || !options.path) return copied;
609
+
610
+ const target = _target(options.path);
611
+ if (!target) return copied;
612
+
613
+ const folders = this.getFoldersIn(target, options.recursive ?? true) ?? [];
614
+ options.filter =
615
+ typeof options.filter === "function" ? options.filter : () => true;
616
+
617
+ for (const folder of folders.map(_format)) {
618
+ if (!options.filter(folder)) continue;
619
+ this.deleteFolder(folder);
620
+ }
621
+ return copied;
622
+ },
623
+
624
+ /**
625
+ * Moves single folder (copy + delete source).
626
+ * @param {Object} options - Move options (same as copyFolder).
627
+ * @returns {boolean|undefined} Success status.
628
+ */
629
+ moveFolder(options) {
630
+ const copied = this.copyFolder(options);
631
+ const target = _target(options.path);
632
+ if (!target) return copied;
633
+
634
+ this.deleteFolder(target);
635
+ return copied;
636
+ },
637
+
638
+ /**
639
+ * Moves multiple files (copy + delete source).
640
+ * @param {Object} options - Move options (same as copyFilesIn).
641
+ * @returns {number|undefined} Count of moved files.
642
+ */
643
+ moveFilesIn(options) {
644
+ const copied = this.copyFilesIn(options);
645
+ if (copied === undefined || !options.path) return copied;
646
+
647
+ const target = _target(options.path ?? process.cwd());
648
+ if (!target) return copied;
649
+
650
+ const files = this.getFilesIn(target, options.recursive ?? true) ?? [];
651
+ const filter =
652
+ typeof options.filter === "function" ? options.filter : () => true;
653
+
654
+ for (const file of files.map(_format)) {
655
+ if (!filter(file)) continue;
656
+ this.deleteFile(file);
657
+ }
658
+ return copied;
659
+ },
660
+
661
+ /**
662
+ * Moves single file (copy + delete source).
663
+ * @param {Object} options - Move options (same as copyFile).
664
+ * @returns {boolean|undefined} Success status.
665
+ */
666
+ moveFile(options) {
667
+ const moved = this.copyFile(options);
668
+ if (moved) {
669
+ const target = _target(options.path, "file");
670
+ if (target) this.deleteFile(target);
671
+ }
672
+ return moved;
673
+ },
674
+
675
+ /**
676
+ * Reads file content synchronously.
677
+ * @param {string} path - File path.
678
+ * @returns {string|undefined} File content or undefined.
679
+ */
680
+ readFile(path) {
681
+ const target = _target(path, "file");
682
+ if (!target) return undefined;
683
+
684
+ try {
685
+ return readFileSync(target, "utf8");
686
+ } catch {
687
+ return undefined;
688
+ }
689
+ },
690
+
691
+ /**
692
+ * Creates file system watcher with event filtering and pause/resume.
693
+ * @param {Object} [options] - Watcher options.
694
+ * @param {string} [options.path=process.cwd()] - Watch path.
695
+ * @param {boolean} [options.recursive=false] - Watch subdirectories.
696
+ * @param {(event: string, file: string) => boolean} [options.filter] - Event filter.
697
+ * @returns {Object|undefined} Watcher API.
698
+ * @example
699
+ * const watcher = nodeComfort.watch({ path: "./src", recursive: true });
700
+ * watcher.on("change", (file) => console.log("Changed:", file));
701
+ * watcher.on("rename", (file) => console.log("Renamed:", file));
702
+ * watcher.on("all", (event, file) => console.log(event, file));
703
+ */
704
+ watch(options = {}) {
705
+ const target = _target(options.path ?? process.cwd());
706
+ if (!target) return undefined;
707
+
708
+ let paused = false;
709
+ const listeners = {};
710
+
711
+ const handler = (event, file) => {
712
+ if (paused) return;
713
+ if (
714
+ typeof options.filter === "function" &&
715
+ !options.filter(event, file)
716
+ ) {
717
+ return;
718
+ }
719
+
720
+ if (listeners[event]) {
721
+ for (const cb of listeners[event]) cb(file);
722
+ }
723
+ if (listeners["all"]) {
724
+ for (const cb of listeners["all"]) cb(event, file);
725
+ }
726
+ };
727
+
728
+ const addListener = (type, callback) => {
729
+ listeners[type] = listeners[type] ?? [];
730
+ listeners[type].push(callback);
731
+ };
732
+
733
+ const watcher = watch(
734
+ target,
735
+ { recursive: options.recursive ?? false },
736
+ (eventType, fileName) => {
737
+ handler(eventType, fileName ? join(target, fileName) : target);
738
+ },
739
+ );
740
+
741
+ return {
742
+ /**
743
+ * Adds event listener.
744
+ * @param {"change"|"rename"|"all"} event - Event type.
745
+ * @param {function} callback - Callback function.
746
+ */
747
+ on(event, callback) {
748
+ addListener(event, callback);
749
+ },
750
+ stop() {
751
+ if (watcher) {
752
+ try {
753
+ watcher.close();
754
+ } catch {}
755
+ }
756
+ },
757
+ pause() {
758
+ paused = true;
759
+ },
760
+ resume() {
761
+ paused = false;
762
+ },
763
+ };
764
+ },
765
+ };