eslint-plugin-tailwind-canonical-classes 1.0.7 → 1.0.10
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/README.md +238 -53
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/rules/tailwind-canonical-classes.d.ts +3 -0
- package/lib/rules/tailwind-canonical-classes.js +204 -0
- package/lib/rules/tailwind-worker.d.ts +1 -0
- package/lib/rules/tailwind-worker.js +13 -0
- package/package.json +56 -11
- package/index.d.ts +0 -10
- package/index.js +0 -7
- package/lib/tailwind-canonical-classes.d.ts +0 -11
- package/lib/tailwind-canonical-classes.js +0 -360
package/README.md
CHANGED
|
@@ -1,31 +1,110 @@
|
|
|
1
1
|
# eslint-plugin-tailwind-canonical-classes
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/eslint-plugin-tailwind-canonical-classes)
|
|
4
|
+
[](https://www.npmjs.com/package/eslint-plugin-tailwind-canonical-classes)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://nodejs.org/)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
> ESLint plugin to enforce canonical Tailwind CSS class names using Tailwind CSS v4's canonicalization API.
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
## 📋 Table of Contents
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
+
- [Features](#-features)
|
|
13
|
+
- [Installation](#-installation)
|
|
14
|
+
- [Quick Start](#-quick-start)
|
|
15
|
+
- [Configuration](#-configuration)
|
|
16
|
+
- [Options](#-options)
|
|
17
|
+
- [Usage Examples](#-usage-examples)
|
|
18
|
+
- [How It Works](#-how-it-works)
|
|
19
|
+
- [Limitations](#-limitations)
|
|
20
|
+
- [Troubleshooting](#-troubleshooting)
|
|
21
|
+
- [Contributing](#-contributing)
|
|
22
|
+
- [License](#-license)
|
|
23
|
+
- [Related Links](#-related-links)
|
|
12
24
|
|
|
13
|
-
##
|
|
25
|
+
## ✨ Features
|
|
26
|
+
|
|
27
|
+
- 🔍 **Automatic Detection**: Automatically detects non-canonical Tailwind CSS class names in your JSX/TSX files
|
|
28
|
+
- 🔧 **Auto-fix Support**: Automatically fixes non-canonical classes using ESLint's auto-fix feature
|
|
29
|
+
- 🎯 **Tailwind CSS v4 Integration**: Uses Tailwind CSS v4's official `canonicalizeCandidates` API
|
|
30
|
+
- 📝 **Multiple Format Support**: Works with string literals, template literals, and JSX expressions
|
|
31
|
+
- ⚡ **Zero Config**: Minimal configuration required to get started
|
|
32
|
+
|
|
33
|
+
## 📦 Installation
|
|
34
|
+
|
|
35
|
+
Install the plugin and its peer dependency:
|
|
14
36
|
|
|
15
37
|
```bash
|
|
16
38
|
npm install --save-dev eslint-plugin-tailwind-canonical-classes @tailwindcss/node
|
|
17
39
|
```
|
|
18
40
|
|
|
19
|
-
|
|
41
|
+
Or with yarn:
|
|
20
42
|
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
- `@tailwindcss/node` package
|
|
43
|
+
```bash
|
|
44
|
+
yarn add -D eslint-plugin-tailwind-canonical-classes @tailwindcss/node
|
|
45
|
+
```
|
|
25
46
|
|
|
26
|
-
|
|
47
|
+
Or with pnpm:
|
|
27
48
|
|
|
28
|
-
|
|
49
|
+
```bash
|
|
50
|
+
pnpm add -D eslint-plugin-tailwind-canonical-classes @tailwindcss/node
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Requirements
|
|
54
|
+
|
|
55
|
+
- **Node.js** >= 18.0.0
|
|
56
|
+
- **ESLint** >= 8.0.0
|
|
57
|
+
- **Tailwind CSS** v4
|
|
58
|
+
- **@tailwindcss/node** package
|
|
59
|
+
|
|
60
|
+
## 🚀 Quick Start
|
|
61
|
+
|
|
62
|
+
1. **Install the plugin** (see [Installation](#-installation))
|
|
63
|
+
|
|
64
|
+
2. **Add to your ESLint config**:
|
|
65
|
+
|
|
66
|
+
**Flat Config (ESLint 9+)** - `eslint.config.mjs`:
|
|
67
|
+
```javascript
|
|
68
|
+
import tailwindCanonicalClasses from 'eslint-plugin-tailwind-canonical-classes';
|
|
69
|
+
|
|
70
|
+
export default [
|
|
71
|
+
{
|
|
72
|
+
plugins: {
|
|
73
|
+
'tailwind-canonical-classes': tailwindCanonicalClasses,
|
|
74
|
+
},
|
|
75
|
+
rules: {
|
|
76
|
+
'tailwind-canonical-classes/tailwind-canonical-classes': [
|
|
77
|
+
'warn',
|
|
78
|
+
{
|
|
79
|
+
cssPath: './app/styles/globals.css',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Legacy Config** - `.eslintrc.js`:
|
|
88
|
+
```javascript
|
|
89
|
+
module.exports = {
|
|
90
|
+
plugins: ['tailwind-canonical-classes'],
|
|
91
|
+
rules: {
|
|
92
|
+
'tailwind-canonical-classes/tailwind-canonical-classes': [
|
|
93
|
+
'warn',
|
|
94
|
+
{
|
|
95
|
+
cssPath: './app/styles/globals.css',
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
3. **Run ESLint**:
|
|
103
|
+
```bash
|
|
104
|
+
npx eslint . --fix
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## ⚙️ Configuration
|
|
29
108
|
|
|
30
109
|
### Flat Config (ESLint 9+)
|
|
31
110
|
|
|
@@ -39,10 +118,10 @@ export default [
|
|
|
39
118
|
},
|
|
40
119
|
rules: {
|
|
41
120
|
'tailwind-canonical-classes/tailwind-canonical-classes': [
|
|
42
|
-
'warn',
|
|
121
|
+
'warn', // or 'error'
|
|
43
122
|
{
|
|
44
|
-
cssPath: './app/styles/globals.css', //
|
|
45
|
-
rootFontSize: 16, // Optional
|
|
123
|
+
cssPath: './app/styles/globals.css', // Required
|
|
124
|
+
rootFontSize: 16, // Optional, default: 16
|
|
46
125
|
},
|
|
47
126
|
],
|
|
48
127
|
},
|
|
@@ -67,34 +146,38 @@ module.exports = {
|
|
|
67
146
|
};
|
|
68
147
|
```
|
|
69
148
|
|
|
70
|
-
## Options
|
|
149
|
+
## 📖 Options
|
|
71
150
|
|
|
72
151
|
### `cssPath` (required)
|
|
73
152
|
|
|
74
|
-
Type
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
153
|
+
- **Type**: `string`
|
|
154
|
+
- **Description**: Path to your Tailwind CSS file
|
|
155
|
+
- **Supported formats**:
|
|
156
|
+
- Relative path: Resolved relative to your project root (where ESLint config is located)
|
|
157
|
+
- Absolute path: Full filesystem path to your CSS file
|
|
79
158
|
|
|
80
|
-
|
|
159
|
+
**Examples**:
|
|
81
160
|
```javascript
|
|
82
|
-
cssPath: './app/styles/globals.css'
|
|
83
|
-
cssPath: '/
|
|
161
|
+
cssPath: './app/styles/globals.css' // Relative to project root
|
|
162
|
+
cssPath: './src/index.css' // Another relative example
|
|
163
|
+
cssPath: '/absolute/path/to/styles.css' // Absolute path
|
|
84
164
|
```
|
|
85
165
|
|
|
86
166
|
### `rootFontSize` (optional)
|
|
87
167
|
|
|
88
|
-
Type
|
|
89
|
-
Default
|
|
168
|
+
- **Type**: `number`
|
|
169
|
+
- **Default**: `16`
|
|
170
|
+
- **Description**: Root font size in pixels for rem calculations. This should match your CSS root font size setting.
|
|
90
171
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
172
|
+
**Example**:
|
|
173
|
+
```javascript
|
|
174
|
+
rootFontSize: 16 // Default (16px = 1rem)
|
|
175
|
+
rootFontSize: 14 // If your root font size is 14px
|
|
176
|
+
```
|
|
94
177
|
|
|
95
|
-
|
|
178
|
+
## 💡 Usage Examples
|
|
96
179
|
|
|
97
|
-
### Example
|
|
180
|
+
### Basic Example
|
|
98
181
|
|
|
99
182
|
**Before:**
|
|
100
183
|
```tsx
|
|
@@ -106,35 +189,137 @@ Once configured, ESLint will automatically check your JSX `className` attributes
|
|
|
106
189
|
<div className="p-1 m-8">Content</div>
|
|
107
190
|
```
|
|
108
191
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
192
|
+
### Supported Formats
|
|
193
|
+
|
|
194
|
+
The plugin supports various class name formats:
|
|
195
|
+
|
|
196
|
+
1. **String literals**:
|
|
197
|
+
```tsx
|
|
198
|
+
<div className="p-4 m-2">Content</div>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
2. **Template literals** (static parts only):
|
|
202
|
+
```tsx
|
|
203
|
+
<div className={`p-4 ${someVar}`}>Content</div>
|
|
204
|
+
// Only "p-4" will be checked, dynamic parts are skipped
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
3. **JSX expression containers**:
|
|
208
|
+
```tsx
|
|
209
|
+
<div className={"p-4px"}>Content</div>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Real-world Example
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
// Before
|
|
216
|
+
function Card({ children }) {
|
|
217
|
+
return (
|
|
218
|
+
<div className="p-16px m-2rem rounded-8px shadow-lg">
|
|
219
|
+
{children}
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// After auto-fix
|
|
225
|
+
function Card({ children }) {
|
|
226
|
+
return (
|
|
227
|
+
<div className="p-4 m-8 rounded-2 shadow-lg">
|
|
228
|
+
{children}
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## 🔧 How It Works
|
|
113
235
|
|
|
114
|
-
|
|
236
|
+
1. **Load Design System**: The plugin loads your Tailwind CSS file using `@tailwindcss/node`'s `__unstable__loadDesignSystem` API
|
|
237
|
+
2. **Extract Classes**: It extracts class names from JSX `className` attributes in your code
|
|
238
|
+
3. **Canonicalize**: For each class, it uses Tailwind's `canonicalizeCandidates` to find the canonical form
|
|
239
|
+
4. **Report & Fix**: If a non-canonical class is found, it reports an error/warning and can auto-fix it
|
|
115
240
|
|
|
116
|
-
|
|
117
|
-
2. It extracts class names from JSX `className` attributes
|
|
118
|
-
3. For each class, it uses Tailwind's `canonicalizeCandidates` to find the canonical form
|
|
119
|
-
4. If a non-canonical class is found, it reports an error/warning and can auto-fix it
|
|
241
|
+
## ⚠️ Limitations
|
|
120
242
|
|
|
121
|
-
|
|
243
|
+
- **Static classes only**: Only works with static class names (no dynamic expressions)
|
|
244
|
+
- **Tailwind CSS v4 required**: Requires Tailwind CSS v4 (not compatible with v3)
|
|
245
|
+
- **CSS file accessibility**: CSS file must be accessible from the ESLint process
|
|
246
|
+
- **Template literals**: Template literals with expressions are partially supported (only static parts are checked)
|
|
122
247
|
|
|
123
|
-
|
|
124
|
-
- Requires Tailwind CSS v4
|
|
125
|
-
- CSS file must be accessible from the ESLint process
|
|
126
|
-
- Template literals with expressions are skipped
|
|
248
|
+
## 🐛 Troubleshooting
|
|
127
249
|
|
|
128
|
-
|
|
250
|
+
### Plugin not detecting classes
|
|
251
|
+
|
|
252
|
+
**Problem**: The plugin isn't reporting any issues with non-canonical classes.
|
|
253
|
+
|
|
254
|
+
**Solutions**:
|
|
255
|
+
1. Verify your `cssPath` is correct and points to a valid Tailwind CSS file
|
|
256
|
+
2. Ensure the CSS file is accessible from where ESLint runs
|
|
257
|
+
3. Check that your Tailwind CSS file contains valid Tailwind directives (`@import "tailwindcss"` or similar)
|
|
258
|
+
4. Verify ESLint is processing your JSX/TSX files (check your ESLint config includes these file types)
|
|
259
|
+
|
|
260
|
+
### Path resolution issues
|
|
261
|
+
|
|
262
|
+
**Problem**: ESLint can't find your CSS file.
|
|
263
|
+
|
|
264
|
+
**Solutions**:
|
|
265
|
+
- Use an absolute path if relative paths aren't working
|
|
266
|
+
- Ensure the path is relative to your ESLint config file location
|
|
267
|
+
- Check file permissions
|
|
268
|
+
|
|
269
|
+
### Auto-fix not working
|
|
270
|
+
|
|
271
|
+
**Problem**: ESLint reports issues but doesn't auto-fix them.
|
|
272
|
+
|
|
273
|
+
**Solutions**:
|
|
274
|
+
- Run ESLint with the `--fix` flag: `npx eslint . --fix`
|
|
275
|
+
- Ensure your editor's ESLint extension has auto-fix enabled
|
|
276
|
+
- Check that the rule severity is set to `'warn'` or `'error'` (not `'off'`)
|
|
277
|
+
|
|
278
|
+
## 🤝 Contributing
|
|
129
279
|
|
|
130
280
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
131
281
|
|
|
132
|
-
|
|
282
|
+
1. Fork the repository
|
|
283
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
284
|
+
3. Commit your changes (`git commit -m 'feat: add some amazing feature'`)
|
|
285
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
286
|
+
5. Open a Pull Request
|
|
287
|
+
|
|
288
|
+
### Development Setup
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
# Clone the repository
|
|
292
|
+
git clone https://github.com/MaisonnatM/eslint-plugin-tailwind-canonical-classes.git
|
|
293
|
+
cd eslint-plugin-tailwind-canonical-classes
|
|
294
|
+
|
|
295
|
+
# Install dependencies
|
|
296
|
+
npm install
|
|
297
|
+
|
|
298
|
+
# Build the project
|
|
299
|
+
npm run build
|
|
300
|
+
|
|
301
|
+
# Run tests
|
|
302
|
+
npm test
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Release Process
|
|
306
|
+
|
|
307
|
+
This project uses [semantic-release](https://github.com/semantic-release/semantic-release) for automated version management and npm publishing. Releases are automatically triggered when commits are pushed to the `main` branch.
|
|
308
|
+
|
|
309
|
+
**Commit Message Format**:
|
|
310
|
+
- `fix:` - Patch release (1.0.8 → 1.0.9)
|
|
311
|
+
- `feat:` - Minor release (1.0.8 → 1.1.0)
|
|
312
|
+
- `feat!:` or `BREAKING CHANGE:` - Major release (1.0.8 → 2.0.0)
|
|
313
|
+
|
|
314
|
+
## 📄 License
|
|
133
315
|
|
|
134
|
-
MIT
|
|
316
|
+
MIT © [Maisonnat Maxence](https://github.com/MaisonnatM)
|
|
135
317
|
|
|
136
|
-
## Related
|
|
318
|
+
## 🔗 Related Links
|
|
137
319
|
|
|
138
|
-
- [Tailwind CSS v4](https://tailwindcss.com/)
|
|
139
|
-
- [ESLint](https://eslint.org/)
|
|
320
|
+
- [Tailwind CSS v4 Documentation](https://tailwindcss.com/)
|
|
321
|
+
- [ESLint Documentation](https://eslint.org/)
|
|
322
|
+
- [npm Package](https://www.npmjs.com/package/eslint-plugin-tailwind-canonical-classes)
|
|
323
|
+
- [GitHub Repository](https://github.com/MaisonnatM/eslint-plugin-tailwind-canonical-classes)
|
|
324
|
+
- [Report an Issue](https://github.com/MaisonnatM/eslint-plugin-tailwind-canonical-classes/issues)
|
|
140
325
|
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { createSyncFn } from 'synckit';
|
|
5
|
+
const workerPath = fileURLToPath(new URL('./tailwind-worker.js', import.meta.url));
|
|
6
|
+
const canonicalizeSync = createSyncFn(workerPath);
|
|
7
|
+
function canonicalizeClasses(cssPath, candidates, rootFontSize = 16) {
|
|
8
|
+
if (!fs.existsSync(cssPath)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const cssContent = fs.readFileSync(cssPath, 'utf-8');
|
|
12
|
+
const basePath = path.dirname(cssPath);
|
|
13
|
+
return canonicalizeSync(cssContent, basePath, candidates, { rem: rootFontSize });
|
|
14
|
+
}
|
|
15
|
+
function splitClasses(className) {
|
|
16
|
+
return className.trim().split(/\s+/).filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
function joinClasses(classes) {
|
|
19
|
+
return classes.join(' ');
|
|
20
|
+
}
|
|
21
|
+
function hasTemplateExpressions(node) {
|
|
22
|
+
if (node.type !== 'TemplateLiteral') {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return node.expressions && node.expressions.length > 0;
|
|
26
|
+
}
|
|
27
|
+
function extractStaticValue(node) {
|
|
28
|
+
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
29
|
+
return node.value;
|
|
30
|
+
}
|
|
31
|
+
if (node.type === 'JSXExpressionContainer') {
|
|
32
|
+
return extractStaticValue(node.expression);
|
|
33
|
+
}
|
|
34
|
+
if (node.type === 'TemplateLiteral' && !hasTemplateExpressions(node)) {
|
|
35
|
+
return node.quasis.map((q) => q.value.cooked).join('');
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function getQuoteChar(source, start, end) {
|
|
40
|
+
const quoteChars = ['"', "'", '`'];
|
|
41
|
+
for (const char of quoteChars) {
|
|
42
|
+
if (source[start] === char && source[end - 1] === char) {
|
|
43
|
+
return char;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return '"';
|
|
47
|
+
}
|
|
48
|
+
const rule = {
|
|
49
|
+
meta: {
|
|
50
|
+
type: 'suggestion',
|
|
51
|
+
docs: {
|
|
52
|
+
description: 'Enforce canonical Tailwind CSS class names using Tailwind CSS v4 canonicalization API',
|
|
53
|
+
},
|
|
54
|
+
fixable: 'code',
|
|
55
|
+
messages: {
|
|
56
|
+
nonCanonical: "Class '{{original}}' should be '{{canonical}}'",
|
|
57
|
+
cssNotFound: 'Could not load Tailwind CSS file: {{path}}',
|
|
58
|
+
},
|
|
59
|
+
schema: [
|
|
60
|
+
{
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
cssPath: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
},
|
|
66
|
+
rootFontSize: {
|
|
67
|
+
type: 'number',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
required: ['cssPath'],
|
|
71
|
+
additionalProperties: false,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
create(context) {
|
|
76
|
+
const options = context.options[0];
|
|
77
|
+
if (!options || !options.cssPath) {
|
|
78
|
+
context.report({
|
|
79
|
+
node: context.getSourceCode().ast,
|
|
80
|
+
messageId: 'cssNotFound',
|
|
81
|
+
data: {
|
|
82
|
+
path: 'not specified',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
let cssPath;
|
|
88
|
+
if (path.isAbsolute(options.cssPath)) {
|
|
89
|
+
cssPath = path.normalize(options.cssPath);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
cssPath = path.normalize(path.resolve(process.cwd(), options.cssPath));
|
|
93
|
+
}
|
|
94
|
+
const rootFontSize = options.rootFontSize ?? 16;
|
|
95
|
+
if (!fs.existsSync(cssPath)) {
|
|
96
|
+
context.report({
|
|
97
|
+
node: context.getSourceCode().ast,
|
|
98
|
+
messageId: 'cssNotFound',
|
|
99
|
+
data: {
|
|
100
|
+
path: cssPath,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
JSXAttribute(node) {
|
|
107
|
+
if (node.name.type !== 'JSXIdentifier' ||
|
|
108
|
+
node.name.name !== 'className') {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const staticValue = extractStaticValue(node.value);
|
|
112
|
+
if (staticValue === null) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const classes = splitClasses(staticValue);
|
|
116
|
+
if (classes.length === 0) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const sourceCode = context.getSourceCode();
|
|
120
|
+
const sourceText = sourceCode.getText();
|
|
121
|
+
const errors = [];
|
|
122
|
+
try {
|
|
123
|
+
const canonicalized = canonicalizeClasses(cssPath, classes, rootFontSize);
|
|
124
|
+
if (canonicalized === null) {
|
|
125
|
+
context.report({
|
|
126
|
+
node,
|
|
127
|
+
messageId: 'cssNotFound',
|
|
128
|
+
data: {
|
|
129
|
+
path: cssPath,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
classes.forEach((className, index) => {
|
|
135
|
+
const canonical = canonicalized[index];
|
|
136
|
+
if (canonical && canonical !== className) {
|
|
137
|
+
errors.push({
|
|
138
|
+
node,
|
|
139
|
+
original: className,
|
|
140
|
+
canonical,
|
|
141
|
+
index,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (errors.length > 0) {
|
|
150
|
+
const valueNode = node.value;
|
|
151
|
+
let fullRangeStart;
|
|
152
|
+
let fullRangeEnd;
|
|
153
|
+
let replacementText;
|
|
154
|
+
if (valueNode.type === 'Literal') {
|
|
155
|
+
fullRangeStart = valueNode.range[0];
|
|
156
|
+
fullRangeEnd = valueNode.range[1];
|
|
157
|
+
const quoteChar = getQuoteChar(sourceText, valueNode.range[0], valueNode.range[1]);
|
|
158
|
+
const fixedClasses = [...classes];
|
|
159
|
+
errors.forEach((error) => {
|
|
160
|
+
fixedClasses[error.index] = error.canonical;
|
|
161
|
+
});
|
|
162
|
+
const fixedValue = joinClasses(fixedClasses);
|
|
163
|
+
replacementText = `${quoteChar}${fixedValue}${quoteChar}`;
|
|
164
|
+
}
|
|
165
|
+
else if (valueNode.type === 'JSXExpressionContainer') {
|
|
166
|
+
const expr = valueNode.expression;
|
|
167
|
+
if (expr.type === 'TemplateLiteral') {
|
|
168
|
+
fullRangeStart = valueNode.range[0];
|
|
169
|
+
fullRangeEnd = valueNode.range[1];
|
|
170
|
+
const fixedClasses = [...classes];
|
|
171
|
+
errors.forEach((error) => {
|
|
172
|
+
fixedClasses[error.index] = error.canonical;
|
|
173
|
+
});
|
|
174
|
+
const fixedValue = joinClasses(fixedClasses);
|
|
175
|
+
replacementText = `{\`${fixedValue}\`}`;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
errors.forEach((error, errorIndex) => {
|
|
185
|
+
context.report({
|
|
186
|
+
node: error.node,
|
|
187
|
+
messageId: 'nonCanonical',
|
|
188
|
+
data: {
|
|
189
|
+
original: error.original,
|
|
190
|
+
canonical: error.canonical,
|
|
191
|
+
},
|
|
192
|
+
fix: errorIndex === 0
|
|
193
|
+
? (fixer) => {
|
|
194
|
+
return fixer.replaceTextRange([fullRangeStart, fullRangeEnd], replacementText);
|
|
195
|
+
}
|
|
196
|
+
: undefined,
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
export default rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { runAsWorker } from 'synckit';
|
|
2
|
+
import { __unstable__loadDesignSystem } from '@tailwindcss/node';
|
|
3
|
+
const designSystemCache = new Map();
|
|
4
|
+
async function canonicalizeInWorker(cssContent, basePath, candidates, options = {}) {
|
|
5
|
+
const cacheKey = basePath;
|
|
6
|
+
let designSystem = designSystemCache.get(cacheKey);
|
|
7
|
+
if (!designSystem) {
|
|
8
|
+
designSystem = await __unstable__loadDesignSystem(cssContent, { base: basePath });
|
|
9
|
+
designSystemCache.set(cacheKey, designSystem);
|
|
10
|
+
}
|
|
11
|
+
return designSystem.canonicalizeCandidates(candidates, options);
|
|
12
|
+
}
|
|
13
|
+
runAsWorker(canonicalizeInWorker);
|
package/package.json
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-tailwind-canonical-classes",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "ESLint plugin to enforce canonical Tailwind CSS class names using Tailwind CSS v4's canonicalization API",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "index.js",
|
|
7
|
-
"types": "index.d.ts",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"types": "./index.d.ts",
|
|
11
|
-
"default": "./index.js"
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
12
|
},
|
|
13
13
|
"./rule": {
|
|
14
|
-
"types": "./lib/tailwind-canonical-classes.d.ts",
|
|
15
|
-
"default": "./lib/tailwind-canonical-classes.js"
|
|
14
|
+
"types": "./lib/rules/tailwind-canonical-classes.d.ts",
|
|
15
|
+
"default": "./lib/rules/tailwind-canonical-classes.js"
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
|
-
"index.js",
|
|
20
|
-
"index.d.ts",
|
|
21
19
|
"lib"
|
|
22
20
|
],
|
|
23
21
|
"keywords": [
|
|
@@ -42,13 +40,60 @@
|
|
|
42
40
|
"engines": {
|
|
43
41
|
"node": ">=18.0.0"
|
|
44
42
|
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc",
|
|
45
|
+
"build:watch": "tsc --watch",
|
|
46
|
+
"test": "npm run build && vitest",
|
|
47
|
+
"test:run": "npm run build && vitest run",
|
|
48
|
+
"prepublishOnly": "npm run build",
|
|
49
|
+
"release": "semantic-release"
|
|
50
|
+
},
|
|
45
51
|
"peerDependencies": {
|
|
46
52
|
"eslint": ">=8.0.0"
|
|
47
53
|
},
|
|
48
54
|
"dependencies": {
|
|
49
|
-
"@tailwindcss/node": "^4.0.0"
|
|
55
|
+
"@tailwindcss/node": "^4.0.0",
|
|
56
|
+
"synckit": "^0.9.3"
|
|
50
57
|
},
|
|
51
58
|
"devDependencies": {
|
|
52
|
-
"eslint": "^
|
|
59
|
+
"@babel/eslint-parser": "^7.28.5",
|
|
60
|
+
"@babel/preset-react": "^7.28.5",
|
|
61
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
62
|
+
"@semantic-release/git": "^10.0.1",
|
|
63
|
+
"@semantic-release/github": "^9.2.6",
|
|
64
|
+
"@semantic-release/npm": "^12.0.1",
|
|
65
|
+
"@types/node": "^20.0.0",
|
|
66
|
+
"eslint": "^9.0.0",
|
|
67
|
+
"semantic-release": "^24.0.0",
|
|
68
|
+
"typescript": "^5.0.0",
|
|
69
|
+
"vitest": "^4.0.16"
|
|
70
|
+
},
|
|
71
|
+
"release": {
|
|
72
|
+
"branches": [
|
|
73
|
+
"main"
|
|
74
|
+
],
|
|
75
|
+
"plugins": [
|
|
76
|
+
"@semantic-release/commit-analyzer",
|
|
77
|
+
"@semantic-release/release-notes-generator",
|
|
78
|
+
[
|
|
79
|
+
"@semantic-release/changelog",
|
|
80
|
+
{
|
|
81
|
+
"changelogFile": "CHANGELOG.md"
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
"@semantic-release/npm",
|
|
85
|
+
[
|
|
86
|
+
"@semantic-release/git",
|
|
87
|
+
{
|
|
88
|
+
"assets": [
|
|
89
|
+
"package.json",
|
|
90
|
+
"package-lock.json",
|
|
91
|
+
"CHANGELOG.md"
|
|
92
|
+
],
|
|
93
|
+
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
"@semantic-release/github"
|
|
97
|
+
]
|
|
53
98
|
}
|
|
54
99
|
}
|
package/index.d.ts
DELETED
package/index.js
DELETED
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { __unstable__loadDesignSystem } from "@tailwindcss/node";
|
|
4
|
-
|
|
5
|
-
// Cache the design system to avoid reloading on every file
|
|
6
|
-
let designSystemCache = null;
|
|
7
|
-
let designSystemPromise = null;
|
|
8
|
-
let designSystemError = null;
|
|
9
|
-
let cssPathCache = null;
|
|
10
|
-
|
|
11
|
-
function resolveCssPath(cssPath, context) {
|
|
12
|
-
if (!cssPath) {
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// If absolute path, use as-is
|
|
17
|
-
if (path.isAbsolute(cssPath)) {
|
|
18
|
-
return cssPath;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// If relative path, resolve relative to the project root (where ESLint config is)
|
|
22
|
-
// Try to find the project root by looking for common config files
|
|
23
|
-
const filename = context.getFilename();
|
|
24
|
-
const fileDir = path.dirname(filename);
|
|
25
|
-
|
|
26
|
-
// Try to resolve relative to the current file's directory first
|
|
27
|
-
const relativeToFile = path.resolve(fileDir, cssPath);
|
|
28
|
-
if (fs.existsSync(relativeToFile)) {
|
|
29
|
-
return relativeToFile;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Try to resolve relative to the working directory (project root)
|
|
33
|
-
const relativeToCwd = path.resolve(process.cwd(), cssPath);
|
|
34
|
-
if (fs.existsSync(relativeToCwd)) {
|
|
35
|
-
return relativeToCwd;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return relativeToCwd; // Return even if doesn't exist, let error handling catch it
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function getDesignSystemSync(cssPath, context) {
|
|
42
|
-
// Reset cache if CSS path changed
|
|
43
|
-
if (cssPathCache !== cssPath) {
|
|
44
|
-
designSystemCache = null;
|
|
45
|
-
designSystemPromise = null;
|
|
46
|
-
designSystemError = null;
|
|
47
|
-
cssPathCache = cssPath;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (designSystemCache) {
|
|
51
|
-
return designSystemCache;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (designSystemError) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (designSystemPromise && !designSystemCache) {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (!designSystemPromise) {
|
|
63
|
-
try {
|
|
64
|
-
const resolvedPath = resolveCssPath(cssPath, context);
|
|
65
|
-
if (!resolvedPath || !fs.existsSync(resolvedPath)) {
|
|
66
|
-
designSystemError = new Error(`CSS file not found: ${cssPath}`);
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const cssContent = fs.readFileSync(resolvedPath, "utf-8");
|
|
71
|
-
const basePath = path.dirname(resolvedPath);
|
|
72
|
-
|
|
73
|
-
designSystemPromise = __unstable__loadDesignSystem(cssContent, {
|
|
74
|
-
base: basePath,
|
|
75
|
-
})
|
|
76
|
-
.then((ds) => {
|
|
77
|
-
designSystemCache = ds;
|
|
78
|
-
return ds;
|
|
79
|
-
})
|
|
80
|
-
.catch((error) => {
|
|
81
|
-
designSystemError = error;
|
|
82
|
-
return null;
|
|
83
|
-
});
|
|
84
|
-
} catch (error) {
|
|
85
|
-
designSystemError = error;
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function getDesignSystemAsync(cssPath, context) {
|
|
94
|
-
// Reset cache if CSS path changed
|
|
95
|
-
if (cssPathCache !== cssPath) {
|
|
96
|
-
designSystemCache = null;
|
|
97
|
-
designSystemPromise = null;
|
|
98
|
-
designSystemError = null;
|
|
99
|
-
cssPathCache = cssPath;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (designSystemCache) {
|
|
103
|
-
return designSystemCache;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (designSystemError) {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (!designSystemPromise) {
|
|
111
|
-
getDesignSystemSync(cssPath, context);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (designSystemPromise) {
|
|
115
|
-
try {
|
|
116
|
-
return await designSystemPromise;
|
|
117
|
-
} catch (error) {
|
|
118
|
-
designSystemError = error;
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function extractClassNames(classNameValue, sourceCode) {
|
|
127
|
-
const classes = [];
|
|
128
|
-
|
|
129
|
-
if (!classNameValue) {
|
|
130
|
-
return classes;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
classNameValue.type === "Literal" &&
|
|
135
|
-
typeof classNameValue.value === "string"
|
|
136
|
-
) {
|
|
137
|
-
const classString = classNameValue.value;
|
|
138
|
-
return classString.split(/\s+/).filter((cls) => cls.trim().length > 0);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (classNameValue.type === "TemplateLiteral") {
|
|
142
|
-
if (classNameValue.expressions && classNameValue.expressions.length > 0) {
|
|
143
|
-
return [];
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const parts = [];
|
|
147
|
-
for (const quasi of classNameValue.quasis) {
|
|
148
|
-
const cooked = quasi.value?.cooked || "";
|
|
149
|
-
if (cooked.trim()) {
|
|
150
|
-
parts.push(cooked.trim());
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const combined = parts.join(" ");
|
|
155
|
-
return combined.split(/\s+/).filter((cls) => cls.trim().length > 0);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (classNameValue.type === "JSXExpressionContainer") {
|
|
159
|
-
return extractClassNames(classNameValue.expression, sourceCode);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return classes;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export default {
|
|
166
|
-
meta: {
|
|
167
|
-
type: "suggestion",
|
|
168
|
-
docs: {
|
|
169
|
-
description: "Enforce canonical Tailwind CSS class names",
|
|
170
|
-
category: "Best Practices",
|
|
171
|
-
recommended: false,
|
|
172
|
-
},
|
|
173
|
-
fixable: "code",
|
|
174
|
-
schema: [
|
|
175
|
-
{
|
|
176
|
-
type: "object",
|
|
177
|
-
properties: {
|
|
178
|
-
cssPath: {
|
|
179
|
-
type: "string",
|
|
180
|
-
description:
|
|
181
|
-
"Path to your Tailwind CSS file (relative to project root or absolute)",
|
|
182
|
-
},
|
|
183
|
-
rootFontSize: {
|
|
184
|
-
type: "number",
|
|
185
|
-
default: 16,
|
|
186
|
-
description: "Root font size in pixels for rem calculations",
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
required: ["cssPath"],
|
|
190
|
-
additionalProperties: false,
|
|
191
|
-
},
|
|
192
|
-
],
|
|
193
|
-
messages: {
|
|
194
|
-
nonCanonical:
|
|
195
|
-
"The class `{{original}}` can be written as `{{canonical}}`",
|
|
196
|
-
cssNotFound:
|
|
197
|
-
"CSS file not found: {{path}}. Please check your cssPath configuration.",
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
|
|
201
|
-
create(context) {
|
|
202
|
-
const options = context.options[0] || {};
|
|
203
|
-
const cssPath = options.cssPath;
|
|
204
|
-
const rootFontSize = options.rootFontSize || 16;
|
|
205
|
-
const sourceCode = context.sourceCode;
|
|
206
|
-
|
|
207
|
-
if (!cssPath) {
|
|
208
|
-
context.report({
|
|
209
|
-
loc: { line: 1, column: 0 },
|
|
210
|
-
messageId: "cssNotFound",
|
|
211
|
-
data: { path: "not specified" },
|
|
212
|
-
});
|
|
213
|
-
return {};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
async JSXAttribute(node) {
|
|
218
|
-
if (node.name.name !== "className") {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const value = node.value;
|
|
223
|
-
if (!value) {
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
let classNameValue = value;
|
|
228
|
-
if (value.type === "JSXExpressionContainer") {
|
|
229
|
-
classNameValue = value.expression;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
classNameValue.type !== "Literal" &&
|
|
234
|
-
classNameValue.type !== "TemplateLiteral" &&
|
|
235
|
-
classNameValue.type !== "JSXExpressionContainer"
|
|
236
|
-
) {
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const classNames = extractClassNames(classNameValue, sourceCode);
|
|
241
|
-
if (classNames.length === 0) {
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Try synchronous first
|
|
246
|
-
let designSystem = getDesignSystemSync(cssPath, context);
|
|
247
|
-
|
|
248
|
-
// If not loaded yet, wait for async load
|
|
249
|
-
if (!designSystem) {
|
|
250
|
-
designSystem = await getDesignSystemAsync(cssPath, context);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (designSystem && designSystem.canonicalizeCandidates) {
|
|
254
|
-
processClasses(
|
|
255
|
-
designSystem,
|
|
256
|
-
classNames,
|
|
257
|
-
classNameValue,
|
|
258
|
-
sourceCode,
|
|
259
|
-
rootFontSize,
|
|
260
|
-
context
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
},
|
|
264
|
-
};
|
|
265
|
-
},
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
function processClasses(
|
|
269
|
-
designSystem,
|
|
270
|
-
classNames,
|
|
271
|
-
classNameValue,
|
|
272
|
-
sourceCode,
|
|
273
|
-
rootFontSize,
|
|
274
|
-
context
|
|
275
|
-
) {
|
|
276
|
-
const issues = [];
|
|
277
|
-
|
|
278
|
-
for (let i = 0; i < classNames.length; i++) {
|
|
279
|
-
const className = classNames[i];
|
|
280
|
-
|
|
281
|
-
try {
|
|
282
|
-
const canonicalized = designSystem.canonicalizeCandidates([className], {
|
|
283
|
-
rem: rootFontSize,
|
|
284
|
-
})[0];
|
|
285
|
-
|
|
286
|
-
if (canonicalized !== className) {
|
|
287
|
-
issues.push({
|
|
288
|
-
original: className,
|
|
289
|
-
canonical: canonicalized,
|
|
290
|
-
index: i,
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
} catch {
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (issues.length > 0) {
|
|
299
|
-
const originalText = sourceCode.getText(classNameValue);
|
|
300
|
-
|
|
301
|
-
const canonicalMap = new Map();
|
|
302
|
-
issues.forEach((issue) => {
|
|
303
|
-
canonicalMap.set(issue.original, issue.canonical);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
const fixedClassNames = classNames.map((className) => {
|
|
307
|
-
return canonicalMap.get(className) || className;
|
|
308
|
-
});
|
|
309
|
-
const fixedClassString = fixedClassNames.join(" ");
|
|
310
|
-
|
|
311
|
-
let fixedText;
|
|
312
|
-
|
|
313
|
-
const quoteMatch = originalText.match(/^(["'`])(.*)\1$/);
|
|
314
|
-
|
|
315
|
-
if (quoteMatch) {
|
|
316
|
-
fixedText = `${quoteMatch[1]}${fixedClassString}${quoteMatch[1]}`;
|
|
317
|
-
} else if (classNameValue.type === "TemplateLiteral") {
|
|
318
|
-
fixedText = `\`${fixedClassString}\``;
|
|
319
|
-
} else {
|
|
320
|
-
const startQuoteMatch = originalText.match(/^(["'`])/);
|
|
321
|
-
if (startQuoteMatch) {
|
|
322
|
-
const quote = startQuoteMatch[1];
|
|
323
|
-
const endQuoteIndex = originalText.lastIndexOf(quote);
|
|
324
|
-
if (endQuoteIndex > 0) {
|
|
325
|
-
fixedText = `${quote}${fixedClassString}${quote}`;
|
|
326
|
-
} else {
|
|
327
|
-
fixedText = fixedClassString;
|
|
328
|
-
}
|
|
329
|
-
} else {
|
|
330
|
-
fixedText = fixedClassString;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
context.report({
|
|
335
|
-
node: classNameValue,
|
|
336
|
-
messageId: "nonCanonical",
|
|
337
|
-
data: {
|
|
338
|
-
original: issues[0].original,
|
|
339
|
-
canonical: issues[0].canonical,
|
|
340
|
-
},
|
|
341
|
-
fix(fixer) {
|
|
342
|
-
if (fixedText !== originalText) {
|
|
343
|
-
return fixer.replaceText(classNameValue, fixedText);
|
|
344
|
-
}
|
|
345
|
-
return null;
|
|
346
|
-
},
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
for (let i = 1; i < issues.length; i++) {
|
|
350
|
-
context.report({
|
|
351
|
-
node: classNameValue,
|
|
352
|
-
messageId: "nonCanonical",
|
|
353
|
-
data: {
|
|
354
|
-
original: issues[i].original,
|
|
355
|
-
canonical: issues[i].canonical,
|
|
356
|
-
},
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|