@monorepolint/rules 0.6.0-alpha.4 → 0.6.0-alpha.6
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/.turbo/turbo-clean.log +1 -1
- package/.turbo/turbo-compile-typescript.log +1 -1
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +443 -92
- package/.turbo/turbo-transpile-typescript.log +5 -5
- package/CHANGELOG.md +112 -0
- package/build/js/index.js +413 -368
- package/build/js/index.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/types/REMOVE.d.ts +2 -0
- package/build/types/REMOVE.d.ts.map +1 -0
- package/build/types/__tests__/alphabeticalDependencies.spec.d.ts +8 -0
- package/build/types/__tests__/alphabeticalDependencies.spec.d.ts.map +1 -0
- package/build/types/__tests__/forceError.spec.d.ts +8 -0
- package/build/types/__tests__/forceError.spec.d.ts.map +1 -0
- package/build/types/__tests__/oncePerPackage.spec.d.ts +8 -0
- package/build/types/__tests__/oncePerPackage.spec.d.ts.map +1 -0
- package/build/types/__tests__/standardTsconfig.spec.d.ts +8 -0
- package/build/types/__tests__/standardTsconfig.spec.d.ts.map +1 -0
- package/build/types/bannedDependencies.d.ts +9 -33
- package/build/types/bannedDependencies.d.ts.map +1 -1
- package/build/types/consistentDependencies.d.ts +6 -6
- package/build/types/consistentDependencies.d.ts.map +1 -1
- package/build/types/consistentVersions.d.ts +6 -10
- package/build/types/consistentVersions.d.ts.map +1 -1
- package/build/types/fileContents.d.ts +3 -2
- package/build/types/fileContents.d.ts.map +1 -1
- package/build/types/index.d.ts +1 -0
- package/build/types/index.d.ts.map +1 -1
- package/build/types/mustSatisfyPeerDependencies.d.ts +12 -190
- package/build/types/mustSatisfyPeerDependencies.d.ts.map +1 -1
- package/build/types/nestedWorkspaces.d.ts +2 -2
- package/build/types/nestedWorkspaces.d.ts.map +1 -1
- package/build/types/oncePerPackage.d.ts +6 -6
- package/build/types/oncePerPackage.d.ts.map +1 -1
- package/build/types/packageEntry.d.ts +11 -33
- package/build/types/packageEntry.d.ts.map +1 -1
- package/build/types/packageOrder.d.ts +2 -1
- package/build/types/packageOrder.d.ts.map +1 -1
- package/build/types/packageScript.d.ts +13 -22
- package/build/types/packageScript.d.ts.map +1 -1
- package/build/types/requireDependency.d.ts +5 -20
- package/build/types/requireDependency.d.ts.map +1 -1
- package/build/types/standardTsconfig.d.ts +12 -19
- package/build/types/standardTsconfig.d.ts.map +1 -1
- package/build/types/util/zodSchemas.d.ts +14 -0
- package/build/types/util/zodSchemas.d.ts.map +1 -0
- package/coverage/block-navigation.js +1 -1
- package/coverage/clover.xml +1420 -1452
- package/coverage/coverage-final.json +21 -19
- package/coverage/index.html +27 -27
- package/coverage/sorter.js +21 -7
- package/coverage/src/REMOVE.ts.html +88 -0
- package/coverage/src/alphabeticalDependencies.ts.html +15 -15
- package/coverage/src/alphabeticalScripts.ts.html +5 -5
- package/coverage/src/bannedDependencies.ts.html +20 -53
- package/coverage/src/consistentDependencies.ts.html +20 -14
- package/coverage/src/consistentVersions.ts.html +330 -183
- package/coverage/src/fileContents.ts.html +223 -88
- package/coverage/src/forceError.ts.html +31 -31
- package/coverage/src/index.html +104 -89
- package/coverage/src/index.ts.html +11 -5
- package/coverage/src/mustSatisfyPeerDependencies.ts.html +15 -501
- package/coverage/src/nestedWorkspaces.ts.html +5 -5
- package/coverage/src/oncePerPackage.ts.html +31 -31
- package/coverage/src/packageEntry.ts.html +121 -91
- package/coverage/src/packageOrder.ts.html +44 -14
- package/coverage/src/packageScript.ts.html +235 -88
- package/coverage/src/requireDependency.ts.html +241 -82
- package/coverage/src/standardTsconfig.ts.html +212 -212
- package/coverage/src/util/checkAlpha.ts.html +40 -40
- package/coverage/src/util/createRuleFactory.ts.html +19 -19
- package/coverage/src/util/index.html +30 -15
- package/coverage/src/util/makeDirectory.ts.html +11 -11
- package/coverage/src/util/packageDependencyGraphService.ts.html +1 -1
- package/coverage/src/util/zodSchemas.ts.html +130 -0
- package/package.json +15 -15
- package/src/REMOVE.ts +1 -0
- package/src/__tests__/alphabeticalDependencies.spec.ts +102 -0
- package/src/__tests__/alphabeticalScripts.spec.ts +18 -0
- package/src/__tests__/bannedDependencies.spec.ts +49 -0
- package/src/__tests__/consistentDependencies.spec.ts +23 -0
- package/src/__tests__/consistentVersions.spec.ts +142 -0
- package/src/__tests__/fileContents.spec.ts +348 -0
- package/src/__tests__/forceError.spec.ts +70 -0
- package/src/__tests__/mustSatisfyPeerDependencies.spec.ts +44 -0
- package/src/__tests__/nestedWorkspaces.spec.ts +14 -0
- package/src/__tests__/oncePerPackage.spec.ts +75 -0
- package/src/__tests__/packageEntry.spec.ts +177 -0
- package/src/__tests__/packageOrder.spec.ts +22 -0
- package/src/__tests__/packageScript.spec.ts +549 -0
- package/src/__tests__/requireDependency.spec.ts +259 -2
- package/src/__tests__/standardTsconfig.spec.ts +91 -0
- package/src/bannedDependencies.ts +14 -25
- package/src/consistentDependencies.ts +10 -8
- package/src/consistentVersions.ts +132 -83
- package/src/fileContents.ts +80 -35
- package/src/index.ts +2 -0
- package/src/mustSatisfyPeerDependencies.ts +10 -172
- package/src/nestedWorkspaces.ts +4 -4
- package/src/oncePerPackage.ts +6 -6
- package/src/packageEntry.ts +60 -50
- package/src/packageOrder.ts +19 -9
- package/src/packageScript.ts +67 -18
- package/src/requireDependency.ts +84 -31
- package/src/standardTsconfig.ts +26 -26
- package/src/util/zodSchemas.ts +15 -0
|
@@ -215,4 +215,53 @@ describe("bannedDependencies", () => {
|
|
|
215
215
|
checkAndSpy({ bannedTransitiveDependencies: ["ccc", "ddd"] }).addErrorSpy,
|
|
216
216
|
).toHaveBeenCalledTimes(2);
|
|
217
217
|
});
|
|
218
|
+
|
|
219
|
+
describe("Options Validation", () => {
|
|
220
|
+
it("should accept valid options", () => {
|
|
221
|
+
const ruleModule = bannedDependencies({ options: { bannedDependencies: ["lodash"] } });
|
|
222
|
+
|
|
223
|
+
// Array format
|
|
224
|
+
expect(() => ruleModule.validateOptions({ bannedDependencies: ["lodash", "moment"] })).not
|
|
225
|
+
.toThrow();
|
|
226
|
+
|
|
227
|
+
// Object format with glob and exact
|
|
228
|
+
expect(() =>
|
|
229
|
+
ruleModule.validateOptions({
|
|
230
|
+
bannedDependencies: {
|
|
231
|
+
glob: ["@types/*"],
|
|
232
|
+
exact: ["lodash"],
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
).not.toThrow();
|
|
236
|
+
|
|
237
|
+
// With transitive dependencies
|
|
238
|
+
expect(() =>
|
|
239
|
+
ruleModule.validateOptions({
|
|
240
|
+
bannedDependencies: ["lodash"],
|
|
241
|
+
bannedTransitiveDependencies: ["moment"],
|
|
242
|
+
})
|
|
243
|
+
).not.toThrow();
|
|
244
|
+
|
|
245
|
+
// Empty arrays
|
|
246
|
+
expect(() => ruleModule.validateOptions({ bannedDependencies: [] })).not.toThrow();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("should reject invalid options", () => {
|
|
250
|
+
const ruleModule = bannedDependencies({ options: { bannedDependencies: ["lodash"] } });
|
|
251
|
+
|
|
252
|
+
expect(() =>
|
|
253
|
+
ruleModule.validateOptions({
|
|
254
|
+
// @ts-expect-error testing invalid input
|
|
255
|
+
bannedDependencies: "string",
|
|
256
|
+
})
|
|
257
|
+
).toThrow();
|
|
258
|
+
// Test a case that should definitely fail - non-object value
|
|
259
|
+
expect(() =>
|
|
260
|
+
ruleModule.validateOptions({
|
|
261
|
+
// @ts-expect-error testing invalid input
|
|
262
|
+
bannedDependencies: 123,
|
|
263
|
+
})
|
|
264
|
+
).toThrow();
|
|
265
|
+
});
|
|
266
|
+
});
|
|
218
267
|
});
|
|
@@ -145,4 +145,27 @@ describe("consistentDependencies", () => {
|
|
|
145
145
|
});
|
|
146
146
|
expect(ignored.addErrorSpy).toHaveBeenCalledTimes(0);
|
|
147
147
|
});
|
|
148
|
+
|
|
149
|
+
describe("Options Validation", () => {
|
|
150
|
+
it("should accept valid options", () => {
|
|
151
|
+
const ruleModule = consistentDependencies({ options: undefined });
|
|
152
|
+
|
|
153
|
+
expect(() => ruleModule.validateOptions(undefined)).not.toThrow();
|
|
154
|
+
expect(() => ruleModule.validateOptions({ ignoredDependencies: ["react", "react-dom"] })).not
|
|
155
|
+
.toThrow();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should reject invalid options", () => {
|
|
159
|
+
const ruleModule = consistentDependencies({ options: undefined });
|
|
160
|
+
|
|
161
|
+
// @ts-expect-error testing invalid input
|
|
162
|
+
expect(() => ruleModule.validateOptions({ ignoredDependencies: undefined })).toThrow();
|
|
163
|
+
// @ts-expect-error testing invalid input
|
|
164
|
+
expect(() => ruleModule.validateOptions({})).toThrow(); // Missing ignoredDependencies property
|
|
165
|
+
// @ts-expect-error testing invalid input
|
|
166
|
+
expect(() => ruleModule.validateOptions({ ignoredDependencies: "string" })).toThrow();
|
|
167
|
+
// @ts-expect-error testing invalid input
|
|
168
|
+
expect(() => ruleModule.validateOptions({ ignoredDependencies: [123] })).toThrow();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
148
171
|
});
|
|
@@ -275,4 +275,146 @@ describe("consistentVersions", () => {
|
|
|
275
275
|
);
|
|
276
276
|
});
|
|
277
277
|
});
|
|
278
|
+
|
|
279
|
+
describe("Protocol Version Strings", () => {
|
|
280
|
+
it("should support catalog: version strings", async () => {
|
|
281
|
+
const { addErrorSpy, check, host } = makeWorkspace();
|
|
282
|
+
const testPackageJsonWithCatalog = {
|
|
283
|
+
name: "test",
|
|
284
|
+
dependencies: {
|
|
285
|
+
"@catalog/lib": "catalog:",
|
|
286
|
+
normalLib: "^1.2.3",
|
|
287
|
+
},
|
|
288
|
+
devDependencies: {
|
|
289
|
+
"@catalog/lib-dev": "catalog:",
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
addPackageJson(host, "./package.json", testPackageJsonWithCatalog);
|
|
294
|
+
|
|
295
|
+
// Should not throw an error when using catalog: versions
|
|
296
|
+
expect(() => {
|
|
297
|
+
check({
|
|
298
|
+
matchDependencyVersions: {
|
|
299
|
+
"@catalog/lib": "catalog:",
|
|
300
|
+
"@catalog/lib-dev": "catalog:",
|
|
301
|
+
normalLib: "^1.2.3",
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
}).not.toThrow();
|
|
305
|
+
|
|
306
|
+
expect(addErrorSpy).toHaveBeenCalledTimes(0);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("should detect mismatched catalog: versions", async () => {
|
|
310
|
+
const { addErrorSpy, check, host } = makeWorkspace();
|
|
311
|
+
const testPackageJsonWithWrongCatalog = {
|
|
312
|
+
name: "test",
|
|
313
|
+
dependencies: {
|
|
314
|
+
"@catalog/lib": "^1.2.3", // Should be catalog:
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
addPackageJson(host, "./package.json", testPackageJsonWithWrongCatalog);
|
|
319
|
+
|
|
320
|
+
check({
|
|
321
|
+
matchDependencyVersions: {
|
|
322
|
+
"@catalog/lib": "catalog:",
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
expect(addErrorSpy).toHaveBeenCalledTimes(1);
|
|
327
|
+
expect(addErrorSpy).toHaveBeenCalledWith(
|
|
328
|
+
expect.objectContaining({
|
|
329
|
+
message: expect.stringContaining(
|
|
330
|
+
"Expected dependency on @catalog/lib to match version defined in monorepolint configuration 'catalog:', got '^1.2.3' instead",
|
|
331
|
+
),
|
|
332
|
+
}),
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("should support workspace: and other protocol version strings", async () => {
|
|
337
|
+
const { addErrorSpy, check, host } = makeWorkspace();
|
|
338
|
+
const testPackageJsonWithProtocol = {
|
|
339
|
+
name: "test",
|
|
340
|
+
dependencies: {
|
|
341
|
+
"@workspace/lib": "workspace:",
|
|
342
|
+
"@link/lib": "link:",
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
addPackageJson(host, "./package.json", testPackageJsonWithProtocol);
|
|
347
|
+
|
|
348
|
+
expect(() => {
|
|
349
|
+
check({
|
|
350
|
+
matchDependencyVersions: {
|
|
351
|
+
"@workspace/lib": "workspace:",
|
|
352
|
+
"@link/lib": "link:",
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
}).not.toThrow();
|
|
356
|
+
|
|
357
|
+
expect(addErrorSpy).toHaveBeenCalledTimes(0);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("should support arrays with mixed protocol and regular versions", async () => {
|
|
361
|
+
const { addErrorSpy, check, host } = makeWorkspace();
|
|
362
|
+
const testPackageJsonWithMixed = {
|
|
363
|
+
name: "test",
|
|
364
|
+
dependencies: {
|
|
365
|
+
"@mixed/lib": "catalog:",
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
addPackageJson(host, "./package.json", testPackageJsonWithMixed);
|
|
370
|
+
|
|
371
|
+
expect(() => {
|
|
372
|
+
check({
|
|
373
|
+
matchDependencyVersions: {
|
|
374
|
+
"@mixed/lib": ["catalog:", "^1.2.3", "workspace:"],
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
}).not.toThrow();
|
|
378
|
+
|
|
379
|
+
expect(addErrorSpy).toHaveBeenCalledTimes(0);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe("Options Validation", () => {
|
|
384
|
+
it("should accept valid options", () => {
|
|
385
|
+
const ruleModule = consistentVersions({
|
|
386
|
+
options: { matchDependencyVersions: { "react": "^18.0.0" } },
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
expect(() =>
|
|
390
|
+
ruleModule.validateOptions({
|
|
391
|
+
matchDependencyVersions: {
|
|
392
|
+
"react": "^18.0.0",
|
|
393
|
+
"lodash": ["^4.17.0", "^4.18.0"],
|
|
394
|
+
},
|
|
395
|
+
})
|
|
396
|
+
).not.toThrow();
|
|
397
|
+
|
|
398
|
+
expect(() =>
|
|
399
|
+
ruleModule.validateOptions({
|
|
400
|
+
matchDependencyVersions: {},
|
|
401
|
+
})
|
|
402
|
+
).not.toThrow();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("should reject invalid options", () => {
|
|
406
|
+
const ruleModule = consistentVersions({
|
|
407
|
+
options: { matchDependencyVersions: { "react": "^18.0.0" } },
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// @ts-expect-error testing invalid input
|
|
411
|
+
expect(() => ruleModule.validateOptions({})).toThrow();
|
|
412
|
+
expect(() =>
|
|
413
|
+
ruleModule.validateOptions({
|
|
414
|
+
// @ts-expect-error testing invalid input
|
|
415
|
+
matchDependencyVersions: { "react": 123 },
|
|
416
|
+
})
|
|
417
|
+
).toThrow();
|
|
418
|
+
});
|
|
419
|
+
});
|
|
278
420
|
});
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { AddErrorOptions, Failure } from "@monorepolint/config";
|
|
10
10
|
import { beforeEach, describe, expect, it, MockInstance, vi } from "vitest";
|
|
11
11
|
import { fileContents } from "../fileContents.js";
|
|
12
|
+
import { REMOVE } from "../REMOVE.js";
|
|
12
13
|
import { createTestingWorkspace, HOST_FACTORIES, TestingWorkspace } from "./utils.js";
|
|
13
14
|
|
|
14
15
|
const EXPECTED_FOO_FILE = "hello world";
|
|
@@ -97,5 +98,352 @@ describe.each(HOST_FACTORIES)("fileContents ($name)", (hostFactory) => {
|
|
|
97
98
|
|
|
98
99
|
expect(workspace.readFile("nested/foo.txt")).toEqual(EXPECTED_FOO_FILE);
|
|
99
100
|
});
|
|
101
|
+
|
|
102
|
+
it("deletes existing file when template is REMOVE", async () => {
|
|
103
|
+
// First create a file to delete
|
|
104
|
+
workspace.writeFile("to-delete.txt", "This file should be deleted");
|
|
105
|
+
|
|
106
|
+
// Verify the file exists before deletion
|
|
107
|
+
expect(workspace.readFile("to-delete.txt")).toEqual("This file should be deleted");
|
|
108
|
+
|
|
109
|
+
await fileContents({
|
|
110
|
+
options: {
|
|
111
|
+
file: "to-delete.txt",
|
|
112
|
+
template: REMOVE,
|
|
113
|
+
},
|
|
114
|
+
}).check(workspace.context);
|
|
115
|
+
|
|
116
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
117
|
+
|
|
118
|
+
const failure: Failure = spy.mock.calls[0][0];
|
|
119
|
+
expect(failure).toMatchObject(
|
|
120
|
+
workspace.failureMatcher({
|
|
121
|
+
file: "to-delete.txt",
|
|
122
|
+
hasFixer: true,
|
|
123
|
+
message: "File should not exist",
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Verify the file has been deleted after running the fixer
|
|
128
|
+
expect(() => workspace.readFile("to-delete.txt")).toThrow();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("handles REMOVE for non-existent file", async () => {
|
|
132
|
+
// Test deleting a file that doesn't exist - with the fix, this should not report an error
|
|
133
|
+
// because actualContent (REMOVE) === expectedContent (REMOVE)
|
|
134
|
+
await fileContents({
|
|
135
|
+
options: {
|
|
136
|
+
file: "non-existent.txt",
|
|
137
|
+
template: REMOVE,
|
|
138
|
+
},
|
|
139
|
+
}).check(workspace.context);
|
|
140
|
+
|
|
141
|
+
// Should not add any errors since the desired state (file not existing) is already achieved
|
|
142
|
+
expect(spy).toHaveBeenCalledTimes(0);
|
|
143
|
+
|
|
144
|
+
// Verify the file still doesn't exist
|
|
145
|
+
// Both host implementations should now throw when trying to read non-existent files
|
|
146
|
+
expect(() => workspace.readFile("non-existent.txt")).toThrow();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("Generator function error handling", () => {
|
|
151
|
+
let workspace: TestingWorkspace;
|
|
152
|
+
let spy: MockInstance<(opts: AddErrorOptions) => void>;
|
|
153
|
+
|
|
154
|
+
beforeEach(async () => {
|
|
155
|
+
workspace = await createTestingWorkspace({
|
|
156
|
+
fixFlag: true,
|
|
157
|
+
host: hostFactory.make(),
|
|
158
|
+
});
|
|
159
|
+
spy = vi.spyOn(workspace.context, "addError");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("handles generator function that throws an exception", async () => {
|
|
163
|
+
const errorMessage = "Generator function failed";
|
|
164
|
+
const throwingGenerator = () => {
|
|
165
|
+
throw new Error(errorMessage);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
await fileContents({
|
|
169
|
+
options: {
|
|
170
|
+
file: "test.txt",
|
|
171
|
+
generator: throwingGenerator,
|
|
172
|
+
},
|
|
173
|
+
}).check(workspace.context);
|
|
174
|
+
|
|
175
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
176
|
+
const failure: Failure = spy.mock.calls[0][0];
|
|
177
|
+
expect(failure).toMatchObject(
|
|
178
|
+
workspace.failureMatcher({
|
|
179
|
+
file: "test.txt",
|
|
180
|
+
hasFixer: false, // This should be an unfixable error
|
|
181
|
+
message: `Generator function failed: ${errorMessage}`,
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
expect(failure.longMessage).toContain(
|
|
185
|
+
`The generator function for file "test.txt" threw an error:`,
|
|
186
|
+
);
|
|
187
|
+
expect(failure.longMessage).toContain(errorMessage);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("handles generator function that returns a rejected Promise", async () => {
|
|
191
|
+
const errorMessage = "Async generator failed";
|
|
192
|
+
const rejectingGenerator = () => Promise.reject(new Error(errorMessage));
|
|
193
|
+
|
|
194
|
+
await fileContents({
|
|
195
|
+
options: {
|
|
196
|
+
file: "test.txt",
|
|
197
|
+
generator: rejectingGenerator,
|
|
198
|
+
},
|
|
199
|
+
}).check(workspace.context);
|
|
200
|
+
|
|
201
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
202
|
+
const failure: Failure = spy.mock.calls[0][0];
|
|
203
|
+
expect(failure).toMatchObject(
|
|
204
|
+
workspace.failureMatcher({
|
|
205
|
+
file: "test.txt",
|
|
206
|
+
hasFixer: false, // This should be an unfixable error
|
|
207
|
+
message: `Generator function failed: ${errorMessage}`,
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
expect(failure.longMessage).toContain(
|
|
211
|
+
`The generator function for file "test.txt" threw an error:`,
|
|
212
|
+
);
|
|
213
|
+
expect(failure.longMessage).toContain(errorMessage);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("handles generator function that returns non-string value", async () => {
|
|
217
|
+
const invalidGenerator = () => 123 as any; // Cast to any to bypass TypeScript
|
|
218
|
+
|
|
219
|
+
await fileContents({
|
|
220
|
+
options: {
|
|
221
|
+
file: "test.txt",
|
|
222
|
+
generator: invalidGenerator,
|
|
223
|
+
},
|
|
224
|
+
}).check(workspace.context);
|
|
225
|
+
|
|
226
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
227
|
+
const failure: Failure = spy.mock.calls[0][0];
|
|
228
|
+
expect(failure).toMatchObject(
|
|
229
|
+
workspace.failureMatcher({
|
|
230
|
+
file: "test.txt",
|
|
231
|
+
hasFixer: false, // This should be an unfixable error
|
|
232
|
+
message:
|
|
233
|
+
"Generator function failed: Generator function must return a string or REMOVE, got number",
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
expect(failure.longMessage).toContain(
|
|
237
|
+
`The generator function for file "test.txt" threw an error:`,
|
|
238
|
+
);
|
|
239
|
+
expect(failure.longMessage).toContain(
|
|
240
|
+
"Generator function must return a string or REMOVE, got number",
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("Template file error scenarios", () => {
|
|
246
|
+
let workspace: TestingWorkspace;
|
|
247
|
+
|
|
248
|
+
beforeEach(async () => {
|
|
249
|
+
workspace = await createTestingWorkspace({
|
|
250
|
+
fixFlag: true,
|
|
251
|
+
host: hostFactory.make(),
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("handles non-existent template file", async () => {
|
|
256
|
+
await expect(
|
|
257
|
+
fileContents({
|
|
258
|
+
options: {
|
|
259
|
+
file: "test.txt",
|
|
260
|
+
templateFile: "non-existent-template.txt",
|
|
261
|
+
},
|
|
262
|
+
}).check(workspace.context),
|
|
263
|
+
).rejects.toThrow(); // Should throw when trying to read non-existent template
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("handles template file read permission errors", async () => {
|
|
267
|
+
// Create a file that simulates permission error by using invalid path
|
|
268
|
+
await expect(
|
|
269
|
+
fileContents({
|
|
270
|
+
options: {
|
|
271
|
+
file: "test.txt",
|
|
272
|
+
templateFile: "\0invalid-path/template.txt", // Null byte in path
|
|
273
|
+
},
|
|
274
|
+
}).check(workspace.context),
|
|
275
|
+
).rejects.toThrow(); // Should throw when trying to read invalid path
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("File system permission errors", () => {
|
|
280
|
+
let workspace: TestingWorkspace;
|
|
281
|
+
|
|
282
|
+
beforeEach(async () => {
|
|
283
|
+
workspace = await createTestingWorkspace({
|
|
284
|
+
fixFlag: true,
|
|
285
|
+
host: hostFactory.make(),
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("handles unwritable directory scenarios", async () => {
|
|
290
|
+
const spy = vi.spyOn(workspace.context, "addError");
|
|
291
|
+
|
|
292
|
+
// Try to write to a path with null bytes (invalid on most filesystems)
|
|
293
|
+
// This should properly expect an error to be thrown since null bytes are invalid paths
|
|
294
|
+
await expect(
|
|
295
|
+
fileContents({
|
|
296
|
+
options: {
|
|
297
|
+
file: "\0invalid-dir/test.txt",
|
|
298
|
+
template: "test content",
|
|
299
|
+
},
|
|
300
|
+
}).check(workspace.context),
|
|
301
|
+
).rejects.toThrow(/Invalid file path.*null bytes/);
|
|
302
|
+
|
|
303
|
+
// The spy should not have been called because the error should occur before adding to errors
|
|
304
|
+
expect(spy).not.toHaveBeenCalled();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("gracefully handles directory creation for nested files", async () => {
|
|
308
|
+
const spy = vi.spyOn(workspace.context, "addError");
|
|
309
|
+
|
|
310
|
+
await fileContents({
|
|
311
|
+
options: {
|
|
312
|
+
file: "deeply/nested/path/test.txt",
|
|
313
|
+
template: "nested content",
|
|
314
|
+
},
|
|
315
|
+
}).check(workspace.context);
|
|
316
|
+
|
|
317
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
318
|
+
expect(workspace.readFile("deeply/nested/path/test.txt")).toEqual("nested content");
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe("Edge cases and file operations", () => {
|
|
323
|
+
let workspace: TestingWorkspace;
|
|
324
|
+
let spy: MockInstance<(opts: AddErrorOptions) => void>;
|
|
325
|
+
|
|
326
|
+
beforeEach(async () => {
|
|
327
|
+
workspace = await createTestingWorkspace({
|
|
328
|
+
fixFlag: true,
|
|
329
|
+
host: hostFactory.make(),
|
|
330
|
+
});
|
|
331
|
+
spy = vi.spyOn(workspace.context, "addError");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("handles empty string content correctly", async () => {
|
|
335
|
+
const spy = vi.spyOn(workspace.context, "addError");
|
|
336
|
+
|
|
337
|
+
await fileContents({
|
|
338
|
+
options: {
|
|
339
|
+
file: "empty.txt",
|
|
340
|
+
template: "",
|
|
341
|
+
},
|
|
342
|
+
}).check(workspace.context);
|
|
343
|
+
|
|
344
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
345
|
+
expect(workspace.readFile("empty.txt")).toEqual("");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("handles very long file content", async () => {
|
|
349
|
+
const longContent = "a".repeat(10000);
|
|
350
|
+
await fileContents({
|
|
351
|
+
options: {
|
|
352
|
+
file: "long.txt",
|
|
353
|
+
template: longContent,
|
|
354
|
+
},
|
|
355
|
+
}).check(workspace.context);
|
|
356
|
+
|
|
357
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
358
|
+
expect(workspace.readFile("long.txt")).toEqual(longContent);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("handles file with special characters in content", async () => {
|
|
362
|
+
const specialContent = "Hello\nWorld\t\r\n🚀";
|
|
363
|
+
await fileContents({
|
|
364
|
+
options: {
|
|
365
|
+
file: "special.txt",
|
|
366
|
+
template: specialContent,
|
|
367
|
+
},
|
|
368
|
+
}).check(workspace.context);
|
|
369
|
+
|
|
370
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
371
|
+
expect(workspace.readFile("special.txt")).toEqual(specialContent);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe("Options Validation", () => {
|
|
376
|
+
it("should accept valid options", () => {
|
|
377
|
+
const ruleModule1 = fileContents({
|
|
378
|
+
options: { file: "README.md", generator: () => "Generated content" },
|
|
379
|
+
});
|
|
380
|
+
const ruleModule2 = fileContents({
|
|
381
|
+
options: { file: "LICENSE", template: "MIT License content" },
|
|
382
|
+
});
|
|
383
|
+
const ruleModule3 = fileContents({
|
|
384
|
+
options: { file: ".gitignore", templateFile: "templates/gitignore.txt" },
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// With generator function
|
|
388
|
+
expect(() =>
|
|
389
|
+
ruleModule1.validateOptions({
|
|
390
|
+
file: "README.md",
|
|
391
|
+
generator: () => "Generated content",
|
|
392
|
+
})
|
|
393
|
+
).not.toThrow();
|
|
394
|
+
|
|
395
|
+
// With template string
|
|
396
|
+
expect(() =>
|
|
397
|
+
ruleModule2.validateOptions({
|
|
398
|
+
file: "LICENSE",
|
|
399
|
+
template: "MIT License content",
|
|
400
|
+
})
|
|
401
|
+
).not.toThrow();
|
|
402
|
+
|
|
403
|
+
// With template file
|
|
404
|
+
expect(() =>
|
|
405
|
+
ruleModule3.validateOptions({
|
|
406
|
+
file: ".gitignore",
|
|
407
|
+
templateFile: "templates/gitignore.txt",
|
|
408
|
+
})
|
|
409
|
+
).not.toThrow();
|
|
410
|
+
|
|
411
|
+
// With template undefined (delete file)
|
|
412
|
+
expect(() =>
|
|
413
|
+
ruleModule2.validateOptions({
|
|
414
|
+
file: "temp.txt",
|
|
415
|
+
template: REMOVE,
|
|
416
|
+
})
|
|
417
|
+
).not.toThrow();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("should reject invalid options", () => {
|
|
421
|
+
const ruleModule = fileContents({ options: { file: "test.txt", template: "content" } });
|
|
422
|
+
|
|
423
|
+
// Missing one of generator/template/templateFile
|
|
424
|
+
|
|
425
|
+
expect(() =>
|
|
426
|
+
ruleModule.validateOptions(
|
|
427
|
+
// @ts-expect-error testing invalid input
|
|
428
|
+
{ file: "test.txt" },
|
|
429
|
+
)
|
|
430
|
+
).toThrow();
|
|
431
|
+
|
|
432
|
+
// Multiple sources not allowed by union type structure
|
|
433
|
+
expect(() =>
|
|
434
|
+
ruleModule.validateOptions(
|
|
435
|
+
// @ts-expect-error testing invalid input
|
|
436
|
+
{
|
|
437
|
+
file: "test.txt",
|
|
438
|
+
generator: () => "",
|
|
439
|
+
template: "content",
|
|
440
|
+
},
|
|
441
|
+
)
|
|
442
|
+
).toThrow();
|
|
443
|
+
|
|
444
|
+
// Missing file property
|
|
445
|
+
// @ts-expect-error testing invalid input
|
|
446
|
+
expect(() => ruleModule.validateOptions({ template: "content" })).toThrow();
|
|
447
|
+
});
|
|
100
448
|
});
|
|
101
449
|
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright 2019 Palantir Technologies, Inc.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the MIT license. See LICENSE file in the project root for details.
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Context } from "@monorepolint/config";
|
|
9
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
|
+
import { forceError } from "../forceError.js";
|
|
11
|
+
import { AddErrorSpy, createTestingWorkspace, HOST_FACTORIES, TestingWorkspace } from "./utils.js";
|
|
12
|
+
|
|
13
|
+
describe.each(HOST_FACTORIES)("forceError ($name)", (hostFactory) => {
|
|
14
|
+
let workspace: TestingWorkspace;
|
|
15
|
+
let spy: AddErrorSpy;
|
|
16
|
+
let context: Context;
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
workspace = await createTestingWorkspace({
|
|
20
|
+
fixFlag: false,
|
|
21
|
+
host: hostFactory.make(),
|
|
22
|
+
});
|
|
23
|
+
context = workspace.context;
|
|
24
|
+
spy = vi.spyOn(workspace.context, "addError");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("always adds a default error", () => {
|
|
28
|
+
forceError({}).check(context);
|
|
29
|
+
|
|
30
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
31
|
+
expect(spy).toHaveBeenCalledWith({
|
|
32
|
+
message: "Forced error (often used to debug package selection)",
|
|
33
|
+
file: context.getPackageJsonPath(),
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("adds a custom error message when provided", () => {
|
|
38
|
+
forceError({ options: { customMessage: "Custom test message" } }).check(context);
|
|
39
|
+
|
|
40
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
41
|
+
expect(spy).toHaveBeenCalledWith({
|
|
42
|
+
message: "Custom test message",
|
|
43
|
+
file: context.getPackageJsonPath(),
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("Options Validation", () => {
|
|
48
|
+
it("should accept valid options", () => {
|
|
49
|
+
const ruleModule = forceError({ options: undefined });
|
|
50
|
+
// @ts-expect-error testing invalid input
|
|
51
|
+
expect(() => ruleModule.validateOptions(null)).not.toThrow();
|
|
52
|
+
// @ts-expect-error testing invalid input
|
|
53
|
+
expect(() => ruleModule.validateOptions(undefined)).not.toThrow();
|
|
54
|
+
expect(() => ruleModule.validateOptions({})).not.toThrow();
|
|
55
|
+
expect(() => ruleModule.validateOptions({ customMessage: "test message" })).not.toThrow();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should reject invalid options", () => {
|
|
59
|
+
const ruleModule = forceError({ options: undefined });
|
|
60
|
+
// @ts-expect-error testing invalid input
|
|
61
|
+
expect(() => ruleModule.validateOptions({ customMessage: 123 })).toThrow();
|
|
62
|
+
// @ts-expect-error testing invalid input
|
|
63
|
+
expect(() => ruleModule.validateOptions({ customMessage: "test", extra: "field" })).toThrow();
|
|
64
|
+
// @ts-expect-error testing invalid input
|
|
65
|
+
expect(() => ruleModule.validateOptions("string")).toThrow();
|
|
66
|
+
// @ts-expect-error testing invalid input
|
|
67
|
+
expect(() => ruleModule.validateOptions([])).not.toThrow(); // Arrays pass typeof object check
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|