stylelint-plugin-rhythmguard 1.4.2 → 1.6.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 +8 -0
- package/README.md +150 -30
- package/package.json +10 -3
- package/src/cli/audit.js +645 -0
- package/src/cli/doctor.js +231 -0
- package/src/cli/index.js +39 -0
- package/src/cli/init.js +128 -0
- package/src/configs/react-tailwind.js +25 -0
- package/src/configs/react-tailwind.mjs +4 -0
- package/src/configs/tailwind.js +9 -0
- package/src/eslint/rules/tailwind-class-use-scale.js +25 -120
- package/src/utils/tailwind-class-analysis.js +221 -0
- package/src/utils/token-map.js +44 -3
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const cwd = process.cwd();
|
|
7
|
+
let issues = 0;
|
|
8
|
+
|
|
9
|
+
function pass(msg) {
|
|
10
|
+
process.stdout.write(`✓ ${msg}\n`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function fail(msg, fix) {
|
|
14
|
+
issues++;
|
|
15
|
+
process.stdout.write(`✗ ${msg}\n`);
|
|
16
|
+
if (fix) {
|
|
17
|
+
process.stdout.write(` → ${fix}\n`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function skip(msg) {
|
|
22
|
+
process.stdout.write(`- ${msg}\n`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check 1: Stylelint installed
|
|
26
|
+
function checkStylelint() {
|
|
27
|
+
try {
|
|
28
|
+
const stylelintPkg = require.resolve('stylelint/package.json', {
|
|
29
|
+
paths: [cwd],
|
|
30
|
+
});
|
|
31
|
+
const version = require(stylelintPkg).version;
|
|
32
|
+
pass(`stylelint installed (v${version})`);
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
fail(
|
|
36
|
+
'stylelint not installed',
|
|
37
|
+
'Run: npm install --save-dev stylelint',
|
|
38
|
+
);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check 2: Config found
|
|
44
|
+
function findConfig() {
|
|
45
|
+
const configFiles = [
|
|
46
|
+
'.stylelintrc',
|
|
47
|
+
'.stylelintrc.json',
|
|
48
|
+
'.stylelintrc.js',
|
|
49
|
+
'.stylelintrc.cjs',
|
|
50
|
+
'.stylelintrc.mjs',
|
|
51
|
+
'.stylelintrc.yml',
|
|
52
|
+
'.stylelintrc.yaml',
|
|
53
|
+
'stylelint.config.js',
|
|
54
|
+
'stylelint.config.cjs',
|
|
55
|
+
'stylelint.config.mjs',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
for (const file of configFiles) {
|
|
59
|
+
const fullPath = path.join(cwd, file);
|
|
60
|
+
if (fs.existsSync(fullPath)) {
|
|
61
|
+
pass(`config found (${file})`);
|
|
62
|
+
return fullPath;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check package.json
|
|
67
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
68
|
+
if (fs.existsSync(pkgPath)) {
|
|
69
|
+
try {
|
|
70
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
71
|
+
if (pkg.stylelint) {
|
|
72
|
+
pass('config found (package.json stylelint key)');
|
|
73
|
+
return pkgPath;
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// ignore
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fail(
|
|
81
|
+
'no Stylelint config found',
|
|
82
|
+
'Run: npx rhythmguard init',
|
|
83
|
+
);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check 3: Config references Rhythmguard
|
|
88
|
+
function checkConfigReferences(configPath) {
|
|
89
|
+
if (!configPath) {
|
|
90
|
+
skip('config reference check skipped (no config)');
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
96
|
+
|
|
97
|
+
if (content.includes('rhythmguard')) {
|
|
98
|
+
pass('config references rhythmguard');
|
|
99
|
+
return content;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check package.json stylelint key
|
|
103
|
+
if (configPath.endsWith('package.json')) {
|
|
104
|
+
const pkg = JSON.parse(content);
|
|
105
|
+
const stylelintConfig = JSON.stringify(pkg.stylelint || {});
|
|
106
|
+
if (stylelintConfig.includes('rhythmguard')) {
|
|
107
|
+
pass('config references rhythmguard');
|
|
108
|
+
return stylelintConfig;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fail(
|
|
113
|
+
'config does not reference rhythmguard',
|
|
114
|
+
'Add: "extends": ["stylelint-plugin-rhythmguard/configs/recommended"]',
|
|
115
|
+
);
|
|
116
|
+
return content;
|
|
117
|
+
} catch {
|
|
118
|
+
fail(
|
|
119
|
+
'could not read config file',
|
|
120
|
+
`Check permissions on ${configPath}`,
|
|
121
|
+
);
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check 4: Token pattern valid
|
|
127
|
+
function checkTokenPattern(configContent) {
|
|
128
|
+
if (!configContent) {
|
|
129
|
+
skip('token pattern check skipped (no config content)');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const patternMatch = configContent.match(
|
|
134
|
+
/tokenPattern['":\s]+['"]([^'"]+)['"]/,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (!patternMatch) {
|
|
138
|
+
skip('token pattern check skipped (not configured)');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
new RegExp(patternMatch[1]);
|
|
144
|
+
pass(`token pattern valid (${patternMatch[1]})`);
|
|
145
|
+
} catch {
|
|
146
|
+
fail(
|
|
147
|
+
`token pattern invalid: ${patternMatch[1]}`,
|
|
148
|
+
'Fix the regex in tokenPattern option',
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check 5: Tailwind config exists
|
|
154
|
+
function checkTailwindConfig(configContent) {
|
|
155
|
+
if (!configContent) {
|
|
156
|
+
skip('tailwind config check skipped (no config content)');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const tailwindPathMatch = configContent.match(
|
|
161
|
+
/tailwindConfigPath['":\s]+['"]([^'"]+)['"]/,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
if (!tailwindPathMatch) {
|
|
165
|
+
skip('tailwind config check skipped (not configured)');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const tailwindPath = path.resolve(cwd, tailwindPathMatch[1]);
|
|
170
|
+
if (fs.existsSync(tailwindPath)) {
|
|
171
|
+
pass(`tailwind config found (${tailwindPathMatch[1]})`);
|
|
172
|
+
} else {
|
|
173
|
+
fail(
|
|
174
|
+
`tailwind config not found at ${tailwindPathMatch[1]}`,
|
|
175
|
+
'Update tailwindConfigPath or remove tokenMapFromTailwindSpacing',
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check 6: Custom syntax installed
|
|
181
|
+
function checkCustomSyntax(configContent) {
|
|
182
|
+
if (!configContent) {
|
|
183
|
+
skip('custom syntax check skipped (no config content)');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const syntaxMatch = configContent.match(
|
|
188
|
+
/customSyntax['":\s]+['"]([^'"]+)['"]/,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (!syntaxMatch) {
|
|
192
|
+
skip('custom syntax check skipped (not configured)');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const pkg = syntaxMatch[1];
|
|
197
|
+
try {
|
|
198
|
+
require.resolve(pkg, { paths: [cwd] });
|
|
199
|
+
pass(`custom syntax installed (${pkg})`);
|
|
200
|
+
} catch {
|
|
201
|
+
fail(
|
|
202
|
+
`custom syntax package not installed: ${pkg}`,
|
|
203
|
+
`Run: npm install --save-dev ${pkg}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function run() {
|
|
209
|
+
process.stdout.write('\nRhythmguard Doctor\n\n');
|
|
210
|
+
|
|
211
|
+
const stylelintOk = checkStylelint();
|
|
212
|
+
const configPath = findConfig();
|
|
213
|
+
const configContent = checkConfigReferences(configPath);
|
|
214
|
+
checkTokenPattern(configContent);
|
|
215
|
+
checkTailwindConfig(configContent);
|
|
216
|
+
checkCustomSyntax(configContent);
|
|
217
|
+
|
|
218
|
+
process.stdout.write('\n');
|
|
219
|
+
|
|
220
|
+
if (issues === 0) {
|
|
221
|
+
process.stdout.write('All checks passed.\n\n');
|
|
222
|
+
} else {
|
|
223
|
+
process.stdout.write(
|
|
224
|
+
`${issues} issue${issues === 1 ? '' : 's'} found.\n\n`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
process.exit(stylelintOk && issues === 0 ? 0 : 1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
run();
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const command = process.argv[2];
|
|
5
|
+
|
|
6
|
+
const HELP = `Usage: rhythmguard <command>
|
|
7
|
+
|
|
8
|
+
Commands:
|
|
9
|
+
audit <dir> Report design-system drift across CSS and Tailwind class strings
|
|
10
|
+
init Scaffold a Rhythmguard config for your project
|
|
11
|
+
doctor Validate your Rhythmguard setup
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--help Show this help message
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
npx rhythmguard audit ./src
|
|
18
|
+
npx rhythmguard audit ./src --json
|
|
19
|
+
npx rhythmguard audit ./src --format markdown
|
|
20
|
+
npx rhythmguard init
|
|
21
|
+
npx rhythmguard doctor
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
if (!command || command === '--help' || command === '-h') {
|
|
25
|
+
process.stdout.write(HELP);
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (command === 'audit') {
|
|
30
|
+
require('./audit');
|
|
31
|
+
} else if (command === 'init') {
|
|
32
|
+
require('./init');
|
|
33
|
+
} else if (command === 'doctor') {
|
|
34
|
+
require('./doctor');
|
|
35
|
+
} else {
|
|
36
|
+
process.stderr.write(`Unknown command: ${command}\n\n`);
|
|
37
|
+
process.stdout.write(HELP);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
package/src/cli/init.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const readline = require('node:readline');
|
|
6
|
+
|
|
7
|
+
function ask(question) {
|
|
8
|
+
const rl = readline.createInterface({
|
|
9
|
+
input: process.stdin,
|
|
10
|
+
output: process.stdout,
|
|
11
|
+
});
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
rl.question(question, (answer) => {
|
|
14
|
+
rl.close();
|
|
15
|
+
resolve(answer.trim().toLowerCase());
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function detect() {
|
|
21
|
+
const cwd = process.cwd();
|
|
22
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
23
|
+
|
|
24
|
+
let pkg = {};
|
|
25
|
+
if (fs.existsSync(pkgPath)) {
|
|
26
|
+
try {
|
|
27
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
28
|
+
} catch {
|
|
29
|
+
// ignore
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const allDeps = {
|
|
34
|
+
...pkg.dependencies,
|
|
35
|
+
...pkg.devDependencies,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const hasTailwindConfig =
|
|
39
|
+
fs.existsSync(path.join(cwd, 'tailwind.config.js')) ||
|
|
40
|
+
fs.existsSync(path.join(cwd, 'tailwind.config.cjs')) ||
|
|
41
|
+
fs.existsSync(path.join(cwd, 'tailwind.config.mjs')) ||
|
|
42
|
+
fs.existsSync(path.join(cwd, 'tailwind.config.ts'));
|
|
43
|
+
|
|
44
|
+
const hasTailwindDep = Boolean(
|
|
45
|
+
allDeps.tailwindcss || allDeps['@tailwindcss/postcss'],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const hasNextConfig =
|
|
49
|
+
fs.existsSync(path.join(cwd, 'next.config.js')) ||
|
|
50
|
+
fs.existsSync(path.join(cwd, 'next.config.mjs')) ||
|
|
51
|
+
fs.existsSync(path.join(cwd, 'next.config.ts'));
|
|
52
|
+
|
|
53
|
+
const hasExistingConfig =
|
|
54
|
+
fs.existsSync(path.join(cwd, '.stylelintrc')) ||
|
|
55
|
+
fs.existsSync(path.join(cwd, '.stylelintrc.json')) ||
|
|
56
|
+
fs.existsSync(path.join(cwd, '.stylelintrc.js')) ||
|
|
57
|
+
fs.existsSync(path.join(cwd, '.stylelintrc.cjs')) ||
|
|
58
|
+
fs.existsSync(path.join(cwd, '.stylelintrc.mjs')) ||
|
|
59
|
+
fs.existsSync(path.join(cwd, '.stylelintrc.yml')) ||
|
|
60
|
+
fs.existsSync(path.join(cwd, '.stylelintrc.yaml')) ||
|
|
61
|
+
fs.existsSync(path.join(cwd, 'stylelint.config.js')) ||
|
|
62
|
+
fs.existsSync(path.join(cwd, 'stylelint.config.cjs')) ||
|
|
63
|
+
fs.existsSync(path.join(cwd, 'stylelint.config.mjs')) ||
|
|
64
|
+
Boolean(pkg.stylelint);
|
|
65
|
+
|
|
66
|
+
const tailwind = hasTailwindConfig || hasTailwindDep;
|
|
67
|
+
const nextjs = hasNextConfig;
|
|
68
|
+
|
|
69
|
+
return { tailwind, nextjs, hasExistingConfig };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function selectProfile(stack) {
|
|
73
|
+
if (stack.nextjs && stack.tailwind) {
|
|
74
|
+
return 'react-tailwind';
|
|
75
|
+
}
|
|
76
|
+
if (stack.tailwind) {
|
|
77
|
+
return 'tailwind';
|
|
78
|
+
}
|
|
79
|
+
return 'recommended';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function run() {
|
|
83
|
+
process.stdout.write('\nRhythmguard Init\n\n');
|
|
84
|
+
|
|
85
|
+
const stack = detect();
|
|
86
|
+
|
|
87
|
+
// Report detection
|
|
88
|
+
const detected = [];
|
|
89
|
+
if (stack.tailwind) detected.push('Tailwind CSS');
|
|
90
|
+
if (stack.nextjs) detected.push('Next.js');
|
|
91
|
+
if (detected.length > 0) {
|
|
92
|
+
process.stdout.write(`Detected: ${detected.join(', ')}\n`);
|
|
93
|
+
} else {
|
|
94
|
+
process.stdout.write('Detected: plain CSS project\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Warn about existing config
|
|
98
|
+
if (stack.hasExistingConfig) {
|
|
99
|
+
process.stdout.write('\n⚠ Existing Stylelint config found.\n');
|
|
100
|
+
const answer = await ask('Overwrite? (y/n) ');
|
|
101
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
102
|
+
process.stdout.write('Aborted.\n');
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const profile = selectProfile(stack);
|
|
108
|
+
process.stdout.write(`\nProfile: ${profile}\n`);
|
|
109
|
+
|
|
110
|
+
const answer = await ask('Write .stylelintrc.json? (y/n) ');
|
|
111
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
112
|
+
process.stdout.write('Aborted.\n');
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const config = {
|
|
117
|
+
extends: [`stylelint-plugin-rhythmguard/configs/${profile}`],
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const configPath = path.join(process.cwd(), '.stylelintrc.json');
|
|
121
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
122
|
+
|
|
123
|
+
process.stdout.write(`\n✓ Wrote ${configPath}\n`);
|
|
124
|
+
process.stdout.write(`\nNext steps:\n`);
|
|
125
|
+
process.stdout.write(` npx stylelint "src/**/*.css"\n\n`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
run();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
extends: [
|
|
5
|
+
'stylelint-plugin-rhythmguard/configs/tailwind',
|
|
6
|
+
],
|
|
7
|
+
ignoreFiles: [
|
|
8
|
+
'.next/**',
|
|
9
|
+
'out/**',
|
|
10
|
+
'node_modules/**',
|
|
11
|
+
],
|
|
12
|
+
overrides: [
|
|
13
|
+
{
|
|
14
|
+
files: ['**/*.module.css'],
|
|
15
|
+
rules: {
|
|
16
|
+
'rhythmguard/use-scale': [
|
|
17
|
+
true,
|
|
18
|
+
{
|
|
19
|
+
propertyGroups: ['spacing', 'radius'],
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
};
|
package/src/configs/tailwind.js
CHANGED
|
@@ -5,4 +5,13 @@ module.exports = {
|
|
|
5
5
|
'stylelint-config-tailwindcss',
|
|
6
6
|
'stylelint-plugin-rhythmguard/configs/strict',
|
|
7
7
|
],
|
|
8
|
+
rules: {
|
|
9
|
+
'rhythmguard/prefer-token': [
|
|
10
|
+
true,
|
|
11
|
+
{
|
|
12
|
+
tokenPattern: '^--spacing-',
|
|
13
|
+
tokenMapFromCssCustomProperties: true,
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
},
|
|
8
17
|
};
|
|
@@ -1,112 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
|
|
5
|
-
nearestScaleValues,
|
|
6
|
-
normalizeScale,
|
|
7
|
-
numbersEqual,
|
|
8
|
-
parseLengthToken,
|
|
9
|
-
toPx,
|
|
10
|
-
} = require('../../utils/length');
|
|
3
|
+
const { formatLength } = require('../../utils/length');
|
|
4
|
+
const { createTailwindClassAnalyzer } = require('../../utils/tailwind-class-analysis');
|
|
11
5
|
|
|
12
6
|
const RULE_NAME = 'tailwind-class-use-scale';
|
|
13
7
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
function getOptions(context) {
|
|
17
|
-
const option = context.options && context.options[0] ? context.options[0] : {};
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
allowNegative: option.allowNegative !== false,
|
|
21
|
-
baseFontSize:
|
|
22
|
-
typeof option.baseFontSize === 'number' &&
|
|
23
|
-
Number.isFinite(option.baseFontSize) &&
|
|
24
|
-
option.baseFontSize > 0
|
|
25
|
-
? option.baseFontSize
|
|
26
|
-
: 16,
|
|
27
|
-
scale: Array.isArray(option.scale)
|
|
28
|
-
? option.scale
|
|
29
|
-
: [0, 4, 8, 12, 16, 24, 32],
|
|
30
|
-
units: Array.isArray(option.units)
|
|
31
|
-
? option.units.map((unit) => String(unit).toLowerCase())
|
|
32
|
-
: ['px', 'rem', 'em'],
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function findClassSegments(value) {
|
|
37
|
-
const segments = [];
|
|
38
|
-
const tokenRegex = /\S+/g;
|
|
39
|
-
let match;
|
|
40
|
-
|
|
41
|
-
while ((match = tokenRegex.exec(value)) !== null) {
|
|
42
|
-
segments.push({
|
|
43
|
-
start: match.index,
|
|
44
|
-
token: match[0],
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return segments;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function analyzeToken(token, options, scalePx) {
|
|
52
|
-
const match = token.match(ARBITRARY_SPACING_CLASS);
|
|
53
|
-
if (!match || !match.groups) {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const parsedLength = parseLengthToken(match.groups.rawValue);
|
|
58
|
-
if (!parsedLength || parsedLength.number === 0 || parsedLength.unit === '') {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (!options.allowNegative && parsedLength.number < 0) {
|
|
63
|
-
return {
|
|
64
|
-
nearest: null,
|
|
65
|
-
parsedLength,
|
|
66
|
-
reason: 'negative',
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (!options.units.includes(parsedLength.unit)) {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const pxValue = toPx(Math.abs(parsedLength.number), parsedLength.unit, options.baseFontSize);
|
|
75
|
-
if (pxValue === null) {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const isOnScale = scalePx.some((entry) => numbersEqual(entry, pxValue));
|
|
80
|
-
if (isOnScale) {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const nearest = nearestScaleValues(pxValue, scalePx);
|
|
85
|
-
if (!nearest) {
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const signedNearest = parsedLength.number < 0
|
|
90
|
-
? -Math.abs(nearest.nearest)
|
|
91
|
-
: nearest.nearest;
|
|
92
|
-
|
|
93
|
-
const replacementValuePx = signedNearest;
|
|
94
|
-
const replacementNumber = parsedLength.unit === 'px'
|
|
95
|
-
? replacementValuePx
|
|
96
|
-
: replacementValuePx / options.baseFontSize;
|
|
97
|
-
|
|
98
|
-
const replacementValue = formatLength(replacementNumber, parsedLength.unit);
|
|
99
|
-
const fixedToken = token.replace(match.groups.rawValue, replacementValue);
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
fixedToken,
|
|
103
|
-
nearest,
|
|
104
|
-
parsedLength,
|
|
105
|
-
reason: 'off-scale',
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function maybeCheckNodeText(node, sourceCode, context, options, scalePx, allowFix) {
|
|
8
|
+
function maybeCheckNodeText(node, sourceCode, context, analyzer, allowFix) {
|
|
110
9
|
const rawText = sourceCode.getText(node);
|
|
111
10
|
let value = null;
|
|
112
11
|
let quote = null;
|
|
@@ -124,16 +23,27 @@ function maybeCheckNodeText(node, sourceCode, context, options, scalePx, allowFi
|
|
|
124
23
|
return;
|
|
125
24
|
}
|
|
126
25
|
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
26
|
+
const findings = analyzer.analyzeClassString(value);
|
|
27
|
+
if (findings.length === 0) {
|
|
129
28
|
return;
|
|
130
29
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
30
|
+
|
|
31
|
+
let fixedValue = value;
|
|
32
|
+
let fixedValueOffset = 0;
|
|
33
|
+
|
|
34
|
+
for (const { analysis, segment } of findings) {
|
|
35
|
+
if (allowFix && analysis.reason !== 'negative' && node.type === 'Literal') {
|
|
36
|
+
const replacementStart = segment.start + fixedValueOffset;
|
|
37
|
+
fixedValue = `${fixedValue.slice(0, replacementStart)}${analysis.fixedToken}${fixedValue.slice(replacementStart + segment.token.length)}`;
|
|
38
|
+
fixedValueOffset += analysis.fixedToken.length - segment.token.length;
|
|
135
39
|
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const fixedText = allowFix && node.type === 'Literal' && fixedValue !== value
|
|
43
|
+
? `${quote}${fixedValue.replace(new RegExp(quote, 'g'), `\\${quote}`)}${quote}`
|
|
44
|
+
: null;
|
|
136
45
|
|
|
46
|
+
for (const { analysis, segment } of findings) {
|
|
137
47
|
const lower = analysis.nearest
|
|
138
48
|
? formatLength(analysis.nearest.lower, 'px')
|
|
139
49
|
: 'n/a';
|
|
@@ -147,12 +57,8 @@ function maybeCheckNodeText(node, sourceCode, context, options, scalePx, allowFi
|
|
|
147
57
|
: `Unexpected Tailwind arbitrary spacing value "${segment.token}". Use scale values (nearest: ${lower} or ${upper}).`,
|
|
148
58
|
node,
|
|
149
59
|
fix:
|
|
150
|
-
|
|
151
|
-
? (fixer) =>
|
|
152
|
-
const nextValue = `${value.slice(0, segment.start)}${analysis.fixedToken}${value.slice(segment.start + segment.token.length)}`;
|
|
153
|
-
const escaped = nextValue.replace(new RegExp(quote, 'g'), `\\${quote}`);
|
|
154
|
-
return fixer.replaceText(node, `${quote}${escaped}${quote}`);
|
|
155
|
-
}
|
|
60
|
+
fixedText && analysis.reason !== 'negative'
|
|
61
|
+
? (fixer) => fixer.replaceText(node, fixedText)
|
|
156
62
|
: null,
|
|
157
63
|
});
|
|
158
64
|
}
|
|
@@ -189,16 +95,15 @@ module.exports = {
|
|
|
189
95
|
],
|
|
190
96
|
},
|
|
191
97
|
create(context) {
|
|
192
|
-
const
|
|
193
|
-
const scalePx = normalizeScale(options.scale, options.baseFontSize);
|
|
98
|
+
const analyzer = createTailwindClassAnalyzer(context.options && context.options[0]);
|
|
194
99
|
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
195
100
|
|
|
196
101
|
return {
|
|
197
102
|
Literal(node) {
|
|
198
|
-
maybeCheckNodeText(node, sourceCode, context,
|
|
103
|
+
maybeCheckNodeText(node, sourceCode, context, analyzer, true);
|
|
199
104
|
},
|
|
200
105
|
TemplateElement(node) {
|
|
201
|
-
maybeCheckNodeText(node, sourceCode, context,
|
|
106
|
+
maybeCheckNodeText(node, sourceCode, context, analyzer, false);
|
|
202
107
|
},
|
|
203
108
|
};
|
|
204
109
|
},
|