@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,506 @@
|
|
|
1
|
+
import {beforeEach, describe, expect, it, vi} from "vitest";
|
|
2
|
+
import type {PackageInfo} from "../discover-packages";
|
|
3
|
+
import {findMatchingBins} from "../find-matching-bins";
|
|
4
|
+
|
|
5
|
+
// Mock the fs module
|
|
6
|
+
vi.mock("node:fs/promises", () => ({
|
|
7
|
+
readFile: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
import * as fs from "node:fs/promises";
|
|
11
|
+
|
|
12
|
+
describe("findMatchingBins", () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.clearAllMocks();
|
|
15
|
+
vi.spyOn(console, "warn").mockImplementation(() => {
|
|
16
|
+
/* shh */
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should find one matching bin with object-style bin configuration", async () => {
|
|
21
|
+
// Arrange
|
|
22
|
+
const packages: PackageInfo[] = [
|
|
23
|
+
{
|
|
24
|
+
name: "@somewhatabstract/x",
|
|
25
|
+
path: "/test/path",
|
|
26
|
+
version: "1.0.0",
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
31
|
+
JSON.stringify({
|
|
32
|
+
name: "@somewhatabstract/x",
|
|
33
|
+
bin: {
|
|
34
|
+
x: "./dist/x.mjs",
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Act
|
|
40
|
+
const matches = await findMatchingBins(packages, "x");
|
|
41
|
+
|
|
42
|
+
// Assert
|
|
43
|
+
expect(matches).toHaveLength(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should match the correct package name", async () => {
|
|
47
|
+
// Arrange
|
|
48
|
+
const packages: PackageInfo[] = [
|
|
49
|
+
{
|
|
50
|
+
name: "@somewhatabstract/x",
|
|
51
|
+
path: "/test/path",
|
|
52
|
+
version: "1.0.0",
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
name: "@somewhatabstract/x",
|
|
59
|
+
bin: {
|
|
60
|
+
x: "./dist/x.mjs",
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Act
|
|
66
|
+
const matches = await findMatchingBins(packages, "x");
|
|
67
|
+
|
|
68
|
+
// Assert
|
|
69
|
+
expect(matches[0].packageName).toBe("@somewhatabstract/x");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should match the correct bin name", async () => {
|
|
73
|
+
// Arrange
|
|
74
|
+
const packages: PackageInfo[] = [
|
|
75
|
+
{
|
|
76
|
+
name: "@somewhatabstract/x",
|
|
77
|
+
path: "/test/path",
|
|
78
|
+
version: "1.0.0",
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
83
|
+
JSON.stringify({
|
|
84
|
+
name: "@somewhatabstract/x",
|
|
85
|
+
bin: {
|
|
86
|
+
x: "./dist/x.mjs",
|
|
87
|
+
},
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Act
|
|
92
|
+
const matches = await findMatchingBins(packages, "x");
|
|
93
|
+
|
|
94
|
+
// Assert
|
|
95
|
+
expect(matches[0].binName).toBe("x");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should include the bin path in the match", async () => {
|
|
99
|
+
// Arrange
|
|
100
|
+
const packages: PackageInfo[] = [
|
|
101
|
+
{
|
|
102
|
+
name: "@somewhatabstract/x",
|
|
103
|
+
path: "/test/path",
|
|
104
|
+
version: "1.0.0",
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
109
|
+
JSON.stringify({
|
|
110
|
+
name: "@somewhatabstract/x",
|
|
111
|
+
bin: {
|
|
112
|
+
x: "./dist/x.mjs",
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Act
|
|
118
|
+
const matches = await findMatchingBins(packages, "x");
|
|
119
|
+
|
|
120
|
+
// Assert
|
|
121
|
+
expect(matches[0].binPath).toBe("/test/path/dist/x.mjs");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should return empty array when no bins match", async () => {
|
|
125
|
+
// Arrange
|
|
126
|
+
const packages: PackageInfo[] = [
|
|
127
|
+
{
|
|
128
|
+
name: "@somewhatabstract/x",
|
|
129
|
+
path: "/test/path",
|
|
130
|
+
version: "1.0.0",
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
135
|
+
JSON.stringify({
|
|
136
|
+
name: "@somewhatabstract/x",
|
|
137
|
+
bin: {
|
|
138
|
+
x: "./dist/x.mjs",
|
|
139
|
+
},
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Act
|
|
144
|
+
const matches = await findMatchingBins(packages, "nonexistent");
|
|
145
|
+
|
|
146
|
+
// Assert
|
|
147
|
+
expect(matches).toHaveLength(0);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should handle packages without bin field", async () => {
|
|
151
|
+
// Arrange
|
|
152
|
+
const packages: PackageInfo[] = [
|
|
153
|
+
{
|
|
154
|
+
name: "no-bin-pkg",
|
|
155
|
+
path: "/test/path",
|
|
156
|
+
version: "1.0.0",
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
161
|
+
JSON.stringify({
|
|
162
|
+
name: "no-bin-pkg",
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Act
|
|
167
|
+
const matches = await findMatchingBins(packages, "anything");
|
|
168
|
+
|
|
169
|
+
// Assert
|
|
170
|
+
expect(matches).toHaveLength(0);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("should handle packages with string-style bin configuration", async () => {
|
|
174
|
+
// Arrange
|
|
175
|
+
const packages: PackageInfo[] = [
|
|
176
|
+
{
|
|
177
|
+
name: "my-cli",
|
|
178
|
+
path: "/test/path",
|
|
179
|
+
version: "1.0.0",
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
184
|
+
JSON.stringify({
|
|
185
|
+
name: "my-cli",
|
|
186
|
+
bin: "./bin/cli.js",
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Act
|
|
191
|
+
const matches = await findMatchingBins(packages, "my-cli");
|
|
192
|
+
|
|
193
|
+
// Assert
|
|
194
|
+
expect(matches).toHaveLength(1);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should skip packages that cannot be read", async () => {
|
|
198
|
+
// Arrange
|
|
199
|
+
const packages: PackageInfo[] = [
|
|
200
|
+
{
|
|
201
|
+
name: "unreadable-pkg",
|
|
202
|
+
path: "/test/path",
|
|
203
|
+
version: "1.0.0",
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
vi.mocked(fs.readFile).mockRejectedValue(
|
|
208
|
+
new Error("ENOENT: no such file or directory"),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Act
|
|
212
|
+
const matches = await findMatchingBins(packages, "anything");
|
|
213
|
+
|
|
214
|
+
// Assert
|
|
215
|
+
expect(matches).toHaveLength(0);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should not match string-style bin when package name differs from binName", async () => {
|
|
219
|
+
// Arrange
|
|
220
|
+
const packages: PackageInfo[] = [
|
|
221
|
+
{
|
|
222
|
+
name: "my-package",
|
|
223
|
+
path: "/test/path",
|
|
224
|
+
version: "1.0.0",
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
229
|
+
JSON.stringify({
|
|
230
|
+
name: "my-package",
|
|
231
|
+
bin: "./bin/script.js",
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// Act
|
|
236
|
+
const matches = await findMatchingBins(packages, "different-name");
|
|
237
|
+
|
|
238
|
+
// Assert
|
|
239
|
+
expect(matches).toHaveLength(0);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("should not match object-style bin when binName not in object", async () => {
|
|
243
|
+
// Arrange
|
|
244
|
+
const packages: PackageInfo[] = [
|
|
245
|
+
{
|
|
246
|
+
name: "my-package",
|
|
247
|
+
path: "/test/path",
|
|
248
|
+
version: "1.0.0",
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
253
|
+
JSON.stringify({
|
|
254
|
+
name: "my-package",
|
|
255
|
+
bin: {
|
|
256
|
+
"other-bin": "./bin/other.js",
|
|
257
|
+
"another-bin": "./bin/another.js",
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Act
|
|
263
|
+
const matches = await findMatchingBins(packages, "missing-bin");
|
|
264
|
+
|
|
265
|
+
// Assert
|
|
266
|
+
expect(matches).toHaveLength(0);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("should reject path traversal attempt with parent directory in string-style bin", async () => {
|
|
270
|
+
// Arrange
|
|
271
|
+
const packages: PackageInfo[] = [
|
|
272
|
+
{
|
|
273
|
+
name: "malicious-package",
|
|
274
|
+
path: "/test/package",
|
|
275
|
+
version: "1.0.0",
|
|
276
|
+
},
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
280
|
+
JSON.stringify({
|
|
281
|
+
name: "malicious-package",
|
|
282
|
+
bin: "../../../etc/passwd",
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// Act
|
|
287
|
+
const matches = await findMatchingBins(packages, "malicious-package");
|
|
288
|
+
|
|
289
|
+
// Assert
|
|
290
|
+
expect(matches).toHaveLength(0);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("should reject path traversal attempt with parent directory in object-style bin", async () => {
|
|
294
|
+
// Arrange
|
|
295
|
+
const packages: PackageInfo[] = [
|
|
296
|
+
{
|
|
297
|
+
name: "malicious-package",
|
|
298
|
+
path: "/test/package",
|
|
299
|
+
version: "1.0.0",
|
|
300
|
+
},
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
304
|
+
JSON.stringify({
|
|
305
|
+
name: "malicious-package",
|
|
306
|
+
bin: {
|
|
307
|
+
"evil-script": "../../../usr/bin/malicious",
|
|
308
|
+
},
|
|
309
|
+
}),
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
// Act
|
|
313
|
+
const matches = await findMatchingBins(packages, "evil-script");
|
|
314
|
+
|
|
315
|
+
// Assert
|
|
316
|
+
expect(matches).toHaveLength(0);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("should reject absolute path in string-style bin", async () => {
|
|
320
|
+
// Arrange
|
|
321
|
+
const packages: PackageInfo[] = [
|
|
322
|
+
{
|
|
323
|
+
name: "malicious-package",
|
|
324
|
+
path: "/test/package",
|
|
325
|
+
version: "1.0.0",
|
|
326
|
+
},
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
330
|
+
JSON.stringify({
|
|
331
|
+
name: "malicious-package",
|
|
332
|
+
bin: "/usr/bin/malicious",
|
|
333
|
+
}),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
// Act
|
|
337
|
+
const matches = await findMatchingBins(packages, "malicious-package");
|
|
338
|
+
|
|
339
|
+
// Assert
|
|
340
|
+
expect(matches).toHaveLength(0);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("should reject absolute path in object-style bin", async () => {
|
|
344
|
+
// Arrange
|
|
345
|
+
const packages: PackageInfo[] = [
|
|
346
|
+
{
|
|
347
|
+
name: "malicious-package",
|
|
348
|
+
path: "/test/package",
|
|
349
|
+
version: "1.0.0",
|
|
350
|
+
},
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
354
|
+
JSON.stringify({
|
|
355
|
+
name: "malicious-package",
|
|
356
|
+
bin: {
|
|
357
|
+
"evil-script": "/usr/bin/malicious",
|
|
358
|
+
},
|
|
359
|
+
}),
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// Act
|
|
363
|
+
const matches = await findMatchingBins(packages, "evil-script");
|
|
364
|
+
|
|
365
|
+
// Assert
|
|
366
|
+
expect(matches).toHaveLength(0);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("should accept bin path in subdirectory", async () => {
|
|
370
|
+
// Arrange
|
|
371
|
+
const packages: PackageInfo[] = [
|
|
372
|
+
{
|
|
373
|
+
name: "good-package",
|
|
374
|
+
path: "/test/package",
|
|
375
|
+
version: "1.0.0",
|
|
376
|
+
},
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
380
|
+
JSON.stringify({
|
|
381
|
+
name: "good-package",
|
|
382
|
+
bin: {
|
|
383
|
+
"good-script": "./bin/nested/script.js",
|
|
384
|
+
},
|
|
385
|
+
}),
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
// Act
|
|
389
|
+
const matches = await findMatchingBins(packages, "good-script");
|
|
390
|
+
|
|
391
|
+
// Assert
|
|
392
|
+
expect(matches).toHaveLength(1);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("should accept bin path at package root", async () => {
|
|
396
|
+
// Arrange
|
|
397
|
+
const packages: PackageInfo[] = [
|
|
398
|
+
{
|
|
399
|
+
name: "good-package",
|
|
400
|
+
path: "/test/package",
|
|
401
|
+
version: "1.0.0",
|
|
402
|
+
},
|
|
403
|
+
];
|
|
404
|
+
|
|
405
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
406
|
+
JSON.stringify({
|
|
407
|
+
name: "good-package",
|
|
408
|
+
bin: {
|
|
409
|
+
"good-script": "./script.js",
|
|
410
|
+
},
|
|
411
|
+
}),
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
// Act
|
|
415
|
+
const matches = await findMatchingBins(packages, "good-script");
|
|
416
|
+
|
|
417
|
+
// Assert
|
|
418
|
+
expect(matches).toHaveLength(1);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("should return empty array when package.json contains invalid JSON", async () => {
|
|
422
|
+
// Arrange
|
|
423
|
+
const packages: PackageInfo[] = [
|
|
424
|
+
{
|
|
425
|
+
name: "invalid-json-pkg",
|
|
426
|
+
path: "/test/path",
|
|
427
|
+
version: "1.0.0",
|
|
428
|
+
},
|
|
429
|
+
];
|
|
430
|
+
|
|
431
|
+
vi.mocked(fs.readFile).mockResolvedValue("{ this is not valid json");
|
|
432
|
+
|
|
433
|
+
// Act
|
|
434
|
+
const matches = await findMatchingBins(packages, "invalid-json-pkg");
|
|
435
|
+
|
|
436
|
+
// Assert
|
|
437
|
+
expect(matches).toHaveLength(0);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("should warn when package.json contains invalid JSON", async () => {
|
|
441
|
+
// Arrange
|
|
442
|
+
const packages: PackageInfo[] = [
|
|
443
|
+
{
|
|
444
|
+
name: "invalid-json-pkg",
|
|
445
|
+
path: "/test/path",
|
|
446
|
+
version: "1.0.0",
|
|
447
|
+
},
|
|
448
|
+
];
|
|
449
|
+
|
|
450
|
+
vi.mocked(fs.readFile).mockResolvedValue("{ this is not valid json");
|
|
451
|
+
|
|
452
|
+
// Act
|
|
453
|
+
await findMatchingBins(packages, "invalid-json-pkg");
|
|
454
|
+
|
|
455
|
+
// Assert
|
|
456
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
457
|
+
expect.stringContaining("invalid JSON"),
|
|
458
|
+
);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("should return empty array when package.json file is not found", async () => {
|
|
462
|
+
// Arrange
|
|
463
|
+
const packages: PackageInfo[] = [
|
|
464
|
+
{
|
|
465
|
+
name: "missing-pkg",
|
|
466
|
+
path: "/test/path",
|
|
467
|
+
version: "1.0.0",
|
|
468
|
+
},
|
|
469
|
+
];
|
|
470
|
+
|
|
471
|
+
const enoentError = Object.assign(
|
|
472
|
+
new Error("ENOENT: no such file or directory"),
|
|
473
|
+
{code: "ENOENT"},
|
|
474
|
+
);
|
|
475
|
+
vi.mocked(fs.readFile).mockRejectedValue(enoentError);
|
|
476
|
+
|
|
477
|
+
// Act
|
|
478
|
+
const matches = await findMatchingBins(packages, "missing-pkg");
|
|
479
|
+
|
|
480
|
+
// Assert
|
|
481
|
+
expect(matches).toHaveLength(0);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("should not warn when package.json file is not found", async () => {
|
|
485
|
+
// Arrange
|
|
486
|
+
const packages: PackageInfo[] = [
|
|
487
|
+
{
|
|
488
|
+
name: "missing-pkg",
|
|
489
|
+
path: "/test/path",
|
|
490
|
+
version: "1.0.0",
|
|
491
|
+
},
|
|
492
|
+
];
|
|
493
|
+
|
|
494
|
+
const enoentError = Object.assign(
|
|
495
|
+
new Error("ENOENT: no such file or directory"),
|
|
496
|
+
{code: "ENOENT"},
|
|
497
|
+
);
|
|
498
|
+
vi.mocked(fs.readFile).mockRejectedValue(enoentError);
|
|
499
|
+
|
|
500
|
+
// Act
|
|
501
|
+
await findMatchingBins(packages, "missing-pkg");
|
|
502
|
+
|
|
503
|
+
// Assert
|
|
504
|
+
expect(console.warn).not.toHaveBeenCalled();
|
|
505
|
+
});
|
|
506
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import {afterEach, beforeEach, describe, expect, it} from "vitest";
|
|
4
|
+
import {HandledError} from "../errors";
|
|
5
|
+
import {findWorkspaceRoot} from "../find-workspace-root";
|
|
6
|
+
|
|
7
|
+
describe("findWorkspaceRoot", () => {
|
|
8
|
+
const testDir = path.join("/tmp", "test-workspace-root");
|
|
9
|
+
const workspaceDir = path.join(testDir, "workspace");
|
|
10
|
+
const nestedDir = path.join(workspaceDir, "nested", "deep");
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
// Create test directory structure
|
|
14
|
+
await fs.mkdir(nestedDir, {recursive: true});
|
|
15
|
+
await fs.writeFile(
|
|
16
|
+
path.join(workspaceDir, "pnpm-workspace.yaml"),
|
|
17
|
+
"packages:\n - 'packages/*'\n",
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
// Clean up
|
|
23
|
+
await fs.rm(testDir, {recursive: true, force: true});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should find workspace root from nested directory", async () => {
|
|
27
|
+
// Arrange
|
|
28
|
+
const searchDir = nestedDir;
|
|
29
|
+
|
|
30
|
+
// Act
|
|
31
|
+
const result = await findWorkspaceRoot(searchDir);
|
|
32
|
+
|
|
33
|
+
// Assert
|
|
34
|
+
expect(result).toBe(workspaceDir);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should find workspace root from workspace directory itself", async () => {
|
|
38
|
+
// Arrange
|
|
39
|
+
const searchDir = workspaceDir;
|
|
40
|
+
|
|
41
|
+
// Act
|
|
42
|
+
const result = await findWorkspaceRoot(searchDir);
|
|
43
|
+
|
|
44
|
+
// Assert
|
|
45
|
+
expect(result).toBe(workspaceDir);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should throw HandledError when not in a workspace", async () => {
|
|
49
|
+
// Arrange
|
|
50
|
+
const nonWorkspaceDir = path.join(testDir, "non-workspace");
|
|
51
|
+
await fs.mkdir(nonWorkspaceDir, {recursive: true});
|
|
52
|
+
|
|
53
|
+
// Act
|
|
54
|
+
const underTest = () => findWorkspaceRoot(nonWorkspaceDir);
|
|
55
|
+
|
|
56
|
+
// Assert
|
|
57
|
+
await expect(underTest).rejects.toThrow(HandledError);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should provide meaningful error message when not in a workspace", async () => {
|
|
61
|
+
// Arrange
|
|
62
|
+
const nonWorkspaceDir = path.join(testDir, "non-workspace");
|
|
63
|
+
await fs.mkdir(nonWorkspaceDir, {recursive: true});
|
|
64
|
+
|
|
65
|
+
// Act
|
|
66
|
+
const underTest = () => findWorkspaceRoot(nonWorkspaceDir);
|
|
67
|
+
|
|
68
|
+
// Assert
|
|
69
|
+
await expect(underTest).rejects.toThrow(
|
|
70
|
+
"Could not find workspace root",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {describe, expect, it} from "vitest";
|
|
2
|
+
import {isNodeExecutable} from "../is-node-executable";
|
|
3
|
+
|
|
4
|
+
describe("isNodeExecutable", () => {
|
|
5
|
+
it("should return true for .js files", () => {
|
|
6
|
+
// Arrange
|
|
7
|
+
const binPath = "/path/to/script.js";
|
|
8
|
+
|
|
9
|
+
// Act
|
|
10
|
+
const result = isNodeExecutable(binPath);
|
|
11
|
+
|
|
12
|
+
// Assert
|
|
13
|
+
expect(result).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should return true for .mjs files", () => {
|
|
17
|
+
// Arrange
|
|
18
|
+
const binPath = "/path/to/script.mjs";
|
|
19
|
+
|
|
20
|
+
// Act
|
|
21
|
+
const result = isNodeExecutable(binPath);
|
|
22
|
+
|
|
23
|
+
// Assert
|
|
24
|
+
expect(result).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should return true for .cjs files", () => {
|
|
28
|
+
// Arrange
|
|
29
|
+
const binPath = "/path/to/script.cjs";
|
|
30
|
+
|
|
31
|
+
// Act
|
|
32
|
+
const result = isNodeExecutable(binPath);
|
|
33
|
+
|
|
34
|
+
// Assert
|
|
35
|
+
expect(result).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should return true for .JS files (case-insensitive)", () => {
|
|
39
|
+
// Arrange
|
|
40
|
+
const binPath = "/path/to/script.JS";
|
|
41
|
+
|
|
42
|
+
// Act
|
|
43
|
+
const result = isNodeExecutable(binPath);
|
|
44
|
+
|
|
45
|
+
// Assert
|
|
46
|
+
expect(result).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should return true for .Js files (case-insensitive)", () => {
|
|
50
|
+
// Arrange
|
|
51
|
+
const binPath = "/path/to/script.Js";
|
|
52
|
+
|
|
53
|
+
// Act
|
|
54
|
+
const result = isNodeExecutable(binPath);
|
|
55
|
+
|
|
56
|
+
// Assert
|
|
57
|
+
expect(result).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should return true for .MJS files (case-insensitive)", () => {
|
|
61
|
+
// Arrange
|
|
62
|
+
const binPath = "/path/to/script.MJS";
|
|
63
|
+
|
|
64
|
+
// Act
|
|
65
|
+
const result = isNodeExecutable(binPath);
|
|
66
|
+
|
|
67
|
+
// Assert
|
|
68
|
+
expect(result).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should return true for .CJS files (case-insensitive)", () => {
|
|
72
|
+
// Arrange
|
|
73
|
+
const binPath = "/path/to/script.CJS";
|
|
74
|
+
|
|
75
|
+
// Act
|
|
76
|
+
const result = isNodeExecutable(binPath);
|
|
77
|
+
|
|
78
|
+
// Assert
|
|
79
|
+
expect(result).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should return true for .Mjs files (case-insensitive)", () => {
|
|
83
|
+
// Arrange
|
|
84
|
+
const binPath = "/path/to/script.Mjs";
|
|
85
|
+
|
|
86
|
+
// Act
|
|
87
|
+
const result = isNodeExecutable(binPath);
|
|
88
|
+
|
|
89
|
+
// Assert
|
|
90
|
+
expect(result).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should return false for files with no extension", () => {
|
|
94
|
+
// Arrange
|
|
95
|
+
const binPath = "/path/to/script";
|
|
96
|
+
|
|
97
|
+
// Act
|
|
98
|
+
const result = isNodeExecutable(binPath);
|
|
99
|
+
|
|
100
|
+
// Assert
|
|
101
|
+
expect(result).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should return false for .sh files", () => {
|
|
105
|
+
// Arrange
|
|
106
|
+
const binPath = "/path/to/script.sh";
|
|
107
|
+
|
|
108
|
+
// Act
|
|
109
|
+
const result = isNodeExecutable(binPath);
|
|
110
|
+
|
|
111
|
+
// Assert
|
|
112
|
+
expect(result).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should return false for files with .js in the middle (e.g. script.js.bak)", () => {
|
|
116
|
+
// Arrange
|
|
117
|
+
const binPath = "/path/to/script.js.bak";
|
|
118
|
+
|
|
119
|
+
// Act
|
|
120
|
+
const result = isNodeExecutable(binPath);
|
|
121
|
+
|
|
122
|
+
// Assert
|
|
123
|
+
expect(result).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
});
|