@simplysm/core-node 13.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/.cache/typecheck-node.tsbuildinfo +1 -0
  2. package/.cache/typecheck-tests-node.tsbuildinfo +1 -0
  3. package/README.md +375 -0
  4. package/dist/core-common/src/common.types.d.ts +74 -0
  5. package/dist/core-common/src/common.types.d.ts.map +1 -0
  6. package/dist/core-common/src/env.d.ts +6 -0
  7. package/dist/core-common/src/env.d.ts.map +1 -0
  8. package/dist/core-common/src/errors/argument-error.d.ts +25 -0
  9. package/dist/core-common/src/errors/argument-error.d.ts.map +1 -0
  10. package/dist/core-common/src/errors/not-implemented-error.d.ts +29 -0
  11. package/dist/core-common/src/errors/not-implemented-error.d.ts.map +1 -0
  12. package/dist/core-common/src/errors/sd-error.d.ts +27 -0
  13. package/dist/core-common/src/errors/sd-error.d.ts.map +1 -0
  14. package/dist/core-common/src/errors/timeout-error.d.ts +31 -0
  15. package/dist/core-common/src/errors/timeout-error.d.ts.map +1 -0
  16. package/dist/core-common/src/extensions/arr-ext.d.ts +15 -0
  17. package/dist/core-common/src/extensions/arr-ext.d.ts.map +1 -0
  18. package/dist/core-common/src/extensions/arr-ext.helpers.d.ts +19 -0
  19. package/dist/core-common/src/extensions/arr-ext.helpers.d.ts.map +1 -0
  20. package/dist/core-common/src/extensions/arr-ext.types.d.ts +215 -0
  21. package/dist/core-common/src/extensions/arr-ext.types.d.ts.map +1 -0
  22. package/dist/core-common/src/extensions/map-ext.d.ts +57 -0
  23. package/dist/core-common/src/extensions/map-ext.d.ts.map +1 -0
  24. package/dist/core-common/src/extensions/set-ext.d.ts +36 -0
  25. package/dist/core-common/src/extensions/set-ext.d.ts.map +1 -0
  26. package/dist/core-common/src/features/debounce-queue.d.ts +53 -0
  27. package/dist/core-common/src/features/debounce-queue.d.ts.map +1 -0
  28. package/dist/core-common/src/features/event-emitter.d.ts +66 -0
  29. package/dist/core-common/src/features/event-emitter.d.ts.map +1 -0
  30. package/dist/core-common/src/features/serial-queue.d.ts +47 -0
  31. package/dist/core-common/src/features/serial-queue.d.ts.map +1 -0
  32. package/dist/core-common/src/index.d.ts +32 -0
  33. package/dist/core-common/src/index.d.ts.map +1 -0
  34. package/dist/core-common/src/types/date-only.d.ts +152 -0
  35. package/dist/core-common/src/types/date-only.d.ts.map +1 -0
  36. package/dist/core-common/src/types/date-time.d.ts +96 -0
  37. package/dist/core-common/src/types/date-time.d.ts.map +1 -0
  38. package/dist/core-common/src/types/lazy-gc-map.d.ts +80 -0
  39. package/dist/core-common/src/types/lazy-gc-map.d.ts.map +1 -0
  40. package/dist/core-common/src/types/time.d.ts +68 -0
  41. package/dist/core-common/src/types/time.d.ts.map +1 -0
  42. package/dist/core-common/src/types/uuid.d.ts +35 -0
  43. package/dist/core-common/src/types/uuid.d.ts.map +1 -0
  44. package/dist/core-common/src/utils/bytes.d.ts +51 -0
  45. package/dist/core-common/src/utils/bytes.d.ts.map +1 -0
  46. package/dist/core-common/src/utils/date-format.d.ts +90 -0
  47. package/dist/core-common/src/utils/date-format.d.ts.map +1 -0
  48. package/dist/core-common/src/utils/json.d.ts +34 -0
  49. package/dist/core-common/src/utils/json.d.ts.map +1 -0
  50. package/dist/core-common/src/utils/num.d.ts +60 -0
  51. package/dist/core-common/src/utils/num.d.ts.map +1 -0
  52. package/dist/core-common/src/utils/obj.d.ts +258 -0
  53. package/dist/core-common/src/utils/obj.d.ts.map +1 -0
  54. package/dist/core-common/src/utils/path.d.ts +23 -0
  55. package/dist/core-common/src/utils/path.d.ts.map +1 -0
  56. package/dist/core-common/src/utils/primitive.d.ts +18 -0
  57. package/dist/core-common/src/utils/primitive.d.ts.map +1 -0
  58. package/dist/core-common/src/utils/str.d.ts +103 -0
  59. package/dist/core-common/src/utils/str.d.ts.map +1 -0
  60. package/dist/core-common/src/utils/template-strings.d.ts +84 -0
  61. package/dist/core-common/src/utils/template-strings.d.ts.map +1 -0
  62. package/dist/core-common/src/utils/transferable.d.ts +47 -0
  63. package/dist/core-common/src/utils/transferable.d.ts.map +1 -0
  64. package/dist/core-common/src/utils/wait.d.ts +19 -0
  65. package/dist/core-common/src/utils/wait.d.ts.map +1 -0
  66. package/dist/core-common/src/utils/xml.d.ts +36 -0
  67. package/dist/core-common/src/utils/xml.d.ts.map +1 -0
  68. package/dist/core-common/src/zip/sd-zip.d.ts +80 -0
  69. package/dist/core-common/src/zip/sd-zip.d.ts.map +1 -0
  70. package/dist/core-node/src/features/fs-watcher.d.ts +70 -0
  71. package/dist/core-node/src/features/fs-watcher.d.ts.map +1 -0
  72. package/dist/core-node/src/index.d.ts +7 -0
  73. package/dist/core-node/src/index.d.ts.map +1 -0
  74. package/dist/core-node/src/utils/fs.d.ts +197 -0
  75. package/dist/core-node/src/utils/fs.d.ts.map +1 -0
  76. package/dist/core-node/src/utils/path.d.ts +75 -0
  77. package/dist/core-node/src/utils/path.d.ts.map +1 -0
  78. package/dist/core-node/src/worker/create-worker.d.ts +23 -0
  79. package/dist/core-node/src/worker/create-worker.d.ts.map +1 -0
  80. package/dist/core-node/src/worker/types.d.ts +67 -0
  81. package/dist/core-node/src/worker/types.d.ts.map +1 -0
  82. package/dist/core-node/src/worker/worker.d.ts +27 -0
  83. package/dist/core-node/src/worker/worker.d.ts.map +1 -0
  84. package/dist/features/fs-watcher.js +100 -0
  85. package/dist/features/fs-watcher.js.map +7 -0
  86. package/dist/index.js +7 -0
  87. package/dist/index.js.map +7 -0
  88. package/dist/utils/fs.js +305 -0
  89. package/dist/utils/fs.js.map +7 -0
  90. package/dist/utils/path.js +48 -0
  91. package/dist/utils/path.js.map +7 -0
  92. package/dist/worker/create-worker.js +85 -0
  93. package/dist/worker/create-worker.js.map +7 -0
  94. package/dist/worker/types.js +1 -0
  95. package/dist/worker/types.js.map +7 -0
  96. package/dist/worker/worker.js +142 -0
  97. package/dist/worker/worker.js.map +7 -0
  98. package/lib/worker-dev-proxy.js +12 -0
  99. package/package.json +23 -0
  100. package/src/features/fs-watcher.ts +176 -0
  101. package/src/index.ts +11 -0
  102. package/src/utils/fs.ts +550 -0
  103. package/src/utils/path.ts +128 -0
  104. package/src/worker/create-worker.ts +141 -0
  105. package/src/worker/types.ts +86 -0
  106. package/src/worker/worker.ts +207 -0
  107. package/tests/utils/fs-watcher.spec.ts +295 -0
  108. package/tests/utils/fs.spec.ts +754 -0
  109. package/tests/utils/path.spec.ts +192 -0
  110. package/tests/worker/fixtures/test-worker.ts +35 -0
  111. package/tests/worker/sd-worker.spec.ts +183 -0
@@ -0,0 +1,550 @@
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 존재 확인
9
+
10
+ /**
11
+ * 파일 또는 디렉토리 존재 확인 (동기).
12
+ * @param targetPath - 확인할 경로
13
+ */
14
+ export function fsExistsSync(targetPath: string): boolean {
15
+ return fs.existsSync(targetPath);
16
+ }
17
+
18
+ /**
19
+ * 파일 또는 디렉토리 존재 확인 (비동기).
20
+ * @param targetPath - 확인할 경로
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 디렉토리 생성
34
+
35
+ /**
36
+ * 디렉토리 생성 (recursive).
37
+ * @param targetPath - 생성할 디렉토리 경로
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
+ * 디렉토리 생성 (recursive, 비동기).
49
+ * @param targetPath - 생성할 디렉토리 경로
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 삭제
62
+
63
+ /**
64
+ * 파일 또는 디렉토리 삭제.
65
+ * @param targetPath - 삭제할 경로
66
+ * @remarks 동기 버전은 재시도 없이 즉시 실패함. 파일 잠금 등 일시적 오류 가능성이 있는 경우 fsRm 사용을 권장함.
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
+ * 파일 또는 디렉토리 삭제 (비동기).
78
+ * @param targetPath - 삭제할 경로
79
+ * @remarks 비동기 버전은 파일 잠금 등의 일시적 오류에 대해 최대 6회(500ms 간격) 재시도함.
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 복사
97
+
98
+ /**
99
+ * 파일 또는 디렉토리 복사.
100
+ *
101
+ * sourcePath가 존재하지 않으면 아무 작업도 수행하지 않고 반환한다.
102
+ *
103
+ * @param sourcePath 복사할 원본 경로
104
+ * @param targetPath 복사 대상 경로
105
+ * @param filter 복사 여부를 결정하는 필터 함수.
106
+ * 각 파일/디렉토리의 **절대 경로**가 전달되며,
107
+ * true를 반환하면 복사, false면 제외.
108
+ * **주의**: 최상위 sourcePath는 필터 대상이 아니며,
109
+ * 모든 하위 항목(자식, 손자 등)에 재귀적으로 filter 함수가 적용된다.
110
+ * 디렉토리에 false를 반환하면 해당 디렉토리와 모든 하위 항목이 건너뛰어짐.
111
+ */
112
+ export function fsCopySync(sourcePath: string, targetPath: string, filter?: (absolutePath: string) => boolean): void {
113
+ if (!fsExistsSync(sourcePath)) {
114
+ return;
115
+ }
116
+
117
+ let stats: fs.Stats;
118
+ try {
119
+ stats = fs.lstatSync(sourcePath);
120
+ } catch (err) {
121
+ throw new SdError(err, sourcePath);
122
+ }
123
+
124
+ if (stats.isDirectory()) {
125
+ fsMkdirSync(targetPath);
126
+
127
+ const children = fsGlobSync(path.resolve(sourcePath, "*"), { dot: true });
128
+
129
+ for (const childPath of children) {
130
+ if (filter !== undefined && !filter(childPath)) {
131
+ continue;
132
+ }
133
+
134
+ const relativeChildPath = path.relative(sourcePath, childPath);
135
+ const childTargetPath = path.resolve(targetPath, relativeChildPath);
136
+ fsCopySync(childPath, childTargetPath, filter);
137
+ }
138
+ } else {
139
+ fsMkdirSync(path.dirname(targetPath));
140
+
141
+ try {
142
+ fs.copyFileSync(sourcePath, targetPath);
143
+ } catch (err) {
144
+ throw new SdError(err, targetPath);
145
+ }
146
+ }
147
+ }
148
+
149
+ /**
150
+ * 파일 또는 디렉토리 복사 (비동기).
151
+ *
152
+ * sourcePath가 존재하지 않으면 아무 작업도 수행하지 않고 반환한다.
153
+ *
154
+ * @param sourcePath 복사할 원본 경로
155
+ * @param targetPath 복사 대상 경로
156
+ * @param filter 복사 여부를 결정하는 필터 함수.
157
+ * 각 파일/디렉토리의 **절대 경로**가 전달되며,
158
+ * true를 반환하면 복사, false면 제외.
159
+ * **주의**: 최상위 sourcePath는 필터 대상이 아니며,
160
+ * 모든 하위 항목(자식, 손자 등)에 재귀적으로 filter 함수가 적용된다.
161
+ * 디렉토리에 false를 반환하면 해당 디렉토리와 모든 하위 항목이 건너뛰어짐.
162
+ */
163
+ export async function fsCopy(
164
+ sourcePath: string,
165
+ targetPath: string,
166
+ filter?: (absolutePath: string) => boolean,
167
+ ): Promise<void> {
168
+ if (!(await fsExists(sourcePath))) {
169
+ return;
170
+ }
171
+
172
+ let stats: fs.Stats;
173
+ try {
174
+ stats = await fs.promises.lstat(sourcePath);
175
+ } catch (err) {
176
+ throw new SdError(err, sourcePath);
177
+ }
178
+
179
+ if (stats.isDirectory()) {
180
+ await fsMkdir(targetPath);
181
+
182
+ const children = await fsGlob(path.resolve(sourcePath, "*"), { dot: true });
183
+
184
+ await children.parallelAsync(async (childPath) => {
185
+ if (filter !== undefined && !filter(childPath)) {
186
+ return;
187
+ }
188
+
189
+ const relativeChildPath = path.relative(sourcePath, childPath);
190
+ const childTargetPath = path.resolve(targetPath, relativeChildPath);
191
+ await fsCopy(childPath, childTargetPath, filter);
192
+ });
193
+ } else {
194
+ await fsMkdir(path.dirname(targetPath));
195
+
196
+ try {
197
+ await fs.promises.copyFile(sourcePath, targetPath);
198
+ } catch (err) {
199
+ throw new SdError(err, targetPath);
200
+ }
201
+ }
202
+ }
203
+
204
+ //#endregion
205
+
206
+ //#region 파일 읽기
207
+
208
+ /**
209
+ * 파일 읽기 (UTF-8 문자열).
210
+ * @param targetPath - 읽을 파일 경로
211
+ */
212
+ export function fsReadSync(targetPath: string): string {
213
+ try {
214
+ return fs.readFileSync(targetPath, "utf-8");
215
+ } catch (err) {
216
+ throw new SdError(err, targetPath);
217
+ }
218
+ }
219
+
220
+ /**
221
+ * 파일 읽기 (UTF-8 문자열, 비동기).
222
+ * @param targetPath - 읽을 파일 경로
223
+ */
224
+ export async function fsRead(targetPath: string): Promise<string> {
225
+ try {
226
+ return await fs.promises.readFile(targetPath, "utf-8");
227
+ } catch (err) {
228
+ throw new SdError(err, targetPath);
229
+ }
230
+ }
231
+
232
+ /**
233
+ * 파일 읽기 (Buffer).
234
+ * @param targetPath - 읽을 파일 경로
235
+ */
236
+ export function fsReadBufferSync(targetPath: string): Buffer {
237
+ try {
238
+ return fs.readFileSync(targetPath);
239
+ } catch (err) {
240
+ throw new SdError(err, targetPath);
241
+ }
242
+ }
243
+
244
+ /**
245
+ * 파일 읽기 (Buffer, 비동기).
246
+ * @param targetPath - 읽을 파일 경로
247
+ */
248
+ export async function fsReadBuffer(targetPath: string): Promise<Buffer> {
249
+ try {
250
+ return await fs.promises.readFile(targetPath);
251
+ } catch (err) {
252
+ throw new SdError(err, targetPath);
253
+ }
254
+ }
255
+
256
+ /**
257
+ * JSON 파일 읽기 (JsonConvert 사용).
258
+ * @param targetPath - 읽을 JSON 파일 경로
259
+ */
260
+ export function fsReadJsonSync<T = unknown>(targetPath: string): T {
261
+ const contents = fsReadSync(targetPath);
262
+ try {
263
+ return jsonParse(contents);
264
+ } catch (err) {
265
+ const preview = contents.length > 500 ? contents.slice(0, 500) + "...(truncated)" : contents;
266
+ throw new SdError(err, targetPath + os.EOL + preview);
267
+ }
268
+ }
269
+
270
+ /**
271
+ * JSON 파일 읽기 (JsonConvert 사용, 비동기).
272
+ * @param targetPath - 읽을 JSON 파일 경로
273
+ */
274
+ export async function fsReadJson<T = unknown>(targetPath: string): Promise<T> {
275
+ const contents = await fsRead(targetPath);
276
+ try {
277
+ return jsonParse<T>(contents);
278
+ } catch (err) {
279
+ const preview = contents.length > 500 ? contents.slice(0, 500) + "...(truncated)" : contents;
280
+ throw new SdError(err, targetPath + os.EOL + preview);
281
+ }
282
+ }
283
+
284
+ //#endregion
285
+
286
+ //#region 파일 쓰기
287
+
288
+ /**
289
+ * 파일 쓰기 (부모 디렉토리 자동 생성).
290
+ * @param targetPath - 쓸 파일 경로
291
+ * @param data - 쓸 데이터 (문자열 또는 바이너리)
292
+ */
293
+ export function fsWriteSync(targetPath: string, data: string | Uint8Array): void {
294
+ fsMkdirSync(path.dirname(targetPath));
295
+
296
+ try {
297
+ fs.writeFileSync(targetPath, data, { flush: true });
298
+ } catch (err) {
299
+ throw new SdError(err, targetPath);
300
+ }
301
+ }
302
+
303
+ /**
304
+ * 파일 쓰기 (부모 디렉토리 자동 생성, 비동기).
305
+ * @param targetPath - 쓸 파일 경로
306
+ * @param data - 쓸 데이터 (문자열 또는 바이너리)
307
+ */
308
+ export async function fsWrite(targetPath: string, data: string | Uint8Array): Promise<void> {
309
+ await fsMkdir(path.dirname(targetPath));
310
+
311
+ try {
312
+ await fs.promises.writeFile(targetPath, data, { flush: true });
313
+ } catch (err) {
314
+ throw new SdError(err, targetPath);
315
+ }
316
+ }
317
+
318
+ /**
319
+ * JSON 파일 쓰기 (JsonConvert 사용).
320
+ * @param targetPath - 쓸 JSON 파일 경로
321
+ * @param data - 쓸 데이터
322
+ * @param options - JSON 직렬화 옵션
323
+ */
324
+ export function fsWriteJsonSync(
325
+ targetPath: string,
326
+ data: unknown,
327
+ options?: {
328
+ replacer?: (this: unknown, key: string | undefined, value: unknown) => unknown;
329
+ space?: string | number;
330
+ },
331
+ ): void {
332
+ const json = jsonStringify(data, options);
333
+ fsWriteSync(targetPath, json);
334
+ }
335
+
336
+ /**
337
+ * JSON 파일 쓰기 (JsonConvert 사용, 비동기).
338
+ * @param targetPath - 쓸 JSON 파일 경로
339
+ * @param data - 쓸 데이터
340
+ * @param options - JSON 직렬화 옵션
341
+ */
342
+ export async function fsWriteJson(
343
+ targetPath: string,
344
+ data: unknown,
345
+ options?: {
346
+ replacer?: (this: unknown, key: string | undefined, value: unknown) => unknown;
347
+ space?: string | number;
348
+ },
349
+ ): Promise<void> {
350
+ const json = jsonStringify(data, options);
351
+ await fsWrite(targetPath, json);
352
+ }
353
+
354
+ //#endregion
355
+
356
+ //#region 디렉토리 읽기
357
+
358
+ /**
359
+ * 디렉토리 내용 읽기.
360
+ * @param targetPath - 읽을 디렉토리 경로
361
+ */
362
+ export function fsReaddirSync(targetPath: string): string[] {
363
+ try {
364
+ return fs.readdirSync(targetPath);
365
+ } catch (err) {
366
+ throw new SdError(err, targetPath);
367
+ }
368
+ }
369
+
370
+ /**
371
+ * 디렉토리 내용 읽기 (비동기).
372
+ * @param targetPath - 읽을 디렉토리 경로
373
+ */
374
+ export async function fsReaddir(targetPath: string): Promise<string[]> {
375
+ try {
376
+ return await fs.promises.readdir(targetPath);
377
+ } catch (err) {
378
+ throw new SdError(err, targetPath);
379
+ }
380
+ }
381
+
382
+ //#endregion
383
+
384
+ //#region 파일 정보
385
+
386
+ /**
387
+ * 파일/디렉토리 정보 (심볼릭 링크 따라감).
388
+ * @param targetPath - 정보를 조회할 경로
389
+ */
390
+ export function fsStatSync(targetPath: string): fs.Stats {
391
+ try {
392
+ return fs.statSync(targetPath);
393
+ } catch (err) {
394
+ throw new SdError(err, targetPath);
395
+ }
396
+ }
397
+
398
+ /**
399
+ * 파일/디렉토리 정보 (심볼릭 링크 따라감, 비동기).
400
+ * @param targetPath - 정보를 조회할 경로
401
+ */
402
+ export async function fsStat(targetPath: string): Promise<fs.Stats> {
403
+ try {
404
+ return await fs.promises.stat(targetPath);
405
+ } catch (err) {
406
+ throw new SdError(err, targetPath);
407
+ }
408
+ }
409
+
410
+ /**
411
+ * 파일/디렉토리 정보 (심볼릭 링크 따라가지 않음).
412
+ * @param targetPath - 정보를 조회할 경로
413
+ */
414
+ export function fsLstatSync(targetPath: string): fs.Stats {
415
+ try {
416
+ return fs.lstatSync(targetPath);
417
+ } catch (err) {
418
+ throw new SdError(err, targetPath);
419
+ }
420
+ }
421
+
422
+ /**
423
+ * 파일/디렉토리 정보 (심볼릭 링크 따라가지 않음, 비동기).
424
+ * @param targetPath - 정보를 조회할 경로
425
+ */
426
+ export async function fsLstat(targetPath: string): Promise<fs.Stats> {
427
+ try {
428
+ return await fs.promises.lstat(targetPath);
429
+ } catch (err) {
430
+ throw new SdError(err, targetPath);
431
+ }
432
+ }
433
+
434
+ //#endregion
435
+
436
+ //#region 글로브
437
+
438
+ /**
439
+ * 글로브 패턴으로 파일 검색.
440
+ * @param pattern - 글로브 패턴 (예: "**\/*.ts")
441
+ * @param options - glob 옵션
442
+ * @returns 매칭된 파일들의 절대 경로 배열
443
+ */
444
+ export function fsGlobSync(pattern: string, options?: GlobOptions): string[] {
445
+ return globRawSync(pattern.replace(/\\/g, "/"), options ?? {}).map((item) => path.resolve(item.toString()));
446
+ }
447
+
448
+ /**
449
+ * 글로브 패턴으로 파일 검색 (비동기).
450
+ * @param pattern - 글로브 패턴 (예: "**\/*.ts")
451
+ * @param options - glob 옵션
452
+ * @returns 매칭된 파일들의 절대 경로 배열
453
+ */
454
+ export async function fsGlob(pattern: string, options?: GlobOptions): Promise<string[]> {
455
+ return (await globRaw(pattern.replace(/\\/g, "/"), options ?? {})).map((item) => path.resolve(item.toString()));
456
+ }
457
+
458
+ //#endregion
459
+
460
+ //#region 유틸리티
461
+
462
+ /**
463
+ * 지정 디렉토리 하위의 빈 디렉토리를 재귀적으로 탐색하여 삭제.
464
+ * 하위 디렉토리가 모두 삭제되어 빈 디렉토리가 된 경우, 해당 디렉토리도 삭제 대상이 됨.
465
+ */
466
+ export async function fsClearEmptyDirectory(dirPath: string): Promise<void> {
467
+ if (!(await fsExists(dirPath))) return;
468
+
469
+ const childNames = await fsReaddir(dirPath);
470
+ let hasFiles = false;
471
+
472
+ for (const childName of childNames) {
473
+ const childPath = path.resolve(dirPath, childName);
474
+ if ((await fsLstat(childPath)).isDirectory()) {
475
+ await fsClearEmptyDirectory(childPath);
476
+ } else {
477
+ hasFiles = true;
478
+ }
479
+ }
480
+
481
+ // 파일이 있었다면 삭제 불가
482
+ if (hasFiles) return;
483
+
484
+ // 파일이 없었던 경우에만 재확인 (하위 디렉토리가 삭제되었을 수 있음)
485
+ if ((await fsReaddir(dirPath)).length === 0) {
486
+ await fsRm(dirPath);
487
+ }
488
+ }
489
+
490
+ /**
491
+ * 시작 경로부터 루트 방향으로 상위 디렉토리를 순회하며 glob 패턴 검색.
492
+ * 각 디렉토리에서 childGlob 패턴에 매칭되는 모든 파일 경로를 수집.
493
+ * @param childGlob - 각 디렉토리에서 검색할 glob 패턴
494
+ * @param fromPath - 검색 시작 경로
495
+ * @param rootPath - 검색 종료 경로 (미지정 시 파일시스템 루트까지).
496
+ * **주의**: fromPath가 rootPath의 자식 경로여야 함.
497
+ * 그렇지 않으면 파일시스템 루트까지 검색함.
498
+ */
499
+ export function fsFindAllParentChildPathsSync(childGlob: string, fromPath: string, rootPath?: string): string[] {
500
+ const resultPaths: string[] = [];
501
+
502
+ let current = fromPath;
503
+ while (current) {
504
+ const potential = path.resolve(current, childGlob);
505
+ const globResults = fsGlobSync(potential);
506
+ resultPaths.push(...globResults);
507
+
508
+ if (current === rootPath) break;
509
+
510
+ const next = path.dirname(current);
511
+ if (next === current) break;
512
+ current = next;
513
+ }
514
+
515
+ return resultPaths;
516
+ }
517
+
518
+ /**
519
+ * 시작 경로부터 루트 방향으로 상위 디렉토리를 순회하며 glob 패턴 검색 (비동기).
520
+ * 각 디렉토리에서 childGlob 패턴에 매칭되는 모든 파일 경로를 수집.
521
+ * @param childGlob - 각 디렉토리에서 검색할 glob 패턴
522
+ * @param fromPath - 검색 시작 경로
523
+ * @param rootPath - 검색 종료 경로 (미지정 시 파일시스템 루트까지).
524
+ * **주의**: fromPath가 rootPath의 자식 경로여야 함.
525
+ * 그렇지 않으면 파일시스템 루트까지 검색함.
526
+ */
527
+ export async function fsFindAllParentChildPaths(
528
+ childGlob: string,
529
+ fromPath: string,
530
+ rootPath?: string,
531
+ ): Promise<string[]> {
532
+ const resultPaths: string[] = [];
533
+
534
+ let current = fromPath;
535
+ while (current) {
536
+ const potential = path.resolve(current, childGlob);
537
+ const globResults = await fsGlob(potential);
538
+ resultPaths.push(...globResults);
539
+
540
+ if (current === rootPath) break;
541
+
542
+ const next = path.dirname(current);
543
+ if (next === current) break;
544
+ current = next;
545
+ }
546
+
547
+ return resultPaths;
548
+ }
549
+
550
+ //#endregion
@@ -0,0 +1,128 @@
1
+ import path from "path";
2
+ import { ArgumentError } from "@simplysm/core-common";
3
+
4
+ //#region Types
5
+
6
+ const NORM = Symbol("NormPath");
7
+
8
+ /**
9
+ * 정규화된 경로를 나타내는 브랜드 타입.
10
+ * pathNorm()을 통해서만 생성 가능.
11
+ */
12
+ export type NormPath = string & {
13
+ [NORM]: never;
14
+ };
15
+
16
+ //#endregion
17
+
18
+ //#region 함수
19
+
20
+ /**
21
+ * POSIX 스타일 경로로 변환 (백슬래시 → 슬래시).
22
+ *
23
+ * @example
24
+ * pathPosix("C:\\Users\\test"); // "C:/Users/test"
25
+ * pathPosix("src", "index.ts"); // "src/index.ts"
26
+ */
27
+ export function pathPosix(...args: string[]): string {
28
+ const resolvedPath = path.join(...args);
29
+ return resolvedPath.replace(/\\/g, "/");
30
+ }
31
+
32
+ /**
33
+ * 파일 경로의 디렉토리를 변경.
34
+ *
35
+ * @example
36
+ * pathChangeFileDirectory("/a/b/c.txt", "/a", "/x");
37
+ * // → "/x/b/c.txt"
38
+ *
39
+ * @throws 파일이 fromDirectory 안에 없으면 에러
40
+ */
41
+ export function pathChangeFileDirectory(filePath: string, fromDirectory: string, toDirectory: string): string {
42
+ if (filePath === fromDirectory) {
43
+ return toDirectory;
44
+ }
45
+
46
+ if (!pathIsChildPath(filePath, fromDirectory)) {
47
+ throw new ArgumentError(`'${filePath}'가 ${fromDirectory} 안에 없습니다.`, { filePath, fromDirectory });
48
+ }
49
+
50
+ return path.resolve(toDirectory, path.relative(fromDirectory, filePath));
51
+ }
52
+
53
+ /**
54
+ * 확장자를 제거한 파일명(basename)을 반환.
55
+ *
56
+ * @example
57
+ * pathGetBasenameWithoutExt("file.txt"); // "file"
58
+ * pathGetBasenameWithoutExt("/path/to/file.spec.ts"); // "file.spec"
59
+ */
60
+ export function pathGetBasenameWithoutExt(filePath: string): string {
61
+ return path.basename(filePath, path.extname(filePath));
62
+ }
63
+
64
+ /**
65
+ * childPath가 parentPath의 자식 경로인지 확인.
66
+ * 같은 경로는 false 반환.
67
+ *
68
+ * 경로는 내부적으로 `pathNorm()`으로 정규화된 후 비교되며,
69
+ * 플랫폼별 경로 구분자(Windows: `\`, Unix: `/`)를 사용한다.
70
+ *
71
+ * @example
72
+ * pathIsChildPath("/a/b/c", "/a/b"); // true
73
+ * pathIsChildPath("/a/b", "/a/b/c"); // false
74
+ * pathIsChildPath("/a/b", "/a/b"); // false (같은 경로)
75
+ */
76
+ export function pathIsChildPath(childPath: string, parentPath: string): boolean {
77
+ const normalizedChild = pathNorm(childPath);
78
+ const normalizedParent = pathNorm(parentPath);
79
+
80
+ // 같은 경로면 false
81
+ if (normalizedChild === normalizedParent) {
82
+ return false;
83
+ }
84
+
85
+ // 부모 경로 + 구분자로 시작하는지 확인
86
+ const parentWithSep = normalizedParent.endsWith(path.sep) ? normalizedParent : normalizedParent + path.sep;
87
+
88
+ return normalizedChild.startsWith(parentWithSep);
89
+ }
90
+
91
+ /**
92
+ * 경로를 정규화하여 NormPath로 반환.
93
+ * 절대 경로로 변환되며, 플랫폼별 구분자로 정규화됨.
94
+ *
95
+ * @example
96
+ * pathNorm("/some/path"); // NormPath
97
+ * pathNorm("relative", "path"); // NormPath (절대 경로로 변환)
98
+ */
99
+ export function pathNorm(...paths: string[]): NormPath {
100
+ return path.resolve(...paths) as NormPath;
101
+ }
102
+
103
+ /**
104
+ * 타겟 경로 목록을 기준으로 파일을 필터링.
105
+ * 파일이 타겟 경로와 같거나 타겟의 자식 경로일 때 포함.
106
+ *
107
+ * @param files - 필터링할 파일 경로 목록.
108
+ * **주의**: cwd 하위의 절대 경로여야 함.
109
+ * cwd 외부 경로는 상대 경로(../ 형태)로 변환되어 처리됨.
110
+ * @param targets - 타겟 경로 목록 (cwd 기준 상대 경로, POSIX 스타일 권장)
111
+ * @param cwd - 현재 작업 디렉토리 (절대 경로)
112
+ * @returns targets가 빈 배열이면 files 그대로, 아니면 타겟 경로 하위 파일만
113
+ *
114
+ * @example
115
+ * const files = ["/proj/src/a.ts", "/proj/src/b.ts", "/proj/tests/c.ts"];
116
+ * pathFilterByTargets(files, ["src"], "/proj");
117
+ * // → ["/proj/src/a.ts", "/proj/src/b.ts"]
118
+ */
119
+ export function pathFilterByTargets(files: string[], targets: string[], cwd: string): string[] {
120
+ if (targets.length === 0) return files;
121
+ const normalizedTargets = targets.map((t) => pathPosix(t));
122
+ return files.filter((file) => {
123
+ const relativePath = pathPosix(path.relative(cwd, file));
124
+ return normalizedTargets.some((target) => relativePath === target || relativePath.startsWith(target + "/"));
125
+ });
126
+ }
127
+
128
+ //#endregion