@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.
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 +443 -92
  5. package/.turbo/turbo-transpile-typescript.log +5 -5
  6. package/CHANGELOG.md +112 -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 +1420 -1452
  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 +15 -15
  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 +31 -31
  61. package/coverage/src/index.html +104 -89
  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 +31 -31
  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 +212 -212
  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 +15 -15
  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
@@ -9,6 +9,7 @@
9
9
  import { Context, Failure } from "@monorepolint/config";
10
10
  import { beforeEach, describe, expect, it, vi } from "vitest";
11
11
  import { packageScript } from "../packageScript.js";
12
+ import { REMOVE } from "../REMOVE.js";
12
13
  import { AddErrorSpy, createTestingWorkspace, HOST_FACTORIES, TestingWorkspace } from "./utils.js";
13
14
 
14
15
  const json = (a: unknown) => JSON.stringify(a, undefined, 2) + "\n";
@@ -252,4 +253,552 @@ describe.each(HOST_FACTORIES)("expectPackageScript ($name)", (hostFactory) => {
252
253
  );
253
254
  });
254
255
  });
256
+
257
+ describe("Missing package.json handling", () => {
258
+ let workspace: TestingWorkspace;
259
+
260
+ beforeEach(async () => {
261
+ workspace = await createTestingWorkspace({
262
+ fixFlag: false,
263
+ host: hostFactory.make(),
264
+ });
265
+ });
266
+
267
+ it("handles gracefully when package.json does not exist", () => {
268
+ // Don't create a package.json file in the current directory
269
+ // Create a child context that points to a directory without package.json
270
+ const childContext = workspace.context.getWorkspaceContext().createChildContext(
271
+ workspace.getFilePath("packages/missing"),
272
+ );
273
+
274
+ expect(() => {
275
+ packageScript({
276
+ options: {
277
+ scripts: {
278
+ build: "tsc",
279
+ },
280
+ },
281
+ }).check(childContext);
282
+ }).toThrow(); // Should throw when trying to get package.json
283
+ });
284
+ });
285
+
286
+ describe("Invalid package.json structure", () => {
287
+ let workspace: TestingWorkspace;
288
+
289
+ beforeEach(async () => {
290
+ workspace = await createTestingWorkspace({
291
+ fixFlag: false,
292
+ host: hostFactory.make(),
293
+ });
294
+ });
295
+
296
+ it("handles package.json with non-object scripts", () => {
297
+ workspace.writeFile(
298
+ "package.json",
299
+ json({
300
+ name: "test-package",
301
+ scripts: "invalid-scripts", // Should be an object
302
+ }),
303
+ );
304
+
305
+ // This actually won't throw - the rule will just add an error for missing scripts block
306
+ // since scripts is not an object, it will be treated as undefined
307
+ const spy = vi.spyOn(workspace.context, "addError");
308
+
309
+ packageScript({
310
+ options: {
311
+ scripts: {
312
+ build: "tsc",
313
+ },
314
+ },
315
+ }).check(workspace.context);
316
+
317
+ // Should add an error because scripts is not a proper object
318
+ expect(spy).toHaveBeenCalled();
319
+ });
320
+
321
+ it("handles malformed JSON in package.json", () => {
322
+ workspace.writeFile("package.json", "{ invalid json }");
323
+
324
+ expect(() => {
325
+ packageScript({
326
+ options: {
327
+ scripts: {
328
+ build: "tsc",
329
+ },
330
+ },
331
+ }).check(workspace.context);
332
+ }).toThrow(); // Should throw when parsing invalid JSON
333
+ });
334
+ });
335
+
336
+ describe("REMOVE symbol usage", () => {
337
+ let workspace: TestingWorkspace;
338
+ let spy: AddErrorSpy;
339
+ let context: Context;
340
+
341
+ beforeEach(async () => {
342
+ workspace = await createTestingWorkspace({
343
+ fixFlag: true,
344
+ host: hostFactory.make(),
345
+ });
346
+ spy = vi.spyOn(workspace.context, "addError");
347
+ context = workspace.context;
348
+ });
349
+
350
+ it("can remove a script using REMOVE in options array", () => {
351
+ workspace.writeFile("package.json", PACKAGE_WITH_SCRIPTS);
352
+
353
+ packageScript({
354
+ options: {
355
+ scripts: {
356
+ [SCRIPT_NAME]: {
357
+ options: ["different-value", REMOVE], // Current value doesn't match either option
358
+ fixValue: REMOVE,
359
+ },
360
+ },
361
+ },
362
+ }).check(context);
363
+
364
+ expect(spy).toHaveBeenCalledTimes(1);
365
+
366
+ const failure: Failure = spy.mock.calls[0][0];
367
+ expect(failure).toMatchObject(
368
+ workspace.failureMatcher({
369
+ file: "package.json",
370
+ hasFixer: true,
371
+ message: expect.stringContaining(
372
+ `Expected standardized script entry for '${SCRIPT_NAME}'`,
373
+ ) as unknown as string,
374
+ }),
375
+ );
376
+
377
+ // Verify script was removed
378
+ expect(JSON.parse(workspace.readFile("package.json")!).scripts).toEqual({});
379
+ });
380
+
381
+ it("handles REMOVE on non-existent script", () => {
382
+ workspace.writeFile("package.json", PACKAGE_WITHOUT_SCRIPTS);
383
+
384
+ packageScript({
385
+ options: {
386
+ scripts: {
387
+ nonExistent: {
388
+ options: ["value", REMOVE],
389
+ fixValue: REMOVE,
390
+ },
391
+ },
392
+ },
393
+ }).check(context);
394
+
395
+ expect(spy).toHaveBeenCalledTimes(1); // Only one for no scripts block - the rule returns early
396
+ });
397
+ });
398
+
399
+ describe("Direct REMOVE syntax", () => {
400
+ let workspace: TestingWorkspace;
401
+ let spy: AddErrorSpy;
402
+ let context: Context;
403
+
404
+ beforeEach(async () => {
405
+ workspace = await createTestingWorkspace({
406
+ fixFlag: true,
407
+ host: hostFactory.make(),
408
+ });
409
+ spy = vi.spyOn(workspace.context, "addError");
410
+ context = workspace.context;
411
+ });
412
+
413
+ it("removes existing script using direct REMOVE syntax", () => {
414
+ workspace.writeFile("package.json", PACKAGE_WITH_SCRIPTS);
415
+
416
+ packageScript({
417
+ options: {
418
+ scripts: {
419
+ [SCRIPT_NAME]: REMOVE,
420
+ },
421
+ },
422
+ }).check(context);
423
+
424
+ expect(spy).toHaveBeenCalledTimes(1);
425
+
426
+ const failure: Failure = spy.mock.calls[0][0];
427
+ expect(failure).toMatchObject(
428
+ workspace.failureMatcher({
429
+ file: "package.json",
430
+ hasFixer: true,
431
+ message: `Script '${SCRIPT_NAME}' should be removed`,
432
+ }),
433
+ );
434
+
435
+ // Verify script was removed
436
+ expect(JSON.parse(workspace.readFile("package.json")!).scripts).toEqual({});
437
+ });
438
+
439
+ it("does not error when REMOVE is specified for non-existent script", () => {
440
+ workspace.writeFile("package.json", PACKAGE_WITH_SCRIPTS);
441
+
442
+ packageScript({
443
+ options: {
444
+ scripts: {
445
+ nonExistentScript: REMOVE,
446
+ },
447
+ },
448
+ }).check(context);
449
+
450
+ expect(spy).toHaveBeenCalledTimes(0);
451
+
452
+ // Original scripts should remain unchanged
453
+ expect(JSON.parse(workspace.readFile("package.json")!).scripts).toEqual({
454
+ [SCRIPT_NAME]: SCRIPT_VALUE,
455
+ });
456
+ });
457
+
458
+ it("handles mix of REMOVE and regular script values", () => {
459
+ const packageWithMultipleScripts = json({
460
+ name: "package-with-multiple-scripts",
461
+ scripts: {
462
+ build: "tsc",
463
+ test: "jest",
464
+ lint: "eslint",
465
+ },
466
+ });
467
+
468
+ workspace.writeFile("package.json", packageWithMultipleScripts);
469
+
470
+ packageScript({
471
+ options: {
472
+ scripts: {
473
+ build: REMOVE, // Remove existing script
474
+ test: "vitest", // Change existing script
475
+ start: "node index.js", // Add new script
476
+ lint: REMOVE, // Remove another existing script
477
+ },
478
+ },
479
+ }).check(context);
480
+
481
+ expect(spy).toHaveBeenCalledTimes(4); // build removal, test change, start addition, lint removal
482
+
483
+ const failures = spy.mock.calls.map(call => call[0]);
484
+
485
+ // We expect 4 failures: build removal, test change, start addition, lint removal
486
+ // The order might vary, so let's check by message content instead of position
487
+ const errorMessages = failures.map(f => f.message);
488
+
489
+ expect(errorMessages).toContain("Script 'build' should be removed");
490
+ expect(errorMessages).toContain("Script 'lint' should be removed");
491
+ expect(
492
+ errorMessages.some(msg => msg.includes("Expected standardized script entry for 'test'")),
493
+ ).toBe(true);
494
+ expect(
495
+ errorMessages.some(msg => msg.includes("Expected standardized script entry for 'start'")),
496
+ ).toBe(true);
497
+
498
+ // Verify final scripts state
499
+ expect(JSON.parse(workspace.readFile("package.json")!).scripts).toEqual({
500
+ test: "vitest",
501
+ start: "node index.js",
502
+ });
503
+ });
504
+
505
+ it("handles REMOVE when scripts block doesn't exist", () => {
506
+ workspace.writeFile("package.json", PACKAGE_WITHOUT_SCRIPTS);
507
+
508
+ packageScript({
509
+ options: {
510
+ scripts: {
511
+ build: REMOVE,
512
+ },
513
+ },
514
+ }).check(context);
515
+
516
+ expect(spy).toHaveBeenCalledTimes(1); // Only for missing scripts block
517
+
518
+ const failure: Failure = spy.mock.calls[0][0];
519
+ expect(failure).toMatchObject(
520
+ workspace.failureMatcher({
521
+ file: "package.json",
522
+ hasFixer: true,
523
+ message: "No scripts block in package.json",
524
+ }),
525
+ );
526
+ });
527
+ });
528
+
529
+ describe("Advanced allowedValues scenarios", () => {
530
+ let workspace: TestingWorkspace;
531
+ let spy: AddErrorSpy;
532
+ let context: Context;
533
+
534
+ beforeEach(async () => {
535
+ workspace = await createTestingWorkspace({
536
+ fixFlag: true,
537
+ host: hostFactory.make(),
538
+ });
539
+ spy = vi.spyOn(workspace.context, "addError");
540
+ context = workspace.context;
541
+ });
542
+
543
+ it("handles multiple options without fixValue (no fixer should be provided)", () => {
544
+ workspace.writeFile("package.json", PACKAGE_WITH_SCRIPTS);
545
+
546
+ packageScript({
547
+ options: {
548
+ scripts: {
549
+ [SCRIPT_NAME]: {
550
+ options: ["option-a", "option-b", "option-c"],
551
+ // No fixValue specified
552
+ },
553
+ },
554
+ },
555
+ }).check(context);
556
+
557
+ expect(spy).toHaveBeenCalledTimes(1);
558
+
559
+ const failure: Failure = spy.mock.calls[0][0];
560
+ expect(failure.fixer).toBeUndefined(); // No fixer when no fixValue
561
+ expect(failure.message).toContain("Expected standardized script entry");
562
+ expect(failure.message).toContain("option-a");
563
+ expect(failure.message).toContain("option-b");
564
+ expect(failure.message).toContain("option-c");
565
+ });
566
+
567
+ it("handles mixed option types: string + undefined + REMOVE", () => {
568
+ workspace.writeFile("package.json", PACKAGE_WITH_SCRIPTS);
569
+
570
+ packageScript({
571
+ options: {
572
+ scripts: {
573
+ [SCRIPT_NAME]: {
574
+ options: ["build-cmd", undefined, REMOVE],
575
+ fixValue: "build-cmd",
576
+ },
577
+ },
578
+ },
579
+ }).check(context);
580
+
581
+ expect(spy).toHaveBeenCalledTimes(1);
582
+
583
+ const failure: Failure = spy.mock.calls[0][0];
584
+ expect(failure.fixer).toBeDefined();
585
+ expect(failure.message).toContain("Expected standardized script entry");
586
+ expect(failure.message).toContain("build-cmd");
587
+ expect(failure.message).toContain("(empty)"); // Should show undefined/REMOVE as (empty)
588
+
589
+ // Verify it fixes to the fixValue
590
+ expect(JSON.parse(workspace.readFile("package.json")!).scripts[SCRIPT_NAME]).toBe(
591
+ "build-cmd",
592
+ );
593
+ });
594
+
595
+ it("allows empty when REMOVE is in options array", () => {
596
+ const packageWithoutSpecificScript = json({
597
+ name: "test-package",
598
+ scripts: {
599
+ "other-script": "other-value",
600
+ },
601
+ });
602
+ workspace.writeFile("package.json", packageWithoutSpecificScript);
603
+
604
+ packageScript({
605
+ options: {
606
+ scripts: {
607
+ "missing-script": {
608
+ options: ["some-value", REMOVE],
609
+ fixValue: "some-value",
610
+ },
611
+ },
612
+ },
613
+ }).check(context);
614
+
615
+ // When REMOVE is in options array, it sets allowEmpty=true
616
+ // So missing script (undefined) should NOT error - this is correct behavior
617
+ expect(spy).toHaveBeenCalledTimes(0);
618
+
619
+ // Original state should remain unchanged since no error occurred
620
+ const finalScripts = JSON.parse(workspace.readFile("package.json")!).scripts;
621
+ expect(finalScripts["missing-script"]).toBeUndefined();
622
+ expect(finalScripts["other-script"]).toBe("other-value");
623
+ });
624
+
625
+ it("handles existing script with REMOVE in options array", () => {
626
+ workspace.writeFile("package.json", PACKAGE_WITH_SCRIPTS);
627
+
628
+ packageScript({
629
+ options: {
630
+ scripts: {
631
+ [SCRIPT_NAME]: {
632
+ options: ["different-value", REMOVE], // Current value doesn't match "different-value"
633
+ fixValue: REMOVE, // Should fix by removing the script
634
+ },
635
+ },
636
+ },
637
+ }).check(context);
638
+
639
+ expect(spy).toHaveBeenCalledTimes(1);
640
+
641
+ const failure: Failure = spy.mock.calls[0][0];
642
+ expect(failure.fixer).toBeDefined();
643
+ expect(failure.message).toContain("Expected standardized script entry");
644
+
645
+ // Should be removed since fixValue is REMOVE
646
+ expect(JSON.parse(workspace.readFile("package.json")!).scripts[SCRIPT_NAME]).toBeUndefined();
647
+ });
648
+
649
+ it("handles fixValue: false (prevents fixing)", () => {
650
+ workspace.writeFile("package.json", PACKAGE_WITH_SCRIPTS);
651
+
652
+ packageScript({
653
+ options: {
654
+ scripts: {
655
+ [SCRIPT_NAME]: {
656
+ options: ["different-value"],
657
+ fixValue: false,
658
+ },
659
+ },
660
+ },
661
+ }).check(context);
662
+
663
+ expect(spy).toHaveBeenCalledTimes(1);
664
+
665
+ const failure: Failure = spy.mock.calls[0][0];
666
+ expect(failure.fixer).toBeUndefined(); // fixValue: false should prevent fixer
667
+ expect(failure.message).toContain("Expected standardized script entry");
668
+
669
+ // Original value should remain unchanged
670
+ expect(JSON.parse(workspace.readFile("package.json")!).scripts[SCRIPT_NAME]).toBe(
671
+ SCRIPT_VALUE,
672
+ );
673
+ });
674
+
675
+ it("formats error messages correctly for complex allowedValues", () => {
676
+ workspace.writeFile("package.json", PACKAGE_WITH_SCRIPTS);
677
+
678
+ packageScript({
679
+ options: {
680
+ scripts: {
681
+ [SCRIPT_NAME]: {
682
+ options: ["cmd-a", "cmd-b", undefined, REMOVE],
683
+ fixValue: "cmd-a",
684
+ },
685
+ },
686
+ },
687
+ }).check(context);
688
+
689
+ expect(spy).toHaveBeenCalledTimes(1);
690
+
691
+ const failure: Failure = spy.mock.calls[0][0];
692
+ const message = failure.message;
693
+
694
+ // Should contain all allowed values in the main message
695
+ expect(message).toContain("'cmd-a'");
696
+ expect(message).toContain("'cmd-b'");
697
+ expect(message).toContain("(empty)"); // Both undefined and REMOVE should show as (empty)
698
+
699
+ // The longMessage contains a diff between expected and actual values
700
+ expect(failure.longMessage).toBeDefined();
701
+ expect(failure.longMessage).toContain("'cmd-a', 'cmd-b', (empty), (empty)"); // This is the expected part of the diff
702
+ expect(failure.longMessage).toContain(SCRIPT_VALUE); // This is the actual current value
703
+ });
704
+
705
+ it("processes multiple scripts with different allowedValues configurations", () => {
706
+ const complexPackage = json({
707
+ name: "complex-package",
708
+ scripts: {
709
+ "script-a": "wrong-value-a",
710
+ "script-b": "wrong-value-b",
711
+ "script-c": "correct-value-c",
712
+ },
713
+ });
714
+ workspace.writeFile("package.json", complexPackage);
715
+
716
+ packageScript({
717
+ options: {
718
+ scripts: {
719
+ "script-a": {
720
+ options: ["correct-a1", "correct-a2"],
721
+ fixValue: "correct-a1",
722
+ },
723
+ "script-b": {
724
+ options: ["correct-b", undefined],
725
+ fixValue: undefined, // Fix to empty (removal)
726
+ },
727
+ "script-c": "correct-value-c", // Already correct, should not error
728
+ "script-d": REMOVE, // Doesn't exist, should not error
729
+ },
730
+ },
731
+ }).check(context);
732
+
733
+ expect(spy).toHaveBeenCalledTimes(2); // Only script-a and script-b should error
734
+
735
+ const failures = spy.mock.calls.map(call => call[0]);
736
+ const messages = failures.map(f => f.message);
737
+
738
+ expect(messages.some(msg => msg.includes("script-a"))).toBe(true);
739
+ expect(messages.some(msg => msg.includes("script-b"))).toBe(true);
740
+ expect(messages.some(msg => msg.includes("script-c"))).toBe(false); // Should not error
741
+ expect(messages.some(msg => msg.includes("script-d"))).toBe(false); // Should not error
742
+
743
+ // Verify final state
744
+ const finalScripts = JSON.parse(workspace.readFile("package.json")!).scripts;
745
+ expect(finalScripts["script-a"]).toBe("correct-a1");
746
+ expect(finalScripts["script-b"]).toBeUndefined(); // Should be removed
747
+ expect(finalScripts["script-c"]).toBe("correct-value-c"); // Unchanged
748
+ expect(finalScripts["script-d"]).toBeUndefined(); // Still doesn't exist
749
+ });
750
+ });
751
+
752
+ describe("Options Validation", () => {
753
+ it("should accept valid options", () => {
754
+ const ruleModule = packageScript({
755
+ options: { scripts: { "build": "tsc" } },
756
+ });
757
+
758
+ expect(() =>
759
+ ruleModule.validateOptions({
760
+ scripts: {
761
+ "build": "tsc",
762
+ "test": {
763
+ options: ["jest", "vitest", undefined],
764
+ fixValue: "jest",
765
+ },
766
+ },
767
+ })
768
+ ).not.toThrow();
769
+
770
+ expect(() =>
771
+ ruleModule.validateOptions({
772
+ scripts: {
773
+ "start": "node index.js",
774
+ "outdated": REMOVE, // Direct REMOVE syntax
775
+ },
776
+ })
777
+ ).not.toThrow();
778
+
779
+ expect(() =>
780
+ ruleModule.validateOptions({
781
+ scripts: {
782
+ "build": REMOVE,
783
+ "test": "jest",
784
+ "lint": {
785
+ options: ["eslint", REMOVE],
786
+ fixValue: REMOVE,
787
+ },
788
+ },
789
+ })
790
+ ).not.toThrow();
791
+ });
792
+
793
+ it("should reject invalid options", () => {
794
+ const ruleModule = packageScript({ options: { scripts: { "build": "tsc" } } });
795
+
796
+ // @ts-expect-error testing invalid input
797
+ expect(() => ruleModule.validateOptions({})).toThrow();
798
+ // @ts-expect-error testing invalid input
799
+ expect(() => ruleModule.validateOptions({ scripts: { "build": 123 } })).toThrow();
800
+ // @ts-expect-error testing invalid input
801
+ expect(() => ruleModule.validateOptions({ scripts: "invalid" })).toThrow();
802
+ });
803
+ });
255
804
  });