@monorepolint/rules 0.6.0-alpha.5 → 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.
Files changed (107) hide show
  1. package/.turbo/turbo-clean.log +1 -1
  2. package/.turbo/turbo-compile-typescript.log +1 -1
  3. package/.turbo/turbo-lint.log +1 -1
  4. package/.turbo/turbo-test.log +446 -94
  5. package/.turbo/turbo-transpile-typescript.log +5 -5
  6. package/CHANGELOG.md +102 -0
  7. package/build/js/index.js +413 -368
  8. package/build/js/index.js.map +1 -1
  9. package/build/tsconfig.tsbuildinfo +1 -1
  10. package/build/types/REMOVE.d.ts +2 -0
  11. package/build/types/REMOVE.d.ts.map +1 -0
  12. package/build/types/__tests__/alphabeticalDependencies.spec.d.ts +8 -0
  13. package/build/types/__tests__/alphabeticalDependencies.spec.d.ts.map +1 -0
  14. package/build/types/__tests__/forceError.spec.d.ts +8 -0
  15. package/build/types/__tests__/forceError.spec.d.ts.map +1 -0
  16. package/build/types/__tests__/oncePerPackage.spec.d.ts +8 -0
  17. package/build/types/__tests__/oncePerPackage.spec.d.ts.map +1 -0
  18. package/build/types/__tests__/standardTsconfig.spec.d.ts +8 -0
  19. package/build/types/__tests__/standardTsconfig.spec.d.ts.map +1 -0
  20. package/build/types/bannedDependencies.d.ts +9 -33
  21. package/build/types/bannedDependencies.d.ts.map +1 -1
  22. package/build/types/consistentDependencies.d.ts +6 -6
  23. package/build/types/consistentDependencies.d.ts.map +1 -1
  24. package/build/types/consistentVersions.d.ts +6 -10
  25. package/build/types/consistentVersions.d.ts.map +1 -1
  26. package/build/types/fileContents.d.ts +3 -2
  27. package/build/types/fileContents.d.ts.map +1 -1
  28. package/build/types/index.d.ts +1 -0
  29. package/build/types/index.d.ts.map +1 -1
  30. package/build/types/mustSatisfyPeerDependencies.d.ts +12 -190
  31. package/build/types/mustSatisfyPeerDependencies.d.ts.map +1 -1
  32. package/build/types/nestedWorkspaces.d.ts +2 -2
  33. package/build/types/nestedWorkspaces.d.ts.map +1 -1
  34. package/build/types/oncePerPackage.d.ts +6 -6
  35. package/build/types/oncePerPackage.d.ts.map +1 -1
  36. package/build/types/packageEntry.d.ts +11 -33
  37. package/build/types/packageEntry.d.ts.map +1 -1
  38. package/build/types/packageOrder.d.ts +2 -1
  39. package/build/types/packageOrder.d.ts.map +1 -1
  40. package/build/types/packageScript.d.ts +13 -22
  41. package/build/types/packageScript.d.ts.map +1 -1
  42. package/build/types/requireDependency.d.ts +5 -20
  43. package/build/types/requireDependency.d.ts.map +1 -1
  44. package/build/types/standardTsconfig.d.ts +12 -19
  45. package/build/types/standardTsconfig.d.ts.map +1 -1
  46. package/build/types/util/zodSchemas.d.ts +14 -0
  47. package/build/types/util/zodSchemas.d.ts.map +1 -0
  48. package/coverage/block-navigation.js +1 -1
  49. package/coverage/clover.xml +1454 -1442
  50. package/coverage/coverage-final.json +21 -19
  51. package/coverage/index.html +27 -27
  52. package/coverage/sorter.js +21 -7
  53. package/coverage/src/REMOVE.ts.html +88 -0
  54. package/coverage/src/alphabeticalDependencies.ts.html +34 -34
  55. package/coverage/src/alphabeticalScripts.ts.html +5 -5
  56. package/coverage/src/bannedDependencies.ts.html +20 -53
  57. package/coverage/src/consistentDependencies.ts.html +20 -14
  58. package/coverage/src/consistentVersions.ts.html +330 -183
  59. package/coverage/src/fileContents.ts.html +223 -88
  60. package/coverage/src/forceError.ts.html +60 -60
  61. package/coverage/src/index.html +127 -112
  62. package/coverage/src/index.ts.html +11 -5
  63. package/coverage/src/mustSatisfyPeerDependencies.ts.html +15 -501
  64. package/coverage/src/nestedWorkspaces.ts.html +5 -5
  65. package/coverage/src/oncePerPackage.ts.html +59 -59
  66. package/coverage/src/packageEntry.ts.html +121 -91
  67. package/coverage/src/packageOrder.ts.html +44 -14
  68. package/coverage/src/packageScript.ts.html +235 -88
  69. package/coverage/src/requireDependency.ts.html +241 -82
  70. package/coverage/src/standardTsconfig.ts.html +193 -193
  71. package/coverage/src/util/checkAlpha.ts.html +40 -40
  72. package/coverage/src/util/createRuleFactory.ts.html +19 -19
  73. package/coverage/src/util/index.html +30 -15
  74. package/coverage/src/util/makeDirectory.ts.html +11 -11
  75. package/coverage/src/util/packageDependencyGraphService.ts.html +1 -1
  76. package/coverage/src/util/zodSchemas.ts.html +130 -0
  77. package/package.json +14 -14
  78. package/src/REMOVE.ts +1 -0
  79. package/src/__tests__/alphabeticalDependencies.spec.ts +102 -0
  80. package/src/__tests__/alphabeticalScripts.spec.ts +18 -0
  81. package/src/__tests__/bannedDependencies.spec.ts +49 -0
  82. package/src/__tests__/consistentDependencies.spec.ts +23 -0
  83. package/src/__tests__/consistentVersions.spec.ts +142 -0
  84. package/src/__tests__/fileContents.spec.ts +348 -0
  85. package/src/__tests__/forceError.spec.ts +70 -0
  86. package/src/__tests__/mustSatisfyPeerDependencies.spec.ts +44 -0
  87. package/src/__tests__/nestedWorkspaces.spec.ts +14 -0
  88. package/src/__tests__/oncePerPackage.spec.ts +75 -0
  89. package/src/__tests__/packageEntry.spec.ts +177 -0
  90. package/src/__tests__/packageOrder.spec.ts +22 -0
  91. package/src/__tests__/packageScript.spec.ts +549 -0
  92. package/src/__tests__/requireDependency.spec.ts +259 -2
  93. package/src/__tests__/standardTsconfig.spec.ts +91 -0
  94. package/src/bannedDependencies.ts +14 -25
  95. package/src/consistentDependencies.ts +10 -8
  96. package/src/consistentVersions.ts +132 -83
  97. package/src/fileContents.ts +80 -35
  98. package/src/index.ts +2 -0
  99. package/src/mustSatisfyPeerDependencies.ts +10 -172
  100. package/src/nestedWorkspaces.ts +4 -4
  101. package/src/oncePerPackage.ts +6 -6
  102. package/src/packageEntry.ts +60 -50
  103. package/src/packageOrder.ts +19 -9
  104. package/src/packageScript.ts +67 -18
  105. package/src/requireDependency.ts +84 -31
  106. package/src/standardTsconfig.ts +26 -26
  107. 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
+ });