@simplysm/core-node 13.0.69 → 13.0.71
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/README.md +17 -354
- package/dist/features/fs-watcher.d.ts +21 -21
- package/dist/features/fs-watcher.d.ts.map +1 -1
- package/dist/features/fs-watcher.js +9 -9
- package/dist/utils/fs.d.ts +96 -96
- package/dist/utils/path.d.ts +22 -22
- package/dist/utils/path.js +1 -1
- package/dist/utils/path.js.map +1 -1
- package/dist/worker/create-worker.d.ts +3 -3
- package/dist/worker/create-worker.js +3 -3
- package/dist/worker/create-worker.js.map +1 -1
- package/dist/worker/types.d.ts +14 -14
- package/dist/worker/worker.d.ts +5 -5
- package/dist/worker/worker.js +12 -12
- package/dist/worker/worker.js.map +1 -1
- package/package.json +6 -5
- package/src/features/fs-watcher.ts +38 -38
- package/src/utils/fs.ts +108 -108
- package/src/utils/path.ts +26 -26
- package/src/worker/create-worker.ts +10 -10
- package/src/worker/types.ts +14 -14
- package/src/worker/worker.ts +29 -29
- package/tests/utils/fs-watcher.spec.ts +339 -0
- package/tests/utils/fs.spec.ts +755 -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 +189 -0
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import {
|
|
6
|
+
fsExistsSync,
|
|
7
|
+
fsExists,
|
|
8
|
+
fsMkdirSync,
|
|
9
|
+
fsMkdir,
|
|
10
|
+
fsRmSync,
|
|
11
|
+
fsRm,
|
|
12
|
+
fsCopySync,
|
|
13
|
+
fsCopy,
|
|
14
|
+
fsReadSync,
|
|
15
|
+
fsRead,
|
|
16
|
+
fsReadBufferSync,
|
|
17
|
+
fsReadBuffer,
|
|
18
|
+
fsReadJsonSync,
|
|
19
|
+
fsReadJson,
|
|
20
|
+
fsWriteSync,
|
|
21
|
+
fsWrite,
|
|
22
|
+
fsWriteJsonSync,
|
|
23
|
+
fsWriteJson,
|
|
24
|
+
fsReaddirSync,
|
|
25
|
+
fsReaddir,
|
|
26
|
+
fsStatSync,
|
|
27
|
+
fsStat,
|
|
28
|
+
fsLstatSync,
|
|
29
|
+
fsLstat,
|
|
30
|
+
fsGlobSync,
|
|
31
|
+
fsGlob,
|
|
32
|
+
fsClearEmptyDirectory,
|
|
33
|
+
fsFindAllParentChildPathsSync,
|
|
34
|
+
fsFindAllParentChildPaths,
|
|
35
|
+
} from "../../src/utils/fs";
|
|
36
|
+
import { SdError } from "@simplysm/core-common";
|
|
37
|
+
|
|
38
|
+
describe("fs functions", () => {
|
|
39
|
+
const testDir = path.join(os.tmpdir(), "fs-utils-test-" + Date.now());
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
//#region exists
|
|
50
|
+
|
|
51
|
+
describe("fsExistsSync", () => {
|
|
52
|
+
it("returns true for existing file", () => {
|
|
53
|
+
const filePath = path.join(testDir, "test.txt");
|
|
54
|
+
fs.writeFileSync(filePath, "test");
|
|
55
|
+
|
|
56
|
+
expect(fsExistsSync(filePath)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("returns false for nonexistent file", () => {
|
|
60
|
+
const filePath = path.join(testDir, "nonexistent.txt");
|
|
61
|
+
expect(fsExistsSync(filePath)).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns true for existing directory", () => {
|
|
65
|
+
expect(fsExistsSync(testDir)).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("fsExists", () => {
|
|
70
|
+
it("returns true for existing file", async () => {
|
|
71
|
+
const filePath = path.join(testDir, "test.txt");
|
|
72
|
+
fs.writeFileSync(filePath, "test");
|
|
73
|
+
|
|
74
|
+
expect(await fsExists(filePath)).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("returns false for nonexistent file", async () => {
|
|
78
|
+
const filePath = path.join(testDir, "nonexistent.txt");
|
|
79
|
+
expect(await fsExists(filePath)).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("returns true for existing directory", async () => {
|
|
83
|
+
expect(await fsExists(testDir)).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
|
|
89
|
+
//#region mkdir
|
|
90
|
+
|
|
91
|
+
describe("fsMkdirSync", () => {
|
|
92
|
+
it("creates directory", () => {
|
|
93
|
+
const dirPath = path.join(testDir, "newdir");
|
|
94
|
+
fsMkdirSync(dirPath);
|
|
95
|
+
|
|
96
|
+
expect(fs.existsSync(dirPath)).toBe(true);
|
|
97
|
+
expect(fs.statSync(dirPath).isDirectory()).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("creates nested directories (recursive)", () => {
|
|
101
|
+
const dirPath = path.join(testDir, "a/b/c");
|
|
102
|
+
fsMkdirSync(dirPath);
|
|
103
|
+
|
|
104
|
+
expect(fs.existsSync(dirPath)).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("passes without error for existing directory", () => {
|
|
108
|
+
expect(() => fsMkdirSync(testDir)).not.toThrow();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("fsMkdir", () => {
|
|
113
|
+
it("creates directory asynchronously", async () => {
|
|
114
|
+
const dirPath = path.join(testDir, "asyncdir");
|
|
115
|
+
await fsMkdir(dirPath);
|
|
116
|
+
|
|
117
|
+
expect(fs.existsSync(dirPath)).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
|
|
123
|
+
//#region rm
|
|
124
|
+
|
|
125
|
+
describe("fsRmSync", () => {
|
|
126
|
+
it("deletes file", () => {
|
|
127
|
+
const filePath = path.join(testDir, "todelete.txt");
|
|
128
|
+
fs.writeFileSync(filePath, "test");
|
|
129
|
+
|
|
130
|
+
fsRmSync(filePath);
|
|
131
|
+
|
|
132
|
+
expect(fs.existsSync(filePath)).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("deletes directory (recursive)", () => {
|
|
136
|
+
const dirPath = path.join(testDir, "todelete");
|
|
137
|
+
fs.mkdirSync(dirPath);
|
|
138
|
+
fs.writeFileSync(path.join(dirPath, "file.txt"), "test");
|
|
139
|
+
|
|
140
|
+
fsRmSync(dirPath);
|
|
141
|
+
|
|
142
|
+
expect(fs.existsSync(dirPath)).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("passes without error for nonexistent path", () => {
|
|
146
|
+
expect(() => fsRmSync(path.join(testDir, "nonexistent"))).not.toThrow();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("fsRm", () => {
|
|
151
|
+
it("deletes file asynchronously", async () => {
|
|
152
|
+
const filePath = path.join(testDir, "asyncdelete.txt");
|
|
153
|
+
fs.writeFileSync(filePath, "test");
|
|
154
|
+
|
|
155
|
+
await fsRm(filePath);
|
|
156
|
+
|
|
157
|
+
expect(fs.existsSync(filePath)).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
//#endregion
|
|
162
|
+
|
|
163
|
+
//#region read/write
|
|
164
|
+
|
|
165
|
+
describe("fsReadSync", () => {
|
|
166
|
+
it("reads file content as UTF-8 string", () => {
|
|
167
|
+
const filePath = path.join(testDir, "read.txt");
|
|
168
|
+
fs.writeFileSync(filePath, "Hello, World!");
|
|
169
|
+
|
|
170
|
+
const content = fsReadSync(filePath);
|
|
171
|
+
|
|
172
|
+
expect(content).toBe("Hello, World!");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("reads Korean content", () => {
|
|
176
|
+
const filePath = path.join(testDir, "korean.txt");
|
|
177
|
+
fs.writeFileSync(filePath, "안녕하세요");
|
|
178
|
+
|
|
179
|
+
const content = fsReadSync(filePath);
|
|
180
|
+
|
|
181
|
+
expect(content).toBe("안녕하세요");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("fsRead", () => {
|
|
186
|
+
it("reads file asynchronously", async () => {
|
|
187
|
+
const filePath = path.join(testDir, "asyncread.txt");
|
|
188
|
+
fs.writeFileSync(filePath, "async content");
|
|
189
|
+
|
|
190
|
+
const content = await fsRead(filePath);
|
|
191
|
+
|
|
192
|
+
expect(content).toBe("async content");
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("fsReadBufferSync", () => {
|
|
197
|
+
it("reads file as Buffer", () => {
|
|
198
|
+
const filePath = path.join(testDir, "buffer.txt");
|
|
199
|
+
fs.writeFileSync(filePath, "buffer content");
|
|
200
|
+
|
|
201
|
+
const buffer = fsReadBufferSync(filePath);
|
|
202
|
+
|
|
203
|
+
expect(buffer instanceof Uint8Array).toBe(true);
|
|
204
|
+
expect(buffer.toString()).toBe("buffer content");
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("fsReadBuffer", () => {
|
|
209
|
+
it("reads file as Buffer asynchronously", async () => {
|
|
210
|
+
const filePath = path.join(testDir, "asyncbuffer.txt");
|
|
211
|
+
fs.writeFileSync(filePath, "async buffer content");
|
|
212
|
+
|
|
213
|
+
const buffer = await fsReadBuffer(filePath);
|
|
214
|
+
|
|
215
|
+
expect(buffer instanceof Uint8Array).toBe(true);
|
|
216
|
+
expect(buffer.toString()).toBe("async buffer content");
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe("fsWriteSync", () => {
|
|
221
|
+
it("writes string to file", () => {
|
|
222
|
+
const filePath = path.join(testDir, "write.txt");
|
|
223
|
+
|
|
224
|
+
fsWriteSync(filePath, "written content");
|
|
225
|
+
|
|
226
|
+
expect(fs.readFileSync(filePath, "utf-8")).toBe("written content");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("writes Buffer to file", () => {
|
|
230
|
+
const filePath = path.join(testDir, "buffer-write.bin");
|
|
231
|
+
const buffer = new Uint8Array([0x00, 0x01, 0x02, 0xff]);
|
|
232
|
+
|
|
233
|
+
fsWriteSync(filePath, buffer);
|
|
234
|
+
|
|
235
|
+
expect(new Uint8Array(fs.readFileSync(filePath))).toEqual(buffer);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("auto-creates parent directory if missing", () => {
|
|
239
|
+
const filePath = path.join(testDir, "sub/dir/write.txt");
|
|
240
|
+
|
|
241
|
+
fsWriteSync(filePath, "nested content");
|
|
242
|
+
|
|
243
|
+
expect(fs.readFileSync(filePath, "utf-8")).toBe("nested content");
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("fsWrite", () => {
|
|
248
|
+
it("writes file asynchronously", async () => {
|
|
249
|
+
const filePath = path.join(testDir, "asyncwrite.txt");
|
|
250
|
+
|
|
251
|
+
await fsWrite(filePath, "async written");
|
|
252
|
+
|
|
253
|
+
expect(fs.readFileSync(filePath, "utf-8")).toBe("async written");
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
//#endregion
|
|
258
|
+
|
|
259
|
+
//#region JSON
|
|
260
|
+
|
|
261
|
+
describe("fsReadJsonSync", () => {
|
|
262
|
+
it("reads JSON file", () => {
|
|
263
|
+
const filePath = path.join(testDir, "data.json");
|
|
264
|
+
fs.writeFileSync(filePath, '{"name": "test", "value": 42}');
|
|
265
|
+
|
|
266
|
+
const data = fsReadJsonSync<{ name: string; value: number }>(filePath);
|
|
267
|
+
|
|
268
|
+
expect(data).toEqual({ name: "test", value: 42 });
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("includes truncated content when reading invalid JSON with over 500 characters", () => {
|
|
272
|
+
const filePath = path.join(testDir, "long-invalid.json");
|
|
273
|
+
const longContent = "{ invalid " + "x".repeat(600) + " }";
|
|
274
|
+
fs.writeFileSync(filePath, longContent);
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
fsReadJsonSync(filePath);
|
|
278
|
+
expect.fail("Should throw error");
|
|
279
|
+
} catch (err) {
|
|
280
|
+
expect((err as Error).message).toContain("...(truncated)");
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("fsWriteJsonSync", () => {
|
|
286
|
+
it("writes JSON file", () => {
|
|
287
|
+
const filePath = path.join(testDir, "output.json");
|
|
288
|
+
const data = { name: "test", value: 42 };
|
|
289
|
+
|
|
290
|
+
fsWriteJsonSync(filePath, data);
|
|
291
|
+
|
|
292
|
+
const content = JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
|
|
293
|
+
expect(content).toEqual(data);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("writes JSON file with formatting", () => {
|
|
297
|
+
const filePath = path.join(testDir, "formatted.json");
|
|
298
|
+
const data = { name: "test" };
|
|
299
|
+
|
|
300
|
+
fsWriteJsonSync(filePath, data, { space: 2 });
|
|
301
|
+
|
|
302
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
303
|
+
expect(content).toContain("\n");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("writes JSON file with replacer option", () => {
|
|
307
|
+
const filePath = path.join(testDir, "replaced.json");
|
|
308
|
+
const data = { name: "test", secret: "hidden" };
|
|
309
|
+
|
|
310
|
+
fsWriteJsonSync(filePath, data, {
|
|
311
|
+
replacer: (_key, value) =>
|
|
312
|
+
typeof value === "string" && value === "hidden" ? undefined : value,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const content = JSON.parse(fs.readFileSync(filePath, "utf-8")) as Record<string, unknown>;
|
|
316
|
+
expect(content).toEqual({ name: "test" });
|
|
317
|
+
expect(content["secret"]).toBeUndefined();
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe("fsReadJson", () => {
|
|
322
|
+
it("reads JSON file asynchronously", async () => {
|
|
323
|
+
const filePath = path.join(testDir, "asyncdata.json");
|
|
324
|
+
fs.writeFileSync(filePath, '{"name": "async", "value": 100}');
|
|
325
|
+
|
|
326
|
+
const data = await fsReadJson<{ name: string; value: number }>(filePath);
|
|
327
|
+
|
|
328
|
+
expect(data).toEqual({ name: "async", value: 100 });
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe("fsWriteJson", () => {
|
|
333
|
+
it("writes JSON file asynchronously", async () => {
|
|
334
|
+
const filePath = path.join(testDir, "asyncoutput.json");
|
|
335
|
+
const data = { name: "async", value: 100 };
|
|
336
|
+
|
|
337
|
+
await fsWriteJson(filePath, data);
|
|
338
|
+
|
|
339
|
+
const content = JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
|
|
340
|
+
expect(content).toEqual(data);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
//#endregion
|
|
345
|
+
|
|
346
|
+
//#region copy
|
|
347
|
+
|
|
348
|
+
describe("fsCopySync", () => {
|
|
349
|
+
it("copies file", () => {
|
|
350
|
+
const source = path.join(testDir, "source.txt");
|
|
351
|
+
const target = path.join(testDir, "target.txt");
|
|
352
|
+
fs.writeFileSync(source, "source content");
|
|
353
|
+
|
|
354
|
+
fsCopySync(source, target);
|
|
355
|
+
|
|
356
|
+
expect(fs.readFileSync(target, "utf-8")).toBe("source content");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("copies directory (recursive)", () => {
|
|
360
|
+
const sourceDir = path.join(testDir, "sourceDir");
|
|
361
|
+
const targetDir = path.join(testDir, "targetDir");
|
|
362
|
+
fs.mkdirSync(sourceDir);
|
|
363
|
+
fs.writeFileSync(path.join(sourceDir, "file.txt"), "content");
|
|
364
|
+
fs.mkdirSync(path.join(sourceDir, "sub"));
|
|
365
|
+
fs.writeFileSync(path.join(sourceDir, "sub/nested.txt"), "nested");
|
|
366
|
+
|
|
367
|
+
fsCopySync(sourceDir, targetDir);
|
|
368
|
+
|
|
369
|
+
expect(fs.existsSync(path.join(targetDir, "file.txt"))).toBe(true);
|
|
370
|
+
expect(fs.existsSync(path.join(targetDir, "sub/nested.txt"))).toBe(true);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("ignores nonexistent source", () => {
|
|
374
|
+
const source = path.join(testDir, "nonexistent");
|
|
375
|
+
const target = path.join(testDir, "target");
|
|
376
|
+
|
|
377
|
+
expect(() => fsCopySync(source, target)).not.toThrow();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("selectively copies with filter option", () => {
|
|
381
|
+
const sourceDir = path.join(testDir, "filterSource");
|
|
382
|
+
const targetDir = path.join(testDir, "filterTarget");
|
|
383
|
+
fs.mkdirSync(sourceDir);
|
|
384
|
+
fs.writeFileSync(path.join(sourceDir, "include.txt"), "include");
|
|
385
|
+
fs.writeFileSync(path.join(sourceDir, "exclude.log"), "exclude");
|
|
386
|
+
|
|
387
|
+
fsCopySync(sourceDir, targetDir, (p) => !p.endsWith(".log"));
|
|
388
|
+
|
|
389
|
+
expect(fs.existsSync(path.join(targetDir, "include.txt"))).toBe(true);
|
|
390
|
+
expect(fs.existsSync(path.join(targetDir, "exclude.log"))).toBe(false);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("skips subdirectories and items when filter excludes directory", () => {
|
|
394
|
+
const sourceDir = path.join(testDir, "filterDirSource");
|
|
395
|
+
const targetDir = path.join(testDir, "filterDirTarget");
|
|
396
|
+
fs.mkdirSync(sourceDir);
|
|
397
|
+
fs.mkdirSync(path.join(sourceDir, "excluded"));
|
|
398
|
+
fs.mkdirSync(path.join(sourceDir, "included"));
|
|
399
|
+
fs.writeFileSync(path.join(sourceDir, "excluded", "nested.txt"), "nested");
|
|
400
|
+
fs.writeFileSync(path.join(sourceDir, "included", "nested.txt"), "nested");
|
|
401
|
+
|
|
402
|
+
fsCopySync(sourceDir, targetDir, (p) => !p.includes("excluded"));
|
|
403
|
+
|
|
404
|
+
expect(fs.existsSync(path.join(targetDir, "excluded"))).toBe(false);
|
|
405
|
+
expect(fs.existsSync(path.join(targetDir, "excluded", "nested.txt"))).toBe(false);
|
|
406
|
+
expect(fs.existsSync(path.join(targetDir, "included"))).toBe(true);
|
|
407
|
+
expect(fs.existsSync(path.join(targetDir, "included", "nested.txt"))).toBe(true);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe("fsCopy", () => {
|
|
412
|
+
it("copies file asynchronously", async () => {
|
|
413
|
+
const source = path.join(testDir, "asyncSource.txt");
|
|
414
|
+
const target = path.join(testDir, "asyncTarget.txt");
|
|
415
|
+
fs.writeFileSync(source, "async source content");
|
|
416
|
+
|
|
417
|
+
await fsCopy(source, target);
|
|
418
|
+
|
|
419
|
+
expect(fs.readFileSync(target, "utf-8")).toBe("async source content");
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("copies directory asynchronously (recursive)", async () => {
|
|
423
|
+
const sourceDir = path.join(testDir, "asyncSourceDir");
|
|
424
|
+
const targetDir = path.join(testDir, "asyncTargetDir");
|
|
425
|
+
fs.mkdirSync(sourceDir);
|
|
426
|
+
fs.writeFileSync(path.join(sourceDir, "file.txt"), "content");
|
|
427
|
+
fs.mkdirSync(path.join(sourceDir, "sub"));
|
|
428
|
+
fs.writeFileSync(path.join(sourceDir, "sub/nested.txt"), "nested");
|
|
429
|
+
|
|
430
|
+
await fsCopy(sourceDir, targetDir);
|
|
431
|
+
|
|
432
|
+
expect(fs.existsSync(path.join(targetDir, "file.txt"))).toBe(true);
|
|
433
|
+
expect(fs.existsSync(path.join(targetDir, "sub/nested.txt"))).toBe(true);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("selectively copies with filter option asynchronously", async () => {
|
|
437
|
+
const sourceDir = path.join(testDir, "asyncFilterSource");
|
|
438
|
+
const targetDir = path.join(testDir, "asyncFilterTarget");
|
|
439
|
+
fs.mkdirSync(sourceDir);
|
|
440
|
+
fs.writeFileSync(path.join(sourceDir, "keep.ts"), "keep");
|
|
441
|
+
fs.writeFileSync(path.join(sourceDir, "skip.js"), "skip");
|
|
442
|
+
|
|
443
|
+
await fsCopy(sourceDir, targetDir, (p) => p.endsWith(".ts"));
|
|
444
|
+
|
|
445
|
+
expect(fs.existsSync(path.join(targetDir, "keep.ts"))).toBe(true);
|
|
446
|
+
expect(fs.existsSync(path.join(targetDir, "skip.js"))).toBe(false);
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
//#endregion
|
|
451
|
+
|
|
452
|
+
//#region readdir
|
|
453
|
+
|
|
454
|
+
describe("fsReaddirSync", () => {
|
|
455
|
+
it("reads directory contents", () => {
|
|
456
|
+
fs.writeFileSync(path.join(testDir, "file1.txt"), "");
|
|
457
|
+
fs.writeFileSync(path.join(testDir, "file2.txt"), "");
|
|
458
|
+
fs.mkdirSync(path.join(testDir, "subdir"));
|
|
459
|
+
|
|
460
|
+
const entries = fsReaddirSync(testDir);
|
|
461
|
+
|
|
462
|
+
expect(entries).toContain("file1.txt");
|
|
463
|
+
expect(entries).toContain("file2.txt");
|
|
464
|
+
expect(entries).toContain("subdir");
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
describe("fsReaddir", () => {
|
|
469
|
+
it("reads directory contents asynchronously", async () => {
|
|
470
|
+
fs.writeFileSync(path.join(testDir, "async1.txt"), "");
|
|
471
|
+
fs.writeFileSync(path.join(testDir, "async2.txt"), "");
|
|
472
|
+
|
|
473
|
+
const entries = await fsReaddir(testDir);
|
|
474
|
+
|
|
475
|
+
expect(entries).toContain("async1.txt");
|
|
476
|
+
expect(entries).toContain("async2.txt");
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
//#endregion
|
|
481
|
+
|
|
482
|
+
//#region stat
|
|
483
|
+
|
|
484
|
+
describe("fsStatSync", () => {
|
|
485
|
+
it("gets file information", () => {
|
|
486
|
+
const filePath = path.join(testDir, "statfile.txt");
|
|
487
|
+
fs.writeFileSync(filePath, "content");
|
|
488
|
+
|
|
489
|
+
const result = fsStatSync(filePath);
|
|
490
|
+
|
|
491
|
+
expect(result.isFile()).toBe(true);
|
|
492
|
+
expect(result.size).toBeGreaterThan(0);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("gets directory information", () => {
|
|
496
|
+
const result = fsStatSync(testDir);
|
|
497
|
+
expect(result.isDirectory()).toBe(true);
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
describe("fsStat", () => {
|
|
502
|
+
it("gets file information asynchronously", async () => {
|
|
503
|
+
const filePath = path.join(testDir, "asyncstatfile.txt");
|
|
504
|
+
fs.writeFileSync(filePath, "async content");
|
|
505
|
+
|
|
506
|
+
const stat = await fsStat(filePath);
|
|
507
|
+
|
|
508
|
+
expect(stat.isFile()).toBe(true);
|
|
509
|
+
expect(stat.size).toBeGreaterThan(0);
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
describe("fsLstatSync", () => {
|
|
514
|
+
it("gets regular file information", () => {
|
|
515
|
+
const filePath = path.join(testDir, "lstatfile.txt");
|
|
516
|
+
fs.writeFileSync(filePath, "content");
|
|
517
|
+
|
|
518
|
+
const stat = fsLstatSync(filePath);
|
|
519
|
+
|
|
520
|
+
expect(stat.isFile()).toBe(true);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("returns symbolic link information for symbolic links", () => {
|
|
524
|
+
const targetPath = path.join(testDir, "target.txt");
|
|
525
|
+
const linkPath = path.join(testDir, "link.txt");
|
|
526
|
+
fs.writeFileSync(targetPath, "target content");
|
|
527
|
+
fs.symlinkSync(targetPath, linkPath);
|
|
528
|
+
|
|
529
|
+
const lstatResult = fsLstatSync(linkPath);
|
|
530
|
+
const statResult = fsStatSync(linkPath);
|
|
531
|
+
|
|
532
|
+
// lstat returns information about the symbolic link itself
|
|
533
|
+
expect(lstatResult.isSymbolicLink()).toBe(true);
|
|
534
|
+
expect(lstatResult.isFile()).toBe(false);
|
|
535
|
+
|
|
536
|
+
// stat returns information about the target of the link
|
|
537
|
+
expect(statResult.isSymbolicLink()).toBe(false);
|
|
538
|
+
expect(statResult.isFile()).toBe(true);
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
describe("fsLstat", () => {
|
|
543
|
+
it("gets file information asynchronously", async () => {
|
|
544
|
+
const filePath = path.join(testDir, "asynclstatfile.txt");
|
|
545
|
+
fs.writeFileSync(filePath, "async content");
|
|
546
|
+
|
|
547
|
+
const stat = await fsLstat(filePath);
|
|
548
|
+
|
|
549
|
+
expect(stat.isFile()).toBe(true);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("returns symbolic link information asynchronously", async () => {
|
|
553
|
+
const targetPath = path.join(testDir, "async-target.txt");
|
|
554
|
+
const linkPath = path.join(testDir, "async-link.txt");
|
|
555
|
+
fs.writeFileSync(targetPath, "target content");
|
|
556
|
+
fs.symlinkSync(targetPath, linkPath);
|
|
557
|
+
|
|
558
|
+
const lstatResult = await fsLstat(linkPath);
|
|
559
|
+
const statResult = await fsStat(linkPath);
|
|
560
|
+
|
|
561
|
+
// lstat returns information about the symbolic link itself
|
|
562
|
+
expect(lstatResult.isSymbolicLink()).toBe(true);
|
|
563
|
+
expect(lstatResult.isFile()).toBe(false);
|
|
564
|
+
|
|
565
|
+
// stat returns information about the target of the link
|
|
566
|
+
expect(statResult.isSymbolicLink()).toBe(false);
|
|
567
|
+
expect(statResult.isFile()).toBe(true);
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
//#endregion
|
|
572
|
+
|
|
573
|
+
//#region glob
|
|
574
|
+
|
|
575
|
+
describe("fsGlobSync", () => {
|
|
576
|
+
it("searches files by glob pattern", () => {
|
|
577
|
+
fs.writeFileSync(path.join(testDir, "a.txt"), "");
|
|
578
|
+
fs.writeFileSync(path.join(testDir, "b.txt"), "");
|
|
579
|
+
fs.writeFileSync(path.join(testDir, "c.js"), "");
|
|
580
|
+
|
|
581
|
+
const txtFiles = fsGlobSync(path.join(testDir, "*.txt"));
|
|
582
|
+
|
|
583
|
+
expect(txtFiles.length).toBe(2);
|
|
584
|
+
expect(txtFiles.some((f) => f.endsWith("a.txt"))).toBe(true);
|
|
585
|
+
expect(txtFiles.some((f) => f.endsWith("b.txt"))).toBe(true);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it("searches nested directories", () => {
|
|
589
|
+
fs.mkdirSync(path.join(testDir, "nested"));
|
|
590
|
+
fs.writeFileSync(path.join(testDir, "nested/deep.txt"), "");
|
|
591
|
+
|
|
592
|
+
const files = fsGlobSync(path.join(testDir, "**/*.txt"));
|
|
593
|
+
|
|
594
|
+
expect(files.some((f) => f.endsWith("deep.txt"))).toBe(true);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("includes hidden files with dot: true option", () => {
|
|
598
|
+
fs.writeFileSync(path.join(testDir, ".hidden"), "");
|
|
599
|
+
fs.writeFileSync(path.join(testDir, "visible"), "");
|
|
600
|
+
|
|
601
|
+
const withoutDot = fsGlobSync(path.join(testDir, "*"));
|
|
602
|
+
const withDot = fsGlobSync(path.join(testDir, "*"), { dot: true });
|
|
603
|
+
|
|
604
|
+
expect(withoutDot.some((f) => f.endsWith(".hidden"))).toBe(false);
|
|
605
|
+
expect(withDot.some((f) => f.endsWith(".hidden"))).toBe(true);
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
describe("fsGlob", () => {
|
|
610
|
+
it("searches files asynchronously by glob pattern", async () => {
|
|
611
|
+
fs.writeFileSync(path.join(testDir, "async.txt"), "");
|
|
612
|
+
|
|
613
|
+
const files = await fsGlob(path.join(testDir, "*.txt"));
|
|
614
|
+
|
|
615
|
+
expect(files.length).toBeGreaterThan(0);
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
//#endregion
|
|
620
|
+
|
|
621
|
+
//#region clearEmptyDirectoryAsync
|
|
622
|
+
|
|
623
|
+
describe("fsClearEmptyDirectory", () => {
|
|
624
|
+
it("recursively deletes empty directories", async () => {
|
|
625
|
+
const emptyDir = path.join(testDir, "empty/nested/deep");
|
|
626
|
+
fs.mkdirSync(emptyDir, { recursive: true });
|
|
627
|
+
|
|
628
|
+
await fsClearEmptyDirectory(path.join(testDir, "empty"));
|
|
629
|
+
|
|
630
|
+
expect(fs.existsSync(path.join(testDir, "empty"))).toBe(false);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it("keeps directories with files", async () => {
|
|
634
|
+
const dirWithFile = path.join(testDir, "notempty");
|
|
635
|
+
fs.mkdirSync(dirWithFile);
|
|
636
|
+
fs.writeFileSync(path.join(dirWithFile, "file.txt"), "content");
|
|
637
|
+
|
|
638
|
+
await fsClearEmptyDirectory(dirWithFile);
|
|
639
|
+
|
|
640
|
+
expect(fs.existsSync(dirWithFile)).toBe(true);
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
//#endregion
|
|
645
|
+
|
|
646
|
+
//#region findAllParentChildPaths
|
|
647
|
+
|
|
648
|
+
describe("fsFindAllParentChildPathsSync", () => {
|
|
649
|
+
it("finds specific file in parent directories", () => {
|
|
650
|
+
const deepDir = path.join(testDir, "a/b/c");
|
|
651
|
+
fs.mkdirSync(deepDir, { recursive: true });
|
|
652
|
+
fs.writeFileSync(path.join(testDir, "marker.txt"), "");
|
|
653
|
+
fs.writeFileSync(path.join(testDir, "a/marker.txt"), "");
|
|
654
|
+
|
|
655
|
+
const results = fsFindAllParentChildPathsSync("marker.txt", deepDir, testDir);
|
|
656
|
+
|
|
657
|
+
expect(results.length).toBe(2);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it("returns empty array when no matching file is found", () => {
|
|
661
|
+
const deepDir = path.join(testDir, "a/b/c");
|
|
662
|
+
fs.mkdirSync(deepDir, { recursive: true });
|
|
663
|
+
|
|
664
|
+
const results = fsFindAllParentChildPathsSync("nonexistent-file.txt", deepDir, testDir);
|
|
665
|
+
|
|
666
|
+
expect(results).toEqual([]);
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
describe("fsFindAllParentChildPaths", () => {
|
|
671
|
+
it("finds specific file in parent directories asynchronously", async () => {
|
|
672
|
+
const deepDir = path.join(testDir, "x/y/z");
|
|
673
|
+
fs.mkdirSync(deepDir, { recursive: true });
|
|
674
|
+
fs.writeFileSync(path.join(testDir, "config.json"), "");
|
|
675
|
+
fs.writeFileSync(path.join(testDir, "x/config.json"), "");
|
|
676
|
+
|
|
677
|
+
const results = await fsFindAllParentChildPaths("config.json", deepDir, testDir);
|
|
678
|
+
|
|
679
|
+
expect(results.length).toBe(2);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it("returns empty array asynchronously when no matching file is found", async () => {
|
|
683
|
+
const deepDir = path.join(testDir, "x/y/z");
|
|
684
|
+
fs.mkdirSync(deepDir, { recursive: true });
|
|
685
|
+
|
|
686
|
+
const results = await fsFindAllParentChildPaths("nonexistent-file.txt", deepDir, testDir);
|
|
687
|
+
|
|
688
|
+
expect(results).toEqual([]);
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
//#endregion
|
|
693
|
+
|
|
694
|
+
//#region Error Cases
|
|
695
|
+
|
|
696
|
+
describe("error cases", () => {
|
|
697
|
+
it("includes path information in SdError when reading nonexistent file", () => {
|
|
698
|
+
const filePath = path.join(testDir, "nonexistent.txt");
|
|
699
|
+
expect(() => fsReadSync(filePath)).toThrow(SdError);
|
|
700
|
+
try {
|
|
701
|
+
fsReadSync(filePath);
|
|
702
|
+
} catch (err) {
|
|
703
|
+
expect((err as Error).message).toContain(filePath);
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it("includes path information in SdError when reading nonexistent file asynchronously", async () => {
|
|
708
|
+
const filePath = path.join(testDir, "nonexistent.txt");
|
|
709
|
+
await expect(fsRead(filePath)).rejects.toThrow(SdError);
|
|
710
|
+
try {
|
|
711
|
+
await fsRead(filePath);
|
|
712
|
+
} catch (err) {
|
|
713
|
+
expect((err as Error).message).toContain(filePath);
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it("throws error when reading nonexistent directory", () => {
|
|
718
|
+
expect(() => fsReaddirSync(path.join(testDir, "nonexistent"))).toThrow();
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it("throws error when stat nonexistent file", () => {
|
|
722
|
+
expect(() => fsStatSync(path.join(testDir, "nonexistent.txt"))).toThrow();
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it("includes path and content information in SdError when reading invalid JSON", () => {
|
|
726
|
+
const filePath = path.join(testDir, "invalid.json");
|
|
727
|
+
const content = "{ invalid json }";
|
|
728
|
+
fs.writeFileSync(filePath, content);
|
|
729
|
+
|
|
730
|
+
expect(() => fsReadJsonSync(filePath)).toThrow(SdError);
|
|
731
|
+
try {
|
|
732
|
+
fsReadJsonSync(filePath);
|
|
733
|
+
} catch (err) {
|
|
734
|
+
expect((err as Error).message).toContain(filePath);
|
|
735
|
+
expect((err as Error).message).toContain(content);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it("includes path and content information in SdError when reading invalid JSON asynchronously", async () => {
|
|
740
|
+
const filePath = path.join(testDir, "invalid-async.json");
|
|
741
|
+
const content = "{ invalid json }";
|
|
742
|
+
fs.writeFileSync(filePath, content);
|
|
743
|
+
|
|
744
|
+
await expect(fsReadJson(filePath)).rejects.toThrow(SdError);
|
|
745
|
+
try {
|
|
746
|
+
await fsReadJson(filePath);
|
|
747
|
+
} catch (err) {
|
|
748
|
+
expect((err as Error).message).toContain(filePath);
|
|
749
|
+
expect((err as Error).message).toContain(content);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
//#endregion
|
|
755
|
+
});
|