tailwind-canonical 0.6.0 → 0.7.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 +103 -21
- package/dist/cli/cli.d.ts +2 -12
- package/dist/cli/cli.js +241 -512
- package/dist/cli/format.d.ts +3 -0
- package/dist/cli/format.js +11 -0
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +0 -1
- package/dist/cli/reporters.d.ts +69 -0
- package/dist/cli/reporters.js +333 -0
- package/dist/cli/types.d.ts +16 -0
- package/dist/cli/types.js +1 -0
- package/dist/cli/watch.d.ts +11 -0
- package/dist/cli/watch.js +33 -0
- package/dist/core/analyzer.d.ts +0 -1
- package/dist/core/analyzer.js +3 -3
- package/dist/core/class-strings.d.ts +0 -1
- package/dist/core/class-strings.js +13 -8
- package/dist/core/config.d.ts +2 -3
- package/dist/core/config.js +34 -44
- package/dist/core/consistency.d.ts +1 -2
- package/dist/core/consistency.js +1 -6
- package/dist/core/deduplicator.d.ts +0 -1
- package/dist/core/deduplicator.js +119 -175
- package/dist/core/fixer.d.ts +0 -1
- package/dist/core/fixer.js +0 -1
- package/dist/core/lexicon.d.ts +1 -1
- package/dist/core/lexicon.js +1 -2
- package/dist/core/merger.d.ts +0 -1
- package/dist/core/merger.js +41 -29
- package/dist/core/rules.d.ts +2 -1
- package/dist/core/rules.js +90 -57
- package/dist/core/scanner.d.ts +0 -1
- package/dist/core/scanner.js +23 -21
- package/dist/core/sorter.d.ts +0 -1
- package/dist/core/sorter.js +85 -10
- package/dist/core/suppressions.d.ts +1 -3
- package/dist/core/suppressions.js +17 -14
- package/dist/core/typos.d.ts +0 -1
- package/dist/core/typos.js +3 -3
- package/dist/eslint/plugin.d.ts +0 -1
- package/dist/eslint/plugin.js +64 -30
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/io/analyzer.d.ts +0 -1
- package/dist/io/analyzer.js +0 -1
- package/dist/io/config.d.ts +9 -1
- package/dist/io/config.js +66 -5
- package/dist/io/consistency.d.ts +0 -1
- package/dist/io/consistency.js +0 -1
- package/dist/io/deduplicator.d.ts +0 -1
- package/dist/io/deduplicator.js +0 -1
- package/dist/io/fixer.d.ts +0 -1
- package/dist/io/fixer.js +0 -1
- package/dist/io/merger.d.ts +0 -1
- package/dist/io/merger.js +0 -1
- package/dist/io/scanner.d.ts +0 -1
- package/dist/io/scanner.js +0 -1
- package/dist/io/sorter.d.ts +0 -1
- package/dist/io/sorter.js +0 -1
- package/dist/io/typos.d.ts +0 -1
- package/dist/io/typos.js +0 -1
- package/package.json +10 -9
- package/dist/cli/cli.d.ts.map +0 -1
- package/dist/cli/cli.js.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/core/analyzer.d.ts.map +0 -1
- package/dist/core/analyzer.js.map +0 -1
- package/dist/core/class-strings.d.ts.map +0 -1
- package/dist/core/class-strings.js.map +0 -1
- package/dist/core/config.d.ts.map +0 -1
- package/dist/core/config.js.map +0 -1
- package/dist/core/consistency.d.ts.map +0 -1
- package/dist/core/consistency.js.map +0 -1
- package/dist/core/deduplicator.d.ts.map +0 -1
- package/dist/core/deduplicator.js.map +0 -1
- package/dist/core/fixer.d.ts.map +0 -1
- package/dist/core/fixer.js.map +0 -1
- package/dist/core/lexicon.d.ts.map +0 -1
- package/dist/core/lexicon.js.map +0 -1
- package/dist/core/merger.d.ts.map +0 -1
- package/dist/core/merger.js.map +0 -1
- package/dist/core/rules.d.ts.map +0 -1
- package/dist/core/rules.js.map +0 -1
- package/dist/core/scanner.d.ts.map +0 -1
- package/dist/core/scanner.js.map +0 -1
- package/dist/core/sorter.d.ts.map +0 -1
- package/dist/core/sorter.js.map +0 -1
- package/dist/core/suppressions.d.ts.map +0 -1
- package/dist/core/suppressions.js.map +0 -1
- package/dist/core/typos.d.ts.map +0 -1
- package/dist/core/typos.js.map +0 -1
- package/dist/eslint/plugin.d.ts.map +0 -1
- package/dist/eslint/plugin.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/io/analyzer.d.ts.map +0 -1
- package/dist/io/analyzer.js.map +0 -1
- package/dist/io/config.d.ts.map +0 -1
- package/dist/io/config.js.map +0 -1
- package/dist/io/consistency.d.ts.map +0 -1
- package/dist/io/consistency.js.map +0 -1
- package/dist/io/deduplicator.d.ts.map +0 -1
- package/dist/io/deduplicator.js.map +0 -1
- package/dist/io/fixer.d.ts.map +0 -1
- package/dist/io/fixer.js.map +0 -1
- package/dist/io/merger.d.ts.map +0 -1
- package/dist/io/merger.js.map +0 -1
- package/dist/io/scanner.d.ts.map +0 -1
- package/dist/io/scanner.js.map +0 -1
- package/dist/io/sorter.d.ts.map +0 -1
- package/dist/io/sorter.js.map +0 -1
- package/dist/io/typos.d.ts.map +0 -1
- package/dist/io/typos.js.map +0 -1
package/dist/cli/cli.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { analyzeFile } from '../core/analyzer.js';
|
|
4
4
|
import { toClassStringOpts } from '../core/class-strings.js';
|
|
5
|
-
import { CONFIG_FILENAMES, loadConfig } from '../core/config.js';
|
|
5
|
+
import { CONFIG_FILENAMES, loadConfig, resolveInitFilename, } from '../core/config.js';
|
|
6
6
|
import { analyzeConsistencyFiles, toConsistencyOptions, } from '../core/consistency.js';
|
|
7
|
-
import { dedupeContent
|
|
8
|
-
import { fixContent
|
|
9
|
-
import { mergeContent
|
|
7
|
+
import { dedupeContent } from '../core/deduplicator.js';
|
|
8
|
+
import { fixContent } from '../core/fixer.js';
|
|
9
|
+
import { mergeContent } from '../core/merger.js';
|
|
10
10
|
import { resolveTargets } from '../core/scanner.js';
|
|
11
|
-
import { sortContent
|
|
11
|
+
import { sortContent } from '../core/sorter.js';
|
|
12
12
|
import { analyzeTyposFile } from '../core/typos.js';
|
|
13
|
-
|
|
13
|
+
import { errMsg, pluralize, timestamp } from './format.js';
|
|
14
|
+
import { analyzeSarifDocument, findingsSarifDocument, logAnalyzeText, logFindings, logTransformCounts, logTyposText, logWatchFindings, sarifDocument, typoSarifDocument, writeJson, } from './reporters.js';
|
|
15
|
+
import { startWatch } from './watch.js';
|
|
14
16
|
const USAGE = 'Usage: tailwind-canonical [--fix] [--merge] [--dedup] [--sort] [--check] [--analyze] [--typos] [--watch] [--reporter json|sarif] <dir|file> [dir|file...]\n tailwind-canonical init';
|
|
15
|
-
const
|
|
16
|
-
"import type { Config } from 'tailwind-canonical'",
|
|
17
|
-
'',
|
|
18
|
-
'export default {',
|
|
17
|
+
const INIT_CONFIG_BODY = [
|
|
19
18
|
' // Run `tailwind-canonical` with no flags using these defaults:',
|
|
20
19
|
" // defaultCommand: { fix: true, dedup: true, sort: true, typos: true, targets: ['./src'] },",
|
|
21
20
|
' // px → token additions/overrides:',
|
|
@@ -23,13 +22,26 @@ const INIT_CONFIG_CONTENT = [
|
|
|
23
22
|
" // customSpacingTokens: { 14: '3.5' },",
|
|
24
23
|
' // Never suggest replacements for classes matching:',
|
|
25
24
|
' // ignorePatterns: [/^font-/],',
|
|
25
|
+
];
|
|
26
|
+
const INIT_CONFIG_TS = [
|
|
27
|
+
"import type { Config } from 'tailwind-canonical'",
|
|
28
|
+
'',
|
|
29
|
+
'export default {',
|
|
30
|
+
...INIT_CONFIG_BODY,
|
|
26
31
|
'} satisfies Config',
|
|
27
32
|
'',
|
|
28
33
|
].join('\n');
|
|
34
|
+
const INIT_CONFIG_JS = [
|
|
35
|
+
"/** @type {import('tailwind-canonical').Config} */",
|
|
36
|
+
'export default {',
|
|
37
|
+
...INIT_CONFIG_BODY,
|
|
38
|
+
'}',
|
|
39
|
+
'',
|
|
40
|
+
].join('\n');
|
|
29
41
|
const HELP_TEXT = [
|
|
30
42
|
USAGE,
|
|
31
43
|
'',
|
|
32
|
-
' init Create a tailwind-canonical
|
|
44
|
+
' init Create a tailwind-canonical config scaffold in the current directory',
|
|
33
45
|
' --fix Auto-replace arbitrary values with canonical Tailwind classes',
|
|
34
46
|
' --dedup Remove redundant classes and collapse shorthands',
|
|
35
47
|
' --merge Resolve conflicting classes via tailwind-merge',
|
|
@@ -51,49 +63,6 @@ const defaultSink = {
|
|
|
51
63
|
write: (s) => process.stdout.write(s),
|
|
52
64
|
};
|
|
53
65
|
const KNOWN_CLASS_FUNCTIONS = ['cn', 'clsx', 'cva'];
|
|
54
|
-
const DEFAULT_ANALYZE_TEXT_OPTIONS = {
|
|
55
|
-
maxScaleGroups: 8,
|
|
56
|
-
maxScaleValues: 5,
|
|
57
|
-
maxRareValues: 12,
|
|
58
|
-
maxPatterns: 10,
|
|
59
|
-
};
|
|
60
|
-
function sarifDocument(rules, results) {
|
|
61
|
-
return {
|
|
62
|
-
$schema: SARIF_SCHEMA,
|
|
63
|
-
version: '2.1.0',
|
|
64
|
-
runs: [
|
|
65
|
-
{
|
|
66
|
-
tool: {
|
|
67
|
-
driver: {
|
|
68
|
-
name: 'tailwind-canonical',
|
|
69
|
-
informationUri: 'https://github.com/peak-lab/tailwind-canonical',
|
|
70
|
-
rules,
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
results,
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
function fileLocations(files) {
|
|
79
|
-
return files.map((uri) => ({
|
|
80
|
-
physicalLocation: {
|
|
81
|
-
artifactLocation: { uri },
|
|
82
|
-
region: { startLine: 1, startColumn: 1 },
|
|
83
|
-
},
|
|
84
|
-
}));
|
|
85
|
-
}
|
|
86
|
-
function errMsg(e) {
|
|
87
|
-
return e instanceof Error ? e.message : String(e);
|
|
88
|
-
}
|
|
89
|
-
function pluralize(n, word) {
|
|
90
|
-
return `${n} ${word}${n !== 1 ? 's' : ''}`;
|
|
91
|
-
}
|
|
92
|
-
function timestamp() {
|
|
93
|
-
const d = new Date();
|
|
94
|
-
const pad = (n) => n.toString().padStart(2, '0');
|
|
95
|
-
return `[${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}]`;
|
|
96
|
-
}
|
|
97
66
|
const REPORTERS = ['text', 'json', 'sarif'];
|
|
98
67
|
const MODE_FLAGS = [
|
|
99
68
|
'--fix',
|
|
@@ -185,35 +154,26 @@ export function parseArgs(argv) {
|
|
|
185
154
|
hasExplicitCheck: argv.includes('--check'),
|
|
186
155
|
};
|
|
187
156
|
}
|
|
157
|
+
/** Explicit CLI flags win; otherwise `defaultCommand` fills the gap. */
|
|
158
|
+
function fallback(explicit, flag, configured) {
|
|
159
|
+
return explicit ? flag : (configured ?? flag);
|
|
160
|
+
}
|
|
188
161
|
function applyDefaultCommand(flags, config) {
|
|
189
162
|
const defaults = config.defaultCommand;
|
|
190
163
|
if (!defaults)
|
|
191
164
|
return flags;
|
|
165
|
+
const mode = flags.hasExplicitMode;
|
|
192
166
|
return {
|
|
193
167
|
...flags,
|
|
194
|
-
fix:
|
|
195
|
-
merge: flags.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
? flags.analyze
|
|
204
|
-
: (defaults.analyze ?? flags.analyze),
|
|
205
|
-
typos: flags.hasExplicitMode
|
|
206
|
-
? flags.typos
|
|
207
|
-
: (defaults.typos ?? flags.typos),
|
|
208
|
-
watch: flags.hasExplicitWatch
|
|
209
|
-
? flags.watch
|
|
210
|
-
: (defaults.watch ?? flags.watch),
|
|
211
|
-
check: flags.hasExplicitCheck
|
|
212
|
-
? flags.check
|
|
213
|
-
: (defaults.check ?? flags.check),
|
|
214
|
-
reporter: flags.hasExplicitReporter
|
|
215
|
-
? flags.reporter
|
|
216
|
-
: (defaults.reporter ?? flags.reporter),
|
|
168
|
+
fix: fallback(mode, flags.fix, defaults.fix),
|
|
169
|
+
merge: fallback(mode, flags.merge, defaults.merge),
|
|
170
|
+
dedup: fallback(mode, flags.dedup, defaults.dedup),
|
|
171
|
+
sort: fallback(mode, flags.sort, defaults.sort),
|
|
172
|
+
analyze: fallback(mode, flags.analyze, defaults.analyze),
|
|
173
|
+
typos: fallback(mode, flags.typos, defaults.typos),
|
|
174
|
+
watch: fallback(flags.hasExplicitWatch, flags.watch, defaults.watch),
|
|
175
|
+
check: fallback(flags.hasExplicitCheck, flags.check, defaults.check),
|
|
176
|
+
reporter: fallback(flags.hasExplicitReporter, flags.reporter, defaults.reporter),
|
|
217
177
|
targets: flags.targets.length > 0 ? flags.targets : (defaults.targets ?? []),
|
|
218
178
|
};
|
|
219
179
|
}
|
|
@@ -274,102 +234,104 @@ function collectTypos(files, config, sink) {
|
|
|
274
234
|
}
|
|
275
235
|
return { findings, hadError };
|
|
276
236
|
}
|
|
277
|
-
function
|
|
278
|
-
|
|
279
|
-
|
|
237
|
+
function readVersion() {
|
|
238
|
+
const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
239
|
+
return pkg.version;
|
|
240
|
+
}
|
|
241
|
+
function findProjectRoot(cwd) {
|
|
242
|
+
let dir = resolve(cwd);
|
|
243
|
+
while (true) {
|
|
244
|
+
if (existsSync(join(dir, '.git')))
|
|
245
|
+
return dir;
|
|
246
|
+
const parent = dirname(dir);
|
|
247
|
+
if (parent === dir)
|
|
248
|
+
return resolve(cwd);
|
|
249
|
+
dir = parent;
|
|
280
250
|
}
|
|
281
|
-
|
|
282
|
-
|
|
251
|
+
}
|
|
252
|
+
function sarifContext(cwd) {
|
|
253
|
+
let version;
|
|
254
|
+
try {
|
|
255
|
+
version = readVersion();
|
|
283
256
|
}
|
|
284
|
-
|
|
285
|
-
|
|
257
|
+
catch {
|
|
258
|
+
version = undefined;
|
|
286
259
|
}
|
|
260
|
+
return { root: findProjectRoot(cwd), version };
|
|
287
261
|
}
|
|
288
|
-
function
|
|
289
|
-
return sarifDocument([
|
|
290
|
-
{
|
|
291
|
-
id: 'color-typo',
|
|
292
|
-
name: 'ColorTypo',
|
|
293
|
-
shortDescription: {
|
|
294
|
-
text: 'Class color name is a likely typo of a Tailwind color',
|
|
295
|
-
},
|
|
296
|
-
},
|
|
297
|
-
], findings.map((f) => ({
|
|
298
|
-
ruleId: 'color-typo',
|
|
299
|
-
message: { text: `${f.original} → ${f.suggestion}` },
|
|
300
|
-
locations: [
|
|
301
|
-
{
|
|
302
|
-
physicalLocation: {
|
|
303
|
-
artifactLocation: { uri: f.file },
|
|
304
|
-
region: { startLine: f.line, startColumn: f.col },
|
|
305
|
-
},
|
|
306
|
-
},
|
|
307
|
-
],
|
|
308
|
-
})));
|
|
309
|
-
}
|
|
310
|
-
function runTypos(files, config, reporter, sink) {
|
|
262
|
+
function runTypos(files, config, reporter, sink, sarif) {
|
|
311
263
|
const { findings, hadError } = collectTypos(files, config, sink);
|
|
312
264
|
if (reporter === 'json') {
|
|
313
265
|
const report = {
|
|
314
266
|
total: findings.length,
|
|
315
267
|
typos: findings,
|
|
316
268
|
};
|
|
317
|
-
sink
|
|
318
|
-
return { exitCode: findings.length > 0 || hadError ? 1 : 0 };
|
|
269
|
+
writeJson(sink, report);
|
|
319
270
|
}
|
|
320
|
-
if (reporter === 'sarif') {
|
|
321
|
-
sink
|
|
322
|
-
return { exitCode: findings.length > 0 || hadError ? 1 : 0 };
|
|
271
|
+
else if (reporter === 'sarif') {
|
|
272
|
+
writeJson(sink, typoSarifDocument(findings, sarif));
|
|
323
273
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const opts = toClassStringOpts(config);
|
|
329
|
-
const counts = { fixed: 0, deduped: 0, merged: 0, sorted: 0 };
|
|
330
|
-
if (flags.fix)
|
|
331
|
-
counts.fixed = fixFile(file, config);
|
|
332
|
-
if (flags.dedup)
|
|
333
|
-
counts.deduped = dedupeFile(file, opts);
|
|
334
|
-
if (flags.merge)
|
|
335
|
-
counts.merged = await mergeFile(file, opts);
|
|
336
|
-
if (flags.sort)
|
|
337
|
-
counts.sorted = sortFile(file, opts, config.sortOrder);
|
|
338
|
-
return counts;
|
|
274
|
+
else {
|
|
275
|
+
logTyposText(findings, sink);
|
|
276
|
+
}
|
|
277
|
+
return exitWith(findings.length > 0 || hadError);
|
|
339
278
|
}
|
|
340
|
-
|
|
279
|
+
function applyTransforms(content, flags, config, twMerge) {
|
|
341
280
|
const opts = toClassStringOpts(config);
|
|
342
|
-
const counts =
|
|
343
|
-
let
|
|
281
|
+
const counts = emptyCounts();
|
|
282
|
+
let result = content;
|
|
344
283
|
if (flags.fix) {
|
|
345
|
-
const
|
|
346
|
-
counts.fixed = count;
|
|
347
|
-
|
|
284
|
+
const fixed = fixContent(result, config);
|
|
285
|
+
counts.fixed = fixed.count;
|
|
286
|
+
result = fixed.result;
|
|
348
287
|
}
|
|
349
288
|
if (flags.dedup) {
|
|
350
|
-
const
|
|
351
|
-
counts.deduped = count;
|
|
352
|
-
|
|
289
|
+
const deduped = dedupeContent(result, opts);
|
|
290
|
+
counts.deduped = deduped.count;
|
|
291
|
+
result = deduped.result;
|
|
353
292
|
}
|
|
354
|
-
if (flags.merge
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
293
|
+
if (flags.merge) {
|
|
294
|
+
if (!twMerge) {
|
|
295
|
+
throw new Error('--merge requires the optional peer dependency "tailwind-merge". Install it with: pnpm add -D tailwind-merge');
|
|
296
|
+
}
|
|
297
|
+
const merged = mergeContent(result, twMerge, opts);
|
|
298
|
+
counts.merged = merged.count;
|
|
299
|
+
result = merged.result;
|
|
358
300
|
}
|
|
359
301
|
if (flags.sort) {
|
|
360
|
-
const
|
|
361
|
-
counts.sorted = count;
|
|
362
|
-
|
|
302
|
+
const sorted = sortContent(result, opts, config.sortOrder);
|
|
303
|
+
counts.sorted = sorted.count;
|
|
304
|
+
result = sorted.result;
|
|
363
305
|
}
|
|
306
|
+
return { counts, result };
|
|
307
|
+
}
|
|
308
|
+
function processFile(file, flags, config, twMerge) {
|
|
309
|
+
const content = readFileSync(file, 'utf8');
|
|
310
|
+
const { counts, result } = applyTransforms(content, flags, config, twMerge);
|
|
311
|
+
// Counts, not text equality: replaceClassStrings also normalizes attribute
|
|
312
|
+
// spacing, which no transform counts. Writing on any textual difference
|
|
313
|
+
// would rewrite files that --check truthfully reports as clean.
|
|
314
|
+
if (totalOf(counts) > 0)
|
|
315
|
+
writeFileSync(file, result, 'utf8');
|
|
364
316
|
return counts;
|
|
365
317
|
}
|
|
366
318
|
function totalOf(c) {
|
|
367
319
|
return c.fixed + c.deduped + c.merged + c.sorted;
|
|
368
320
|
}
|
|
369
|
-
function
|
|
370
|
-
return
|
|
371
|
-
|
|
372
|
-
|
|
321
|
+
function emptyCounts() {
|
|
322
|
+
return { fixed: 0, deduped: 0, merged: 0, sorted: 0 };
|
|
323
|
+
}
|
|
324
|
+
function addCounts(target, source) {
|
|
325
|
+
target.fixed += source.fixed;
|
|
326
|
+
target.deduped += source.deduped;
|
|
327
|
+
target.merged += source.merged;
|
|
328
|
+
target.sorted += source.sorted;
|
|
329
|
+
}
|
|
330
|
+
function isTransforming(flags) {
|
|
331
|
+
return flags.fix || flags.dedup || flags.merge || flags.sort;
|
|
332
|
+
}
|
|
333
|
+
function exitWith(failed) {
|
|
334
|
+
return { exitCode: failed ? 1 : 0 };
|
|
373
335
|
}
|
|
374
336
|
function findUnconfiguredClassFunctions(files, config) {
|
|
375
337
|
const configured = new Set(config.functionNames ?? []);
|
|
@@ -392,93 +354,7 @@ function findUnconfiguredClassFunctions(files, config) {
|
|
|
392
354
|
}
|
|
393
355
|
return [...found].sort();
|
|
394
356
|
}
|
|
395
|
-
function
|
|
396
|
-
const normalized = file.replace(/\\/g, '/');
|
|
397
|
-
const parts = normalized.split('/');
|
|
398
|
-
if (parts.length <= 4)
|
|
399
|
-
return normalized;
|
|
400
|
-
return `.../${parts.slice(-4).join('/')}`;
|
|
401
|
-
}
|
|
402
|
-
function withMore(values, limit, format) {
|
|
403
|
-
const shown = values.slice(0, limit).map(format);
|
|
404
|
-
const remaining = values.length - shown.length;
|
|
405
|
-
if (remaining > 0)
|
|
406
|
-
shown.push(`+${remaining} more`);
|
|
407
|
-
return shown.join(', ');
|
|
408
|
-
}
|
|
409
|
-
function scaleValueSummary(property, value) {
|
|
410
|
-
return `${scaleClass(property, value.value)} (${pluralize(value.count, 'use')}, ${pluralize(value.files.length, 'file')})`;
|
|
411
|
-
}
|
|
412
|
-
function scaleTotalUses(scale) {
|
|
413
|
-
return scale.values.reduce((sum, value) => sum + value.count, 0);
|
|
414
|
-
}
|
|
415
|
-
function logAnalyzeText(report, issueCount, config, sink) {
|
|
416
|
-
const textOptions = {
|
|
417
|
-
maxScaleGroups: config.analyze?.maxScaleGroups ??
|
|
418
|
-
DEFAULT_ANALYZE_TEXT_OPTIONS.maxScaleGroups,
|
|
419
|
-
maxScaleValues: config.analyze?.maxScaleValues ??
|
|
420
|
-
DEFAULT_ANALYZE_TEXT_OPTIONS.maxScaleValues,
|
|
421
|
-
maxRareValues: config.analyze?.maxRareValues ??
|
|
422
|
-
DEFAULT_ANALYZE_TEXT_OPTIONS.maxRareValues,
|
|
423
|
-
maxPatterns: config.analyze?.maxPatterns ?? DEFAULT_ANALYZE_TEXT_OPTIONS.maxPatterns,
|
|
424
|
-
};
|
|
425
|
-
sink.log('tailwind-canonical analyze');
|
|
426
|
-
sink.log(`Files analyzed: ${report.filesAnalyzed}`);
|
|
427
|
-
sink.log(`Issue groups: ${issueCount} (${pluralize(report.colorVariants.length, 'color')}, ${pluralize(report.scaleInconsistencies.length, 'scale')}, ${pluralize(report.combinations.length, 'pattern')})`);
|
|
428
|
-
if (report.rareScaleValues.length > 0) {
|
|
429
|
-
sink.log(`Rare values: ${report.rareScaleValues.length}`);
|
|
430
|
-
}
|
|
431
|
-
if (issueCount === 0) {
|
|
432
|
-
sink.log('\nNo cross-file inconsistencies found');
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
if (report.colorVariants.length > 0) {
|
|
436
|
-
sink.log('\nColor variants');
|
|
437
|
-
for (const group of report.colorVariants) {
|
|
438
|
-
const tokens = group.variants
|
|
439
|
-
.map((v) => `${group.property}-${v.token} x${v.count}`)
|
|
440
|
-
.join(', ');
|
|
441
|
-
sink.log(` - ${group.property}/${group.family}: ${tokens}`);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
if (report.scaleInconsistencies.length > 0) {
|
|
445
|
-
sink.log('\nScale inconsistency groups');
|
|
446
|
-
const scales = [...report.scaleInconsistencies].sort((a, b) => scaleTotalUses(b) - scaleTotalUses(a) ||
|
|
447
|
-
a.property.localeCompare(b.property));
|
|
448
|
-
for (const scale of scales.slice(0, textOptions.maxScaleGroups)) {
|
|
449
|
-
const totalUses = scaleTotalUses(scale);
|
|
450
|
-
const files = new Set(scale.values.flatMap((value) => value.files));
|
|
451
|
-
sink.log(` - ${scale.property}: ${scale.values.length} values, ${totalUses} uses, ${pluralize(files.size, 'file')}`);
|
|
452
|
-
sink.log(` Top: ${withMore(scale.values, textOptions.maxScaleValues, (value) => scaleValueSummary(scale.property, value))}`);
|
|
453
|
-
}
|
|
454
|
-
const remaining = scales.length - textOptions.maxScaleGroups;
|
|
455
|
-
if (remaining > 0)
|
|
456
|
-
sink.log(` - +${remaining} more scale groups`);
|
|
457
|
-
}
|
|
458
|
-
if (report.rareScaleValues.length > 0) {
|
|
459
|
-
sink.log('\nRare scale values');
|
|
460
|
-
for (const rare of report.rareScaleValues.slice(0, textOptions.maxRareValues)) {
|
|
461
|
-
const example = rare.files[0]
|
|
462
|
-
? `; e.g. ${compactPath(rare.files[0])}`
|
|
463
|
-
: '';
|
|
464
|
-
sink.log(` - ${rare.className}: ${pluralize(rare.count, 'use')} in ${pluralize(rare.files.length, 'file')} (${rare.propertyCount} ${rare.property} uses total)${example}`);
|
|
465
|
-
}
|
|
466
|
-
const remaining = report.rareScaleValues.length - textOptions.maxRareValues;
|
|
467
|
-
if (remaining > 0)
|
|
468
|
-
sink.log(` - +${remaining} more rare values`);
|
|
469
|
-
}
|
|
470
|
-
if (report.combinations.length > 0) {
|
|
471
|
-
sink.log('\nRepeated patterns');
|
|
472
|
-
for (const combo of report.combinations.slice(0, textOptions.maxPatterns)) {
|
|
473
|
-
sink.log(` - Pattern: "${combo.classes.join(' ')}" repeated in ${pluralize(combo.files.length, 'file')}`);
|
|
474
|
-
}
|
|
475
|
-
const remaining = report.combinations.length - textOptions.maxPatterns;
|
|
476
|
-
if (remaining > 0)
|
|
477
|
-
sink.log(` - +${remaining} more repeated patterns`);
|
|
478
|
-
}
|
|
479
|
-
sink.log(`\nFound ${pluralize(issueCount, 'consistency issue')} across ${pluralize(report.filesAnalyzed, 'file')}`);
|
|
480
|
-
}
|
|
481
|
-
function runAnalyze(files, config, reporter, sink) {
|
|
357
|
+
function runAnalyze(files, config, reporter, sink, sarif) {
|
|
482
358
|
let hadError = false;
|
|
483
359
|
const options = toConsistencyOptions(config);
|
|
484
360
|
const report = analyzeConsistencyFiles(files, config, options, (file, err) => {
|
|
@@ -496,135 +372,15 @@ function runAnalyze(files, config, reporter, sink) {
|
|
|
496
372
|
sink.error(`Warning: detected ${calls} calls but functionNames does not include them; --analyze may miss class strings.`);
|
|
497
373
|
}
|
|
498
374
|
if (reporter === 'json') {
|
|
499
|
-
sink
|
|
500
|
-
return { exitCode: issueCount > 0 || hadError ? 1 : 0 };
|
|
375
|
+
writeJson(sink, report);
|
|
501
376
|
}
|
|
502
|
-
if (reporter === 'sarif') {
|
|
503
|
-
|
|
504
|
-
for (const group of report.colorVariants) {
|
|
505
|
-
const tokens = group.variants.map((v) => v.token).join(', ');
|
|
506
|
-
const files = [...new Set(group.variants.flatMap((v) => v.files))];
|
|
507
|
-
results.push({
|
|
508
|
-
ruleId: 'color-variant-inconsistency',
|
|
509
|
-
message: {
|
|
510
|
-
text: `${group.variants.length} ${group.family} color variants for ${group.property}: ${tokens}`,
|
|
511
|
-
},
|
|
512
|
-
locations: fileLocations(files),
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
for (const scale of report.scaleInconsistencies) {
|
|
516
|
-
const values = scale.values
|
|
517
|
-
.map((v) => scaleClass(scale.property, v.value))
|
|
518
|
-
.join(' vs ');
|
|
519
|
-
const files = [...new Set(scale.values.flatMap((v) => v.files))];
|
|
520
|
-
results.push({
|
|
521
|
-
ruleId: 'scale-inconsistency',
|
|
522
|
-
message: { text: `${scale.property} inconsistency: ${values}` },
|
|
523
|
-
locations: fileLocations(files),
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
for (const rare of report.rareScaleValues) {
|
|
527
|
-
results.push({
|
|
528
|
-
ruleId: 'rare-scale-value',
|
|
529
|
-
message: {
|
|
530
|
-
text: `${rare.className} is rare for ${rare.property}: ${rare.count} occurrence(s) in ${rare.files.length} file(s), within ${rare.propertyCount} ${rare.property} uses`,
|
|
531
|
-
},
|
|
532
|
-
locations: fileLocations(rare.files),
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
for (const combo of report.combinations) {
|
|
536
|
-
results.push({
|
|
537
|
-
ruleId: 'repeated-combination',
|
|
538
|
-
message: {
|
|
539
|
-
text: `Repeated class combination: ${combo.classes.join(' ')}`,
|
|
540
|
-
},
|
|
541
|
-
locations: fileLocations(combo.files),
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
const sarifOutput = sarifDocument([
|
|
545
|
-
{
|
|
546
|
-
id: 'color-variant-inconsistency',
|
|
547
|
-
name: 'ColorVariantInconsistency',
|
|
548
|
-
shortDescription: {
|
|
549
|
-
text: 'Multiple color variants of the same family used for one property',
|
|
550
|
-
},
|
|
551
|
-
},
|
|
552
|
-
{
|
|
553
|
-
id: 'scale-inconsistency',
|
|
554
|
-
name: 'ScaleInconsistency',
|
|
555
|
-
shortDescription: {
|
|
556
|
-
text: 'Inconsistent scale values used for the same property',
|
|
557
|
-
},
|
|
558
|
-
},
|
|
559
|
-
{
|
|
560
|
-
id: 'rare-scale-value',
|
|
561
|
-
name: 'RareScaleValue',
|
|
562
|
-
shortDescription: {
|
|
563
|
-
text: 'A scale value appears rarely within an otherwise common property',
|
|
564
|
-
},
|
|
565
|
-
},
|
|
566
|
-
{
|
|
567
|
-
id: 'repeated-combination',
|
|
568
|
-
name: 'RepeatedCombination',
|
|
569
|
-
shortDescription: {
|
|
570
|
-
text: 'Identical class combination repeated across files',
|
|
571
|
-
},
|
|
572
|
-
},
|
|
573
|
-
], results);
|
|
574
|
-
sink.write(`${JSON.stringify(sarifOutput, null, 2)}\n`);
|
|
575
|
-
return { exitCode: issueCount > 0 || hadError ? 1 : 0 };
|
|
576
|
-
}
|
|
577
|
-
logAnalyzeText(report, issueCount, config, sink);
|
|
578
|
-
return { exitCode: issueCount > 0 || hadError ? 1 : 0 };
|
|
579
|
-
}
|
|
580
|
-
function startWatch(files, flags, config, sink) {
|
|
581
|
-
const transforming = flags.fix || flags.dedup || flags.merge || flags.sort;
|
|
582
|
-
const fileSet = new Set(files);
|
|
583
|
-
const inFlight = new Set();
|
|
584
|
-
const timers = new Map();
|
|
585
|
-
const dirs = new Set(files.map((f) => dirname(f)));
|
|
586
|
-
sink.log(`Watching ${pluralize(files.length, 'file')}... (Ctrl+C to stop)`);
|
|
587
|
-
for (const dir of dirs) {
|
|
588
|
-
fsWatch(dir, { recursive: true }, (_, filename) => {
|
|
589
|
-
if (!filename)
|
|
590
|
-
return;
|
|
591
|
-
const full = resolve(dir, filename);
|
|
592
|
-
if (!fileSet.has(full) || inFlight.has(full))
|
|
593
|
-
return;
|
|
594
|
-
clearTimeout(timers.get(full));
|
|
595
|
-
timers.set(full, setTimeout(() => {
|
|
596
|
-
if (transforming) {
|
|
597
|
-
inFlight.add(full);
|
|
598
|
-
processFile(full, flags, config)
|
|
599
|
-
.then((counts) => {
|
|
600
|
-
const total = totalOf(counts);
|
|
601
|
-
if (total > 0) {
|
|
602
|
-
sink.log(`${timestamp()} ${full} — ${pluralize(total, 'change')} applied`);
|
|
603
|
-
}
|
|
604
|
-
})
|
|
605
|
-
.catch((err) => {
|
|
606
|
-
sink.error(`${timestamp()} ${full} — error: ${err instanceof Error ? err.message : String(err)}`);
|
|
607
|
-
})
|
|
608
|
-
.finally(() => inFlight.delete(full));
|
|
609
|
-
}
|
|
610
|
-
else {
|
|
611
|
-
const findings = analyzeFile(full, config);
|
|
612
|
-
if (findings.length > 0) {
|
|
613
|
-
sink.log(`${timestamp()} ${full} — ${pluralize(findings.length, 'finding')}`);
|
|
614
|
-
for (const f of findings) {
|
|
615
|
-
const tag = f.suggestion.isCustomToken ? ' [custom token]' : '';
|
|
616
|
-
sink.log(` ${f.line}:${f.col} ${f.suggestion.original} → ${f.suggestion.canonical}${tag}`);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}, 50));
|
|
621
|
-
});
|
|
377
|
+
else if (reporter === 'sarif') {
|
|
378
|
+
writeJson(sink, analyzeSarifDocument(report, sarif));
|
|
622
379
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
return { exitCode: 0, watching: true };
|
|
380
|
+
else {
|
|
381
|
+
logAnalyzeText(report, issueCount, config, sink);
|
|
382
|
+
}
|
|
383
|
+
return exitWith(issueCount > 0 || hadError);
|
|
628
384
|
}
|
|
629
385
|
function runInit(cwd, sink) {
|
|
630
386
|
const existing = CONFIG_FILENAMES.find((name) => existsSync(join(cwd, name)));
|
|
@@ -632,8 +388,9 @@ function runInit(cwd, sink) {
|
|
|
632
388
|
sink.error(`${existing} already exists`);
|
|
633
389
|
return { exitCode: 1 };
|
|
634
390
|
}
|
|
635
|
-
const filename =
|
|
636
|
-
|
|
391
|
+
const filename = resolveInitFilename(cwd);
|
|
392
|
+
const content = /\.m?ts$/.test(filename) ? INIT_CONFIG_TS : INIT_CONFIG_JS;
|
|
393
|
+
writeFileSync(join(cwd, filename), content, 'utf8');
|
|
637
394
|
sink.log(`Created ${filename}`);
|
|
638
395
|
sink.log('Uncomment defaultCommand to set your CLI defaults.');
|
|
639
396
|
sink.log('Then run `npx tailwind-canonical` to lint your project.');
|
|
@@ -647,8 +404,7 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
647
404
|
}
|
|
648
405
|
if (flags.version) {
|
|
649
406
|
try {
|
|
650
|
-
|
|
651
|
-
sink.log(pkg.version);
|
|
407
|
+
sink.log(readVersion());
|
|
652
408
|
return { exitCode: 0 };
|
|
653
409
|
}
|
|
654
410
|
catch (err) {
|
|
@@ -680,7 +436,7 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
680
436
|
config = await loadConfig(cwd);
|
|
681
437
|
}
|
|
682
438
|
catch (err) {
|
|
683
|
-
sink.error(`tailwind-canonical: ${
|
|
439
|
+
sink.error(`tailwind-canonical: ${errMsg(err)}`);
|
|
684
440
|
return { exitCode: 1 };
|
|
685
441
|
}
|
|
686
442
|
flags = applyDefaultCommand(flags, config);
|
|
@@ -688,12 +444,7 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
688
444
|
sink.error(USAGE);
|
|
689
445
|
return { exitCode: 1 };
|
|
690
446
|
}
|
|
691
|
-
if (flags.check &&
|
|
692
|
-
!flags.analyze &&
|
|
693
|
-
!flags.fix &&
|
|
694
|
-
!flags.dedup &&
|
|
695
|
-
!flags.merge &&
|
|
696
|
-
!flags.sort) {
|
|
447
|
+
if (flags.check && !flags.analyze && !isTransforming(flags)) {
|
|
697
448
|
sink.error('--check requires at least one of --fix, --dedup, --merge, --sort');
|
|
698
449
|
return { exitCode: 1 };
|
|
699
450
|
}
|
|
@@ -716,125 +467,126 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
716
467
|
sink.error(`No files matched: ${flags.targets.join(' ')}`);
|
|
717
468
|
return { exitCode: 1 };
|
|
718
469
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
470
|
+
const sarif = sarifContext(cwd);
|
|
471
|
+
if (flags.analyze) {
|
|
472
|
+
return runAnalyze(files, config, flags.reporter, sink, sarif);
|
|
473
|
+
}
|
|
474
|
+
const transforming = isTransforming(flags);
|
|
722
475
|
if (flags.typos && !transforming) {
|
|
723
|
-
return runTypos(files, config, flags.reporter, sink);
|
|
476
|
+
return runTypos(files, config, flags.reporter, sink, sarif);
|
|
477
|
+
}
|
|
478
|
+
if (flags.watch && !flags.typos && !flags.check) {
|
|
479
|
+
return startWatch({
|
|
480
|
+
files,
|
|
481
|
+
sink,
|
|
482
|
+
processFile: (file) => transforming
|
|
483
|
+
? watchTransform(file, flags, config, sink, twMerge)
|
|
484
|
+
: logWatchFindings(file, analyzeFile(file, config), timestamp(), sink),
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
return transforming
|
|
488
|
+
? runTransforms(files, flags, config, sink, sarif, twMerge)
|
|
489
|
+
: runLint(files, config, flags.reporter, sink, sarif);
|
|
490
|
+
}
|
|
491
|
+
function watchTransform(file, flags, config, sink, twMerge) {
|
|
492
|
+
const total = totalOf(processFile(file, flags, config, twMerge));
|
|
493
|
+
if (total > 0) {
|
|
494
|
+
sink.log(`${timestamp()} ${file} — ${pluralize(total, 'change')} applied`);
|
|
724
495
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const
|
|
496
|
+
}
|
|
497
|
+
function runTransforms(files, flags, config, sink, sarif, twMerge) {
|
|
498
|
+
const totals = emptyCounts();
|
|
728
499
|
const changedFiles = [];
|
|
729
500
|
let hadError = false;
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
if (totalOf(counts) > 0)
|
|
742
|
-
changedFiles.push(file);
|
|
743
|
-
if (flags.reporter === 'text') {
|
|
744
|
-
if (counts.fixed > 0)
|
|
745
|
-
sink.log(` ${flags.check ? 'would fix' : 'fixed '} ${file} (${pluralize(counts.fixed, 'replacement')})`);
|
|
746
|
-
if (counts.deduped > 0)
|
|
747
|
-
sink.log(` ${flags.check ? 'would dedup' : 'deduped'} ${file} (${pluralize(counts.deduped, 'class string')})`);
|
|
748
|
-
if (counts.merged > 0)
|
|
749
|
-
sink.log(` ${flags.check ? 'would merge' : 'merged '} ${file} (${pluralize(counts.merged, 'conflict')})`);
|
|
750
|
-
if (counts.sorted > 0)
|
|
751
|
-
sink.log(` ${flags.check ? 'would sort' : 'sorted '} ${file} (${pluralize(counts.sorted, 'class string')})`);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
else {
|
|
755
|
-
const findings = analyzeFile(file, config);
|
|
756
|
-
allFindings.push(...findings);
|
|
757
|
-
if (flags.reporter === 'text') {
|
|
758
|
-
for (const f of findings) {
|
|
759
|
-
const tag = f.suggestion.isCustomToken ? ' [custom token]' : '';
|
|
760
|
-
sink.log(` ${f.file}:${f.line}:${f.col} ${f.suggestion.original} → ${f.suggestion.canonical}${tag}`);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
catch (err) {
|
|
766
|
-
hadError = true;
|
|
767
|
-
sink.error(`${file}: ${errMsg(err)}`);
|
|
501
|
+
for (const file of files) {
|
|
502
|
+
try {
|
|
503
|
+
const counts = flags.check
|
|
504
|
+
? applyTransforms(readFileSync(file, 'utf8'), flags, config, twMerge)
|
|
505
|
+
.counts
|
|
506
|
+
: processFile(file, flags, config, twMerge);
|
|
507
|
+
addCounts(totals, counts);
|
|
508
|
+
if (totalOf(counts) > 0)
|
|
509
|
+
changedFiles.push(file);
|
|
510
|
+
if (flags.reporter === 'text') {
|
|
511
|
+
logTransformCounts(counts, file, flags.check, sink);
|
|
768
512
|
}
|
|
769
513
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
if (transforming) {
|
|
774
|
-
const typoResult = flags.typos
|
|
775
|
-
? collectTypos(files, config, sink)
|
|
776
|
-
: undefined;
|
|
777
|
-
const totalChanges = totalOf(totals);
|
|
778
|
-
const exitCode = hadError ||
|
|
779
|
-
(flags.check && totalChanges > 0) ||
|
|
780
|
-
(typoResult && (typoResult.findings.length > 0 || typoResult.hadError))
|
|
781
|
-
? 1
|
|
782
|
-
: 0;
|
|
783
|
-
if (flags.reporter === 'json') {
|
|
784
|
-
const report = {
|
|
785
|
-
files: files.length,
|
|
786
|
-
changedFiles,
|
|
787
|
-
fixed: totals.fixed,
|
|
788
|
-
deduped: totals.deduped,
|
|
789
|
-
merged: totals.merged,
|
|
790
|
-
sorted: totals.sorted,
|
|
791
|
-
};
|
|
792
|
-
if (flags.check)
|
|
793
|
-
report.check = true;
|
|
794
|
-
if (typoResult) {
|
|
795
|
-
report.typoTotal = typoResult.findings.length;
|
|
796
|
-
report.typos = typoResult.findings;
|
|
797
|
-
}
|
|
798
|
-
sink.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
799
|
-
return { exitCode };
|
|
514
|
+
catch (err) {
|
|
515
|
+
hadError = true;
|
|
516
|
+
sink.error(`${file}: ${errMsg(err)}`);
|
|
800
517
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
518
|
+
}
|
|
519
|
+
const typoResult = flags.typos
|
|
520
|
+
? collectTypos(files, config, sink)
|
|
521
|
+
: undefined;
|
|
522
|
+
const totalChanges = totalOf(totals);
|
|
523
|
+
const result = exitWith(hadError ||
|
|
524
|
+
(flags.check && totalChanges > 0) ||
|
|
525
|
+
(typoResult !== undefined &&
|
|
526
|
+
(typoResult.findings.length > 0 || typoResult.hadError)));
|
|
527
|
+
if (flags.reporter === 'json') {
|
|
528
|
+
const report = {
|
|
529
|
+
files: files.length,
|
|
530
|
+
changedFiles,
|
|
531
|
+
...totals,
|
|
532
|
+
};
|
|
533
|
+
if (flags.check)
|
|
534
|
+
report.check = true;
|
|
535
|
+
if (typoResult) {
|
|
536
|
+
report.typoTotal = typoResult.findings.length;
|
|
537
|
+
report.typos = typoResult.findings;
|
|
807
538
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
539
|
+
writeJson(sink, report);
|
|
540
|
+
return result;
|
|
541
|
+
}
|
|
542
|
+
if (flags.reporter === 'sarif') {
|
|
543
|
+
const doc = typoResult
|
|
544
|
+
? typoSarifDocument(typoResult.findings, sarif)
|
|
545
|
+
: sarifDocument([], [], sarif);
|
|
546
|
+
writeJson(sink, doc);
|
|
547
|
+
return result;
|
|
548
|
+
}
|
|
549
|
+
if (flags.check) {
|
|
550
|
+
sink.log(totalChanges > 0
|
|
551
|
+
? `\n✖ ${pluralize(totalChanges, 'pending change')} across ${pluralize(changedFiles.length, 'file')} (run without --check to apply)`
|
|
552
|
+
: '✓ No pending changes');
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
const parts = [];
|
|
556
|
+
if (flags.fix)
|
|
557
|
+
parts.push(pluralize(totals.fixed, 'replacement'));
|
|
558
|
+
if (flags.dedup)
|
|
559
|
+
parts.push(pluralize(totals.deduped, 'dedup'));
|
|
560
|
+
if (flags.merge)
|
|
561
|
+
parts.push(`${pluralize(totals.merged, 'conflict')} merged`);
|
|
562
|
+
if (flags.sort)
|
|
563
|
+
parts.push(`${totals.sorted} sorted`);
|
|
564
|
+
sink.log(`\n✓ Fixed ${parts.join(', ')} across ${pluralize(files.length, 'file')}`);
|
|
565
|
+
}
|
|
566
|
+
if (typoResult)
|
|
567
|
+
logTyposText(typoResult.findings, sink);
|
|
568
|
+
return result;
|
|
569
|
+
}
|
|
570
|
+
function runLint(files, config, reporter, sink, sarif) {
|
|
571
|
+
const findings = [];
|
|
572
|
+
let hadError = false;
|
|
573
|
+
for (const file of files) {
|
|
574
|
+
try {
|
|
575
|
+
const fileFindings = analyzeFile(file, config);
|
|
576
|
+
findings.push(...fileFindings);
|
|
577
|
+
if (reporter === 'text')
|
|
578
|
+
logFindings(fileFindings, sink);
|
|
815
579
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
parts.push(pluralize(totals.fixed, 'replacement'));
|
|
820
|
-
if (flags.dedup)
|
|
821
|
-
parts.push(pluralize(totals.deduped, 'dedup'));
|
|
822
|
-
if (flags.merge)
|
|
823
|
-
parts.push(`${pluralize(totals.merged, 'conflict')} merged`);
|
|
824
|
-
if (flags.sort)
|
|
825
|
-
parts.push(`${totals.sorted} sorted`);
|
|
826
|
-
sink.log(`\n✓ Fixed ${parts.join(', ')} across ${pluralize(files.length, 'file')}`);
|
|
580
|
+
catch (err) {
|
|
581
|
+
hadError = true;
|
|
582
|
+
sink.error(`${file}: ${errMsg(err)}`);
|
|
827
583
|
}
|
|
828
|
-
if (typoResult)
|
|
829
|
-
logTyposText(typoResult.findings, sink);
|
|
830
|
-
return { exitCode };
|
|
831
584
|
}
|
|
832
|
-
|
|
833
|
-
if (flags.reporter === 'json') {
|
|
585
|
+
if (reporter === 'json') {
|
|
834
586
|
const report = {
|
|
835
587
|
files: files.length,
|
|
836
|
-
total:
|
|
837
|
-
findings:
|
|
588
|
+
total: findings.length,
|
|
589
|
+
findings: findings.map((f) => ({
|
|
838
590
|
file: f.file,
|
|
839
591
|
line: f.line,
|
|
840
592
|
col: f.col,
|
|
@@ -843,41 +595,18 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
843
595
|
isCustomToken: f.suggestion.isCustomToken,
|
|
844
596
|
})),
|
|
845
597
|
};
|
|
846
|
-
sink
|
|
847
|
-
return
|
|
598
|
+
writeJson(sink, report);
|
|
599
|
+
return exitWith(findings.length > 0 || hadError);
|
|
848
600
|
}
|
|
849
|
-
if (
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
text: 'Arbitrary value has a canonical Tailwind equivalent',
|
|
856
|
-
},
|
|
857
|
-
},
|
|
858
|
-
], allFindings.map((f) => ({
|
|
859
|
-
ruleId: 'no-arbitrary-canonical',
|
|
860
|
-
message: {
|
|
861
|
-
text: `${f.suggestion.original} → ${f.suggestion.canonical}`,
|
|
862
|
-
},
|
|
863
|
-
locations: [
|
|
864
|
-
{
|
|
865
|
-
physicalLocation: {
|
|
866
|
-
artifactLocation: { uri: f.file },
|
|
867
|
-
region: { startLine: f.line, startColumn: f.col },
|
|
868
|
-
},
|
|
869
|
-
},
|
|
870
|
-
],
|
|
871
|
-
})));
|
|
872
|
-
sink.write(`${JSON.stringify(sarifOutput, null, 2)}\n`);
|
|
873
|
-
return { exitCode: totalFindings > 0 || hadError ? 1 : 0 };
|
|
874
|
-
}
|
|
875
|
-
if (totalFindings > 0) {
|
|
876
|
-
sink.log(`\n✖ Found ${totalFindings} non-canonical class${totalFindings !== 1 ? 'es' : ''}`);
|
|
601
|
+
if (reporter === 'sarif') {
|
|
602
|
+
writeJson(sink, findingsSarifDocument(findings, sarif));
|
|
603
|
+
return exitWith(findings.length > 0 || hadError);
|
|
604
|
+
}
|
|
605
|
+
if (findings.length > 0) {
|
|
606
|
+
sink.log(`\n✖ Found ${findings.length} non-canonical class${findings.length !== 1 ? 'es' : ''}`);
|
|
877
607
|
sink.log(' Run with --fix to auto-replace\n');
|
|
878
|
-
return
|
|
608
|
+
return exitWith(true);
|
|
879
609
|
}
|
|
880
610
|
sink.log('✓ No non-canonical classes found');
|
|
881
|
-
return
|
|
611
|
+
return exitWith(hadError);
|
|
882
612
|
}
|
|
883
|
-
//# sourceMappingURL=cli.js.map
|