@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.
- package/.cache/typecheck-node.tsbuildinfo +1 -0
- package/.cache/typecheck-tests-node.tsbuildinfo +1 -0
- package/README.md +375 -0
- package/dist/core-common/src/common.types.d.ts +74 -0
- package/dist/core-common/src/common.types.d.ts.map +1 -0
- package/dist/core-common/src/env.d.ts +6 -0
- package/dist/core-common/src/env.d.ts.map +1 -0
- package/dist/core-common/src/errors/argument-error.d.ts +25 -0
- package/dist/core-common/src/errors/argument-error.d.ts.map +1 -0
- package/dist/core-common/src/errors/not-implemented-error.d.ts +29 -0
- package/dist/core-common/src/errors/not-implemented-error.d.ts.map +1 -0
- package/dist/core-common/src/errors/sd-error.d.ts +27 -0
- package/dist/core-common/src/errors/sd-error.d.ts.map +1 -0
- package/dist/core-common/src/errors/timeout-error.d.ts +31 -0
- package/dist/core-common/src/errors/timeout-error.d.ts.map +1 -0
- package/dist/core-common/src/extensions/arr-ext.d.ts +15 -0
- package/dist/core-common/src/extensions/arr-ext.d.ts.map +1 -0
- package/dist/core-common/src/extensions/arr-ext.helpers.d.ts +19 -0
- package/dist/core-common/src/extensions/arr-ext.helpers.d.ts.map +1 -0
- package/dist/core-common/src/extensions/arr-ext.types.d.ts +215 -0
- package/dist/core-common/src/extensions/arr-ext.types.d.ts.map +1 -0
- package/dist/core-common/src/extensions/map-ext.d.ts +57 -0
- package/dist/core-common/src/extensions/map-ext.d.ts.map +1 -0
- package/dist/core-common/src/extensions/set-ext.d.ts +36 -0
- package/dist/core-common/src/extensions/set-ext.d.ts.map +1 -0
- package/dist/core-common/src/features/debounce-queue.d.ts +53 -0
- package/dist/core-common/src/features/debounce-queue.d.ts.map +1 -0
- package/dist/core-common/src/features/event-emitter.d.ts +66 -0
- package/dist/core-common/src/features/event-emitter.d.ts.map +1 -0
- package/dist/core-common/src/features/serial-queue.d.ts +47 -0
- package/dist/core-common/src/features/serial-queue.d.ts.map +1 -0
- package/dist/core-common/src/index.d.ts +32 -0
- package/dist/core-common/src/index.d.ts.map +1 -0
- package/dist/core-common/src/types/date-only.d.ts +152 -0
- package/dist/core-common/src/types/date-only.d.ts.map +1 -0
- package/dist/core-common/src/types/date-time.d.ts +96 -0
- package/dist/core-common/src/types/date-time.d.ts.map +1 -0
- package/dist/core-common/src/types/lazy-gc-map.d.ts +80 -0
- package/dist/core-common/src/types/lazy-gc-map.d.ts.map +1 -0
- package/dist/core-common/src/types/time.d.ts +68 -0
- package/dist/core-common/src/types/time.d.ts.map +1 -0
- package/dist/core-common/src/types/uuid.d.ts +35 -0
- package/dist/core-common/src/types/uuid.d.ts.map +1 -0
- package/dist/core-common/src/utils/bytes.d.ts +51 -0
- package/dist/core-common/src/utils/bytes.d.ts.map +1 -0
- package/dist/core-common/src/utils/date-format.d.ts +90 -0
- package/dist/core-common/src/utils/date-format.d.ts.map +1 -0
- package/dist/core-common/src/utils/json.d.ts +34 -0
- package/dist/core-common/src/utils/json.d.ts.map +1 -0
- package/dist/core-common/src/utils/num.d.ts +60 -0
- package/dist/core-common/src/utils/num.d.ts.map +1 -0
- package/dist/core-common/src/utils/obj.d.ts +258 -0
- package/dist/core-common/src/utils/obj.d.ts.map +1 -0
- package/dist/core-common/src/utils/path.d.ts +23 -0
- package/dist/core-common/src/utils/path.d.ts.map +1 -0
- package/dist/core-common/src/utils/primitive.d.ts +18 -0
- package/dist/core-common/src/utils/primitive.d.ts.map +1 -0
- package/dist/core-common/src/utils/str.d.ts +103 -0
- package/dist/core-common/src/utils/str.d.ts.map +1 -0
- package/dist/core-common/src/utils/template-strings.d.ts +84 -0
- package/dist/core-common/src/utils/template-strings.d.ts.map +1 -0
- package/dist/core-common/src/utils/transferable.d.ts +47 -0
- package/dist/core-common/src/utils/transferable.d.ts.map +1 -0
- package/dist/core-common/src/utils/wait.d.ts +19 -0
- package/dist/core-common/src/utils/wait.d.ts.map +1 -0
- package/dist/core-common/src/utils/xml.d.ts +36 -0
- package/dist/core-common/src/utils/xml.d.ts.map +1 -0
- package/dist/core-common/src/zip/sd-zip.d.ts +80 -0
- package/dist/core-common/src/zip/sd-zip.d.ts.map +1 -0
- package/dist/core-node/src/features/fs-watcher.d.ts +70 -0
- package/dist/core-node/src/features/fs-watcher.d.ts.map +1 -0
- package/dist/core-node/src/index.d.ts +7 -0
- package/dist/core-node/src/index.d.ts.map +1 -0
- package/dist/core-node/src/utils/fs.d.ts +197 -0
- package/dist/core-node/src/utils/fs.d.ts.map +1 -0
- package/dist/core-node/src/utils/path.d.ts +75 -0
- package/dist/core-node/src/utils/path.d.ts.map +1 -0
- package/dist/core-node/src/worker/create-worker.d.ts +23 -0
- package/dist/core-node/src/worker/create-worker.d.ts.map +1 -0
- package/dist/core-node/src/worker/types.d.ts +67 -0
- package/dist/core-node/src/worker/types.d.ts.map +1 -0
- package/dist/core-node/src/worker/worker.d.ts +27 -0
- package/dist/core-node/src/worker/worker.d.ts.map +1 -0
- package/dist/features/fs-watcher.js +100 -0
- package/dist/features/fs-watcher.js.map +7 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +7 -0
- package/dist/utils/fs.js +305 -0
- package/dist/utils/fs.js.map +7 -0
- package/dist/utils/path.js +48 -0
- package/dist/utils/path.js.map +7 -0
- package/dist/worker/create-worker.js +85 -0
- package/dist/worker/create-worker.js.map +7 -0
- package/dist/worker/types.js +1 -0
- package/dist/worker/types.js.map +7 -0
- package/dist/worker/worker.js +142 -0
- package/dist/worker/worker.js.map +7 -0
- package/lib/worker-dev-proxy.js +12 -0
- package/package.json +23 -0
- package/src/features/fs-watcher.ts +176 -0
- package/src/index.ts +11 -0
- package/src/utils/fs.ts +550 -0
- package/src/utils/path.ts +128 -0
- package/src/worker/create-worker.ts +141 -0
- package/src/worker/types.ts +86 -0
- package/src/worker/worker.ts +207 -0
- package/tests/utils/fs-watcher.spec.ts +295 -0
- package/tests/utils/fs.spec.ts +754 -0
- package/tests/utils/path.spec.ts +192 -0
- package/tests/worker/fixtures/test-worker.ts +35 -0
- package/tests/worker/sd-worker.spec.ts +183 -0
package/src/utils/fs.ts
ADDED
|
@@ -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
|