tailwind-canonical 0.3.0 → 0.4.1
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 +85 -9
- package/dist/cli/cli.d.ts +9 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +409 -78
- package/dist/cli/cli.js.map +1 -1
- package/dist/core/analyzer.d.ts +2 -1
- package/dist/core/analyzer.d.ts.map +1 -1
- package/dist/core/analyzer.js +5 -15
- package/dist/core/analyzer.js.map +1 -1
- package/dist/core/class-strings.d.ts +5 -1
- package/dist/core/class-strings.d.ts.map +1 -1
- package/dist/core/class-strings.js +74 -55
- package/dist/core/class-strings.js.map +1 -1
- package/dist/core/config.d.ts +3 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +91 -33
- package/dist/core/config.js.map +1 -1
- package/dist/core/consistency.d.ts +18 -1
- package/dist/core/consistency.d.ts.map +1 -1
- package/dist/core/consistency.js +76 -110
- package/dist/core/consistency.js.map +1 -1
- package/dist/core/deduplicator.d.ts +5 -1
- package/dist/core/deduplicator.d.ts.map +1 -1
- package/dist/core/deduplicator.js +22 -17
- package/dist/core/deduplicator.js.map +1 -1
- package/dist/core/fixer.d.ts +5 -1
- package/dist/core/fixer.d.ts.map +1 -1
- package/dist/core/fixer.js +6 -10
- package/dist/core/fixer.js.map +1 -1
- package/dist/core/lexicon.d.ts +16 -0
- package/dist/core/lexicon.d.ts.map +1 -0
- package/dist/core/lexicon.js +139 -0
- package/dist/core/lexicon.js.map +1 -0
- package/dist/core/merger.d.ts +5 -1
- package/dist/core/merger.d.ts.map +1 -1
- package/dist/core/merger.js +3 -8
- package/dist/core/merger.js.map +1 -1
- package/dist/core/rules.d.ts +17 -0
- package/dist/core/rules.d.ts.map +1 -1
- package/dist/core/rules.js +18 -9
- package/dist/core/rules.js.map +1 -1
- package/dist/core/scanner.d.ts +5 -2
- package/dist/core/scanner.d.ts.map +1 -1
- package/dist/core/scanner.js +12 -80
- package/dist/core/scanner.js.map +1 -1
- package/dist/core/sorter.d.ts +5 -1
- package/dist/core/sorter.d.ts.map +1 -1
- package/dist/core/sorter.js +9 -10
- package/dist/core/sorter.js.map +1 -1
- package/dist/core/suppressions.d.ts +4 -0
- package/dist/core/suppressions.d.ts.map +1 -1
- package/dist/core/suppressions.js +10 -4
- package/dist/core/suppressions.js.map +1 -1
- package/dist/core/typos.d.ts +14 -0
- package/dist/core/typos.d.ts.map +1 -0
- package/dist/core/typos.js +71 -0
- package/dist/core/typos.js.map +1 -0
- package/dist/eslint/plugin.d.ts +3 -95
- package/dist/eslint/plugin.d.ts.map +1 -1
- package/dist/eslint/plugin.js +68 -31
- package/dist/eslint/plugin.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -4
- package/dist/index.js.map +1 -1
- package/dist/io/analyzer.d.ts +4 -0
- package/dist/io/analyzer.d.ts.map +1 -0
- package/dist/io/analyzer.js +7 -0
- package/dist/io/analyzer.js.map +1 -0
- package/dist/io/config.d.ts +3 -0
- package/dist/io/config.d.ts.map +1 -0
- package/dist/io/config.js +13 -0
- package/dist/io/config.js.map +1 -0
- package/dist/io/consistency.d.ts +4 -0
- package/dist/io/consistency.d.ts.map +1 -0
- package/dist/io/consistency.js +21 -0
- package/dist/io/consistency.js.map +1 -0
- package/dist/io/deduplicator.d.ts +3 -0
- package/dist/io/deduplicator.d.ts.map +1 -0
- package/dist/io/deduplicator.js +10 -0
- package/dist/io/deduplicator.js.map +1 -0
- package/dist/io/fixer.d.ts +3 -0
- package/dist/io/fixer.d.ts.map +1 -0
- package/dist/io/fixer.js +10 -0
- package/dist/io/fixer.js.map +1 -0
- package/dist/io/merger.d.ts +3 -0
- package/dist/io/merger.d.ts.map +1 -0
- package/dist/io/merger.js +17 -0
- package/dist/io/merger.js.map +1 -0
- package/dist/io/scanner.d.ts +4 -0
- package/dist/io/scanner.d.ts.map +1 -0
- package/dist/io/scanner.js +84 -0
- package/dist/io/scanner.js.map +1 -0
- package/dist/io/sorter.d.ts +4 -0
- package/dist/io/sorter.d.ts.map +1 -0
- package/dist/io/sorter.js +10 -0
- package/dist/io/sorter.js.map +1 -0
- package/dist/io/typos.d.ts +4 -0
- package/dist/io/typos.d.ts.map +1 -0
- package/dist/io/typos.js +7 -0
- package/dist/io/typos.js.map +1 -0
- package/package.json +22 -13
package/dist/cli/cli.js
CHANGED
|
@@ -1,20 +1,55 @@
|
|
|
1
|
-
import { watch as fsWatch } from 'node:fs';
|
|
1
|
+
import { watch as fsWatch, readFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
3
|
import { analyzeFile } from '../core/analyzer.js';
|
|
4
|
+
import { toClassStringOpts } from '../core/class-strings.js';
|
|
4
5
|
import { loadConfig } from '../core/config.js';
|
|
5
|
-
import { analyzeConsistencyFiles } from '../core/consistency.js';
|
|
6
|
+
import { analyzeConsistencyFiles, toConsistencyOptions, } from '../core/consistency.js';
|
|
6
7
|
import { dedupeFile } from '../core/deduplicator.js';
|
|
7
8
|
import { fixFile } from '../core/fixer.js';
|
|
8
9
|
import { mergeFile } from '../core/merger.js';
|
|
9
10
|
import { resolveTargets } from '../core/scanner.js';
|
|
10
11
|
import { sortFile } from '../core/sorter.js';
|
|
12
|
+
import { analyzeTyposFile } from '../core/typos.js';
|
|
11
13
|
const SARIF_SCHEMA = 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json';
|
|
12
|
-
const USAGE = 'Usage: tailwind-canonical [--fix] [--merge] [--dedup] [--sort] [--analyze] [--watch] [--reporter json|sarif] <dir|file> [dir|file...]';
|
|
13
|
-
|
|
14
|
+
const USAGE = 'Usage: tailwind-canonical [--fix] [--merge] [--dedup] [--sort] [--analyze] [--typos] [--watch] [--reporter json|sarif] <dir|file> [dir|file...]';
|
|
15
|
+
const defaultSink = {
|
|
14
16
|
log: (s) => console.log(s),
|
|
15
17
|
error: (s) => console.error(s),
|
|
16
18
|
write: (s) => process.stdout.write(s),
|
|
17
19
|
};
|
|
20
|
+
const KNOWN_CLASS_FUNCTIONS = ['cn', 'clsx', 'cva'];
|
|
21
|
+
const DEFAULT_ANALYZE_TEXT_OPTIONS = {
|
|
22
|
+
maxScaleGroups: 8,
|
|
23
|
+
maxScaleValues: 5,
|
|
24
|
+
maxRareValues: 12,
|
|
25
|
+
maxPatterns: 10,
|
|
26
|
+
};
|
|
27
|
+
function sarifDocument(rules, results) {
|
|
28
|
+
return {
|
|
29
|
+
$schema: SARIF_SCHEMA,
|
|
30
|
+
version: '2.1.0',
|
|
31
|
+
runs: [
|
|
32
|
+
{
|
|
33
|
+
tool: {
|
|
34
|
+
driver: {
|
|
35
|
+
name: 'tailwind-canonical',
|
|
36
|
+
informationUri: 'https://github.com/peak-lab/tailwind-canonical',
|
|
37
|
+
rules,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
results,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function fileLocations(files) {
|
|
46
|
+
return files.map((uri) => ({
|
|
47
|
+
physicalLocation: {
|
|
48
|
+
artifactLocation: { uri },
|
|
49
|
+
region: { startLine: 1, startColumn: 1 },
|
|
50
|
+
},
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
18
53
|
function errMsg(e) {
|
|
19
54
|
return e instanceof Error ? e.message : String(e);
|
|
20
55
|
}
|
|
@@ -26,17 +61,55 @@ function timestamp() {
|
|
|
26
61
|
const pad = (n) => n.toString().padStart(2, '0');
|
|
27
62
|
return `[${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}]`;
|
|
28
63
|
}
|
|
64
|
+
const REPORTERS = ['text', 'json', 'sarif'];
|
|
65
|
+
const KNOWN_FLAGS = new Set([
|
|
66
|
+
'--fix',
|
|
67
|
+
'--merge',
|
|
68
|
+
'--dedup',
|
|
69
|
+
'--sort',
|
|
70
|
+
'--watch',
|
|
71
|
+
'--analyze',
|
|
72
|
+
'--typos',
|
|
73
|
+
'--reporter',
|
|
74
|
+
]);
|
|
75
|
+
function isReporter(value) {
|
|
76
|
+
return REPORTERS.includes(value);
|
|
77
|
+
}
|
|
29
78
|
export function parseArgs(argv) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
79
|
+
let reporter = 'text';
|
|
80
|
+
let reporterRaw;
|
|
81
|
+
const consumed = new Set();
|
|
82
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
83
|
+
const arg = argv[i];
|
|
84
|
+
if (arg === '--reporter') {
|
|
85
|
+
consumed.add(i);
|
|
86
|
+
reporterRaw = argv[i + 1];
|
|
87
|
+
if (i + 1 < argv.length)
|
|
88
|
+
consumed.add(i + 1);
|
|
89
|
+
}
|
|
90
|
+
else if (arg.startsWith('--reporter=')) {
|
|
91
|
+
consumed.add(i);
|
|
92
|
+
reporterRaw = arg.slice('--reporter='.length);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
let error;
|
|
96
|
+
if (reporterRaw !== undefined) {
|
|
97
|
+
if (isReporter(reporterRaw)) {
|
|
98
|
+
reporter = reporterRaw;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
error = `Unknown reporter: ${reporterRaw} (expected ${REPORTERS.join('|')})`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!error) {
|
|
105
|
+
const unknown = argv.find((a, i) => a.startsWith('--') &&
|
|
106
|
+
!consumed.has(i) &&
|
|
107
|
+
!KNOWN_FLAGS.has(a) &&
|
|
108
|
+
!a.startsWith('--reporter='));
|
|
109
|
+
if (unknown)
|
|
110
|
+
error = `Unknown flag: ${unknown}`;
|
|
111
|
+
}
|
|
112
|
+
const targets = argv.filter((a, i) => !(a.startsWith('--') || consumed.has(i)));
|
|
40
113
|
return {
|
|
41
114
|
fix: argv.includes('--fix'),
|
|
42
115
|
merge: argv.includes('--merge'),
|
|
@@ -44,15 +117,102 @@ export function parseArgs(argv) {
|
|
|
44
117
|
sort: argv.includes('--sort'),
|
|
45
118
|
watch: argv.includes('--watch'),
|
|
46
119
|
analyze: argv.includes('--analyze'),
|
|
120
|
+
typos: argv.includes('--typos'),
|
|
47
121
|
reporter,
|
|
48
122
|
targets,
|
|
123
|
+
error,
|
|
49
124
|
};
|
|
50
125
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
126
|
+
const TRANSFORM_FLAGS = [
|
|
127
|
+
{ key: 'fix', flag: '--fix' },
|
|
128
|
+
{ key: 'dedup', flag: '--dedup' },
|
|
129
|
+
{ key: 'merge', flag: '--merge' },
|
|
130
|
+
{ key: 'sort', flag: '--sort' },
|
|
131
|
+
];
|
|
132
|
+
/**
|
|
133
|
+
* Modes are mutually exclusive with a fixed precedence: analyze > typos >
|
|
134
|
+
* transform/check. `--watch` is only honored in transform/check mode. This
|
|
135
|
+
* computes warnings for every flag that the active mode will ignore. Pure and
|
|
136
|
+
* order-stable so it can be unit-tested via the injectable sink.
|
|
137
|
+
*/
|
|
138
|
+
export function flagWarnings(flags) {
|
|
139
|
+
const warnings = [];
|
|
140
|
+
if (flags.analyze) {
|
|
141
|
+
for (const { key, flag } of TRANSFORM_FLAGS) {
|
|
142
|
+
if (flags[key])
|
|
143
|
+
warnings.push(`${flag} ignored: --analyze takes priority`);
|
|
144
|
+
}
|
|
145
|
+
if (flags.typos)
|
|
146
|
+
warnings.push('--typos ignored: --analyze takes priority');
|
|
147
|
+
if (flags.watch)
|
|
148
|
+
warnings.push('--watch ignored: not supported with --analyze');
|
|
149
|
+
return warnings;
|
|
150
|
+
}
|
|
151
|
+
if (flags.typos) {
|
|
152
|
+
for (const { key, flag } of TRANSFORM_FLAGS) {
|
|
153
|
+
if (flags[key])
|
|
154
|
+
warnings.push(`${flag} ignored: --typos takes priority`);
|
|
155
|
+
}
|
|
156
|
+
if (flags.watch)
|
|
157
|
+
warnings.push('--watch ignored: not supported with --typos');
|
|
158
|
+
return warnings;
|
|
159
|
+
}
|
|
160
|
+
return warnings;
|
|
161
|
+
}
|
|
162
|
+
function runTypos(files, config, reporter, sink) {
|
|
163
|
+
let hadError = false;
|
|
164
|
+
const findings = [];
|
|
165
|
+
for (const file of files) {
|
|
166
|
+
try {
|
|
167
|
+
findings.push(...analyzeTyposFile(file, config));
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
hadError = true;
|
|
171
|
+
sink.error(`${file}: ${errMsg(err)}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (reporter === 'json') {
|
|
175
|
+
const report = {
|
|
176
|
+
total: findings.length,
|
|
177
|
+
typos: findings,
|
|
178
|
+
};
|
|
179
|
+
sink.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
180
|
+
return { exitCode: findings.length > 0 || hadError ? 1 : 0 };
|
|
181
|
+
}
|
|
182
|
+
if (reporter === 'sarif') {
|
|
183
|
+
const sarifOutput = sarifDocument([
|
|
184
|
+
{
|
|
185
|
+
id: 'color-typo',
|
|
186
|
+
name: 'ColorTypo',
|
|
187
|
+
shortDescription: {
|
|
188
|
+
text: 'Class color name is a likely typo of a Tailwind color',
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
], findings.map((f) => ({
|
|
192
|
+
ruleId: 'color-typo',
|
|
193
|
+
message: { text: `${f.original} → ${f.suggestion}` },
|
|
194
|
+
locations: [
|
|
195
|
+
{
|
|
196
|
+
physicalLocation: {
|
|
197
|
+
artifactLocation: { uri: f.file },
|
|
198
|
+
region: { startLine: f.line, startColumn: f.col },
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
})));
|
|
203
|
+
sink.write(`${JSON.stringify(sarifOutput, null, 2)}\n`);
|
|
204
|
+
return { exitCode: findings.length > 0 || hadError ? 1 : 0 };
|
|
205
|
+
}
|
|
206
|
+
for (const f of findings) {
|
|
207
|
+
sink.log(` ${f.file}:${f.line}:${f.col} ${f.original} → ${f.suggestion} [typo]`);
|
|
208
|
+
}
|
|
209
|
+
if (findings.length === 0) {
|
|
210
|
+
sink.log('✓ No likely typos found');
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
sink.log(`\n✖ Found ${pluralize(findings.length, 'likely typo')}`);
|
|
214
|
+
}
|
|
215
|
+
return { exitCode: findings.length > 0 || hadError ? 1 : 0 };
|
|
56
216
|
}
|
|
57
217
|
async function processFile(file, flags, config) {
|
|
58
218
|
const opts = toClassStringOpts(config);
|
|
@@ -70,40 +230,215 @@ async function processFile(file, flags, config) {
|
|
|
70
230
|
function totalOf(c) {
|
|
71
231
|
return c.fixed + c.deduped + c.merged + c.sorted;
|
|
72
232
|
}
|
|
233
|
+
function scaleClass(property, value) {
|
|
234
|
+
return value.startsWith('-')
|
|
235
|
+
? `-${property}-${value.slice(1)}`
|
|
236
|
+
: `${property}-${value}`;
|
|
237
|
+
}
|
|
238
|
+
function findUnconfiguredClassFunctions(files, config) {
|
|
239
|
+
const configured = new Set(config.functionNames ?? []);
|
|
240
|
+
const missing = KNOWN_CLASS_FUNCTIONS.filter((name) => !configured.has(name));
|
|
241
|
+
if (missing.length === 0)
|
|
242
|
+
return [];
|
|
243
|
+
const found = new Set();
|
|
244
|
+
const re = new RegExp(`\\b(${missing.join('|')})\\s*\\(`);
|
|
245
|
+
for (const file of files) {
|
|
246
|
+
let content;
|
|
247
|
+
try {
|
|
248
|
+
content = readFileSync(file, 'utf8');
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const match = content.match(re);
|
|
254
|
+
if (match?.[1])
|
|
255
|
+
found.add(match[1]);
|
|
256
|
+
}
|
|
257
|
+
return [...found].sort();
|
|
258
|
+
}
|
|
259
|
+
function compactPath(file) {
|
|
260
|
+
const normalized = file.replace(/\\/g, '/');
|
|
261
|
+
const parts = normalized.split('/');
|
|
262
|
+
if (parts.length <= 4)
|
|
263
|
+
return normalized;
|
|
264
|
+
return `.../${parts.slice(-4).join('/')}`;
|
|
265
|
+
}
|
|
266
|
+
function withMore(values, limit, format) {
|
|
267
|
+
const shown = values.slice(0, limit).map(format);
|
|
268
|
+
const remaining = values.length - shown.length;
|
|
269
|
+
if (remaining > 0)
|
|
270
|
+
shown.push(`+${remaining} more`);
|
|
271
|
+
return shown.join(', ');
|
|
272
|
+
}
|
|
273
|
+
function scaleValueSummary(property, value) {
|
|
274
|
+
return `${scaleClass(property, value.value)} (${pluralize(value.count, 'use')}, ${pluralize(value.files.length, 'file')})`;
|
|
275
|
+
}
|
|
276
|
+
function scaleTotalUses(scale) {
|
|
277
|
+
return scale.values.reduce((sum, value) => sum + value.count, 0);
|
|
278
|
+
}
|
|
279
|
+
function logAnalyzeText(report, issueCount, config, sink) {
|
|
280
|
+
const textOptions = {
|
|
281
|
+
maxScaleGroups: config.analyze?.maxScaleGroups ??
|
|
282
|
+
DEFAULT_ANALYZE_TEXT_OPTIONS.maxScaleGroups,
|
|
283
|
+
maxScaleValues: config.analyze?.maxScaleValues ??
|
|
284
|
+
DEFAULT_ANALYZE_TEXT_OPTIONS.maxScaleValues,
|
|
285
|
+
maxRareValues: config.analyze?.maxRareValues ??
|
|
286
|
+
DEFAULT_ANALYZE_TEXT_OPTIONS.maxRareValues,
|
|
287
|
+
maxPatterns: config.analyze?.maxPatterns ?? DEFAULT_ANALYZE_TEXT_OPTIONS.maxPatterns,
|
|
288
|
+
};
|
|
289
|
+
sink.log('tailwind-canonical analyze');
|
|
290
|
+
sink.log(`Files analyzed: ${report.filesAnalyzed}`);
|
|
291
|
+
sink.log(`Issue groups: ${issueCount} (${pluralize(report.colorVariants.length, 'color')}, ${pluralize(report.scaleInconsistencies.length, 'scale')}, ${pluralize(report.combinations.length, 'pattern')})`);
|
|
292
|
+
if (report.rareScaleValues.length > 0) {
|
|
293
|
+
sink.log(`Rare values: ${report.rareScaleValues.length}`);
|
|
294
|
+
}
|
|
295
|
+
if (issueCount === 0) {
|
|
296
|
+
sink.log('\nNo cross-file inconsistencies found');
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (report.colorVariants.length > 0) {
|
|
300
|
+
sink.log('\nColor variants');
|
|
301
|
+
for (const group of report.colorVariants) {
|
|
302
|
+
const tokens = group.variants
|
|
303
|
+
.map((v) => `${group.property}-${v.token} x${v.count}`)
|
|
304
|
+
.join(', ');
|
|
305
|
+
sink.log(` - ${group.property}/${group.family}: ${tokens}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (report.scaleInconsistencies.length > 0) {
|
|
309
|
+
sink.log('\nScale inconsistency groups');
|
|
310
|
+
const scales = [...report.scaleInconsistencies].sort((a, b) => scaleTotalUses(b) - scaleTotalUses(a) ||
|
|
311
|
+
a.property.localeCompare(b.property));
|
|
312
|
+
for (const scale of scales.slice(0, textOptions.maxScaleGroups)) {
|
|
313
|
+
const totalUses = scaleTotalUses(scale);
|
|
314
|
+
const files = new Set(scale.values.flatMap((value) => value.files));
|
|
315
|
+
sink.log(` - ${scale.property}: ${scale.values.length} values, ${totalUses} uses, ${pluralize(files.size, 'file')}`);
|
|
316
|
+
sink.log(` Top: ${withMore(scale.values, textOptions.maxScaleValues, (value) => scaleValueSummary(scale.property, value))}`);
|
|
317
|
+
}
|
|
318
|
+
const remaining = scales.length - textOptions.maxScaleGroups;
|
|
319
|
+
if (remaining > 0)
|
|
320
|
+
sink.log(` - +${remaining} more scale groups`);
|
|
321
|
+
}
|
|
322
|
+
if (report.rareScaleValues.length > 0) {
|
|
323
|
+
sink.log('\nRare scale values');
|
|
324
|
+
for (const rare of report.rareScaleValues.slice(0, textOptions.maxRareValues)) {
|
|
325
|
+
const example = rare.files[0]
|
|
326
|
+
? `; e.g. ${compactPath(rare.files[0])}`
|
|
327
|
+
: '';
|
|
328
|
+
sink.log(` - ${rare.className}: ${pluralize(rare.count, 'use')} in ${pluralize(rare.files.length, 'file')} (${rare.propertyCount} ${rare.property} uses total)${example}`);
|
|
329
|
+
}
|
|
330
|
+
const remaining = report.rareScaleValues.length - textOptions.maxRareValues;
|
|
331
|
+
if (remaining > 0)
|
|
332
|
+
sink.log(` - +${remaining} more rare values`);
|
|
333
|
+
}
|
|
334
|
+
if (report.combinations.length > 0) {
|
|
335
|
+
sink.log('\nRepeated patterns');
|
|
336
|
+
for (const combo of report.combinations.slice(0, textOptions.maxPatterns)) {
|
|
337
|
+
sink.log(` - Pattern: "${combo.classes.join(' ')}" repeated in ${pluralize(combo.files.length, 'file')}`);
|
|
338
|
+
}
|
|
339
|
+
const remaining = report.combinations.length - textOptions.maxPatterns;
|
|
340
|
+
if (remaining > 0)
|
|
341
|
+
sink.log(` - +${remaining} more repeated patterns`);
|
|
342
|
+
}
|
|
343
|
+
sink.log(`\nFound ${pluralize(issueCount, 'consistency issue')} across ${pluralize(report.filesAnalyzed, 'file')}`);
|
|
344
|
+
}
|
|
73
345
|
function runAnalyze(files, config, reporter, sink) {
|
|
74
346
|
let hadError = false;
|
|
75
|
-
const
|
|
347
|
+
const options = toConsistencyOptions(config);
|
|
348
|
+
const report = analyzeConsistencyFiles(files, config, options, (file, err) => {
|
|
76
349
|
hadError = true;
|
|
77
350
|
sink.error(`${file}: ${errMsg(err)}`);
|
|
78
351
|
});
|
|
79
352
|
const issueCount = report.colorVariants.length +
|
|
80
353
|
report.scaleInconsistencies.length +
|
|
81
354
|
report.combinations.length;
|
|
355
|
+
const unconfiguredFunctions = findUnconfiguredClassFunctions(files, config);
|
|
356
|
+
if (unconfiguredFunctions.length > 0) {
|
|
357
|
+
const calls = unconfiguredFunctions
|
|
358
|
+
.map((name) => `${name}(...)`)
|
|
359
|
+
.join(', ');
|
|
360
|
+
sink.error(`Warning: detected ${calls} calls but functionNames does not include them; --analyze may miss class strings.`);
|
|
361
|
+
}
|
|
82
362
|
if (reporter === 'json') {
|
|
83
363
|
sink.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
84
364
|
return { exitCode: issueCount > 0 || hadError ? 1 : 0 };
|
|
85
365
|
}
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
.join(', ');
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
366
|
+
if (reporter === 'sarif') {
|
|
367
|
+
const results = [];
|
|
368
|
+
for (const group of report.colorVariants) {
|
|
369
|
+
const tokens = group.variants.map((v) => v.token).join(', ');
|
|
370
|
+
const files = [...new Set(group.variants.flatMap((v) => v.files))];
|
|
371
|
+
results.push({
|
|
372
|
+
ruleId: 'color-variant-inconsistency',
|
|
373
|
+
message: {
|
|
374
|
+
text: `${group.variants.length} ${group.family} color variants for ${group.property}: ${tokens}`,
|
|
375
|
+
},
|
|
376
|
+
locations: fileLocations(files),
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
for (const scale of report.scaleInconsistencies) {
|
|
380
|
+
const values = scale.values
|
|
381
|
+
.map((v) => scaleClass(scale.property, v.value))
|
|
382
|
+
.join(' vs ');
|
|
383
|
+
const files = [...new Set(scale.values.flatMap((v) => v.files))];
|
|
384
|
+
results.push({
|
|
385
|
+
ruleId: 'scale-inconsistency',
|
|
386
|
+
message: { text: `${scale.property} inconsistency: ${values}` },
|
|
387
|
+
locations: fileLocations(files),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
for (const rare of report.rareScaleValues) {
|
|
391
|
+
results.push({
|
|
392
|
+
ruleId: 'rare-scale-value',
|
|
393
|
+
message: {
|
|
394
|
+
text: `${rare.className} is rare for ${rare.property}: ${rare.count} occurrence(s) in ${rare.files.length} file(s), within ${rare.propertyCount} ${rare.property} uses`,
|
|
395
|
+
},
|
|
396
|
+
locations: fileLocations(rare.files),
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
for (const combo of report.combinations) {
|
|
400
|
+
results.push({
|
|
401
|
+
ruleId: 'repeated-combination',
|
|
402
|
+
message: {
|
|
403
|
+
text: `Repeated class combination: ${combo.classes.join(' ')}`,
|
|
404
|
+
},
|
|
405
|
+
locations: fileLocations(combo.files),
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
const sarifOutput = sarifDocument([
|
|
409
|
+
{
|
|
410
|
+
id: 'color-variant-inconsistency',
|
|
411
|
+
name: 'ColorVariantInconsistency',
|
|
412
|
+
shortDescription: {
|
|
413
|
+
text: 'Multiple color variants of the same family used for one property',
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
id: 'scale-inconsistency',
|
|
418
|
+
name: 'ScaleInconsistency',
|
|
419
|
+
shortDescription: {
|
|
420
|
+
text: 'Inconsistent scale values used for the same property',
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
id: 'rare-scale-value',
|
|
425
|
+
name: 'RareScaleValue',
|
|
426
|
+
shortDescription: {
|
|
427
|
+
text: 'A scale value appears rarely within an otherwise common property',
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
id: 'repeated-combination',
|
|
432
|
+
name: 'RepeatedCombination',
|
|
433
|
+
shortDescription: {
|
|
434
|
+
text: 'Identical class combination repeated across files',
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
], results);
|
|
438
|
+
sink.write(`${JSON.stringify(sarifOutput, null, 2)}\n`);
|
|
439
|
+
return { exitCode: issueCount > 0 || hadError ? 1 : 0 };
|
|
106
440
|
}
|
|
441
|
+
logAnalyzeText(report, issueCount, config, sink);
|
|
107
442
|
return { exitCode: issueCount > 0 || hadError ? 1 : 0 };
|
|
108
443
|
}
|
|
109
444
|
function startWatch(files, flags, config, sink) {
|
|
@@ -149,7 +484,7 @@ function startWatch(files, flags, config, sink) {
|
|
|
149
484
|
}, 50));
|
|
150
485
|
});
|
|
151
486
|
}
|
|
152
|
-
process.
|
|
487
|
+
process.once('SIGINT', () => {
|
|
153
488
|
sink.log('\nWatcher stopped.');
|
|
154
489
|
process.exit(0);
|
|
155
490
|
});
|
|
@@ -157,6 +492,10 @@ function startWatch(files, flags, config, sink) {
|
|
|
157
492
|
}
|
|
158
493
|
export async function run(argv, cwd, sink = defaultSink) {
|
|
159
494
|
const flags = parseArgs(argv);
|
|
495
|
+
if (flags.error) {
|
|
496
|
+
sink.error(flags.error);
|
|
497
|
+
return { exitCode: 1 };
|
|
498
|
+
}
|
|
160
499
|
if (flags.targets.length === 0) {
|
|
161
500
|
sink.error(USAGE);
|
|
162
501
|
return { exitCode: 1 };
|
|
@@ -178,9 +517,14 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
178
517
|
sink.error(`tailwind-canonical: ${err instanceof Error ? err.message : String(err)}`);
|
|
179
518
|
return { exitCode: 1 };
|
|
180
519
|
}
|
|
520
|
+
for (const warning of flagWarnings(flags)) {
|
|
521
|
+
sink.error(`Warning: ${warning}`);
|
|
522
|
+
}
|
|
181
523
|
const files = await resolveTargets(flags.targets);
|
|
182
524
|
if (flags.analyze)
|
|
183
525
|
return runAnalyze(files, config, flags.reporter, sink);
|
|
526
|
+
if (flags.typos)
|
|
527
|
+
return runTypos(files, config, flags.reporter, sink);
|
|
184
528
|
const transforming = flags.fix || flags.dedup || flags.merge || flags.sort;
|
|
185
529
|
const totals = { fixed: 0, deduped: 0, merged: 0, sorted: 0 };
|
|
186
530
|
const allFindings = [];
|
|
@@ -229,14 +573,15 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
229
573
|
return startWatch(files, flags, config, sink);
|
|
230
574
|
if (transforming) {
|
|
231
575
|
if (flags.reporter === 'json') {
|
|
232
|
-
|
|
576
|
+
const report = {
|
|
233
577
|
files: files.length,
|
|
234
578
|
changedFiles,
|
|
235
579
|
fixed: totals.fixed,
|
|
236
580
|
deduped: totals.deduped,
|
|
237
581
|
merged: totals.merged,
|
|
238
582
|
sorted: totals.sorted,
|
|
239
|
-
}
|
|
583
|
+
};
|
|
584
|
+
sink.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
240
585
|
return { exitCode: hadError ? 1 : 0 };
|
|
241
586
|
}
|
|
242
587
|
const parts = [];
|
|
@@ -253,7 +598,7 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
253
598
|
}
|
|
254
599
|
const totalFindings = allFindings.length;
|
|
255
600
|
if (flags.reporter === 'json') {
|
|
256
|
-
|
|
601
|
+
const report = {
|
|
257
602
|
files: files.length,
|
|
258
603
|
total: totalFindings,
|
|
259
604
|
findings: allFindings.map((f) => ({
|
|
@@ -264,47 +609,33 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
264
609
|
canonical: f.suggestion.canonical,
|
|
265
610
|
isCustomToken: f.suggestion.isCustomToken,
|
|
266
611
|
})),
|
|
267
|
-
}
|
|
612
|
+
};
|
|
613
|
+
sink.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
268
614
|
return { exitCode: totalFindings > 0 || hadError ? 1 : 0 };
|
|
269
615
|
}
|
|
270
616
|
if (flags.reporter === 'sarif') {
|
|
271
|
-
const sarifOutput =
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
617
|
+
const sarifOutput = sarifDocument([
|
|
618
|
+
{
|
|
619
|
+
id: 'no-arbitrary-canonical',
|
|
620
|
+
name: 'NoArbitraryCanonical',
|
|
621
|
+
shortDescription: {
|
|
622
|
+
text: 'Arbitrary value has a canonical Tailwind equivalent',
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
], allFindings.map((f) => ({
|
|
626
|
+
ruleId: 'no-arbitrary-canonical',
|
|
627
|
+
message: {
|
|
628
|
+
text: `${f.suggestion.original} → ${f.suggestion.canonical}`,
|
|
629
|
+
},
|
|
630
|
+
locations: [
|
|
275
631
|
{
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
informationUri: 'https://github.com/peak-lab/tailwind-canonical',
|
|
280
|
-
rules: [
|
|
281
|
-
{
|
|
282
|
-
id: 'no-arbitrary-canonical',
|
|
283
|
-
name: 'NoArbitraryCanonical',
|
|
284
|
-
shortDescription: {
|
|
285
|
-
text: 'Arbitrary value has a canonical Tailwind equivalent',
|
|
286
|
-
},
|
|
287
|
-
},
|
|
288
|
-
],
|
|
289
|
-
},
|
|
632
|
+
physicalLocation: {
|
|
633
|
+
artifactLocation: { uri: f.file },
|
|
634
|
+
region: { startLine: f.line, startColumn: f.col },
|
|
290
635
|
},
|
|
291
|
-
results: allFindings.map((f) => ({
|
|
292
|
-
ruleId: 'no-arbitrary-canonical',
|
|
293
|
-
message: {
|
|
294
|
-
text: `${f.suggestion.original} → ${f.suggestion.canonical}`,
|
|
295
|
-
},
|
|
296
|
-
locations: [
|
|
297
|
-
{
|
|
298
|
-
physicalLocation: {
|
|
299
|
-
artifactLocation: { uri: f.file },
|
|
300
|
-
region: { startLine: f.line, startColumn: f.col },
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
],
|
|
304
|
-
})),
|
|
305
636
|
},
|
|
306
637
|
],
|
|
307
|
-
};
|
|
638
|
+
})));
|
|
308
639
|
sink.write(`${JSON.stringify(sarifOutput, null, 2)}\n`);
|
|
309
640
|
return { exitCode: totalFindings > 0 || hadError ? 1 : 0 };
|
|
310
641
|
}
|