eslint-plugin-tailwind-palette-guard 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +742 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 johanlyckenvik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# eslint-plugin-tailwind-palette-guard
|
|
2
|
+
|
|
3
|
+
ESLint plugin that enforces semantic design tokens over hardcoded colors in Tailwind CSS and inline styles.
|
|
4
|
+
|
|
5
|
+
## Why?
|
|
6
|
+
|
|
7
|
+
Hardcoded palette colors (`text-red-500`, `bg-white`) and inline color styles (`style={{ color: 'red' }}`) scatter raw color values across your codebase, making theme changes painful and dark mode support inconsistent. Semantic tokens (`text-destructive`, `bg-background`) provide a single source of truth.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- ESLint 9+ (flat config only)
|
|
12
|
+
- Node.js 18+
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -D eslint-plugin-tailwind-palette-guard
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Use a preset or configure manually:
|
|
23
|
+
|
|
24
|
+
**Recommended** — enables `no-palette-colors` only:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
// eslint.config.js
|
|
28
|
+
import tailwindPaletteGuard from "eslint-plugin-tailwind-palette-guard";
|
|
29
|
+
|
|
30
|
+
export default [tailwindPaletteGuard.configs.recommended];
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Strict** — enables both rules:
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
// eslint.config.js
|
|
37
|
+
import tailwindPaletteGuard from "eslint-plugin-tailwind-palette-guard";
|
|
38
|
+
|
|
39
|
+
export default [tailwindPaletteGuard.configs.strict];
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Manual** — full control:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
// eslint.config.js
|
|
46
|
+
import tailwindPaletteGuard from "eslint-plugin-tailwind-palette-guard";
|
|
47
|
+
|
|
48
|
+
export default [
|
|
49
|
+
{
|
|
50
|
+
plugins: {
|
|
51
|
+
"tailwind-palette-guard": tailwindPaletteGuard,
|
|
52
|
+
},
|
|
53
|
+
rules: {
|
|
54
|
+
"tailwind-palette-guard/no-palette-colors": ["warn", {
|
|
55
|
+
allowedColors: ["stroke-gray-100"],
|
|
56
|
+
allowedFiles: ["**/icons/**"],
|
|
57
|
+
}],
|
|
58
|
+
"tailwind-palette-guard/no-inline-color-styles": ["warn", {
|
|
59
|
+
allowedProperties: ["fill"],
|
|
60
|
+
allowedValues: ["transparent"],
|
|
61
|
+
allowedFiles: ["**/charts/**"],
|
|
62
|
+
}],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Rules
|
|
69
|
+
|
|
70
|
+
### `no-palette-colors`
|
|
71
|
+
|
|
72
|
+
Flags Tailwind classes that use hardcoded colors instead of semantic design tokens.
|
|
73
|
+
|
|
74
|
+
#### What it detects
|
|
75
|
+
|
|
76
|
+
**Palette colors** — `{prefix}-{color}-{shade}` with optional `/{opacity}`:
|
|
77
|
+
|
|
78
|
+
| Pattern | Example |
|
|
79
|
+
| ------- | ------- |
|
|
80
|
+
| Static className | `className="text-red-500"` |
|
|
81
|
+
| cn/clsx/cx/twMerge/twJoin | `cn("text-red-500", cond && "bg-green-100")` |
|
|
82
|
+
| Object syntax | `cn({ "text-red-500": isError })` |
|
|
83
|
+
| cva definitions | `cva("base", { variants: { v: { danger: "text-red-500" } } })` |
|
|
84
|
+
| Template literals | `` className={`px-4 text-red-500 ${x}`} `` |
|
|
85
|
+
| Ternary expressions | `className={isError ? "text-red-500" : "text-green-500"}` |
|
|
86
|
+
| Standalone strings\* | `const color = "text-red-500"` |
|
|
87
|
+
|
|
88
|
+
\*Requires `checkAllStrings: true` (opt-in).
|
|
89
|
+
|
|
90
|
+
**Bare colors** (flagged by default) — `{prefix}-white`, `{prefix}-black`:
|
|
91
|
+
|
|
92
|
+
| Example | Use instead |
|
|
93
|
+
| ------- | ----------- |
|
|
94
|
+
| `bg-white` | `bg-background` |
|
|
95
|
+
| `text-black` | `text-foreground` |
|
|
96
|
+
|
|
97
|
+
> `transparent` is **not** flagged — it's a CSS reset, not a color choice.
|
|
98
|
+
|
|
99
|
+
**Arbitrary color values** — `{prefix}-[color]`:
|
|
100
|
+
|
|
101
|
+
| Example | Why flagged |
|
|
102
|
+
| ------- | ----------- |
|
|
103
|
+
| `text-[#ff0000]` | Hardcoded hex color |
|
|
104
|
+
| `bg-[red]` | Named CSS color |
|
|
105
|
+
| `border-[rgb(255,0,0)]` | Color function |
|
|
106
|
+
| `text-[hsl(0,100%,50%)]` | Color function |
|
|
107
|
+
|
|
108
|
+
CSS variable references like `text-[var(--color)]` are **not** flagged. Non-color arbitrary values like `text-[14px]` or `w-[100px]` are also **not** flagged.
|
|
109
|
+
|
|
110
|
+
#### What it ignores
|
|
111
|
+
|
|
112
|
+
- Semantic tokens: `text-destructive`, `bg-success`, `border-warning`
|
|
113
|
+
- Non-color utilities: `rounded-lg`, `p-4`, `flex`, `text-sm`
|
|
114
|
+
- CSS inheritance keywords: `text-inherit`, `text-current`
|
|
115
|
+
- Transparent: `bg-transparent`, `border-transparent`, etc.
|
|
116
|
+
- Tailwind modifiers are stripped before matching: `hover:`, `dark:`, `focus:`, `[&>svg]:`, etc.
|
|
117
|
+
- `!important` prefix: `!text-red-500` is still detected
|
|
118
|
+
|
|
119
|
+
#### Supported prefixes
|
|
120
|
+
|
|
121
|
+
`bg`, `text`, `border`, `ring`, `fill`, `stroke`, `shadow`, `outline`, `decoration`, `accent`, `caret`, `divide`, `from`, `via`, `to`, `placeholder`
|
|
122
|
+
|
|
123
|
+
#### Supported palette colors
|
|
124
|
+
|
|
125
|
+
`red`, `green`, `blue`, `amber`, `yellow`, `orange`, `emerald`, `gray`, `slate`, `violet`, `cyan`, `pink`, `purple`, `teal`, `lime`, `indigo`, `fuchsia`, `rose`, `sky`, `zinc`, `neutral`, `stone`
|
|
126
|
+
|
|
127
|
+
#### Configuration
|
|
128
|
+
|
|
129
|
+
| Option | Type | Default | Description |
|
|
130
|
+
| ------ | ---- | ------- | ----------- |
|
|
131
|
+
| `allowedColors` | `string[]` | `[]` | Specific classes to allow (exact match) |
|
|
132
|
+
| `allowedFiles` | `string[]` | `[]` | Glob patterns for files to skip entirely |
|
|
133
|
+
| `allowBareColors` | `boolean` | `false` | When `true`, allows `bg-white`, `text-black`, etc. |
|
|
134
|
+
| `checkAllStrings` | `boolean` | `false` | When `true`, scans all string literals (not just `className` and utility calls). May be noisy in test files. |
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
### `no-inline-color-styles`
|
|
139
|
+
|
|
140
|
+
Flags inline `style` attributes that set color CSS properties with literal values, and SVG presentation attributes (`fill`, `stroke`, etc.) with hardcoded colors.
|
|
141
|
+
|
|
142
|
+
#### What it detects
|
|
143
|
+
|
|
144
|
+
```jsx
|
|
145
|
+
// Inline styles — all flagged:
|
|
146
|
+
<div style={{ color: "red" }} />
|
|
147
|
+
<div style={{ backgroundColor: "#ff0000" }} />
|
|
148
|
+
<div style={{ borderColor: "rgb(255, 0, 0)" }} />
|
|
149
|
+
<div style={{ color: isError ? "red" : "green" }} />
|
|
150
|
+
|
|
151
|
+
// SVG attributes — all flagged:
|
|
152
|
+
<svg fill="red" />
|
|
153
|
+
<circle stroke="#000" />
|
|
154
|
+
<path fill="#ff0000" />
|
|
155
|
+
<stop stopColor="rgb(255, 0, 0)" />
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
#### What it allows
|
|
159
|
+
|
|
160
|
+
```jsx
|
|
161
|
+
// Inline styles — all pass:
|
|
162
|
+
<div style={{ color: "var(--text-primary)" }} /> // CSS variables
|
|
163
|
+
<div style={{ color: "inherit" }} /> // CSS global keywords
|
|
164
|
+
<div style={{ color: "currentColor" }} /> // currentColor
|
|
165
|
+
<div style={{ color: myColor }} /> // Dynamic expressions
|
|
166
|
+
<div style={{ display: "flex" }} /> // Non-color properties
|
|
167
|
+
|
|
168
|
+
// SVG attributes — all pass:
|
|
169
|
+
<svg fill="none" /> // none (common for stroked icons)
|
|
170
|
+
<svg fill="currentColor" /> // currentColor
|
|
171
|
+
<svg fill="url(#gradient)" /> // Gradient/pattern references
|
|
172
|
+
<svg fill="var(--icon-color)" /> // CSS variables
|
|
173
|
+
<svg fill={iconColor} /> // Dynamic expressions
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
> **Note:** This rule uses a deny-by-default approach for inline style values. Any string literal that isn't a CSS variable (`var(--...)`), global keyword (`inherit`, `currentColor`, etc.), or explicitly allowed via `allowedValues` will be flagged — including `"transparent"`. Use `allowedValues: ["transparent"]` to allow it. For SVG attributes, `none`, `transparent`, and `url()` references are allowed by default.
|
|
177
|
+
|
|
178
|
+
<details>
|
|
179
|
+
<summary>Monitored CSS properties (inline styles)</summary>
|
|
180
|
+
|
|
181
|
+
`color`, `backgroundColor`, `borderColor`, `borderTopColor`, `borderRightColor`, `borderBottomColor`, `borderLeftColor`, `borderBlockColor`, `borderBlockStartColor`, `borderBlockEndColor`, `borderInlineColor`, `borderInlineStartColor`, `borderInlineEndColor`, `outlineColor`, `textDecorationColor`, `fill`, `stroke`, `caretColor`, `accentColor`, `columnRuleColor`, `floodColor`, `lightingColor`, `stopColor`
|
|
182
|
+
|
|
183
|
+
</details>
|
|
184
|
+
|
|
185
|
+
<details>
|
|
186
|
+
<summary>Monitored SVG attributes</summary>
|
|
187
|
+
|
|
188
|
+
`fill`, `stroke`, `color`, `stopColor`, `stop-color`, `floodColor`, `flood-color`, `lightingColor`, `lighting-color`
|
|
189
|
+
|
|
190
|
+
</details>
|
|
191
|
+
|
|
192
|
+
#### Configuration
|
|
193
|
+
|
|
194
|
+
| Option | Type | Default | Description |
|
|
195
|
+
| ------ | ---- | ------- | ----------- |
|
|
196
|
+
| `allowedProperties` | `string[]` | `[]` | CSS properties or SVG attributes to skip (e.g. `["fill"]`) |
|
|
197
|
+
| `allowedValues` | `string[]` | `[]` | Values to allow (e.g. `["transparent"]`) |
|
|
198
|
+
| `allowedFiles` | `string[]` | `[]` | Glob patterns for files to skip entirely |
|
|
199
|
+
|
|
200
|
+
## Configs
|
|
201
|
+
|
|
202
|
+
| Config | Rules |
|
|
203
|
+
| ------ | ----- |
|
|
204
|
+
| `recommended` | `no-palette-colors: warn` |
|
|
205
|
+
| `strict` | `no-palette-colors: warn` + `no-inline-color-styles: warn` |
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
// src/rules/no-inline-color-styles.ts
|
|
2
|
+
import { minimatch } from "minimatch";
|
|
3
|
+
|
|
4
|
+
// src/utils/css-colors.ts
|
|
5
|
+
var COLOR_PROPERTIES = /* @__PURE__ */ new Set([
|
|
6
|
+
"color",
|
|
7
|
+
"backgroundColor",
|
|
8
|
+
"borderColor",
|
|
9
|
+
"borderTopColor",
|
|
10
|
+
"borderRightColor",
|
|
11
|
+
"borderBottomColor",
|
|
12
|
+
"borderLeftColor",
|
|
13
|
+
"borderBlockColor",
|
|
14
|
+
"borderBlockStartColor",
|
|
15
|
+
"borderBlockEndColor",
|
|
16
|
+
"borderInlineColor",
|
|
17
|
+
"borderInlineStartColor",
|
|
18
|
+
"borderInlineEndColor",
|
|
19
|
+
"outlineColor",
|
|
20
|
+
"textDecorationColor",
|
|
21
|
+
"fill",
|
|
22
|
+
"stroke",
|
|
23
|
+
"caretColor",
|
|
24
|
+
"accentColor",
|
|
25
|
+
"columnRuleColor",
|
|
26
|
+
"floodColor",
|
|
27
|
+
"lightingColor",
|
|
28
|
+
"stopColor"
|
|
29
|
+
]);
|
|
30
|
+
var SVG_COLOR_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
31
|
+
"fill",
|
|
32
|
+
"stroke",
|
|
33
|
+
"color",
|
|
34
|
+
"stopColor",
|
|
35
|
+
"stop-color",
|
|
36
|
+
"floodColor",
|
|
37
|
+
"flood-color",
|
|
38
|
+
"lightingColor",
|
|
39
|
+
"lighting-color"
|
|
40
|
+
]);
|
|
41
|
+
var CSS_GLOBAL_VALUES = /* @__PURE__ */ new Set([
|
|
42
|
+
"inherit",
|
|
43
|
+
"initial",
|
|
44
|
+
"unset",
|
|
45
|
+
"revert",
|
|
46
|
+
"revert-layer",
|
|
47
|
+
"currentColor",
|
|
48
|
+
"currentcolor"
|
|
49
|
+
]);
|
|
50
|
+
var SVG_ALLOWED_VALUES = /* @__PURE__ */ new Set([
|
|
51
|
+
"none",
|
|
52
|
+
"transparent",
|
|
53
|
+
"inherit",
|
|
54
|
+
"initial",
|
|
55
|
+
"unset",
|
|
56
|
+
"revert",
|
|
57
|
+
"currentColor",
|
|
58
|
+
"currentcolor",
|
|
59
|
+
"context-fill",
|
|
60
|
+
"context-stroke"
|
|
61
|
+
]);
|
|
62
|
+
var CSS_NAMED_COLORS = /* @__PURE__ */ new Set([
|
|
63
|
+
"aliceblue",
|
|
64
|
+
"antiquewhite",
|
|
65
|
+
"aqua",
|
|
66
|
+
"aquamarine",
|
|
67
|
+
"azure",
|
|
68
|
+
"beige",
|
|
69
|
+
"bisque",
|
|
70
|
+
"black",
|
|
71
|
+
"blanchedalmond",
|
|
72
|
+
"blue",
|
|
73
|
+
"blueviolet",
|
|
74
|
+
"brown",
|
|
75
|
+
"burlywood",
|
|
76
|
+
"cadetblue",
|
|
77
|
+
"chartreuse",
|
|
78
|
+
"chocolate",
|
|
79
|
+
"coral",
|
|
80
|
+
"cornflowerblue",
|
|
81
|
+
"cornsilk",
|
|
82
|
+
"crimson",
|
|
83
|
+
"cyan",
|
|
84
|
+
"darkblue",
|
|
85
|
+
"darkcyan",
|
|
86
|
+
"darkgoldenrod",
|
|
87
|
+
"darkgray",
|
|
88
|
+
"darkgreen",
|
|
89
|
+
"darkgrey",
|
|
90
|
+
"darkkhaki",
|
|
91
|
+
"darkmagenta",
|
|
92
|
+
"darkolivegreen",
|
|
93
|
+
"darkorange",
|
|
94
|
+
"darkorchid",
|
|
95
|
+
"darkred",
|
|
96
|
+
"darksalmon",
|
|
97
|
+
"darkseagreen",
|
|
98
|
+
"darkslateblue",
|
|
99
|
+
"darkslategray",
|
|
100
|
+
"darkslategrey",
|
|
101
|
+
"darkturquoise",
|
|
102
|
+
"darkviolet",
|
|
103
|
+
"deeppink",
|
|
104
|
+
"deepskyblue",
|
|
105
|
+
"dimgray",
|
|
106
|
+
"dimgrey",
|
|
107
|
+
"dodgerblue",
|
|
108
|
+
"firebrick",
|
|
109
|
+
"floralwhite",
|
|
110
|
+
"forestgreen",
|
|
111
|
+
"fuchsia",
|
|
112
|
+
"gainsboro",
|
|
113
|
+
"ghostwhite",
|
|
114
|
+
"gold",
|
|
115
|
+
"goldenrod",
|
|
116
|
+
"gray",
|
|
117
|
+
"green",
|
|
118
|
+
"greenyellow",
|
|
119
|
+
"grey",
|
|
120
|
+
"honeydew",
|
|
121
|
+
"hotpink",
|
|
122
|
+
"indianred",
|
|
123
|
+
"indigo",
|
|
124
|
+
"ivory",
|
|
125
|
+
"khaki",
|
|
126
|
+
"lavender",
|
|
127
|
+
"lavenderblush",
|
|
128
|
+
"lawngreen",
|
|
129
|
+
"lemonchiffon",
|
|
130
|
+
"lightblue",
|
|
131
|
+
"lightcoral",
|
|
132
|
+
"lightcyan",
|
|
133
|
+
"lightgoldenrodyellow",
|
|
134
|
+
"lightgray",
|
|
135
|
+
"lightgreen",
|
|
136
|
+
"lightgrey",
|
|
137
|
+
"lightpink",
|
|
138
|
+
"lightsalmon",
|
|
139
|
+
"lightseagreen",
|
|
140
|
+
"lightskyblue",
|
|
141
|
+
"lightslategray",
|
|
142
|
+
"lightslategrey",
|
|
143
|
+
"lightsteelblue",
|
|
144
|
+
"lightyellow",
|
|
145
|
+
"lime",
|
|
146
|
+
"limegreen",
|
|
147
|
+
"linen",
|
|
148
|
+
"magenta",
|
|
149
|
+
"maroon",
|
|
150
|
+
"mediumaquamarine",
|
|
151
|
+
"mediumblue",
|
|
152
|
+
"mediumorchid",
|
|
153
|
+
"mediumpurple",
|
|
154
|
+
"mediumseagreen",
|
|
155
|
+
"mediumslateblue",
|
|
156
|
+
"mediumspringgreen",
|
|
157
|
+
"mediumturquoise",
|
|
158
|
+
"mediumvioletred",
|
|
159
|
+
"midnightblue",
|
|
160
|
+
"mintcream",
|
|
161
|
+
"mistyrose",
|
|
162
|
+
"moccasin",
|
|
163
|
+
"navajowhite",
|
|
164
|
+
"navy",
|
|
165
|
+
"oldlace",
|
|
166
|
+
"olive",
|
|
167
|
+
"olivedrab",
|
|
168
|
+
"orange",
|
|
169
|
+
"orangered",
|
|
170
|
+
"orchid",
|
|
171
|
+
"palegoldenrod",
|
|
172
|
+
"palegreen",
|
|
173
|
+
"paleturquoise",
|
|
174
|
+
"palevioletred",
|
|
175
|
+
"papayawhip",
|
|
176
|
+
"peachpuff",
|
|
177
|
+
"peru",
|
|
178
|
+
"pink",
|
|
179
|
+
"plum",
|
|
180
|
+
"powderblue",
|
|
181
|
+
"purple",
|
|
182
|
+
"rebeccapurple",
|
|
183
|
+
"red",
|
|
184
|
+
"rosybrown",
|
|
185
|
+
"royalblue",
|
|
186
|
+
"saddlebrown",
|
|
187
|
+
"salmon",
|
|
188
|
+
"sandybrown",
|
|
189
|
+
"seagreen",
|
|
190
|
+
"seashell",
|
|
191
|
+
"sienna",
|
|
192
|
+
"silver",
|
|
193
|
+
"skyblue",
|
|
194
|
+
"slateblue",
|
|
195
|
+
"slategray",
|
|
196
|
+
"slategrey",
|
|
197
|
+
"snow",
|
|
198
|
+
"springgreen",
|
|
199
|
+
"steelblue",
|
|
200
|
+
"tan",
|
|
201
|
+
"teal",
|
|
202
|
+
"thistle",
|
|
203
|
+
"tomato",
|
|
204
|
+
"turquoise",
|
|
205
|
+
"violet",
|
|
206
|
+
"wheat",
|
|
207
|
+
"white",
|
|
208
|
+
"whitesmoke",
|
|
209
|
+
"yellow",
|
|
210
|
+
"yellowgreen"
|
|
211
|
+
]);
|
|
212
|
+
function isColorValue(value) {
|
|
213
|
+
const trimmed = value.trim().toLowerCase();
|
|
214
|
+
if (/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(trimmed)) return true;
|
|
215
|
+
if (/^(rgba?|hsla?|oklch|oklab|lch|lab|color)\(/.test(trimmed)) return true;
|
|
216
|
+
if (CSS_NAMED_COLORS.has(trimmed)) return true;
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/rules/no-inline-color-styles.ts
|
|
221
|
+
function isColorProperty(name, allowedProperties) {
|
|
222
|
+
if (allowedProperties.has(name)) return false;
|
|
223
|
+
return COLOR_PROPERTIES.has(name);
|
|
224
|
+
}
|
|
225
|
+
function isDisallowedValue(value, allowedValues) {
|
|
226
|
+
const trimmed = value.trim();
|
|
227
|
+
if (allowedValues.has(trimmed)) return false;
|
|
228
|
+
if (CSS_GLOBAL_VALUES.has(trimmed)) return false;
|
|
229
|
+
if (trimmed.startsWith("var(")) return false;
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
function isSvgColorAttribute(name, allowedProperties) {
|
|
233
|
+
if (allowedProperties.has(name)) return false;
|
|
234
|
+
return SVG_COLOR_ATTRIBUTES.has(name);
|
|
235
|
+
}
|
|
236
|
+
function isDisallowedSvgValue(value, allowedValues) {
|
|
237
|
+
const trimmed = value.trim();
|
|
238
|
+
if (allowedValues.has(trimmed)) return false;
|
|
239
|
+
if (SVG_ALLOWED_VALUES.has(trimmed)) return false;
|
|
240
|
+
if (trimmed.startsWith("var(")) return false;
|
|
241
|
+
if (trimmed.startsWith("url(")) return false;
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
var rule = {
|
|
245
|
+
meta: {
|
|
246
|
+
type: "suggestion",
|
|
247
|
+
docs: {
|
|
248
|
+
description: "Disallow inline style color properties and SVG color attributes; use Tailwind classes or design tokens instead"
|
|
249
|
+
},
|
|
250
|
+
messages: {
|
|
251
|
+
inlineColor: "Avoid inline color style '{{property}}: {{value}}'. Use a Tailwind class or design token instead.",
|
|
252
|
+
svgColor: `Avoid hardcoded SVG color '{{attribute}}="{{value}}"'. Use a design token (e.g. currentColor or a CSS variable) instead.`
|
|
253
|
+
},
|
|
254
|
+
schema: [
|
|
255
|
+
{
|
|
256
|
+
type: "object",
|
|
257
|
+
properties: {
|
|
258
|
+
allowedProperties: {
|
|
259
|
+
type: "array",
|
|
260
|
+
items: { type: "string" }
|
|
261
|
+
},
|
|
262
|
+
allowedValues: {
|
|
263
|
+
type: "array",
|
|
264
|
+
items: { type: "string" }
|
|
265
|
+
},
|
|
266
|
+
allowedFiles: {
|
|
267
|
+
type: "array",
|
|
268
|
+
items: { type: "string" }
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
additionalProperties: false
|
|
272
|
+
}
|
|
273
|
+
]
|
|
274
|
+
},
|
|
275
|
+
create(context) {
|
|
276
|
+
const options = context.options[0] ?? {};
|
|
277
|
+
const allowedProperties = new Set(options.allowedProperties ?? []);
|
|
278
|
+
const allowedValues = new Set(options.allowedValues ?? []);
|
|
279
|
+
const allowedFiles = options.allowedFiles ?? [];
|
|
280
|
+
if (allowedFiles.length > 0 && allowedFiles.some((pattern) => minimatch(context.filename, pattern, { dot: true }))) {
|
|
281
|
+
return {};
|
|
282
|
+
}
|
|
283
|
+
function checkStyleValue(node, property, value) {
|
|
284
|
+
switch (value.type) {
|
|
285
|
+
case "Literal":
|
|
286
|
+
if (typeof value.value === "string") {
|
|
287
|
+
if (isDisallowedValue(value.value, allowedValues)) {
|
|
288
|
+
context.report({
|
|
289
|
+
node: value,
|
|
290
|
+
messageId: "inlineColor",
|
|
291
|
+
data: { property, value: value.value }
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
break;
|
|
296
|
+
case "TemplateLiteral":
|
|
297
|
+
if (value.expressions.length === 0 && value.quasis.length === 1) {
|
|
298
|
+
const raw = value.quasis[0].value.raw;
|
|
299
|
+
if (isDisallowedValue(raw, allowedValues)) {
|
|
300
|
+
context.report({
|
|
301
|
+
node: value,
|
|
302
|
+
messageId: "inlineColor",
|
|
303
|
+
data: { property, value: raw }
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
break;
|
|
308
|
+
case "ConditionalExpression":
|
|
309
|
+
checkStyleValue(node, property, value.consequent);
|
|
310
|
+
checkStyleValue(node, property, value.alternate);
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
// Inline style={{ color: "red" }}
|
|
316
|
+
'JSXAttribute[name.name="style"]'(node) {
|
|
317
|
+
const attr = node;
|
|
318
|
+
if (!attr.value) return;
|
|
319
|
+
if (attr.value.type !== "JSXExpressionContainer") return;
|
|
320
|
+
const expr = attr.value.expression;
|
|
321
|
+
if (!expr || expr.type !== "ObjectExpression") return;
|
|
322
|
+
const obj = expr;
|
|
323
|
+
for (const prop of obj.properties) {
|
|
324
|
+
if (prop.type !== "Property") continue;
|
|
325
|
+
let propertyName;
|
|
326
|
+
if (prop.key.type === "Identifier") {
|
|
327
|
+
propertyName = prop.key.name;
|
|
328
|
+
} else if (prop.key.type === "Literal" && typeof prop.key.value === "string") {
|
|
329
|
+
propertyName = prop.key.value;
|
|
330
|
+
}
|
|
331
|
+
if (!propertyName) continue;
|
|
332
|
+
if (!isColorProperty(propertyName, allowedProperties)) continue;
|
|
333
|
+
checkStyleValue(node, propertyName, prop.value);
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
// SVG color attributes: <svg fill="red">, <circle stroke="#000">
|
|
337
|
+
JSXAttribute(node) {
|
|
338
|
+
const attr = node;
|
|
339
|
+
if (attr.name.type !== "JSXIdentifier") return;
|
|
340
|
+
const attrName = attr.name.name;
|
|
341
|
+
if (attrName === "style" || attrName === "className") return;
|
|
342
|
+
if (!isSvgColorAttribute(attrName, allowedProperties)) return;
|
|
343
|
+
if (!attr.value) return;
|
|
344
|
+
if (attr.value.type === "Literal" && typeof attr.value.value === "string") {
|
|
345
|
+
if (isDisallowedSvgValue(attr.value.value, allowedValues)) {
|
|
346
|
+
context.report({
|
|
347
|
+
node: attr.value,
|
|
348
|
+
messageId: "svgColor",
|
|
349
|
+
data: { attribute: attrName, value: attr.value.value }
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
} else if (attr.value.type === "JSXExpressionContainer" && attr.value.expression) {
|
|
353
|
+
const expr = attr.value.expression;
|
|
354
|
+
if (expr.type === "Literal" && typeof expr.value === "string") {
|
|
355
|
+
if (isDisallowedSvgValue(expr.value, allowedValues)) {
|
|
356
|
+
context.report({
|
|
357
|
+
node: expr,
|
|
358
|
+
messageId: "svgColor",
|
|
359
|
+
data: { attribute: attrName, value: expr.value }
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
} else if (expr.type === "ConditionalExpression") {
|
|
363
|
+
for (const branch of [expr.consequent, expr.alternate]) {
|
|
364
|
+
if (branch.type === "Literal" && typeof branch.value === "string") {
|
|
365
|
+
if (isDisallowedSvgValue(branch.value, allowedValues)) {
|
|
366
|
+
context.report({
|
|
367
|
+
node: branch,
|
|
368
|
+
messageId: "svgColor",
|
|
369
|
+
data: { attribute: attrName, value: branch.value }
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
var no_inline_color_styles_default = rule;
|
|
381
|
+
|
|
382
|
+
// src/rules/no-palette-colors.ts
|
|
383
|
+
import { minimatch as minimatch2 } from "minimatch";
|
|
384
|
+
|
|
385
|
+
// src/utils/class-extractors.ts
|
|
386
|
+
var CLASS_UTILITY_NAMES = /* @__PURE__ */ new Set(["cn", "clsx", "classnames", "cx", "twMerge", "twJoin"]);
|
|
387
|
+
function isClassUtility(node) {
|
|
388
|
+
if (node.type !== "CallExpression") return false;
|
|
389
|
+
const callee = node.callee;
|
|
390
|
+
if (callee.type === "Identifier") {
|
|
391
|
+
return CLASS_UTILITY_NAMES.has(callee.name);
|
|
392
|
+
}
|
|
393
|
+
if (callee.type === "MemberExpression" && callee.property.type === "Identifier") {
|
|
394
|
+
return CLASS_UTILITY_NAMES.has(callee.property.name);
|
|
395
|
+
}
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
function isCvaCall(node) {
|
|
399
|
+
if (node.type !== "CallExpression") return false;
|
|
400
|
+
const callee = node.callee;
|
|
401
|
+
return callee.type === "Identifier" && callee.name === "cva";
|
|
402
|
+
}
|
|
403
|
+
function* extractStringNodes(node) {
|
|
404
|
+
if (!node) return;
|
|
405
|
+
switch (node.type) {
|
|
406
|
+
case "Literal":
|
|
407
|
+
if (typeof node.value === "string") {
|
|
408
|
+
yield { value: node.value, node };
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
case "TemplateLiteral":
|
|
412
|
+
for (const quasi of node.quasis) {
|
|
413
|
+
if (quasi.value.raw) {
|
|
414
|
+
yield { value: quasi.value.raw, node: quasi };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
break;
|
|
418
|
+
case "ConditionalExpression":
|
|
419
|
+
yield* extractStringNodes(node.consequent);
|
|
420
|
+
yield* extractStringNodes(node.alternate);
|
|
421
|
+
break;
|
|
422
|
+
case "LogicalExpression":
|
|
423
|
+
yield* extractStringNodes(node.left);
|
|
424
|
+
yield* extractStringNodes(node.right);
|
|
425
|
+
break;
|
|
426
|
+
case "ObjectExpression":
|
|
427
|
+
for (const prop of node.properties) {
|
|
428
|
+
if (prop.type === "Property") {
|
|
429
|
+
yield* extractStringNodes(prop.key);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
break;
|
|
433
|
+
case "ArrayExpression":
|
|
434
|
+
for (const element of node.elements) {
|
|
435
|
+
if (element) {
|
|
436
|
+
yield* extractStringNodes(element);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
break;
|
|
440
|
+
case "CallExpression":
|
|
441
|
+
for (const arg of node.arguments) {
|
|
442
|
+
yield* extractStringNodes(arg);
|
|
443
|
+
}
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function* extractCvaStrings(node) {
|
|
448
|
+
if (node.type !== "CallExpression") return;
|
|
449
|
+
const [baseArg, configArg] = node.arguments;
|
|
450
|
+
if (baseArg) {
|
|
451
|
+
yield* extractStringNodes(baseArg);
|
|
452
|
+
}
|
|
453
|
+
if (configArg && configArg.type === "ObjectExpression") {
|
|
454
|
+
for (const prop of configArg.properties) {
|
|
455
|
+
if (prop.type !== "Property" || prop.key.type !== "Identifier") continue;
|
|
456
|
+
if (prop.key.name === "variants" && prop.value.type === "ObjectExpression") {
|
|
457
|
+
for (const variantGroup of prop.value.properties) {
|
|
458
|
+
if (variantGroup.type === "Property" && variantGroup.value.type === "ObjectExpression") {
|
|
459
|
+
for (const variantOption of variantGroup.value.properties) {
|
|
460
|
+
if (variantOption.type === "Property") {
|
|
461
|
+
yield* extractStringNodes(variantOption.value);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
} else if (prop.key.name === "compoundVariants" && prop.value.type === "ArrayExpression") {
|
|
467
|
+
for (const element of prop.value.elements) {
|
|
468
|
+
if (element && element.type === "ObjectExpression") {
|
|
469
|
+
for (const cvProp of element.properties) {
|
|
470
|
+
if (cvProp.type === "Property" && cvProp.key.type === "Identifier" && (cvProp.key.name === "class" || cvProp.key.name === "className")) {
|
|
471
|
+
yield* extractStringNodes(cvProp.value);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/utils/palette-regex.ts
|
|
482
|
+
var PREFIXES = [
|
|
483
|
+
"bg",
|
|
484
|
+
"text",
|
|
485
|
+
"border",
|
|
486
|
+
"ring",
|
|
487
|
+
"fill",
|
|
488
|
+
"stroke",
|
|
489
|
+
"shadow",
|
|
490
|
+
"outline",
|
|
491
|
+
"decoration",
|
|
492
|
+
"accent",
|
|
493
|
+
"caret",
|
|
494
|
+
"divide",
|
|
495
|
+
"from",
|
|
496
|
+
"via",
|
|
497
|
+
"to",
|
|
498
|
+
"placeholder"
|
|
499
|
+
];
|
|
500
|
+
var COLORS = [
|
|
501
|
+
"red",
|
|
502
|
+
"green",
|
|
503
|
+
"blue",
|
|
504
|
+
"amber",
|
|
505
|
+
"yellow",
|
|
506
|
+
"orange",
|
|
507
|
+
"emerald",
|
|
508
|
+
"gray",
|
|
509
|
+
"slate",
|
|
510
|
+
"violet",
|
|
511
|
+
"cyan",
|
|
512
|
+
"pink",
|
|
513
|
+
"purple",
|
|
514
|
+
"teal",
|
|
515
|
+
"lime",
|
|
516
|
+
"indigo",
|
|
517
|
+
"fuchsia",
|
|
518
|
+
"rose",
|
|
519
|
+
"sky",
|
|
520
|
+
"zinc",
|
|
521
|
+
"neutral",
|
|
522
|
+
"stone"
|
|
523
|
+
];
|
|
524
|
+
var SHADES = [
|
|
525
|
+
"50",
|
|
526
|
+
"100",
|
|
527
|
+
"200",
|
|
528
|
+
"300",
|
|
529
|
+
"400",
|
|
530
|
+
"500",
|
|
531
|
+
"600",
|
|
532
|
+
"700",
|
|
533
|
+
"800",
|
|
534
|
+
"900",
|
|
535
|
+
"950"
|
|
536
|
+
];
|
|
537
|
+
var BARE_COLORS = ["white", "black"];
|
|
538
|
+
var prefixGroup = PREFIXES.join("|");
|
|
539
|
+
var colorGroup = COLORS.join("|");
|
|
540
|
+
var shadeGroup = SHADES.join("|");
|
|
541
|
+
var bareGroup = BARE_COLORS.join("|");
|
|
542
|
+
var PALETTE_REGEX = new RegExp(
|
|
543
|
+
`^(${prefixGroup})-(${colorGroup})-(${shadeGroup})(\\/\\d+)?$`
|
|
544
|
+
);
|
|
545
|
+
var BARE_COLOR_REGEX = new RegExp(
|
|
546
|
+
`^(${prefixGroup})-(${bareGroup})(\\/\\d+)?$`
|
|
547
|
+
);
|
|
548
|
+
var ARBITRARY_COLOR_REGEX = new RegExp(
|
|
549
|
+
`^(${prefixGroup})-\\[([^\\]]+)\\](\\/\\d+)?$`
|
|
550
|
+
);
|
|
551
|
+
function stripModifiers(className) {
|
|
552
|
+
const lastColon = className.lastIndexOf(":");
|
|
553
|
+
if (lastColon === -1) return className;
|
|
554
|
+
return className.slice(lastColon + 1);
|
|
555
|
+
}
|
|
556
|
+
function cleanClass(className) {
|
|
557
|
+
const base = stripModifiers(className);
|
|
558
|
+
return base.startsWith("!") ? base.slice(1) : base;
|
|
559
|
+
}
|
|
560
|
+
function isPaletteColor(className) {
|
|
561
|
+
return PALETTE_REGEX.test(cleanClass(className));
|
|
562
|
+
}
|
|
563
|
+
function isBareColor(className) {
|
|
564
|
+
return BARE_COLOR_REGEX.test(cleanClass(className));
|
|
565
|
+
}
|
|
566
|
+
function isArbitraryColor(className) {
|
|
567
|
+
const cleaned = cleanClass(className);
|
|
568
|
+
const match = ARBITRARY_COLOR_REGEX.exec(cleaned);
|
|
569
|
+
if (!match) return false;
|
|
570
|
+
const bracketContent = match[2];
|
|
571
|
+
if (bracketContent.startsWith("var(")) return false;
|
|
572
|
+
return isColorValue(bracketContent);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/utils/string-scanner.ts
|
|
576
|
+
function scanForPaletteColors(classString, options = {}) {
|
|
577
|
+
const { allowedColors = /* @__PURE__ */ new Set(), allowBareColors = false } = options;
|
|
578
|
+
const violations = [];
|
|
579
|
+
for (const token of classString.split(/\s+/)) {
|
|
580
|
+
if (!token) continue;
|
|
581
|
+
if (allowedColors.has(token)) continue;
|
|
582
|
+
if (isPaletteColor(token)) {
|
|
583
|
+
violations.push(token);
|
|
584
|
+
} else if (!allowBareColors && isBareColor(token)) {
|
|
585
|
+
violations.push(token);
|
|
586
|
+
} else if (isArbitraryColor(token)) {
|
|
587
|
+
violations.push(token);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return violations;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/rules/no-palette-colors.ts
|
|
594
|
+
var rule2 = {
|
|
595
|
+
meta: {
|
|
596
|
+
type: "suggestion",
|
|
597
|
+
docs: {
|
|
598
|
+
description: "Disallow hardcoded Tailwind palette colors; use semantic design tokens instead"
|
|
599
|
+
},
|
|
600
|
+
messages: {
|
|
601
|
+
paletteColor: "Avoid hardcoded palette color '{{className}}'. Use a semantic design token instead."
|
|
602
|
+
},
|
|
603
|
+
schema: [
|
|
604
|
+
{
|
|
605
|
+
type: "object",
|
|
606
|
+
properties: {
|
|
607
|
+
allowedColors: {
|
|
608
|
+
type: "array",
|
|
609
|
+
items: { type: "string" }
|
|
610
|
+
},
|
|
611
|
+
allowedFiles: {
|
|
612
|
+
type: "array",
|
|
613
|
+
items: { type: "string" }
|
|
614
|
+
},
|
|
615
|
+
allowBareColors: {
|
|
616
|
+
type: "boolean"
|
|
617
|
+
},
|
|
618
|
+
checkAllStrings: {
|
|
619
|
+
type: "boolean"
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
additionalProperties: false
|
|
623
|
+
}
|
|
624
|
+
]
|
|
625
|
+
},
|
|
626
|
+
create(context) {
|
|
627
|
+
const options = context.options[0] ?? {};
|
|
628
|
+
const allowedColors = new Set(options.allowedColors ?? []);
|
|
629
|
+
const allowedFiles = options.allowedFiles ?? [];
|
|
630
|
+
const allowBareColors = options.allowBareColors ?? false;
|
|
631
|
+
const checkAllStrings = options.checkAllStrings ?? false;
|
|
632
|
+
if (allowedFiles.length > 0 && allowedFiles.some((pattern) => minimatch2(context.filename, pattern, { dot: true }))) {
|
|
633
|
+
return {};
|
|
634
|
+
}
|
|
635
|
+
const scanOptions = { allowedColors, allowBareColors };
|
|
636
|
+
const handledNodes = /* @__PURE__ */ new WeakSet();
|
|
637
|
+
function reportViolations(node, classString) {
|
|
638
|
+
for (const className of scanForPaletteColors(classString, scanOptions)) {
|
|
639
|
+
context.report({
|
|
640
|
+
node,
|
|
641
|
+
messageId: "paletteColor",
|
|
642
|
+
data: { className }
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function processNode(node) {
|
|
647
|
+
for (const extracted of extractStringNodes(node)) {
|
|
648
|
+
if (handledNodes.has(extracted.node)) continue;
|
|
649
|
+
handledNodes.add(extracted.node);
|
|
650
|
+
reportViolations(extracted.node, extracted.value);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
// className="..." / className={`...`} / className={cond ? "..." : "..."}
|
|
655
|
+
'JSXAttribute[name.name="className"]'(node) {
|
|
656
|
+
const attr = node;
|
|
657
|
+
if (!attr.value) return;
|
|
658
|
+
if (attr.value.type === "Literal" && typeof attr.value.value === "string") {
|
|
659
|
+
const literalNode = attr.value;
|
|
660
|
+
if (!handledNodes.has(literalNode)) {
|
|
661
|
+
handledNodes.add(literalNode);
|
|
662
|
+
reportViolations(literalNode, attr.value.value);
|
|
663
|
+
}
|
|
664
|
+
} else if (attr.value.type === "JSXExpressionContainer") {
|
|
665
|
+
processNode(attr.value.expression);
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
// cn(...), clsx(...), classnames(...), cva(...)
|
|
669
|
+
CallExpression(node) {
|
|
670
|
+
if (node.type !== "CallExpression") return;
|
|
671
|
+
if (isCvaCall(node)) {
|
|
672
|
+
for (const extracted of extractCvaStrings(node)) {
|
|
673
|
+
if (handledNodes.has(extracted.node)) continue;
|
|
674
|
+
handledNodes.add(extracted.node);
|
|
675
|
+
reportViolations(extracted.node, extracted.value);
|
|
676
|
+
}
|
|
677
|
+
} else if (isClassUtility(node)) {
|
|
678
|
+
for (const arg of node.arguments) {
|
|
679
|
+
processNode(arg);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
// Catch-all: any string literal not already handled by specific visitors
|
|
684
|
+
// Only enabled when checkAllStrings is true (opt-in)
|
|
685
|
+
...checkAllStrings ? {
|
|
686
|
+
Literal(node) {
|
|
687
|
+
if (handledNodes.has(node)) return;
|
|
688
|
+
if (node.type !== "Literal") return;
|
|
689
|
+
if (typeof node.value !== "string") return;
|
|
690
|
+
reportViolations(node, node.value);
|
|
691
|
+
},
|
|
692
|
+
TemplateLiteral(node) {
|
|
693
|
+
if (handledNodes.has(node)) return;
|
|
694
|
+
if (node.type !== "TemplateLiteral") return;
|
|
695
|
+
for (const quasi of node.quasis) {
|
|
696
|
+
if (handledNodes.has(quasi)) continue;
|
|
697
|
+
if (quasi.value.raw) {
|
|
698
|
+
reportViolations(quasi, quasi.value.raw);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
} : {}
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
var no_palette_colors_default = rule2;
|
|
707
|
+
|
|
708
|
+
// src/index.ts
|
|
709
|
+
var plugin = {
|
|
710
|
+
meta: {
|
|
711
|
+
name: "eslint-plugin-tailwind-palette-guard",
|
|
712
|
+
version: "0.1.0"
|
|
713
|
+
},
|
|
714
|
+
rules: {
|
|
715
|
+
"no-palette-colors": no_palette_colors_default,
|
|
716
|
+
"no-inline-color-styles": no_inline_color_styles_default
|
|
717
|
+
},
|
|
718
|
+
configs: {}
|
|
719
|
+
};
|
|
720
|
+
Object.assign(plugin.configs, {
|
|
721
|
+
recommended: {
|
|
722
|
+
plugins: {
|
|
723
|
+
"tailwind-palette-guard": plugin
|
|
724
|
+
},
|
|
725
|
+
rules: {
|
|
726
|
+
"tailwind-palette-guard/no-palette-colors": "warn"
|
|
727
|
+
}
|
|
728
|
+
},
|
|
729
|
+
strict: {
|
|
730
|
+
plugins: {
|
|
731
|
+
"tailwind-palette-guard": plugin
|
|
732
|
+
},
|
|
733
|
+
rules: {
|
|
734
|
+
"tailwind-palette-guard/no-palette-colors": "warn",
|
|
735
|
+
"tailwind-palette-guard/no-inline-color-styles": "warn"
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
var index_default = plugin;
|
|
740
|
+
export {
|
|
741
|
+
index_default as default
|
|
742
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-tailwind-palette-guard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ESLint plugin that flags hardcoded Tailwind palette colors in favor of semantic design tokens",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"eslint",
|
|
7
|
+
"eslintplugin",
|
|
8
|
+
"tailwind",
|
|
9
|
+
"tailwindcss",
|
|
10
|
+
"design-tokens",
|
|
11
|
+
"lint"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/johanlyckenvik/eslint-plugin-tailwind-palette-guard.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/johanlyckenvik/eslint-plugin-tailwind-palette-guard/issues"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/johanlyckenvik/eslint-plugin-tailwind-palette-guard#readme",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "johanlyckenvik",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest",
|
|
39
|
+
"prepublishOnly": "npm run build",
|
|
40
|
+
"tsc": "tsc --noEmit"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"eslint": ">=9"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/eslint": "^9.6.0",
|
|
50
|
+
"@types/node": "^22.0.0",
|
|
51
|
+
"eslint": "^9.0.0",
|
|
52
|
+
"tsup": "^8.0.0",
|
|
53
|
+
"typescript": "^5.7.0",
|
|
54
|
+
"vitest": "^3.0.0"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"minimatch": "^10.0.0"
|
|
58
|
+
}
|
|
59
|
+
}
|