stylelint-plugin-rhythmguard 1.1.0 → 1.2.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/CHANGELOG.md +16 -0
- package/README.md +63 -0
- package/package.json +5 -1
- package/src/configs/tailwind.js +8 -0
- package/src/index.js +1 -0
- package/src/rules/no-offscale-transform/index.js +47 -5
- package/src/rules/prefer-token/index.js +19 -2
- package/src/rules/use-scale/index.js +31 -1
- package/src/utils/value-utils.js +18 -5
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ The format follows Keep a Changelog principles and semantic versioning.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.2.0] - 2026-02-17
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Tailwind integration guidance in README:
|
|
14
|
+
- exact enforcement boundary (CSS declarations vs class strings)
|
|
15
|
+
- recommended layered setup with `stylelint-config-tailwindcss`, `eslint-plugin-tailwindcss`, and `prettier-plugin-tailwindcss`
|
|
16
|
+
- architecture direction for thorough Tailwind coverage.
|
|
17
|
+
- New shared config entry point: `stylelint-plugin-rhythmguard/configs/tailwind`.
|
|
18
|
+
- Tailwind-oriented test coverage for transform token functions and nested translate values.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Hardened transform translate parsing to handle nested function values consistently.
|
|
23
|
+
- `use-scale` and `no-offscale-transform` now respect `enforceInsideMathFunctions` in transform translation contexts.
|
|
24
|
+
|
|
9
25
|
## [1.1.0] - 2026-02-17
|
|
10
26
|
|
|
11
27
|
### Added
|
package/README.md
CHANGED
|
@@ -61,10 +61,20 @@ npm install --save-dev stylelint stylelint-plugin-rhythmguard
|
|
|
61
61
|
}
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
### Tailwind config
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"plugins": ["stylelint-plugin-rhythmguard"],
|
|
69
|
+
"extends": ["stylelint-plugin-rhythmguard/configs/tailwind"]
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
64
73
|
Stable shared config entry points:
|
|
65
74
|
|
|
66
75
|
- `stylelint-plugin-rhythmguard/configs/recommended`
|
|
67
76
|
- `stylelint-plugin-rhythmguard/configs/strict`
|
|
77
|
+
- `stylelint-plugin-rhythmguard/configs/tailwind`
|
|
68
78
|
|
|
69
79
|
### Full custom setup
|
|
70
80
|
|
|
@@ -317,6 +327,59 @@ Options:
|
|
|
317
327
|
|
|
318
328
|
`rhythmguard/no-offscale-transform` accepts the same scale options as `rhythmguard/use-scale`, but only for transform translation properties.
|
|
319
329
|
|
|
330
|
+
## Tailwind CSS Integration
|
|
331
|
+
|
|
332
|
+
Rhythmguard works well in Tailwind projects, but it enforces what Stylelint can parse: CSS declarations.
|
|
333
|
+
|
|
334
|
+
### What Rhythmguard covers in Tailwind projects
|
|
335
|
+
|
|
336
|
+
- custom CSS in `globals.css`, `components.css`, `utilities.css`
|
|
337
|
+
- CSS Modules (for example `*.module.css`)
|
|
338
|
+
- declarations inside `@layer` blocks
|
|
339
|
+
|
|
340
|
+
### What Rhythmguard does not cover
|
|
341
|
+
|
|
342
|
+
- Tailwind class strings in templates/JSX/TSX, for example:
|
|
343
|
+
- `class="p-4 gap-2"`
|
|
344
|
+
- `class="p-[13px] translate-y-[18px]"`
|
|
345
|
+
|
|
346
|
+
Those are not Stylelint declaration nodes, so they are outside this plugin's scope.
|
|
347
|
+
|
|
348
|
+
### Recommended stack for full Tailwind enforcement
|
|
349
|
+
|
|
350
|
+
Use both layers:
|
|
351
|
+
|
|
352
|
+
1. Stylelint + Rhythmguard for CSS declaration governance.
|
|
353
|
+
2. Tailwind-aware class-string linting/formatting for template utility usage.
|
|
354
|
+
|
|
355
|
+
Suggested setup:
|
|
356
|
+
|
|
357
|
+
```json
|
|
358
|
+
{
|
|
359
|
+
"extends": ["stylelint-plugin-rhythmguard/configs/tailwind"]
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Then pair with:
|
|
364
|
+
|
|
365
|
+
- `eslint-plugin-tailwindcss` for class-string rules (including arbitrary-value governance).
|
|
366
|
+
- `prettier-plugin-tailwindcss` for deterministic class ordering.
|
|
367
|
+
|
|
368
|
+
Detailed setup reference: [`docs/TAILWIND.md`](./docs/TAILWIND.md).
|
|
369
|
+
|
|
370
|
+
### Tailwind token function support
|
|
371
|
+
|
|
372
|
+
By default, `tokenFunctions` includes `theme`, so values like `theme(spacing.4)` are treated as tokenized values.
|
|
373
|
+
|
|
374
|
+
### Product direction
|
|
375
|
+
|
|
376
|
+
We should extend Tailwind coverage thoroughly, but in the right architecture:
|
|
377
|
+
|
|
378
|
+
- keep `stylelint-plugin-rhythmguard` focused on CSS declaration enforcement
|
|
379
|
+
- add a complementary Tailwind class-string layer (ESLint/plugin side) for utility classes
|
|
380
|
+
|
|
381
|
+
This avoids brittle parsing hacks and gives full coverage without compromising rule quality.
|
|
382
|
+
|
|
320
383
|
## Programmatic Presets
|
|
321
384
|
|
|
322
385
|
```js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stylelint-plugin-rhythmguard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Stylelint plugin for spacing scale and token enforcement",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"stylelint",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
".": "./src/index.js",
|
|
19
19
|
"./configs/recommended": "./src/configs/recommended.js",
|
|
20
20
|
"./configs/strict": "./src/configs/strict.js",
|
|
21
|
+
"./configs/tailwind": "./src/configs/tailwind.js",
|
|
21
22
|
"./presets": "./src/presets/index.js",
|
|
22
23
|
"./rules/use-scale": "./src/rules/use-scale/index.js",
|
|
23
24
|
"./rules/prefer-token": "./src/rules/prefer-token/index.js",
|
|
@@ -59,6 +60,9 @@
|
|
|
59
60
|
"peerDependencies": {
|
|
60
61
|
"stylelint": "^16.0.0"
|
|
61
62
|
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"stylelint-config-tailwindcss": "^1.0.1"
|
|
65
|
+
},
|
|
62
66
|
"devDependencies": {
|
|
63
67
|
"@eslint/js": "^9.22.0",
|
|
64
68
|
"c8": "^10.1.3",
|
package/src/index.js
CHANGED
|
@@ -14,6 +14,8 @@ const {
|
|
|
14
14
|
const { buildScaleOptions } = require('../../utils/options');
|
|
15
15
|
const {
|
|
16
16
|
declarationValueIndex,
|
|
17
|
+
isMathFunction,
|
|
18
|
+
walkRootValueNodes,
|
|
17
19
|
walkTransformTranslateNodes,
|
|
18
20
|
} = require('../../utils/value-utils');
|
|
19
21
|
|
|
@@ -138,15 +140,55 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
138
140
|
};
|
|
139
141
|
|
|
140
142
|
if (prop === 'transform') {
|
|
141
|
-
walkTransformTranslateNodes(parsed, (node) => {
|
|
143
|
+
walkTransformTranslateNodes(parsed, (node, parentFunctionName) => {
|
|
144
|
+
if (node.type === 'function') {
|
|
145
|
+
if (isMathFunction(node.value) && !options.enforceInsideMathFunctions) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (node.type !== 'word') {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (
|
|
157
|
+
parentFunctionName &&
|
|
158
|
+
isMathFunction(parentFunctionName) &&
|
|
159
|
+
!options.enforceInsideMathFunctions
|
|
160
|
+
) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
142
164
|
checkNode(node);
|
|
165
|
+
return false;
|
|
143
166
|
});
|
|
144
167
|
} else {
|
|
145
|
-
|
|
146
|
-
if (node.type === '
|
|
147
|
-
|
|
168
|
+
walkRootValueNodes(parsed, (node, parentFunctionName) => {
|
|
169
|
+
if (node.type === 'function') {
|
|
170
|
+
if (isMathFunction(node.value) && !options.enforceInsideMathFunctions) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return false;
|
|
148
175
|
}
|
|
149
|
-
|
|
176
|
+
|
|
177
|
+
if (node.type !== 'word') {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (
|
|
182
|
+
parentFunctionName &&
|
|
183
|
+
isMathFunction(parentFunctionName) &&
|
|
184
|
+
!options.enforceInsideMathFunctions
|
|
185
|
+
) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
checkNode(node);
|
|
190
|
+
return false;
|
|
191
|
+
});
|
|
150
192
|
}
|
|
151
193
|
|
|
152
194
|
if (changed) {
|
|
@@ -142,8 +142,25 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
142
142
|
};
|
|
143
143
|
|
|
144
144
|
if (prop === 'transform') {
|
|
145
|
-
walkTransformTranslateNodes(parsed, (node) => {
|
|
146
|
-
|
|
145
|
+
walkTransformTranslateNodes(parsed, (node, parentFunctionName) => {
|
|
146
|
+
if (node.type === 'function') {
|
|
147
|
+
if (isTokenFunction(node, options.tokenFunctions, tokenRegex)) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (isMathFunction(node.value)) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (node.type !== 'word') {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
changed = checkWordNode(node, parentFunctionName) || changed;
|
|
163
|
+
return false;
|
|
147
164
|
});
|
|
148
165
|
} else {
|
|
149
166
|
walkRootValueNodes(parsed, (node, parentFunctionName) => {
|
|
@@ -170,7 +170,35 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
170
170
|
let changed = false;
|
|
171
171
|
|
|
172
172
|
if (prop === 'transform') {
|
|
173
|
-
walkTransformTranslateNodes(parsed, (node) => {
|
|
173
|
+
walkTransformTranslateNodes(parsed, (node, parentFunctionName) => {
|
|
174
|
+
if (node.type === 'function') {
|
|
175
|
+
if (isTokenFunction(node, options.tokenFunctions, tokenRegex)) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (isMathFunction(node.value) && !options.enforceInsideMathFunctions) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (node.type !== 'word') {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (isKeyword(node.value, options.ignoreValues)) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (
|
|
195
|
+
parentFunctionName &&
|
|
196
|
+
isMathFunction(parentFunctionName) &&
|
|
197
|
+
!options.enforceInsideMathFunctions
|
|
198
|
+
) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
174
202
|
changed =
|
|
175
203
|
checkLengthValue({
|
|
176
204
|
decl,
|
|
@@ -179,6 +207,8 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
179
207
|
report,
|
|
180
208
|
scalePx,
|
|
181
209
|
}) || changed;
|
|
210
|
+
|
|
211
|
+
return false;
|
|
182
212
|
});
|
|
183
213
|
} else {
|
|
184
214
|
walkRootValueNodes(parsed, (node, parentFunctionName) => {
|
package/src/utils/value-utils.js
CHANGED
|
@@ -77,6 +77,23 @@ function walkRootValueNodes(parsed, walkNode, state) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
function walkTransformTranslateNodes(parsed, walkNode) {
|
|
80
|
+
const walkNodes = (nodes, parentFunctionName) => {
|
|
81
|
+
for (const node of nodes) {
|
|
82
|
+
if (node.type === 'function') {
|
|
83
|
+
const fnName = node.value.toLowerCase();
|
|
84
|
+
const skipChildren = walkNode(node, parentFunctionName);
|
|
85
|
+
if (skipChildren) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
walkNodes(node.nodes, fnName);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
walkNode(node, parentFunctionName);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
80
97
|
for (const node of parsed.nodes) {
|
|
81
98
|
if (node.type !== 'function') {
|
|
82
99
|
continue;
|
|
@@ -86,11 +103,7 @@ function walkTransformTranslateNodes(parsed, walkNode) {
|
|
|
86
103
|
continue;
|
|
87
104
|
}
|
|
88
105
|
|
|
89
|
-
|
|
90
|
-
if (child.type === 'word') {
|
|
91
|
-
walkNode(child, node.value.toLowerCase());
|
|
92
|
-
}
|
|
93
|
-
}
|
|
106
|
+
walkNodes(node.nodes, node.value.toLowerCase());
|
|
94
107
|
}
|
|
95
108
|
}
|
|
96
109
|
|