eslint-plugin-primer-react 8.3.0-rc.4b58a28 → 8.4.0-rc.6ac270a

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.
@@ -16,7 +16,7 @@ jobs:
16
16
  name: Check for changeset
17
17
  runs-on: ubuntu-latest
18
18
  steps:
19
- - uses: actions/checkout@v4
19
+ - uses: actions/checkout@v5
20
20
  - name: 'Check for changeset'
21
21
  uses: brettcannon/check-for-changed-files@v1
22
22
  with:
@@ -9,7 +9,7 @@ jobs:
9
9
  format:
10
10
  runs-on: ubuntu-latest
11
11
  steps:
12
- - uses: actions/checkout@v4
12
+ - uses: actions/checkout@v5
13
13
  - name: Use Node.js
14
14
  uses: actions/setup-node@v4
15
15
  with:
@@ -21,7 +21,7 @@ jobs:
21
21
  test:
22
22
  runs-on: ubuntu-latest
23
23
  steps:
24
- - uses: actions/checkout@v4
24
+ - uses: actions/checkout@v5
25
25
  - name: Use Node.js
26
26
  uses: actions/setup-node@v4
27
27
  with:
@@ -33,7 +33,7 @@ jobs:
33
33
  lint:
34
34
  runs-on: ubuntu-latest
35
35
  steps:
36
- - uses: actions/checkout@v4
36
+ - uses: actions/checkout@v5
37
37
  - name: Use Node.js
38
38
  uses: actions/setup-node@v4
39
39
  with:
@@ -8,7 +8,7 @@ jobs:
8
8
  runs-on: ubuntu-latest
9
9
  steps:
10
10
  - name: Checkout repository
11
- uses: actions/checkout@v4
11
+ uses: actions/checkout@v5
12
12
  with:
13
13
  # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
14
14
  fetch-depth: 0
@@ -15,7 +15,7 @@ jobs:
15
15
  runs-on: ubuntu-latest
16
16
  steps:
17
17
  - name: Checkout repository
18
- uses: actions/checkout@v4
18
+ uses: actions/checkout@v5
19
19
  with:
20
20
  # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
21
21
  fetch-depth: 0
@@ -11,7 +11,7 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  steps:
13
13
  - name: Checkout repository
14
- uses: actions/checkout@v4
14
+ uses: actions/checkout@v5
15
15
  with:
16
16
  # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
17
17
  fetch-depth: 0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # eslint-plugin-primer-react
2
2
 
3
+ ## 8.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#437](https://github.com/primer/eslint-plugin-primer-react/pull/437) [`9270d40`](https://github.com/primer/eslint-plugin-primer-react/commit/9270d40d73bd046e21156b68ef6bd13a20008585) Thanks [@copilot-swe-agent](https://github.com/apps/copilot-swe-agent)! - Add spread-props-first rule to ensure spread props come before other props
8
+
3
9
  ## 8.3.0
4
10
 
5
11
  ### Minor Changes
@@ -0,0 +1,66 @@
1
+ # Ensure spread props come before other props (spread-props-first)
2
+
3
+ Spread props should come before other named props to avoid unintentionally overriding props. When spread props are placed after named props, they can override the named props, which is often unintended and can lead to UI bugs.
4
+
5
+ ## Rule details
6
+
7
+ This rule enforces that all spread props (`{...rest}`, `{...props}`, etc.) come before any named props in JSX elements.
8
+
9
+ 👎 Examples of **incorrect** code for this rule:
10
+
11
+ ```jsx
12
+ /* eslint primer-react/spread-props-first: "error" */
13
+
14
+ // ❌ Spread after named prop
15
+ <Example className="..." {...rest} />
16
+
17
+ // ❌ Spread in the middle
18
+ <Example className="..." {...rest} id="foo" />
19
+
20
+ // ❌ Multiple spreads after named props
21
+ <Example className="..." {...rest} {...other} />
22
+ ```
23
+
24
+ 👍 Examples of **correct** code for this rule:
25
+
26
+ ```jsx
27
+ /* eslint primer-react/spread-props-first: "error" */
28
+
29
+ // ✅ Spread before named props
30
+ <Example {...rest} className="..." />
31
+
32
+ // ✅ Multiple spreads before named props
33
+ <Example {...rest} {...other} className="..." />
34
+
35
+ // ✅ Only spread props
36
+ <Example {...rest} />
37
+
38
+ // ✅ Only named props
39
+ <Example className="..." id="foo" />
40
+ ```
41
+
42
+ ## Why this matters
43
+
44
+ Placing spread props after named props can cause unexpected behavior:
45
+
46
+ ```jsx
47
+ // ❌ Bad: className might get overridden by rest
48
+ <Button className="custom-class" {...rest} />
49
+
50
+ // If rest = { className: "other-class" }
51
+ // Result: className="other-class" (custom-class is lost!)
52
+
53
+ // ✅ Good: className will override any className in rest
54
+ <Button {...rest} className="custom-class" />
55
+
56
+ // If rest = { className: "other-class" }
57
+ // Result: className="custom-class" (as intended)
58
+ ```
59
+
60
+ ## Options
61
+
62
+ This rule has no configuration options.
63
+
64
+ ## When to use autofix
65
+
66
+ This rule includes an autofix that will automatically reorder your props to place all spread props first. The autofix is safe to use as it preserves the order of spreads relative to each other and the order of named props relative to each other.
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "eslint-plugin-primer-react",
3
- "version": "8.2.1",
3
+ "version": "8.3.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "eslint-plugin-primer-react",
9
- "version": "8.2.1",
9
+ "version": "8.3.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@styled-system/props": "^5.1.5",
@@ -956,11 +956,12 @@
956
956
  }
957
957
  },
958
958
  "node_modules/@eslint/config-array": {
959
- "version": "0.21.0",
960
- "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
961
- "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
959
+ "version": "0.21.1",
960
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
961
+ "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
962
+ "license": "Apache-2.0",
962
963
  "dependencies": {
963
- "@eslint/object-schema": "^2.1.6",
964
+ "@eslint/object-schema": "^2.1.7",
964
965
  "debug": "^4.3.1",
965
966
  "minimatch": "^3.1.2"
966
967
  },
@@ -972,6 +973,7 @@
972
973
  "version": "1.1.12",
973
974
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
974
975
  "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
976
+ "license": "MIT",
975
977
  "dependencies": {
976
978
  "balanced-match": "^1.0.0",
977
979
  "concat-map": "0.0.1"
@@ -981,6 +983,7 @@
981
983
  "version": "3.1.2",
982
984
  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
983
985
  "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
986
+ "license": "ISC",
984
987
  "dependencies": {
985
988
  "brace-expansion": "^1.1.7"
986
989
  },
@@ -989,9 +992,9 @@
989
992
  }
990
993
  },
991
994
  "node_modules/@eslint/config-helpers": {
992
- "version": "0.4.0",
993
- "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz",
994
- "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==",
995
+ "version": "0.4.1",
996
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz",
997
+ "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==",
995
998
  "license": "Apache-2.0",
996
999
  "dependencies": {
997
1000
  "@eslint/core": "^0.16.0"
@@ -1082,9 +1085,9 @@
1082
1085
  }
1083
1086
  },
1084
1087
  "node_modules/@eslint/js": {
1085
- "version": "9.37.0",
1086
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz",
1087
- "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==",
1088
+ "version": "9.38.0",
1089
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz",
1090
+ "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==",
1088
1091
  "license": "MIT",
1089
1092
  "engines": {
1090
1093
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1094,9 +1097,10 @@
1094
1097
  }
1095
1098
  },
1096
1099
  "node_modules/@eslint/object-schema": {
1097
- "version": "2.1.6",
1098
- "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
1099
- "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
1100
+ "version": "2.1.7",
1101
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
1102
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
1103
+ "license": "Apache-2.0",
1100
1104
  "engines": {
1101
1105
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1102
1106
  }
@@ -2482,15 +2486,15 @@
2482
2486
  }
2483
2487
  },
2484
2488
  "node_modules/@typescript-eslint/utils": {
2485
- "version": "8.45.0",
2486
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.45.0.tgz",
2487
- "integrity": "sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==",
2489
+ "version": "8.46.2",
2490
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz",
2491
+ "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==",
2488
2492
  "license": "MIT",
2489
2493
  "dependencies": {
2490
2494
  "@eslint-community/eslint-utils": "^4.7.0",
2491
- "@typescript-eslint/scope-manager": "8.45.0",
2492
- "@typescript-eslint/types": "8.45.0",
2493
- "@typescript-eslint/typescript-estree": "8.45.0"
2495
+ "@typescript-eslint/scope-manager": "8.46.2",
2496
+ "@typescript-eslint/types": "8.46.2",
2497
+ "@typescript-eslint/typescript-estree": "8.46.2"
2494
2498
  },
2495
2499
  "engines": {
2496
2500
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2505,13 +2509,13 @@
2505
2509
  }
2506
2510
  },
2507
2511
  "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/project-service": {
2508
- "version": "8.45.0",
2509
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.45.0.tgz",
2510
- "integrity": "sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==",
2512
+ "version": "8.46.2",
2513
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz",
2514
+ "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==",
2511
2515
  "license": "MIT",
2512
2516
  "dependencies": {
2513
- "@typescript-eslint/tsconfig-utils": "^8.45.0",
2514
- "@typescript-eslint/types": "^8.45.0",
2517
+ "@typescript-eslint/tsconfig-utils": "^8.46.2",
2518
+ "@typescript-eslint/types": "^8.46.2",
2515
2519
  "debug": "^4.3.4"
2516
2520
  },
2517
2521
  "engines": {
@@ -2526,13 +2530,13 @@
2526
2530
  }
2527
2531
  },
2528
2532
  "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": {
2529
- "version": "8.45.0",
2530
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.45.0.tgz",
2531
- "integrity": "sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==",
2533
+ "version": "8.46.2",
2534
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz",
2535
+ "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==",
2532
2536
  "license": "MIT",
2533
2537
  "dependencies": {
2534
- "@typescript-eslint/types": "8.45.0",
2535
- "@typescript-eslint/visitor-keys": "8.45.0"
2538
+ "@typescript-eslint/types": "8.46.2",
2539
+ "@typescript-eslint/visitor-keys": "8.46.2"
2536
2540
  },
2537
2541
  "engines": {
2538
2542
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2543,9 +2547,9 @@
2543
2547
  }
2544
2548
  },
2545
2549
  "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/tsconfig-utils": {
2546
- "version": "8.45.0",
2547
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.45.0.tgz",
2548
- "integrity": "sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==",
2550
+ "version": "8.46.2",
2551
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz",
2552
+ "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==",
2549
2553
  "license": "MIT",
2550
2554
  "engines": {
2551
2555
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2559,9 +2563,9 @@
2559
2563
  }
2560
2564
  },
2561
2565
  "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": {
2562
- "version": "8.45.0",
2563
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.45.0.tgz",
2564
- "integrity": "sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==",
2566
+ "version": "8.46.2",
2567
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz",
2568
+ "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==",
2565
2569
  "license": "MIT",
2566
2570
  "engines": {
2567
2571
  "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2572,15 +2576,15 @@
2572
2576
  }
2573
2577
  },
2574
2578
  "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": {
2575
- "version": "8.45.0",
2576
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.45.0.tgz",
2577
- "integrity": "sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==",
2579
+ "version": "8.46.2",
2580
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz",
2581
+ "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==",
2578
2582
  "license": "MIT",
2579
2583
  "dependencies": {
2580
- "@typescript-eslint/project-service": "8.45.0",
2581
- "@typescript-eslint/tsconfig-utils": "8.45.0",
2582
- "@typescript-eslint/types": "8.45.0",
2583
- "@typescript-eslint/visitor-keys": "8.45.0",
2584
+ "@typescript-eslint/project-service": "8.46.2",
2585
+ "@typescript-eslint/tsconfig-utils": "8.46.2",
2586
+ "@typescript-eslint/types": "8.46.2",
2587
+ "@typescript-eslint/visitor-keys": "8.46.2",
2584
2588
  "debug": "^4.3.4",
2585
2589
  "fast-glob": "^3.3.2",
2586
2590
  "is-glob": "^4.0.3",
@@ -2600,12 +2604,12 @@
2600
2604
  }
2601
2605
  },
2602
2606
  "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": {
2603
- "version": "8.45.0",
2604
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.45.0.tgz",
2605
- "integrity": "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==",
2607
+ "version": "8.46.2",
2608
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz",
2609
+ "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==",
2606
2610
  "license": "MIT",
2607
2611
  "dependencies": {
2608
- "@typescript-eslint/types": "8.45.0",
2612
+ "@typescript-eslint/types": "8.46.2",
2609
2613
  "eslint-visitor-keys": "^4.2.1"
2610
2614
  },
2611
2615
  "engines": {
@@ -4148,24 +4152,23 @@
4148
4152
  }
4149
4153
  },
4150
4154
  "node_modules/eslint": {
4151
- "version": "9.37.0",
4152
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz",
4153
- "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
4155
+ "version": "9.38.0",
4156
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
4157
+ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
4154
4158
  "license": "MIT",
4155
4159
  "dependencies": {
4156
4160
  "@eslint-community/eslint-utils": "^4.8.0",
4157
4161
  "@eslint-community/regexpp": "^4.12.1",
4158
- "@eslint/config-array": "^0.21.0",
4159
- "@eslint/config-helpers": "^0.4.0",
4162
+ "@eslint/config-array": "^0.21.1",
4163
+ "@eslint/config-helpers": "^0.4.1",
4160
4164
  "@eslint/core": "^0.16.0",
4161
4165
  "@eslint/eslintrc": "^3.3.1",
4162
- "@eslint/js": "9.37.0",
4166
+ "@eslint/js": "9.38.0",
4163
4167
  "@eslint/plugin-kit": "^0.4.0",
4164
4168
  "@humanfs/node": "^0.16.6",
4165
4169
  "@humanwhocodes/module-importer": "^1.0.1",
4166
4170
  "@humanwhocodes/retry": "^0.4.2",
4167
4171
  "@types/estree": "^1.0.6",
4168
- "@types/json-schema": "^7.0.15",
4169
4172
  "ajv": "^6.12.4",
4170
4173
  "chalk": "^4.0.0",
4171
4174
  "cross-spawn": "^7.0.6",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-primer-react",
3
- "version": "8.3.0-rc.4b58a28",
3
+ "version": "8.4.0-rc.6ac270a",
4
4
  "description": "ESLint rules for Primer React",
5
5
  "main": "src/index.js",
6
6
  "engines": {
package/src/index.js CHANGED
@@ -20,6 +20,7 @@ module.exports = {
20
20
  'enforce-css-module-identifier-casing': require('./rules/enforce-css-module-identifier-casing'),
21
21
  'enforce-css-module-default-import': require('./rules/enforce-css-module-default-import'),
22
22
  'use-styled-react-import': require('./rules/use-styled-react-import'),
23
+ 'spread-props-first': require('./rules/spread-props-first'),
23
24
  },
24
25
  configs: {
25
26
  recommended: require('./configs/recommended'),
@@ -0,0 +1,123 @@
1
+ const rule = require('../spread-props-first')
2
+ const {RuleTester} = require('eslint')
3
+
4
+ const ruleTester = new RuleTester({
5
+ languageOptions: {
6
+ ecmaVersion: 'latest',
7
+ sourceType: 'module',
8
+ parserOptions: {
9
+ ecmaFeatures: {
10
+ jsx: true,
11
+ },
12
+ },
13
+ },
14
+ })
15
+
16
+ ruleTester.run('spread-props-first', rule, {
17
+ valid: [
18
+ // Spread props before named props
19
+ `<Example {...rest} className="foo" />`,
20
+ // Multiple spreads before named props
21
+ `<Example {...rest} {...other} className="foo" id="bar" />`,
22
+ // Only spread props
23
+ `<Example {...rest} />`,
24
+ // Only named props
25
+ `<Example className="foo" id="bar" />`,
26
+ // Empty element
27
+ `<Example />`,
28
+ // Spread first, then named props
29
+ `<Example {...rest} className="foo" onClick={handleClick} />`,
30
+ // Multiple spreads at the beginning
31
+ `<Example {...props1} {...props2} {...props3} className="foo" />`,
32
+ ],
33
+ invalid: [
34
+ // Named prop before spread
35
+ {
36
+ code: `<Example className="foo" {...rest} />`,
37
+ output: `<Example {...rest} className="foo" />`,
38
+ errors: [
39
+ {
40
+ messageId: 'spreadPropsFirst',
41
+ data: {spreadProp: '{...rest}', namedProp: 'className'},
42
+ },
43
+ ],
44
+ },
45
+ // Multiple named props before spread
46
+ {
47
+ code: `<Example className="foo" id="bar" {...rest} />`,
48
+ output: `<Example {...rest} className="foo" id="bar" />`,
49
+ errors: [
50
+ {
51
+ messageId: 'spreadPropsFirst',
52
+ data: {spreadProp: '{...rest}', namedProp: 'id'},
53
+ },
54
+ ],
55
+ },
56
+ // Named prop with expression before spread
57
+ {
58
+ code: `<Example onClick={handleClick} {...rest} />`,
59
+ output: `<Example {...rest} onClick={handleClick} />`,
60
+ errors: [
61
+ {
62
+ messageId: 'spreadPropsFirst',
63
+ data: {spreadProp: '{...rest}', namedProp: 'onClick'},
64
+ },
65
+ ],
66
+ },
67
+ // Mixed order with multiple spreads
68
+ {
69
+ code: `<Example className="foo" {...rest} id="bar" {...other} />`,
70
+ output: `<Example {...rest} {...other} className="foo" id="bar" />`,
71
+ errors: [
72
+ {
73
+ messageId: 'spreadPropsFirst',
74
+ data: {spreadProp: '{...rest}', namedProp: 'id'},
75
+ },
76
+ ],
77
+ },
78
+ // Named prop before multiple spreads
79
+ {
80
+ code: `<Example className="foo" {...rest} {...other} />`,
81
+ output: `<Example {...rest} {...other} className="foo" />`,
82
+ errors: [
83
+ {
84
+ messageId: 'spreadPropsFirst',
85
+ data: {spreadProp: '{...rest}', namedProp: 'className'},
86
+ },
87
+ ],
88
+ },
89
+ // Complex example with many props
90
+ {
91
+ code: `<Example className="foo" id="bar" onClick={handleClick} {...rest} disabled />`,
92
+ output: `<Example {...rest} className="foo" id="bar" onClick={handleClick} disabled />`,
93
+ errors: [
94
+ {
95
+ messageId: 'spreadPropsFirst',
96
+ data: {spreadProp: '{...rest}', namedProp: 'disabled'},
97
+ },
98
+ ],
99
+ },
100
+ // Boolean prop before spread
101
+ {
102
+ code: `<Example disabled {...rest} />`,
103
+ output: `<Example {...rest} disabled />`,
104
+ errors: [
105
+ {
106
+ messageId: 'spreadPropsFirst',
107
+ data: {spreadProp: '{...rest}', namedProp: 'disabled'},
108
+ },
109
+ ],
110
+ },
111
+ // Spread in the middle
112
+ {
113
+ code: `<Example className="foo" {...rest} id="bar" />`,
114
+ output: `<Example {...rest} className="foo" id="bar" />`,
115
+ errors: [
116
+ {
117
+ messageId: 'spreadPropsFirst',
118
+ data: {spreadProp: '{...rest}', namedProp: 'id'},
119
+ },
120
+ ],
121
+ },
122
+ ],
123
+ })
@@ -0,0 +1,81 @@
1
+ module.exports = {
2
+ meta: {
3
+ type: 'problem',
4
+ fixable: 'code',
5
+ schema: [],
6
+ messages: {
7
+ spreadPropsFirst:
8
+ 'Spread props should come before other props to avoid unintentional overrides. Move {{spreadProp}} before {{namedProp}}.',
9
+ },
10
+ },
11
+ create(context) {
12
+ return {
13
+ JSXOpeningElement(node) {
14
+ const attributes = node.attributes
15
+
16
+ // Track if we've seen a named prop before a spread
17
+ let lastNamedPropIndex = -1
18
+ let firstSpreadAfterNamedPropIndex = -1
19
+
20
+ for (let i = 0; i < attributes.length; i++) {
21
+ const attr = attributes[i]
22
+
23
+ if (attr.type === 'JSXAttribute') {
24
+ // This is a named prop
25
+ lastNamedPropIndex = i
26
+ } else if (attr.type === 'JSXSpreadAttribute' && lastNamedPropIndex !== -1) {
27
+ // This is a spread prop that comes after a named prop
28
+ if (firstSpreadAfterNamedPropIndex === -1) {
29
+ firstSpreadAfterNamedPropIndex = i
30
+ }
31
+ }
32
+ }
33
+
34
+ // If we found a spread after a named prop, report it
35
+ if (firstSpreadAfterNamedPropIndex !== -1) {
36
+ const sourceCode = context.sourceCode
37
+ const spreadAttr = attributes[firstSpreadAfterNamedPropIndex]
38
+ const namedAttr = attributes[lastNamedPropIndex]
39
+
40
+ context.report({
41
+ node: spreadAttr,
42
+ messageId: 'spreadPropsFirst',
43
+ data: {
44
+ spreadProp: sourceCode.getText(spreadAttr),
45
+ namedProp: namedAttr.name.name,
46
+ },
47
+ fix(fixer) {
48
+ // Collect all spreads and named props
49
+ const spreads = []
50
+ const namedProps = []
51
+
52
+ for (const attr of attributes) {
53
+ if (attr.type === 'JSXSpreadAttribute') {
54
+ spreads.push(attr)
55
+ } else if (attr.type === 'JSXAttribute') {
56
+ namedProps.push(attr)
57
+ }
58
+ }
59
+
60
+ // Generate the reordered attributes text
61
+ const reorderedAttrs = [...spreads, ...namedProps]
62
+ const fixes = []
63
+
64
+ // Replace each attribute with its new position
65
+ for (let i = 0; i < attributes.length; i++) {
66
+ const newAttr = reorderedAttrs[i]
67
+ const oldAttr = attributes[i]
68
+
69
+ if (newAttr !== oldAttr) {
70
+ fixes.push(fixer.replaceText(oldAttr, sourceCode.getText(newAttr)))
71
+ }
72
+ }
73
+
74
+ return fixes
75
+ },
76
+ })
77
+ }
78
+ },
79
+ }
80
+ },
81
+ }