@simplysm/core-node 13.0.76 → 13.0.78

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/src/utils/fs.ts CHANGED
@@ -1,562 +1,570 @@
1
- import path from "path";
2
- import fs from "fs";
3
- import os from "os";
4
- import { glob as globRaw, type GlobOptions, globSync as globRawSync } from "glob";
5
- import { jsonParse, jsonStringify, SdError } from "@simplysm/core-common";
6
- import "@simplysm/core-common";
7
-
8
- //#region Existence Check
9
-
10
- /**
11
- * Checks if a file or directory exists (synchronous).
12
- * @param targetPath - Path to check
13
- */
14
- export function fsExistsSync(targetPath: string): boolean {
15
- return fs.existsSync(targetPath);
16
- }
17
-
18
- /**
19
- * Checks if a file or directory exists (asynchronous).
20
- * @param targetPath - Path to check
21
- */
22
- export async function fsExists(targetPath: string): Promise<boolean> {
23
- try {
24
- await fs.promises.access(targetPath);
25
- return true;
26
- } catch {
27
- return false;
28
- }
29
- }
30
-
31
- //#endregion
32
-
33
- //#region Create Directory
34
-
35
- /**
36
- * Creates a directory (recursive).
37
- * @param targetPath - Directory path to create
38
- */
39
- export function fsMkdirSync(targetPath: string): void {
40
- try {
41
- fs.mkdirSync(targetPath, { recursive: true });
42
- } catch (err) {
43
- throw new SdError(err, targetPath);
44
- }
45
- }
46
-
47
- /**
48
- * Creates a directory (recursive, asynchronous).
49
- * @param targetPath - Directory path to create
50
- */
51
- export async function fsMkdir(targetPath: string): Promise<void> {
52
- try {
53
- await fs.promises.mkdir(targetPath, { recursive: true });
54
- } catch (err) {
55
- throw new SdError(err, targetPath);
56
- }
57
- }
58
-
59
- //#endregion
60
-
61
- //#region Delete
62
-
63
- /**
64
- * Deletes a file or directory.
65
- * @param targetPath - Path to delete
66
- * @remarks The synchronous version fails immediately without retries. Use fsRm for cases with potential transient errors like file locks.
67
- */
68
- export function fsRmSync(targetPath: string): void {
69
- try {
70
- fs.rmSync(targetPath, { recursive: true, force: true });
71
- } catch (err) {
72
- throw new SdError(err, targetPath);
73
- }
74
- }
75
-
76
- /**
77
- * Deletes a file or directory (asynchronous).
78
- * @param targetPath - Path to delete
79
- * @remarks The asynchronous version retries up to 6 times (500ms interval) for transient errors like file locks.
80
- */
81
- export async function fsRm(targetPath: string): Promise<void> {
82
- try {
83
- await fs.promises.rm(targetPath, {
84
- recursive: true,
85
- force: true,
86
- retryDelay: 500,
87
- maxRetries: 6,
88
- });
89
- } catch (err) {
90
- throw new SdError(err, targetPath);
91
- }
92
- }
93
-
94
- //#endregion
95
-
96
- //#region Copy
97
-
98
- /**
99
- * Copies a file or directory.
100
- *
101
- * If sourcePath does not exist, no action is performed and the function returns.
102
- *
103
- * @param sourcePath Path of the source to copy
104
- * @param targetPath Destination path for the copy
105
- * @param filter A filter function that determines whether to copy.
106
- * The **absolute path** of each file/directory is passed.
107
- * Returns true to copy, false to exclude.
108
- * **Note**: The top-level sourcePath is not subject to filtering;
109
- * the filter function is applied recursively to all children (direct and indirect).
110
- * Returning false for a directory skips that directory and all its contents.
111
- */
112
- export function fsCopySync(
113
- sourcePath: string,
114
- targetPath: string,
115
- filter?: (absolutePath: string) => boolean,
116
- ): void {
117
- if (!fsExistsSync(sourcePath)) {
118
- return;
119
- }
120
-
121
- let stats: fs.Stats;
122
- try {
123
- stats = fs.lstatSync(sourcePath);
124
- } catch (err) {
125
- throw new SdError(err, sourcePath);
126
- }
127
-
128
- if (stats.isDirectory()) {
129
- fsMkdirSync(targetPath);
130
-
131
- const children = fsGlobSync(path.resolve(sourcePath, "*"), { dot: true });
132
-
133
- for (const childPath of children) {
134
- if (filter !== undefined && !filter(childPath)) {
135
- continue;
136
- }
137
-
138
- const relativeChildPath = path.relative(sourcePath, childPath);
139
- const childTargetPath = path.resolve(targetPath, relativeChildPath);
140
- fsCopySync(childPath, childTargetPath, filter);
141
- }
142
- } else {
143
- fsMkdirSync(path.dirname(targetPath));
144
-
145
- try {
146
- fs.copyFileSync(sourcePath, targetPath);
147
- } catch (err) {
148
- throw new SdError(err, targetPath);
149
- }
150
- }
151
- }
152
-
153
- /**
154
- * Copies a file or directory (asynchronous).
155
- *
156
- * If sourcePath does not exist, no action is performed and the function returns.
157
- *
158
- * @param sourcePath Path of the source to copy
159
- * @param targetPath Destination path for the copy
160
- * @param filter A filter function that determines whether to copy.
161
- * The **absolute path** of each file/directory is passed.
162
- * Returns true to copy, false to exclude.
163
- * **Note**: The top-level sourcePath is not subject to filtering;
164
- * the filter function is applied recursively to all children (direct and indirect).
165
- * Returning false for a directory skips that directory and all its contents.
166
- */
167
- export async function fsCopy(
168
- sourcePath: string,
169
- targetPath: string,
170
- filter?: (absolutePath: string) => boolean,
171
- ): Promise<void> {
172
- if (!(await fsExists(sourcePath))) {
173
- return;
174
- }
175
-
176
- let stats: fs.Stats;
177
- try {
178
- stats = await fs.promises.lstat(sourcePath);
179
- } catch (err) {
180
- throw new SdError(err, sourcePath);
181
- }
182
-
183
- if (stats.isDirectory()) {
184
- await fsMkdir(targetPath);
185
-
186
- const children = await fsGlob(path.resolve(sourcePath, "*"), { dot: true });
187
-
188
- await children.parallelAsync(async (childPath) => {
189
- if (filter !== undefined && !filter(childPath)) {
190
- return;
191
- }
192
-
193
- const relativeChildPath = path.relative(sourcePath, childPath);
194
- const childTargetPath = path.resolve(targetPath, relativeChildPath);
195
- await fsCopy(childPath, childTargetPath, filter);
196
- });
197
- } else {
198
- await fsMkdir(path.dirname(targetPath));
199
-
200
- try {
201
- await fs.promises.copyFile(sourcePath, targetPath);
202
- } catch (err) {
203
- throw new SdError(err, targetPath);
204
- }
205
- }
206
- }
207
-
208
- //#endregion
209
-
210
- //#region Read File
211
-
212
- /**
213
- * Reads a file as a UTF-8 string.
214
- * @param targetPath - Path of the file to read
215
- */
216
- export function fsReadSync(targetPath: string): string {
217
- try {
218
- return fs.readFileSync(targetPath, "utf-8");
219
- } catch (err) {
220
- throw new SdError(err, targetPath);
221
- }
222
- }
223
-
224
- /**
225
- * Reads a file as a UTF-8 string (asynchronous).
226
- * @param targetPath - Path of the file to read
227
- */
228
- export async function fsRead(targetPath: string): Promise<string> {
229
- try {
230
- return await fs.promises.readFile(targetPath, "utf-8");
231
- } catch (err) {
232
- throw new SdError(err, targetPath);
233
- }
234
- }
235
-
236
- /**
237
- * Reads a file as a Buffer.
238
- * @param targetPath - Path of the file to read
239
- */
240
- export function fsReadBufferSync(targetPath: string): Buffer {
241
- try {
242
- return fs.readFileSync(targetPath);
243
- } catch (err) {
244
- throw new SdError(err, targetPath);
245
- }
246
- }
247
-
248
- /**
249
- * Reads a file as a Buffer (asynchronous).
250
- * @param targetPath - Path of the file to read
251
- */
252
- export async function fsReadBuffer(targetPath: string): Promise<Buffer> {
253
- try {
254
- return await fs.promises.readFile(targetPath);
255
- } catch (err) {
256
- throw new SdError(err, targetPath);
257
- }
258
- }
259
-
260
- /**
261
- * Reads a JSON file (using JsonConvert).
262
- * @param targetPath - Path of the JSON file to read
263
- */
264
- export function fsReadJsonSync<TData = unknown>(targetPath: string): TData {
265
- const contents = fsReadSync(targetPath);
266
- try {
267
- return jsonParse(contents);
268
- } catch (err) {
269
- const preview = contents.length > 500 ? contents.slice(0, 500) + "...(truncated)" : contents;
270
- throw new SdError(err, targetPath + os.EOL + preview);
271
- }
272
- }
273
-
274
- /**
275
- * Reads a JSON file (using JsonConvert, asynchronous).
276
- * @param targetPath - Path of the JSON file to read
277
- */
278
- export async function fsReadJson<TData = unknown>(targetPath: string): Promise<TData> {
279
- const contents = await fsRead(targetPath);
280
- try {
281
- return jsonParse<TData>(contents);
282
- } catch (err) {
283
- const preview = contents.length > 500 ? contents.slice(0, 500) + "...(truncated)" : contents;
284
- throw new SdError(err, targetPath + os.EOL + preview);
285
- }
286
- }
287
-
288
- //#endregion
289
-
290
- //#region Write File
291
-
292
- /**
293
- * Writes data to a file (auto-creates parent directories).
294
- * @param targetPath - Path of the file to write
295
- * @param data - Data to write (string or binary)
296
- */
297
- export function fsWriteSync(targetPath: string, data: string | Uint8Array): void {
298
- fsMkdirSync(path.dirname(targetPath));
299
-
300
- try {
301
- fs.writeFileSync(targetPath, data, { flush: true });
302
- } catch (err) {
303
- throw new SdError(err, targetPath);
304
- }
305
- }
306
-
307
- /**
308
- * Writes data to a file (auto-creates parent directories, asynchronous).
309
- * @param targetPath - Path of the file to write
310
- * @param data - Data to write (string or binary)
311
- */
312
- export async function fsWrite(targetPath: string, data: string | Uint8Array): Promise<void> {
313
- await fsMkdir(path.dirname(targetPath));
314
-
315
- try {
316
- await fs.promises.writeFile(targetPath, data, { flush: true });
317
- } catch (err) {
318
- throw new SdError(err, targetPath);
319
- }
320
- }
321
-
322
- /**
323
- * Writes data to a JSON file (using JsonConvert).
324
- * @param targetPath - Path of the JSON file to write
325
- * @param data - Data to write
326
- * @param options - JSON serialization options
327
- */
328
- export function fsWriteJsonSync(
329
- targetPath: string,
330
- data: unknown,
331
- options?: {
332
- replacer?: (this: unknown, key: string | undefined, value: unknown) => unknown;
333
- space?: string | number;
334
- },
335
- ): void {
336
- const json = jsonStringify(data, options);
337
- fsWriteSync(targetPath, json);
338
- }
339
-
340
- /**
341
- * Writes data to a JSON file (using JsonConvert, asynchronous).
342
- * @param targetPath - Path of the JSON file to write
343
- * @param data - Data to write
344
- * @param options - JSON serialization options
345
- */
346
- export async function fsWriteJson(
347
- targetPath: string,
348
- data: unknown,
349
- options?: {
350
- replacer?: (this: unknown, key: string | undefined, value: unknown) => unknown;
351
- space?: string | number;
352
- },
353
- ): Promise<void> {
354
- const json = jsonStringify(data, options);
355
- await fsWrite(targetPath, json);
356
- }
357
-
358
- //#endregion
359
-
360
- //#region Read Directory
361
-
362
- /**
363
- * Reads the contents of a directory.
364
- * @param targetPath - Path of the directory to read
365
- */
366
- export function fsReaddirSync(targetPath: string): string[] {
367
- try {
368
- return fs.readdirSync(targetPath);
369
- } catch (err) {
370
- throw new SdError(err, targetPath);
371
- }
372
- }
373
-
374
- /**
375
- * Reads the contents of a directory (asynchronous).
376
- * @param targetPath - Path of the directory to read
377
- */
378
- export async function fsReaddir(targetPath: string): Promise<string[]> {
379
- try {
380
- return await fs.promises.readdir(targetPath);
381
- } catch (err) {
382
- throw new SdError(err, targetPath);
383
- }
384
- }
385
-
386
- //#endregion
387
-
388
- //#region File Information
389
-
390
- /**
391
- * Gets file/directory information (follows symbolic links).
392
- * @param targetPath - Path to query information for
393
- */
394
- export function fsStatSync(targetPath: string): fs.Stats {
395
- try {
396
- return fs.statSync(targetPath);
397
- } catch (err) {
398
- throw new SdError(err, targetPath);
399
- }
400
- }
401
-
402
- /**
403
- * Gets file/directory information (follows symbolic links, asynchronous).
404
- * @param targetPath - Path to query information for
405
- */
406
- export async function fsStat(targetPath: string): Promise<fs.Stats> {
407
- try {
408
- return await fs.promises.stat(targetPath);
409
- } catch (err) {
410
- throw new SdError(err, targetPath);
411
- }
412
- }
413
-
414
- /**
415
- * Gets file/directory information (does not follow symbolic links).
416
- * @param targetPath - Path to query information for
417
- */
418
- export function fsLstatSync(targetPath: string): fs.Stats {
419
- try {
420
- return fs.lstatSync(targetPath);
421
- } catch (err) {
422
- throw new SdError(err, targetPath);
423
- }
424
- }
425
-
426
- /**
427
- * Gets file/directory information (does not follow symbolic links, asynchronous).
428
- * @param targetPath - Path to query information for
429
- */
430
- export async function fsLstat(targetPath: string): Promise<fs.Stats> {
431
- try {
432
- return await fs.promises.lstat(targetPath);
433
- } catch (err) {
434
- throw new SdError(err, targetPath);
435
- }
436
- }
437
-
438
- //#endregion
439
-
440
- //#region Glob
441
-
442
- /**
443
- * Searches for files using a glob pattern.
444
- * @param pattern - Glob pattern (e.g., "**\/*.ts")
445
- * @param options - glob options
446
- * @returns Array of absolute paths for matched files
447
- */
448
- export function fsGlobSync(pattern: string, options?: GlobOptions): string[] {
449
- return globRawSync(pattern.replace(/\\/g, "/"), options ?? {}).map((item) =>
450
- path.resolve(item.toString()),
451
- );
452
- }
453
-
454
- /**
455
- * Searches for files using a glob pattern (asynchronous).
456
- * @param pattern - Glob pattern (e.g., "**\/*.ts")
457
- * @param options - glob options
458
- * @returns Array of absolute paths for matched files
459
- */
460
- export async function fsGlob(pattern: string, options?: GlobOptions): Promise<string[]> {
461
- return (await globRaw(pattern.replace(/\\/g, "/"), options ?? {})).map((item) =>
462
- path.resolve(item.toString()),
463
- );
464
- }
465
-
466
- //#endregion
467
-
468
- //#region Utilities
469
-
470
- /**
471
- * Recursively searches and deletes empty directories under a specified directory.
472
- * If all child directories are deleted and a parent becomes empty, it will also be deleted.
473
- */
474
- export async function fsClearEmptyDirectory(dirPath: string): Promise<void> {
475
- if (!(await fsExists(dirPath))) return;
476
-
477
- const childNames = await fsReaddir(dirPath);
478
- let hasFiles = false;
479
-
480
- for (const childName of childNames) {
481
- const childPath = path.resolve(dirPath, childName);
482
- if ((await fsLstat(childPath)).isDirectory()) {
483
- await fsClearEmptyDirectory(childPath);
484
- } else {
485
- hasFiles = true;
486
- }
487
- }
488
-
489
- // If there are files, cannot delete
490
- if (hasFiles) return;
491
-
492
- // Only re-check if there were no files (child directories may have been deleted)
493
- if ((await fsReaddir(dirPath)).length === 0) {
494
- await fsRm(dirPath);
495
- }
496
- }
497
-
498
- /**
499
- * Searches for files matching a glob pattern by traversing parent directories from a start path towards the root.
500
- * Collects all file paths matching the childGlob pattern in each directory.
501
- * @param childGlob - Glob pattern to search for in each directory
502
- * @param fromPath - Path to start searching from
503
- * @param rootPath - Path to stop searching at (if not specified, searches to filesystem root).
504
- * **Note**: fromPath must be a child path of rootPath.
505
- * Otherwise, searches to the filesystem root.
506
- */
507
- export function fsFindAllParentChildPathsSync(
508
- childGlob: string,
509
- fromPath: string,
510
- rootPath?: string,
511
- ): string[] {
512
- const resultPaths: string[] = [];
513
-
514
- let current = fromPath;
515
- while (current) {
516
- const potential = path.resolve(current, childGlob);
517
- const globResults = fsGlobSync(potential);
518
- resultPaths.push(...globResults);
519
-
520
- if (current === rootPath) break;
521
-
522
- const next = path.dirname(current);
523
- if (next === current) break;
524
- current = next;
525
- }
526
-
527
- return resultPaths;
528
- }
529
-
530
- /**
531
- * Searches for files matching a glob pattern by traversing parent directories from a start path towards the root (asynchronous).
532
- * Collects all file paths matching the childGlob pattern in each directory.
533
- * @param childGlob - Glob pattern to search for in each directory
534
- * @param fromPath - Path to start searching from
535
- * @param rootPath - Path to stop searching at (if not specified, searches to filesystem root).
536
- * **Note**: fromPath must be a child path of rootPath.
537
- * Otherwise, searches to the filesystem root.
538
- */
539
- export async function fsFindAllParentChildPaths(
540
- childGlob: string,
541
- fromPath: string,
542
- rootPath?: string,
543
- ): Promise<string[]> {
544
- const resultPaths: string[] = [];
545
-
546
- let current = fromPath;
547
- while (current) {
548
- const potential = path.resolve(current, childGlob);
549
- const globResults = await fsGlob(potential);
550
- resultPaths.push(...globResults);
551
-
552
- if (current === rootPath) break;
553
-
554
- const next = path.dirname(current);
555
- if (next === current) break;
556
- current = next;
557
- }
558
-
559
- return resultPaths;
560
- }
561
-
562
- //#endregion
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import { glob as globRaw, type GlobOptions, globSync as globRawSync } from "glob";
5
+ import { json, SdError } from "@simplysm/core-common";
6
+ import "@simplysm/core-common";
7
+
8
+ //#region Existence Check
9
+
10
+ /**
11
+ * Checks if a file or directory exists (synchronous).
12
+ * @param targetPath - Path to check
13
+ */
14
+ export function existsSync(targetPath: string): boolean {
15
+ return fs.existsSync(targetPath);
16
+ }
17
+
18
+ /**
19
+ * Checks if a file or directory exists (asynchronous).
20
+ * @param targetPath - Path to check
21
+ */
22
+ export async function exists(targetPath: string): Promise<boolean> {
23
+ try {
24
+ await fs.promises.access(targetPath);
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ //#endregion
32
+
33
+ //#region Create Directory
34
+
35
+ /**
36
+ * Creates a directory (recursive).
37
+ * @param targetPath - Directory path to create
38
+ */
39
+ export function mkdirSync(targetPath: string): void {
40
+ try {
41
+ fs.mkdirSync(targetPath, { recursive: true });
42
+ } catch (err) {
43
+ throw new SdError(err, targetPath);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Creates a directory (recursive, asynchronous).
49
+ * @param targetPath - Directory path to create
50
+ */
51
+ export async function mkdir(targetPath: string): Promise<void> {
52
+ try {
53
+ await fs.promises.mkdir(targetPath, { recursive: true });
54
+ } catch (err) {
55
+ throw new SdError(err, targetPath);
56
+ }
57
+ }
58
+
59
+ //#endregion
60
+
61
+ //#region Delete
62
+
63
+ /**
64
+ * Deletes a file or directory.
65
+ * @param targetPath - Path to delete
66
+ * @remarks The synchronous version fails immediately without retries. Use rm for cases with potential transient errors like file locks.
67
+ */
68
+ export function rmSync(targetPath: string): void {
69
+ try {
70
+ fs.rmSync(targetPath, { recursive: true, force: true });
71
+ } catch (err) {
72
+ throw new SdError(err, targetPath);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Deletes a file or directory (asynchronous).
78
+ * @param targetPath - Path to delete
79
+ * @remarks The asynchronous version retries up to 6 times (500ms interval) for transient errors like file locks.
80
+ */
81
+ export async function rm(targetPath: string): Promise<void> {
82
+ try {
83
+ await fs.promises.rm(targetPath, {
84
+ recursive: true,
85
+ force: true,
86
+ retryDelay: 500,
87
+ maxRetries: 6,
88
+ });
89
+ } catch (err) {
90
+ throw new SdError(err, targetPath);
91
+ }
92
+ }
93
+
94
+ //#endregion
95
+
96
+ //#region Copy
97
+
98
+ interface CopyEntry {
99
+ sourcePath: string;
100
+ targetPath: string;
101
+ }
102
+
103
+ function collectCopyEntries(
104
+ sourcePath: string,
105
+ targetPath: string,
106
+ children: string[],
107
+ filter?: (absolutePath: string) => boolean,
108
+ ): CopyEntry[] {
109
+ const entries: CopyEntry[] = [];
110
+ for (const childPath of children) {
111
+ if (filter !== undefined && !filter(childPath)) {
112
+ continue;
113
+ }
114
+ const relativeChildPath = path.relative(sourcePath, childPath);
115
+ const childTargetPath = path.resolve(targetPath, relativeChildPath);
116
+ entries.push({ sourcePath: childPath, targetPath: childTargetPath });
117
+ }
118
+ return entries;
119
+ }
120
+
121
+ /**
122
+ * Copies a file or directory.
123
+ *
124
+ * If sourcePath does not exist, no action is performed and the function returns.
125
+ *
126
+ * @param sourcePath Path of the source to copy
127
+ * @param targetPath Destination path for the copy
128
+ * @param filter A filter function that determines whether to copy.
129
+ * The **absolute path** of each file/directory is passed.
130
+ * Returns true to copy, false to exclude.
131
+ * **Note**: The top-level sourcePath is not subject to filtering;
132
+ * the filter function is applied recursively to all children (direct and indirect).
133
+ * Returning false for a directory skips that directory and all its contents.
134
+ */
135
+ export function copySync(
136
+ sourcePath: string,
137
+ targetPath: string,
138
+ filter?: (absolutePath: string) => boolean,
139
+ ): void {
140
+ if (!existsSync(sourcePath)) {
141
+ return;
142
+ }
143
+
144
+ let stats: fs.Stats;
145
+ try {
146
+ stats = fs.lstatSync(sourcePath);
147
+ } catch (err) {
148
+ throw new SdError(err, sourcePath);
149
+ }
150
+
151
+ if (stats.isDirectory()) {
152
+ mkdirSync(targetPath);
153
+ const children = globSync(path.resolve(sourcePath, "*"), { dot: true });
154
+ for (const entry of collectCopyEntries(sourcePath, targetPath, children, filter)) {
155
+ copySync(entry.sourcePath, entry.targetPath, filter);
156
+ }
157
+ } else {
158
+ mkdirSync(path.dirname(targetPath));
159
+
160
+ try {
161
+ fs.copyFileSync(sourcePath, targetPath);
162
+ } catch (err) {
163
+ throw new SdError(err, targetPath);
164
+ }
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Copies a file or directory (asynchronous).
170
+ *
171
+ * If sourcePath does not exist, no action is performed and the function returns.
172
+ *
173
+ * @param sourcePath Path of the source to copy
174
+ * @param targetPath Destination path for the copy
175
+ * @param filter A filter function that determines whether to copy.
176
+ * The **absolute path** of each file/directory is passed.
177
+ * Returns true to copy, false to exclude.
178
+ * **Note**: The top-level sourcePath is not subject to filtering;
179
+ * the filter function is applied recursively to all children (direct and indirect).
180
+ * Returning false for a directory skips that directory and all its contents.
181
+ */
182
+ export async function copy(
183
+ sourcePath: string,
184
+ targetPath: string,
185
+ filter?: (absolutePath: string) => boolean,
186
+ ): Promise<void> {
187
+ if (!(await exists(sourcePath))) {
188
+ return;
189
+ }
190
+
191
+ let stats: fs.Stats;
192
+ try {
193
+ stats = await fs.promises.lstat(sourcePath);
194
+ } catch (err) {
195
+ throw new SdError(err, sourcePath);
196
+ }
197
+
198
+ if (stats.isDirectory()) {
199
+ await mkdir(targetPath);
200
+ const children = await glob(path.resolve(sourcePath, "*"), { dot: true });
201
+ await collectCopyEntries(sourcePath, targetPath, children, filter)
202
+ .parallelAsync(async (entry) => {
203
+ await copy(entry.sourcePath, entry.targetPath, filter);
204
+ });
205
+ } else {
206
+ await mkdir(path.dirname(targetPath));
207
+
208
+ try {
209
+ await fs.promises.copyFile(sourcePath, targetPath);
210
+ } catch (err) {
211
+ throw new SdError(err, targetPath);
212
+ }
213
+ }
214
+ }
215
+
216
+ //#endregion
217
+
218
+ //#region Read File
219
+
220
+ /**
221
+ * Reads a file as a UTF-8 string.
222
+ * @param targetPath - Path of the file to read
223
+ */
224
+ export function readSync(targetPath: string): string {
225
+ try {
226
+ return fs.readFileSync(targetPath, "utf-8");
227
+ } catch (err) {
228
+ throw new SdError(err, targetPath);
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Reads a file as a UTF-8 string (asynchronous).
234
+ * @param targetPath - Path of the file to read
235
+ */
236
+ export async function read(targetPath: string): Promise<string> {
237
+ try {
238
+ return await fs.promises.readFile(targetPath, "utf-8");
239
+ } catch (err) {
240
+ throw new SdError(err, targetPath);
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Reads a file as a Buffer.
246
+ * @param targetPath - Path of the file to read
247
+ */
248
+ export function readBufferSync(targetPath: string): Buffer {
249
+ try {
250
+ return fs.readFileSync(targetPath);
251
+ } catch (err) {
252
+ throw new SdError(err, targetPath);
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Reads a file as a Buffer (asynchronous).
258
+ * @param targetPath - Path of the file to read
259
+ */
260
+ export async function readBuffer(targetPath: string): Promise<Buffer> {
261
+ try {
262
+ return await fs.promises.readFile(targetPath);
263
+ } catch (err) {
264
+ throw new SdError(err, targetPath);
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Reads a JSON file (using JsonConvert).
270
+ * @param targetPath - Path of the JSON file to read
271
+ */
272
+ export function readJsonSync<TData = unknown>(targetPath: string): TData {
273
+ const contents = readSync(targetPath);
274
+ try {
275
+ return json.parse(contents);
276
+ } catch (err) {
277
+ const preview = contents.length > 500 ? contents.slice(0, 500) + "...(truncated)" : contents;
278
+ throw new SdError(err, targetPath + os.EOL + preview);
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Reads a JSON file (using JsonConvert, asynchronous).
284
+ * @param targetPath - Path of the JSON file to read
285
+ */
286
+ export async function readJson<TData = unknown>(targetPath: string): Promise<TData> {
287
+ const contents = await read(targetPath);
288
+ try {
289
+ return json.parse<TData>(contents);
290
+ } catch (err) {
291
+ const preview = contents.length > 500 ? contents.slice(0, 500) + "...(truncated)" : contents;
292
+ throw new SdError(err, targetPath + os.EOL + preview);
293
+ }
294
+ }
295
+
296
+ //#endregion
297
+
298
+ //#region Write File
299
+
300
+ /**
301
+ * Writes data to a file (auto-creates parent directories).
302
+ * @param targetPath - Path of the file to write
303
+ * @param data - Data to write (string or binary)
304
+ */
305
+ export function writeSync(targetPath: string, data: string | Uint8Array): void {
306
+ mkdirSync(path.dirname(targetPath));
307
+
308
+ try {
309
+ fs.writeFileSync(targetPath, data, { flush: true });
310
+ } catch (err) {
311
+ throw new SdError(err, targetPath);
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Writes data to a file (auto-creates parent directories, asynchronous).
317
+ * @param targetPath - Path of the file to write
318
+ * @param data - Data to write (string or binary)
319
+ */
320
+ export async function write(targetPath: string, data: string | Uint8Array): Promise<void> {
321
+ await mkdir(path.dirname(targetPath));
322
+
323
+ try {
324
+ await fs.promises.writeFile(targetPath, data, { flush: true });
325
+ } catch (err) {
326
+ throw new SdError(err, targetPath);
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Writes data to a JSON file (using JsonConvert).
332
+ * @param targetPath - Path of the JSON file to write
333
+ * @param data - Data to write
334
+ * @param options - JSON serialization options
335
+ */
336
+ export function writeJsonSync(
337
+ targetPath: string,
338
+ data: unknown,
339
+ options?: {
340
+ replacer?: (this: unknown, key: string | undefined, value: unknown) => unknown;
341
+ space?: string | number;
342
+ },
343
+ ): void {
344
+ const jsonStr = json.stringify(data, options);
345
+ writeSync(targetPath, jsonStr);
346
+ }
347
+
348
+ /**
349
+ * Writes data to a JSON file (using JsonConvert, asynchronous).
350
+ * @param targetPath - Path of the JSON file to write
351
+ * @param data - Data to write
352
+ * @param options - JSON serialization options
353
+ */
354
+ export async function writeJson(
355
+ targetPath: string,
356
+ data: unknown,
357
+ options?: {
358
+ replacer?: (this: unknown, key: string | undefined, value: unknown) => unknown;
359
+ space?: string | number;
360
+ },
361
+ ): Promise<void> {
362
+ const jsonStr = json.stringify(data, options);
363
+ await write(targetPath, jsonStr);
364
+ }
365
+
366
+ //#endregion
367
+
368
+ //#region Read Directory
369
+
370
+ /**
371
+ * Reads the contents of a directory.
372
+ * @param targetPath - Path of the directory to read
373
+ */
374
+ export function readdirSync(targetPath: string): string[] {
375
+ try {
376
+ return fs.readdirSync(targetPath);
377
+ } catch (err) {
378
+ throw new SdError(err, targetPath);
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Reads the contents of a directory (asynchronous).
384
+ * @param targetPath - Path of the directory to read
385
+ */
386
+ export async function readdir(targetPath: string): Promise<string[]> {
387
+ try {
388
+ return await fs.promises.readdir(targetPath);
389
+ } catch (err) {
390
+ throw new SdError(err, targetPath);
391
+ }
392
+ }
393
+
394
+ //#endregion
395
+
396
+ //#region File Information
397
+
398
+ /**
399
+ * Gets file/directory information (follows symbolic links).
400
+ * @param targetPath - Path to query information for
401
+ */
402
+ export function statSync(targetPath: string): fs.Stats {
403
+ try {
404
+ return fs.statSync(targetPath);
405
+ } catch (err) {
406
+ throw new SdError(err, targetPath);
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Gets file/directory information (follows symbolic links, asynchronous).
412
+ * @param targetPath - Path to query information for
413
+ */
414
+ export async function stat(targetPath: string): Promise<fs.Stats> {
415
+ try {
416
+ return await fs.promises.stat(targetPath);
417
+ } catch (err) {
418
+ throw new SdError(err, targetPath);
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Gets file/directory information (does not follow symbolic links).
424
+ * @param targetPath - Path to query information for
425
+ */
426
+ export function lstatSync(targetPath: string): fs.Stats {
427
+ try {
428
+ return fs.lstatSync(targetPath);
429
+ } catch (err) {
430
+ throw new SdError(err, targetPath);
431
+ }
432
+ }
433
+
434
+ /**
435
+ * Gets file/directory information (does not follow symbolic links, asynchronous).
436
+ * @param targetPath - Path to query information for
437
+ */
438
+ export async function lstat(targetPath: string): Promise<fs.Stats> {
439
+ try {
440
+ return await fs.promises.lstat(targetPath);
441
+ } catch (err) {
442
+ throw new SdError(err, targetPath);
443
+ }
444
+ }
445
+
446
+ //#endregion
447
+
448
+ //#region Glob
449
+
450
+ /**
451
+ * Searches for files using a glob pattern.
452
+ * @param pattern - Glob pattern (e.g., "**\/*.ts")
453
+ * @param options - glob options
454
+ * @returns Array of absolute paths for matched files
455
+ */
456
+ export function globSync(pattern: string, options?: GlobOptions): string[] {
457
+ return globRawSync(pattern.replace(/\\/g, "/"), options ?? {}).map((item) =>
458
+ path.resolve(item.toString()),
459
+ );
460
+ }
461
+
462
+ /**
463
+ * Searches for files using a glob pattern (asynchronous).
464
+ * @param pattern - Glob pattern (e.g., "**\/*.ts")
465
+ * @param options - glob options
466
+ * @returns Array of absolute paths for matched files
467
+ */
468
+ export async function glob(pattern: string, options?: GlobOptions): Promise<string[]> {
469
+ return (await globRaw(pattern.replace(/\\/g, "/"), options ?? {})).map((item) =>
470
+ path.resolve(item.toString()),
471
+ );
472
+ }
473
+
474
+ //#endregion
475
+
476
+ //#region Utilities
477
+
478
+ /**
479
+ * Recursively searches and deletes empty directories under a specified directory.
480
+ * If all child directories are deleted and a parent becomes empty, it will also be deleted.
481
+ */
482
+ export async function clearEmptyDirectory(dirPath: string): Promise<void> {
483
+ if (!(await exists(dirPath))) return;
484
+
485
+ const childNames = await readdir(dirPath);
486
+ let hasFiles = false;
487
+
488
+ for (const childName of childNames) {
489
+ const childPath = path.resolve(dirPath, childName);
490
+ if ((await lstat(childPath)).isDirectory()) {
491
+ await clearEmptyDirectory(childPath);
492
+ } else {
493
+ hasFiles = true;
494
+ }
495
+ }
496
+
497
+ // If there are files, cannot delete
498
+ if (hasFiles) return;
499
+
500
+ // Only re-check if there were no files (child directories may have been deleted)
501
+ if ((await readdir(dirPath)).length === 0) {
502
+ await rm(dirPath);
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Searches for files matching a glob pattern by traversing parent directories from a start path towards the root.
508
+ * Collects all file paths matching the childGlob pattern in each directory.
509
+ * @param childGlob - Glob pattern to search for in each directory
510
+ * @param fromPath - Path to start searching from
511
+ * @param rootPath - Path to stop searching at (if not specified, searches to filesystem root).
512
+ * **Note**: fromPath must be a child path of rootPath.
513
+ * Otherwise, searches to the filesystem root.
514
+ */
515
+ export function findAllParentChildPathsSync(
516
+ childGlob: string,
517
+ fromPath: string,
518
+ rootPath?: string,
519
+ ): string[] {
520
+ const resultPaths: string[] = [];
521
+
522
+ let current = fromPath;
523
+ while (current) {
524
+ const potential = path.resolve(current, childGlob);
525
+ const globResults = globSync(potential);
526
+ resultPaths.push(...globResults);
527
+
528
+ if (current === rootPath) break;
529
+
530
+ const next = path.dirname(current);
531
+ if (next === current) break;
532
+ current = next;
533
+ }
534
+
535
+ return resultPaths;
536
+ }
537
+
538
+ /**
539
+ * Searches for files matching a glob pattern by traversing parent directories from a start path towards the root (asynchronous).
540
+ * Collects all file paths matching the childGlob pattern in each directory.
541
+ * @param childGlob - Glob pattern to search for in each directory
542
+ * @param fromPath - Path to start searching from
543
+ * @param rootPath - Path to stop searching at (if not specified, searches to filesystem root).
544
+ * **Note**: fromPath must be a child path of rootPath.
545
+ * Otherwise, searches to the filesystem root.
546
+ */
547
+ export async function findAllParentChildPaths(
548
+ childGlob: string,
549
+ fromPath: string,
550
+ rootPath?: string,
551
+ ): Promise<string[]> {
552
+ const resultPaths: string[] = [];
553
+
554
+ let current = fromPath;
555
+ while (current) {
556
+ const potential = path.resolve(current, childGlob);
557
+ const globResults = await glob(potential);
558
+ resultPaths.push(...globResults);
559
+
560
+ if (current === rootPath) break;
561
+
562
+ const next = path.dirname(current);
563
+ if (next === current) break;
564
+ current = next;
565
+ }
566
+
567
+ return resultPaths;
568
+ }
569
+
570
+ //#endregion