@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,192 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import {
|
|
4
|
+
pathPosix,
|
|
5
|
+
pathNorm,
|
|
6
|
+
pathIsChildPath,
|
|
7
|
+
pathChangeFileDirectory,
|
|
8
|
+
pathBasenameWithoutExt,
|
|
9
|
+
pathFilterByTargets,
|
|
10
|
+
type NormPath,
|
|
11
|
+
} from "../../src/utils/path";
|
|
12
|
+
|
|
13
|
+
describe("path functions", () => {
|
|
14
|
+
//#region posix
|
|
15
|
+
|
|
16
|
+
describe("pathPosix", () => {
|
|
17
|
+
it("converts single path argument to POSIX style", () => {
|
|
18
|
+
const result = pathPosix("C:\\Users\\test\\file.txt");
|
|
19
|
+
expect(result).toBe("C:/Users/test/file.txt");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("combines multiple path arguments and converts to POSIX style", () => {
|
|
23
|
+
const result = pathPosix("C:\\Users", "test", "file.txt");
|
|
24
|
+
expect(result).toBe("C:/Users/test/file.txt");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("keeps already POSIX-style path as is", () => {
|
|
28
|
+
const result = pathPosix("/usr/local/bin");
|
|
29
|
+
expect(result).toBe("/usr/local/bin");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("handles mixed path separators", () => {
|
|
33
|
+
const result = pathPosix("C:/Users\\test/file.txt");
|
|
34
|
+
expect(result).toBe("C:/Users/test/file.txt");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
|
|
40
|
+
//#region norm
|
|
41
|
+
|
|
42
|
+
describe("pathNorm", () => {
|
|
43
|
+
it("normalizes path and returns NormPath type", () => {
|
|
44
|
+
const result: NormPath = pathNorm("./test/../file.txt");
|
|
45
|
+
expect(result).toBe(path.resolve("./test/../file.txt"));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("combines multiple path arguments and normalizes", () => {
|
|
49
|
+
const basePath = path.resolve("/base");
|
|
50
|
+
const result = pathNorm(basePath, "sub", "file.txt");
|
|
51
|
+
expect(result).toBe(path.resolve(basePath, "sub", "file.txt"));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("converts relative path to absolute path", () => {
|
|
55
|
+
const result = pathNorm("relative/path");
|
|
56
|
+
expect(path.isAbsolute(result)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
|
|
62
|
+
//#region isChildPath
|
|
63
|
+
|
|
64
|
+
describe("pathIsChildPath", () => {
|
|
65
|
+
it("returns true for child path", () => {
|
|
66
|
+
const parent = pathNorm("/parent/dir");
|
|
67
|
+
const child = pathNorm("/parent/dir/child/file.txt");
|
|
68
|
+
expect(pathIsChildPath(child, parent)).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns false for same path", () => {
|
|
72
|
+
const parent = pathNorm("/parent/dir");
|
|
73
|
+
const child = pathNorm("/parent/dir");
|
|
74
|
+
expect(pathIsChildPath(child, parent)).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("returns false for non-child path", () => {
|
|
78
|
+
const parent = pathNorm("/parent/dir");
|
|
79
|
+
const child = pathNorm("/other/dir/file.txt");
|
|
80
|
+
expect(pathIsChildPath(child, parent)).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("returns false when only part of parent path matches", () => {
|
|
84
|
+
const parent = pathNorm("/parent/dir");
|
|
85
|
+
const child = pathNorm("/parent/directory/file.txt");
|
|
86
|
+
expect(pathIsChildPath(child, parent)).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
|
|
92
|
+
//#region changeFileDirectory
|
|
93
|
+
|
|
94
|
+
describe("pathChangeFileDirectory", () => {
|
|
95
|
+
it("changes file directory", () => {
|
|
96
|
+
const file = pathNorm("/source/sub/file.txt");
|
|
97
|
+
const from = pathNorm("/source");
|
|
98
|
+
const to = pathNorm("/target");
|
|
99
|
+
|
|
100
|
+
const result = pathChangeFileDirectory(file, from, to);
|
|
101
|
+
expect(result).toBe(pathNorm("/target/sub/file.txt"));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("changes directory in nested path", () => {
|
|
105
|
+
const file = pathNorm("/a/b/c/d/file.txt");
|
|
106
|
+
const from = pathNorm("/a/b");
|
|
107
|
+
const to = pathNorm("/x/y");
|
|
108
|
+
|
|
109
|
+
const result = pathChangeFileDirectory(file, from, to);
|
|
110
|
+
expect(result).toBe(pathNorm("/x/y/c/d/file.txt"));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("throws error when file is not inside fromDirectory", () => {
|
|
114
|
+
const file = pathNorm("/other/path/file.txt");
|
|
115
|
+
const from = pathNorm("/source");
|
|
116
|
+
const to = pathNorm("/target");
|
|
117
|
+
|
|
118
|
+
expect(() => pathChangeFileDirectory(file, from, to)).toThrow();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("returns toDirectory when filePath and fromDirectory are the same", () => {
|
|
122
|
+
const file = pathNorm("/source");
|
|
123
|
+
const from = pathNorm("/source");
|
|
124
|
+
const to = pathNorm("/target");
|
|
125
|
+
|
|
126
|
+
const result = pathChangeFileDirectory(file, from, to);
|
|
127
|
+
expect(result).toBe(to);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
//#endregion
|
|
132
|
+
|
|
133
|
+
//#region basenameWithoutExt
|
|
134
|
+
|
|
135
|
+
describe("pathBasenameWithoutExt", () => {
|
|
136
|
+
it("removes single extension (returns basename only)", () => {
|
|
137
|
+
const result = pathBasenameWithoutExt("/path/to/file.txt");
|
|
138
|
+
expect(result).toBe("file");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("removes only last extension in multiple extensions", () => {
|
|
142
|
+
const result = pathBasenameWithoutExt("/path/to/file.spec.ts");
|
|
143
|
+
expect(result).toBe("file.spec");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("returns basename for file without extension", () => {
|
|
147
|
+
const result = pathBasenameWithoutExt("/path/to/file");
|
|
148
|
+
expect(result).toBe("file");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("returns hidden file (starting with dot) as is", () => {
|
|
152
|
+
const result = pathBasenameWithoutExt("/path/to/.gitignore");
|
|
153
|
+
expect(result).toBe(".gitignore");
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
//#endregion
|
|
158
|
+
|
|
159
|
+
//#region filterByTargets
|
|
160
|
+
|
|
161
|
+
describe("pathFilterByTargets", () => {
|
|
162
|
+
const cwd = "/proj";
|
|
163
|
+
const files = ["/proj/src/a.ts", "/proj/src/b.ts", "/proj/tests/c.ts", "/proj/lib/d.ts"];
|
|
164
|
+
|
|
165
|
+
it("returns all files if targets array is empty", () => {
|
|
166
|
+
const result = pathFilterByTargets(files, [], cwd);
|
|
167
|
+
expect(result).toEqual(files);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("filters by single target", () => {
|
|
171
|
+
const result = pathFilterByTargets(files, ["src"], cwd);
|
|
172
|
+
expect(result).toEqual(["/proj/src/a.ts", "/proj/src/b.ts"]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("filters by multiple targets", () => {
|
|
176
|
+
const result = pathFilterByTargets(files, ["src", "tests"], cwd);
|
|
177
|
+
expect(result).toEqual(["/proj/src/a.ts", "/proj/src/b.ts", "/proj/tests/c.ts"]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("returns empty array when no matching file is found", () => {
|
|
181
|
+
const result = pathFilterByTargets(files, ["nonexistent"], cwd);
|
|
182
|
+
expect(result).toEqual([]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("filters by exact file path", () => {
|
|
186
|
+
const result = pathFilterByTargets(files, ["src/a.ts"], cwd);
|
|
187
|
+
expect(result).toEqual(["/proj/src/a.ts"]);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
//#endregion
|
|
192
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createWorker } from "../../../src";
|
|
2
|
+
|
|
3
|
+
interface TestWorkerEvents extends Record<string, unknown> {
|
|
4
|
+
progress: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const methods = {
|
|
8
|
+
add: (a: number, b: number) => {
|
|
9
|
+
sender.send("progress", 50);
|
|
10
|
+
return a + b;
|
|
11
|
+
},
|
|
12
|
+
echo: (message: string) => `Echo: ${message}`,
|
|
13
|
+
throwError: () => {
|
|
14
|
+
throw new Error("Intentional error");
|
|
15
|
+
},
|
|
16
|
+
delay: async (ms: number) => {
|
|
17
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
|
+
return ms;
|
|
19
|
+
},
|
|
20
|
+
noReturn: () => {},
|
|
21
|
+
logMessage: (message: string) => {
|
|
22
|
+
console.log(message);
|
|
23
|
+
return "logged";
|
|
24
|
+
},
|
|
25
|
+
crash: () => {
|
|
26
|
+
// Exit immediately
|
|
27
|
+
setImmediate(() => process.exit(1));
|
|
28
|
+
return "crashing...";
|
|
29
|
+
},
|
|
30
|
+
getEnv: (key: string) => process.env[key],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const sender = createWorker<typeof methods, TestWorkerEvents>(methods);
|
|
34
|
+
|
|
35
|
+
export default sender;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import type { WorkerProxy } from "../../src/worker/types";
|
|
4
|
+
import { Worker } from "../../src/worker/worker";
|
|
5
|
+
import type * as TestWorkerModule from "./fixtures/test-worker";
|
|
6
|
+
|
|
7
|
+
describe("SdWorker", () => {
|
|
8
|
+
const workerPath = path.resolve(import.meta.dirname, "fixtures/test-worker.ts");
|
|
9
|
+
let worker: WorkerProxy<typeof TestWorkerModule> | undefined;
|
|
10
|
+
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
if (worker) {
|
|
13
|
+
await worker.terminate();
|
|
14
|
+
worker = undefined;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
//#region Method invocation
|
|
19
|
+
|
|
20
|
+
describe("method invocation", () => {
|
|
21
|
+
it("calls worker method and returns result", async () => {
|
|
22
|
+
worker = Worker.create<typeof TestWorkerModule>(workerPath);
|
|
23
|
+
|
|
24
|
+
const result = await worker.add(10, 20);
|
|
25
|
+
|
|
26
|
+
expect(result).toBe(30);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("calls method that returns string", async () => {
|
|
30
|
+
worker = Worker.create<typeof TestWorkerModule>(workerPath);
|
|
31
|
+
|
|
32
|
+
const result = await worker.echo("Hello");
|
|
33
|
+
|
|
34
|
+
expect(result).toBe("Echo: Hello");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("rejects when error occurs in worker", async () => {
|
|
38
|
+
worker = Worker.create<typeof TestWorkerModule>(workerPath);
|
|
39
|
+
|
|
40
|
+
await expect(worker.throwError()).rejects.toThrow();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("throws error when calling nonexistent method", async () => {
|
|
44
|
+
worker = Worker.create<typeof TestWorkerModule>(workerPath);
|
|
45
|
+
|
|
46
|
+
// Bypass type system to call nonexistent method
|
|
47
|
+
const unknownWorker = worker as unknown as { unknownMethod: () => Promise<void> };
|
|
48
|
+
|
|
49
|
+
await expect(unknownWorker.unknownMethod()).rejects.toThrow(
|
|
50
|
+
"Unknown method: unknownMethod",
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("handles multiple concurrent requests", async () => {
|
|
55
|
+
worker = Worker.create<typeof TestWorkerModule>(workerPath);
|
|
56
|
+
|
|
57
|
+
const [result1, result2, result3] = await Promise.all([
|
|
58
|
+
worker.add(1, 2),
|
|
59
|
+
worker.add(3, 4),
|
|
60
|
+
worker.add(5, 6),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
expect(result1).toBe(3);
|
|
64
|
+
expect(result2).toBe(7);
|
|
65
|
+
expect(result3).toBe(11);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("calls method that returns void", async () => {
|
|
69
|
+
worker = Worker.create<typeof TestWorkerModule>(workerPath);
|
|
70
|
+
|
|
71
|
+
const result = await worker.noReturn();
|
|
72
|
+
|
|
73
|
+
expect(result).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
|
|
79
|
+
//#region Events
|
|
80
|
+
|
|
81
|
+
describe("events", () => {
|
|
82
|
+
it("receives events from worker", async () => {
|
|
83
|
+
worker = Worker.create<typeof TestWorkerModule>(workerPath);
|
|
84
|
+
|
|
85
|
+
const events: number[] = [];
|
|
86
|
+
worker.on("progress", (value) => {
|
|
87
|
+
events.push(value);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await worker.add(1, 2);
|
|
91
|
+
|
|
92
|
+
expect(events).toContain(50);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("removes event listener with off()", async () => {
|
|
96
|
+
worker = Worker.create<typeof TestWorkerModule>(workerPath);
|
|
97
|
+
|
|
98
|
+
const events: number[] = [];
|
|
99
|
+
const listener = (value: number) => {
|
|
100
|
+
events.push(value);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
worker.on("progress", listener);
|
|
104
|
+
await worker.add(1, 2);
|
|
105
|
+
|
|
106
|
+
// Remove listener
|
|
107
|
+
worker.off("progress", listener);
|
|
108
|
+
await worker.add(3, 4);
|
|
109
|
+
|
|
110
|
+
// Should only receive event from first call
|
|
111
|
+
expect(events).toHaveLength(1);
|
|
112
|
+
expect(events[0]).toBe(50);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
117
|
+
|
|
118
|
+
//#region Terminate
|
|
119
|
+
|
|
120
|
+
describe("terminate", () => {
|
|
121
|
+
it("rejects pending requests with Worker terminated error", async () => {
|
|
122
|
+
worker = Worker.create<typeof TestWorkerModule>(workerPath);
|
|
123
|
+
|
|
124
|
+
const runPromise = worker.delay(5000);
|
|
125
|
+
|
|
126
|
+
// Prepare to catch error before terminating worker
|
|
127
|
+
const errorPromise = runPromise.catch((err: unknown) => err);
|
|
128
|
+
|
|
129
|
+
// Terminate worker
|
|
130
|
+
await worker.terminate();
|
|
131
|
+
worker = undefined;
|
|
132
|
+
|
|
133
|
+
const error = await errorPromise;
|
|
134
|
+
expect(error).toBeInstanceOf(Error);
|
|
135
|
+
expect((error as Error).message).toBe("Worker terminated (method: delay)");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("rejects pending requests when worker crashes", async () => {
|
|
139
|
+
worker = Worker.create<typeof TestWorkerModule>(workerPath);
|
|
140
|
+
|
|
141
|
+
// Call long delay first to put it in pending state
|
|
142
|
+
const delayPromise = worker.delay(5000).catch((err: unknown) => err);
|
|
143
|
+
|
|
144
|
+
// Call crash after short wait (while delay is pending)
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
146
|
+
await worker.crash();
|
|
147
|
+
|
|
148
|
+
// When worker crashes, pending delay request should be rejected
|
|
149
|
+
const error = await delayPromise;
|
|
150
|
+
expect(error).toBeInstanceOf(Error);
|
|
151
|
+
expect((error as Error).message).toContain("Worker crashed");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
//#endregion
|
|
156
|
+
|
|
157
|
+
//#region stdout/stderr
|
|
158
|
+
|
|
159
|
+
describe("stdout/stderr", () => {
|
|
160
|
+
it("forwards worker console.log output to main process", async () => {
|
|
161
|
+
worker = Worker.create<typeof TestWorkerModule>(workerPath);
|
|
162
|
+
|
|
163
|
+
const result = await worker.logMessage("test message");
|
|
164
|
+
|
|
165
|
+
// If method returns normally, stdout piping is working
|
|
166
|
+
expect(result).toBe("logged");
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
//#endregion
|
|
171
|
+
|
|
172
|
+
//#region env option
|
|
173
|
+
|
|
174
|
+
describe("env option", () => {
|
|
175
|
+
it("passes env option to worker", async () => {
|
|
176
|
+
worker = Worker.create<typeof TestWorkerModule>(workerPath, {
|
|
177
|
+
env: {
|
|
178
|
+
TEST_ENV_VAR: "test-value-123",
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const result = await worker.getEnv("TEST_ENV_VAR");
|
|
183
|
+
|
|
184
|
+
expect(result).toBe("test-value-123");
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
//#endregion
|
|
189
|
+
});
|