@somewhatabstract/x 0.0.1 → 0.1.0
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/.github/dependabot.yml +28 -0
- package/.github/workflows/codeql-analysis.yml +29 -29
- package/.github/workflows/dependabot-pr-approval.yml +36 -0
- package/.github/workflows/nodejs.yml +84 -86
- package/.github/workflows/release.yml +4 -7
- package/.vscode/settings.json +19 -0
- package/CHANGELOG.md +23 -0
- package/CONTRIBUTING.md +3 -3
- package/README.md +132 -1
- package/biome.json +39 -0
- package/dist/x.mjs +278 -3
- package/package.json +14 -4
- package/src/__tests__/build-environment.test.ts +285 -0
- package/src/__tests__/discover-packages.test.ts +196 -0
- package/src/__tests__/errors.test.ts +59 -0
- package/src/__tests__/execute-script.test.ts +1042 -0
- package/src/__tests__/find-matching-bins.test.ts +506 -0
- package/src/__tests__/find-workspace-root.test.ts +73 -0
- package/src/__tests__/is-node-executable.test.ts +125 -0
- package/src/__tests__/resolve-bin-path.test.ts +344 -0
- package/src/__tests__/x-impl.test.ts +306 -7
- package/src/__tests__/x.test.ts +236 -0
- package/src/bin/x.ts +55 -1
- package/src/build-environment.ts +98 -0
- package/src/discover-packages.ts +35 -0
- package/src/errors.ts +10 -0
- package/src/execute-script.ts +56 -0
- package/src/find-matching-bins.ts +72 -0
- package/src/find-workspace-root.ts +24 -0
- package/src/is-node-executable.ts +16 -0
- package/src/resolve-bin-path.ts +48 -0
- package/src/x-impl.ts +95 -4
- package/tsconfig-types.json +2 -4
- package/tsconfig.json +5 -13
- package/tsdown.config.ts +1 -1
- package/vitest.config.ts +1 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import {describe, expect, it} from "vitest";
|
|
3
|
+
import type {PackageInfo} from "../discover-packages";
|
|
4
|
+
import {resolveBinPath} from "../resolve-bin-path";
|
|
5
|
+
|
|
6
|
+
describe("resolveBinPath", () => {
|
|
7
|
+
const mockPackage: PackageInfo = {
|
|
8
|
+
name: "test-package",
|
|
9
|
+
path: "/workspace/packages/test-package",
|
|
10
|
+
version: "1.0.0",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe("basic functionality", () => {
|
|
14
|
+
it("should resolve string-style bin when binName matches package name", () => {
|
|
15
|
+
// Arrange
|
|
16
|
+
const bin = "bin/cli.js";
|
|
17
|
+
const binName = "test-package";
|
|
18
|
+
|
|
19
|
+
// Act
|
|
20
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
21
|
+
|
|
22
|
+
// Assert
|
|
23
|
+
expect(result).toBe(
|
|
24
|
+
path.resolve("/workspace/packages/test-package", "bin/cli.js"),
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should return null for string-style bin when binName doesn't match package name", () => {
|
|
29
|
+
// Arrange
|
|
30
|
+
const bin = "bin/cli.js";
|
|
31
|
+
const binName = "different-name";
|
|
32
|
+
|
|
33
|
+
// Act
|
|
34
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
35
|
+
|
|
36
|
+
// Assert
|
|
37
|
+
expect(result).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should resolve object-style bin when binName exists in bin object", () => {
|
|
41
|
+
// Arrange
|
|
42
|
+
const bin = {
|
|
43
|
+
cli: "bin/cli.js",
|
|
44
|
+
server: "bin/server.js",
|
|
45
|
+
};
|
|
46
|
+
const binName = "cli";
|
|
47
|
+
|
|
48
|
+
// Act
|
|
49
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
50
|
+
|
|
51
|
+
// Assert
|
|
52
|
+
expect(result).toBe(
|
|
53
|
+
path.resolve("/workspace/packages/test-package", "bin/cli.js"),
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should return null for object-style bin when binName doesn't exist", () => {
|
|
58
|
+
// Arrange
|
|
59
|
+
const bin = {
|
|
60
|
+
cli: "bin/cli.js",
|
|
61
|
+
};
|
|
62
|
+
const binName = "nonexistent";
|
|
63
|
+
|
|
64
|
+
// Act
|
|
65
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
66
|
+
|
|
67
|
+
// Assert
|
|
68
|
+
expect(result).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should return null when bin is null", () => {
|
|
72
|
+
// Arrange
|
|
73
|
+
const bin = null;
|
|
74
|
+
const binName = "test-package";
|
|
75
|
+
|
|
76
|
+
// Act
|
|
77
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
78
|
+
|
|
79
|
+
// Assert
|
|
80
|
+
expect(result).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should return null when bin is undefined", () => {
|
|
84
|
+
// Arrange
|
|
85
|
+
const bin = undefined;
|
|
86
|
+
const binName = "test-package";
|
|
87
|
+
|
|
88
|
+
// Act
|
|
89
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
90
|
+
|
|
91
|
+
// Assert
|
|
92
|
+
expect(result).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("security - path traversal protection", () => {
|
|
97
|
+
it("should reject parent directory traversal in string-style bin", () => {
|
|
98
|
+
// Arrange
|
|
99
|
+
const bin = "../../../etc/passwd";
|
|
100
|
+
const binName = "test-package";
|
|
101
|
+
|
|
102
|
+
// Act
|
|
103
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
104
|
+
|
|
105
|
+
// Assert
|
|
106
|
+
expect(result).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should reject parent directory traversal in object-style bin", () => {
|
|
110
|
+
// Arrange
|
|
111
|
+
const bin = {
|
|
112
|
+
malicious: "../../../usr/bin/malicious",
|
|
113
|
+
};
|
|
114
|
+
const binName = "malicious";
|
|
115
|
+
|
|
116
|
+
// Act
|
|
117
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
118
|
+
|
|
119
|
+
// Assert
|
|
120
|
+
expect(result).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should reject absolute paths in string-style bin", () => {
|
|
124
|
+
// Arrange
|
|
125
|
+
const bin = "/usr/bin/malicious";
|
|
126
|
+
const binName = "test-package";
|
|
127
|
+
|
|
128
|
+
// Act
|
|
129
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
130
|
+
|
|
131
|
+
// Assert
|
|
132
|
+
expect(result).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should reject absolute paths in object-style bin", () => {
|
|
136
|
+
// Arrange
|
|
137
|
+
const bin = {
|
|
138
|
+
malicious: "/usr/bin/malicious",
|
|
139
|
+
};
|
|
140
|
+
const binName = "malicious";
|
|
141
|
+
|
|
142
|
+
// Act
|
|
143
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
144
|
+
|
|
145
|
+
// Assert
|
|
146
|
+
expect(result).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should accept valid subdirectory paths", () => {
|
|
150
|
+
// Arrange
|
|
151
|
+
const bin = "bin/nested/script.js";
|
|
152
|
+
const binName = "test-package";
|
|
153
|
+
|
|
154
|
+
// Act
|
|
155
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
156
|
+
|
|
157
|
+
// Assert
|
|
158
|
+
expect(result).toBe(
|
|
159
|
+
path.resolve(
|
|
160
|
+
"/workspace/packages/test-package",
|
|
161
|
+
"bin/nested/script.js",
|
|
162
|
+
),
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should accept valid paths at package root", () => {
|
|
167
|
+
// Arrange
|
|
168
|
+
const bin = "index.js";
|
|
169
|
+
const binName = "test-package";
|
|
170
|
+
|
|
171
|
+
// Act
|
|
172
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
173
|
+
|
|
174
|
+
// Assert
|
|
175
|
+
expect(result).toBe(
|
|
176
|
+
path.resolve("/workspace/packages/test-package", "index.js"),
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should handle edge case where bin path equals package directory", () => {
|
|
181
|
+
// Arrange
|
|
182
|
+
const bin = ".";
|
|
183
|
+
const binName = "test-package";
|
|
184
|
+
|
|
185
|
+
// Act
|
|
186
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
187
|
+
|
|
188
|
+
// Assert
|
|
189
|
+
expect(result).toBe(
|
|
190
|
+
path.resolve("/workspace/packages/test-package"),
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should reject path that tries to escape with multiple parent references", () => {
|
|
195
|
+
// Arrange
|
|
196
|
+
const bin = {
|
|
197
|
+
escape: "../../../../../../etc/shadow",
|
|
198
|
+
};
|
|
199
|
+
const binName = "escape";
|
|
200
|
+
|
|
201
|
+
// Act
|
|
202
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
203
|
+
|
|
204
|
+
// Assert
|
|
205
|
+
expect(result).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("should handle path with backslashes (valid filename chars on Unix)", () => {
|
|
209
|
+
// Arrange
|
|
210
|
+
// On Unix, backslashes are valid filename characters, not path separators
|
|
211
|
+
// On Windows, Node.js normalizes these to forward slashes before our check
|
|
212
|
+
const bin = "bin\\script.js";
|
|
213
|
+
const binName = "test-package";
|
|
214
|
+
|
|
215
|
+
// Act
|
|
216
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
217
|
+
|
|
218
|
+
// Assert
|
|
219
|
+
// Should resolve successfully since backslashes are just filename chars on Unix
|
|
220
|
+
expect(result).toBe(
|
|
221
|
+
path.resolve(
|
|
222
|
+
"/workspace/packages/test-package",
|
|
223
|
+
"bin\\script.js",
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("type handling", () => {
|
|
230
|
+
it("should return null when bin is a number", () => {
|
|
231
|
+
// Arrange
|
|
232
|
+
const bin: any = 123;
|
|
233
|
+
const binName = "test-package";
|
|
234
|
+
|
|
235
|
+
// Act
|
|
236
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
237
|
+
|
|
238
|
+
// Assert
|
|
239
|
+
expect(result).toBeNull();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("should return null when bin is a boolean", () => {
|
|
243
|
+
// Arrange
|
|
244
|
+
const bin: any = true;
|
|
245
|
+
const binName = "test-package";
|
|
246
|
+
|
|
247
|
+
// Act
|
|
248
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
249
|
+
|
|
250
|
+
// Assert
|
|
251
|
+
expect(result).toBeNull();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("should return null when bin is an empty object", () => {
|
|
255
|
+
// Arrange
|
|
256
|
+
const bin = {};
|
|
257
|
+
const binName = "test-package";
|
|
258
|
+
|
|
259
|
+
// Act
|
|
260
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
261
|
+
|
|
262
|
+
// Assert
|
|
263
|
+
expect(result).toBeNull();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("should return null when bin is an array", () => {
|
|
267
|
+
// Arrange
|
|
268
|
+
const bin: any = ["bin/cli.js"];
|
|
269
|
+
const binName = "test-package";
|
|
270
|
+
|
|
271
|
+
// Act
|
|
272
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
273
|
+
|
|
274
|
+
// Assert
|
|
275
|
+
expect(result).toBeNull();
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("edge cases", () => {
|
|
280
|
+
it("should handle package path with trailing slash", () => {
|
|
281
|
+
// Arrange
|
|
282
|
+
const packageWithTrailingSlash: PackageInfo = {
|
|
283
|
+
name: "test-package",
|
|
284
|
+
path: "/workspace/packages/test-package/",
|
|
285
|
+
version: "1.0.0",
|
|
286
|
+
};
|
|
287
|
+
const bin = "bin/cli.js";
|
|
288
|
+
const binName = "test-package";
|
|
289
|
+
|
|
290
|
+
// Act
|
|
291
|
+
const result = resolveBinPath(
|
|
292
|
+
packageWithTrailingSlash,
|
|
293
|
+
bin,
|
|
294
|
+
binName,
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
// Assert
|
|
298
|
+
expect(result).toBe(
|
|
299
|
+
path.resolve("/workspace/packages/test-package/", "bin/cli.js"),
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("should handle bin path with leading slash", () => {
|
|
304
|
+
// Arrange
|
|
305
|
+
const bin = "/bin/cli.js";
|
|
306
|
+
const binName = "test-package";
|
|
307
|
+
|
|
308
|
+
// Act
|
|
309
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
310
|
+
|
|
311
|
+
// Assert
|
|
312
|
+
expect(result).toBeNull();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("should handle bin path with only dots", () => {
|
|
316
|
+
// Arrange
|
|
317
|
+
const bin = "..";
|
|
318
|
+
const binName = "test-package";
|
|
319
|
+
|
|
320
|
+
// Act
|
|
321
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
322
|
+
|
|
323
|
+
// Assert
|
|
324
|
+
expect(result).toBeNull();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("should resolve relative path with current directory reference", () => {
|
|
328
|
+
// Arrange
|
|
329
|
+
const bin = "./bin/cli.js";
|
|
330
|
+
const binName = "test-package";
|
|
331
|
+
|
|
332
|
+
// Act
|
|
333
|
+
const result = resolveBinPath(mockPackage, bin, binName);
|
|
334
|
+
|
|
335
|
+
// Assert
|
|
336
|
+
expect(result).toBe(
|
|
337
|
+
path.resolve(
|
|
338
|
+
"/workspace/packages/test-package",
|
|
339
|
+
"./bin/cli.js",
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
});
|
|
@@ -1,15 +1,314 @@
|
|
|
1
|
-
import {describe, expect, it, vi} from "vitest";
|
|
2
|
-
import
|
|
1
|
+
import {beforeEach, describe, expect, it, vi} from "vitest";
|
|
2
|
+
import * as discoverPackagesModule from "../discover-packages";
|
|
3
|
+
import {HandledError} from "../errors";
|
|
4
|
+
import * as executeScriptModule from "../execute-script";
|
|
5
|
+
import * as findMatchingBinsModule from "../find-matching-bins";
|
|
6
|
+
import * as findWorkspaceRootModule from "../find-workspace-root";
|
|
7
|
+
import {xImpl} from "../x-impl";
|
|
8
|
+
|
|
9
|
+
// Mock the modules
|
|
10
|
+
vi.mock("../find-workspace-root");
|
|
11
|
+
vi.mock("../discover-packages");
|
|
12
|
+
vi.mock("../find-matching-bins");
|
|
13
|
+
vi.mock("../execute-script");
|
|
3
14
|
|
|
4
15
|
describe("xImpl", () => {
|
|
5
|
-
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
// Suppress console output in tests
|
|
19
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
20
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should return exit code 1 when script not found", async () => {
|
|
24
|
+
// Arrange
|
|
25
|
+
const scriptName = "nonexistent-script";
|
|
26
|
+
|
|
27
|
+
vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockResolvedValue(
|
|
28
|
+
"/test/workspace",
|
|
29
|
+
);
|
|
30
|
+
vi.mocked(discoverPackagesModule.discoverPackages).mockResolvedValue([
|
|
31
|
+
{name: "pkg1", path: "/test/pkg1", version: "1.0.0"},
|
|
32
|
+
]);
|
|
33
|
+
vi.mocked(findMatchingBinsModule.findMatchingBins).mockResolvedValue(
|
|
34
|
+
[],
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Act
|
|
38
|
+
const result = await xImpl(scriptName);
|
|
39
|
+
|
|
40
|
+
// Assert
|
|
41
|
+
expect(result.exitCode).toBe(1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should return exit code 1 when no packages found", async () => {
|
|
45
|
+
// Arrange
|
|
46
|
+
const scriptName = "test-script";
|
|
47
|
+
|
|
48
|
+
vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockResolvedValue(
|
|
49
|
+
"/test/workspace",
|
|
50
|
+
);
|
|
51
|
+
vi.mocked(discoverPackagesModule.discoverPackages).mockResolvedValue(
|
|
52
|
+
[],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Act
|
|
56
|
+
const result = await xImpl(scriptName);
|
|
57
|
+
|
|
58
|
+
// Assert
|
|
59
|
+
expect(result.exitCode).toBe(1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should return exit code 1 when multiple bins match", async () => {
|
|
63
|
+
// Arrange
|
|
64
|
+
const scriptName = "test-script";
|
|
65
|
+
|
|
66
|
+
vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockResolvedValue(
|
|
67
|
+
"/test/workspace",
|
|
68
|
+
);
|
|
69
|
+
vi.mocked(discoverPackagesModule.discoverPackages).mockResolvedValue([
|
|
70
|
+
{name: "pkg1", path: "/test/pkg1", version: "1.0.0"},
|
|
71
|
+
{name: "pkg2", path: "/test/pkg2", version: "1.0.0"},
|
|
72
|
+
]);
|
|
73
|
+
vi.mocked(findMatchingBinsModule.findMatchingBins).mockResolvedValue([
|
|
74
|
+
{
|
|
75
|
+
packageName: "pkg1",
|
|
76
|
+
packagePath: "/test/pkg1",
|
|
77
|
+
binName: "test-script",
|
|
78
|
+
binPath: "/test/pkg1/bin/test",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
packageName: "pkg2",
|
|
82
|
+
packagePath: "/test/pkg2",
|
|
83
|
+
binName: "test-script",
|
|
84
|
+
binPath: "/test/pkg2/bin/test",
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
// Act
|
|
89
|
+
const result = await xImpl(scriptName);
|
|
90
|
+
|
|
91
|
+
// Assert
|
|
92
|
+
expect(result.exitCode).toBe(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should execute script in dry-run mode", async () => {
|
|
96
|
+
// Arrange
|
|
97
|
+
const scriptName = "test-script";
|
|
98
|
+
const options = {dryRun: true};
|
|
99
|
+
|
|
100
|
+
vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockResolvedValue(
|
|
101
|
+
"/test/workspace",
|
|
102
|
+
);
|
|
103
|
+
vi.mocked(discoverPackagesModule.discoverPackages).mockResolvedValue([
|
|
104
|
+
{name: "pkg1", path: "/test/pkg1", version: "1.0.0"},
|
|
105
|
+
]);
|
|
106
|
+
vi.mocked(findMatchingBinsModule.findMatchingBins).mockResolvedValue([
|
|
107
|
+
{
|
|
108
|
+
packageName: "pkg1",
|
|
109
|
+
packagePath: "/test/pkg1",
|
|
110
|
+
binName: "test-script",
|
|
111
|
+
binPath: "/test/pkg1/bin/test",
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
// Act
|
|
116
|
+
const result = await xImpl(scriptName, [], options);
|
|
117
|
+
|
|
118
|
+
// Assert
|
|
119
|
+
expect(result.exitCode).toBe(0);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should not execute script when dry-run is true", async () => {
|
|
123
|
+
// Arrange
|
|
124
|
+
const scriptName = "test-script";
|
|
125
|
+
const options = {dryRun: true};
|
|
126
|
+
|
|
127
|
+
vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockResolvedValue(
|
|
128
|
+
"/test/workspace",
|
|
129
|
+
);
|
|
130
|
+
vi.mocked(discoverPackagesModule.discoverPackages).mockResolvedValue([
|
|
131
|
+
{name: "pkg1", path: "/test/pkg1", version: "1.0.0"},
|
|
132
|
+
]);
|
|
133
|
+
vi.mocked(findMatchingBinsModule.findMatchingBins).mockResolvedValue([
|
|
134
|
+
{
|
|
135
|
+
packageName: "pkg1",
|
|
136
|
+
packagePath: "/test/pkg1",
|
|
137
|
+
binName: "test-script",
|
|
138
|
+
binPath: "/test/pkg1/bin/test",
|
|
139
|
+
},
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
// Act
|
|
143
|
+
await xImpl(scriptName, [], options);
|
|
144
|
+
|
|
145
|
+
// Assert
|
|
146
|
+
expect(executeScriptModule.executeScript).not.toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should execute script and return its exit code", async () => {
|
|
150
|
+
// Arrange
|
|
151
|
+
const scriptName = "test-script";
|
|
152
|
+
|
|
153
|
+
vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockResolvedValue(
|
|
154
|
+
"/test/workspace",
|
|
155
|
+
);
|
|
156
|
+
vi.mocked(discoverPackagesModule.discoverPackages).mockResolvedValue([
|
|
157
|
+
{name: "pkg1", path: "/test/pkg1", version: "1.0.0"},
|
|
158
|
+
]);
|
|
159
|
+
vi.mocked(findMatchingBinsModule.findMatchingBins).mockResolvedValue([
|
|
160
|
+
{
|
|
161
|
+
packageName: "pkg1",
|
|
162
|
+
packagePath: "/test/pkg1",
|
|
163
|
+
binName: "test-script",
|
|
164
|
+
binPath: "/test/pkg1/bin/test",
|
|
165
|
+
},
|
|
166
|
+
]);
|
|
167
|
+
vi.mocked(executeScriptModule.executeScript).mockResolvedValue(0);
|
|
168
|
+
|
|
169
|
+
// Act
|
|
170
|
+
const result = await xImpl(scriptName);
|
|
171
|
+
|
|
172
|
+
// Assert
|
|
173
|
+
expect(result.exitCode).toBe(0);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should pass workspace root to executeScript", async () => {
|
|
177
|
+
// Arrange
|
|
178
|
+
const scriptName = "test-script";
|
|
179
|
+
const workspaceRoot = "/test/workspace";
|
|
180
|
+
|
|
181
|
+
vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockResolvedValue(
|
|
182
|
+
workspaceRoot,
|
|
183
|
+
);
|
|
184
|
+
vi.mocked(discoverPackagesModule.discoverPackages).mockResolvedValue([
|
|
185
|
+
{name: "pkg1", path: "/test/pkg1", version: "1.0.0"},
|
|
186
|
+
]);
|
|
187
|
+
vi.mocked(findMatchingBinsModule.findMatchingBins).mockResolvedValue([
|
|
188
|
+
{
|
|
189
|
+
packageName: "pkg1",
|
|
190
|
+
packagePath: "/test/pkg1",
|
|
191
|
+
binName: "test-script",
|
|
192
|
+
binPath: "/test/pkg1/bin/test",
|
|
193
|
+
},
|
|
194
|
+
]);
|
|
195
|
+
vi.mocked(executeScriptModule.executeScript).mockResolvedValue(0);
|
|
196
|
+
|
|
197
|
+
// Act
|
|
198
|
+
await xImpl(scriptName);
|
|
199
|
+
|
|
200
|
+
// Assert
|
|
201
|
+
expect(executeScriptModule.executeScript).toHaveBeenCalledWith(
|
|
202
|
+
expect.any(Object),
|
|
203
|
+
expect.any(Array),
|
|
204
|
+
workspaceRoot,
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("should pass args to executeScript", async () => {
|
|
209
|
+
// Arrange
|
|
210
|
+
const scriptName = "test-script";
|
|
211
|
+
const args = ["--flag", "value"];
|
|
212
|
+
|
|
213
|
+
vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockResolvedValue(
|
|
214
|
+
"/test/workspace",
|
|
215
|
+
);
|
|
216
|
+
vi.mocked(discoverPackagesModule.discoverPackages).mockResolvedValue([
|
|
217
|
+
{name: "pkg1", path: "/test/pkg1", version: "1.0.0"},
|
|
218
|
+
]);
|
|
219
|
+
vi.mocked(findMatchingBinsModule.findMatchingBins).mockResolvedValue([
|
|
220
|
+
{
|
|
221
|
+
packageName: "pkg1",
|
|
222
|
+
packagePath: "/test/pkg1",
|
|
223
|
+
binName: "test-script",
|
|
224
|
+
binPath: "/test/pkg1/bin/test",
|
|
225
|
+
},
|
|
226
|
+
]);
|
|
227
|
+
vi.mocked(executeScriptModule.executeScript).mockResolvedValue(0);
|
|
228
|
+
|
|
229
|
+
// Act
|
|
230
|
+
await xImpl(scriptName, args);
|
|
231
|
+
|
|
232
|
+
// Assert
|
|
233
|
+
expect(executeScriptModule.executeScript).toHaveBeenCalledWith(
|
|
234
|
+
expect.any(Object),
|
|
235
|
+
args,
|
|
236
|
+
expect.any(String),
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("should return exit code 1 on unhandled error", async () => {
|
|
241
|
+
// Arrange
|
|
242
|
+
const scriptName = "test-script";
|
|
243
|
+
|
|
244
|
+
vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockRejectedValue(
|
|
245
|
+
new HandledError("Test error"),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// Act
|
|
249
|
+
const result = await xImpl(scriptName);
|
|
250
|
+
|
|
251
|
+
// Assert
|
|
252
|
+
expect(result.exitCode).toBe(1);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("should rethrow non-HandledError errors", async () => {
|
|
256
|
+
// Arrange
|
|
257
|
+
const scriptName = "test-script";
|
|
258
|
+
const unexpectedError = new Error("Unexpected error");
|
|
259
|
+
|
|
260
|
+
vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockRejectedValue(
|
|
261
|
+
unexpectedError,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Act
|
|
265
|
+
const underTest = () => xImpl(scriptName);
|
|
266
|
+
|
|
267
|
+
// Assert
|
|
268
|
+
await expect(underTest).rejects.toThrow("Unexpected error");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("should return exit code 1 when script name is empty", async () => {
|
|
272
|
+
// Arrange
|
|
273
|
+
const scriptName = "";
|
|
274
|
+
|
|
275
|
+
// Act
|
|
276
|
+
const result = await xImpl(scriptName);
|
|
277
|
+
|
|
278
|
+
// Assert
|
|
279
|
+
expect(result.exitCode).toBe(1);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("should return exit code 1 when script name is only whitespace", async () => {
|
|
283
|
+
// Arrange
|
|
284
|
+
const scriptName = " ";
|
|
285
|
+
|
|
286
|
+
// Act
|
|
287
|
+
const result = await xImpl(scriptName);
|
|
288
|
+
|
|
289
|
+
// Assert
|
|
290
|
+
expect(result.exitCode).toBe(1);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("should return exit code 1 when script name contains forward slash", async () => {
|
|
294
|
+
// Arrange
|
|
295
|
+
const scriptName = "bin/script";
|
|
296
|
+
|
|
297
|
+
// Act
|
|
298
|
+
const result = await xImpl(scriptName);
|
|
299
|
+
|
|
300
|
+
// Assert
|
|
301
|
+
expect(result.exitCode).toBe(1);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("should return exit code 1 when script name contains backslash", async () => {
|
|
6
305
|
// Arrange
|
|
7
|
-
const
|
|
306
|
+
const scriptName = "bin\\script";
|
|
8
307
|
|
|
9
308
|
// Act
|
|
10
|
-
xImpl();
|
|
309
|
+
const result = await xImpl(scriptName);
|
|
11
310
|
|
|
12
311
|
// Assert
|
|
13
|
-
expect(
|
|
312
|
+
expect(result.exitCode).toBe(1);
|
|
14
313
|
});
|
|
15
|
-
});
|
|
314
|
+
});
|