tailwind-canonical 0.7.0 → 0.8.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/README.md +63 -6
- package/dist/cli/cli.d.ts +2 -11
- package/dist/cli/cli.js +176 -471
- package/dist/cli/format.d.ts +3 -0
- package/dist/cli/format.js +11 -0
- 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.js +3 -2
- package/dist/core/class-strings.js +13 -7
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.js +25 -42
- package/dist/core/consistency.d.ts +1 -1
- package/dist/core/deduplicator.js +110 -170
- package/dist/core/merger.js +9 -3
- package/dist/core/rules.d.ts +5 -0
- package/dist/core/rules.js +453 -62
- package/dist/core/scanner.js +23 -20
- package/dist/core/sorter.js +80 -7
- package/dist/core/suppressions.d.ts +1 -2
- package/dist/core/suppressions.js +17 -13
- package/dist/core/typos.js +3 -2
- package/dist/eslint/plugin.js +62 -28
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/dist/cli/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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';
|
|
@@ -10,7 +10,9 @@ import { mergeContent } from '../core/merger.js';
|
|
|
10
10
|
import { resolveTargets } from '../core/scanner.js';
|
|
11
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
17
|
const INIT_CONFIG_BODY = [
|
|
16
18
|
' // Run `tailwind-canonical` with no flags using these defaults:',
|
|
@@ -61,49 +63,6 @@ const defaultSink = {
|
|
|
61
63
|
write: (s) => process.stdout.write(s),
|
|
62
64
|
};
|
|
63
65
|
const KNOWN_CLASS_FUNCTIONS = ['cn', 'clsx', 'cva'];
|
|
64
|
-
const DEFAULT_ANALYZE_TEXT_OPTIONS = {
|
|
65
|
-
maxScaleGroups: 8,
|
|
66
|
-
maxScaleValues: 5,
|
|
67
|
-
maxRareValues: 12,
|
|
68
|
-
maxPatterns: 10,
|
|
69
|
-
};
|
|
70
|
-
function sarifDocument(rules, results) {
|
|
71
|
-
return {
|
|
72
|
-
$schema: SARIF_SCHEMA,
|
|
73
|
-
version: '2.1.0',
|
|
74
|
-
runs: [
|
|
75
|
-
{
|
|
76
|
-
tool: {
|
|
77
|
-
driver: {
|
|
78
|
-
name: 'tailwind-canonical',
|
|
79
|
-
informationUri: 'https://github.com/peak-lab/tailwind-canonical',
|
|
80
|
-
rules,
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
results,
|
|
84
|
-
},
|
|
85
|
-
],
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
function fileLocations(files) {
|
|
89
|
-
return files.map((uri) => ({
|
|
90
|
-
physicalLocation: {
|
|
91
|
-
artifactLocation: { uri },
|
|
92
|
-
region: { startLine: 1, startColumn: 1 },
|
|
93
|
-
},
|
|
94
|
-
}));
|
|
95
|
-
}
|
|
96
|
-
function errMsg(e) {
|
|
97
|
-
return e instanceof Error ? e.message : String(e);
|
|
98
|
-
}
|
|
99
|
-
function pluralize(n, word) {
|
|
100
|
-
return `${n} ${word}${n !== 1 ? 's' : ''}`;
|
|
101
|
-
}
|
|
102
|
-
function timestamp() {
|
|
103
|
-
const d = new Date();
|
|
104
|
-
const pad = (n) => n.toString().padStart(2, '0');
|
|
105
|
-
return `[${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}]`;
|
|
106
|
-
}
|
|
107
66
|
const REPORTERS = ['text', 'json', 'sarif'];
|
|
108
67
|
const MODE_FLAGS = [
|
|
109
68
|
'--fix',
|
|
@@ -275,59 +234,51 @@ function collectTypos(files, config, sink) {
|
|
|
275
234
|
}
|
|
276
235
|
return { findings, hadError };
|
|
277
236
|
}
|
|
278
|
-
function
|
|
279
|
-
|
|
280
|
-
|
|
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;
|
|
281
250
|
}
|
|
282
|
-
|
|
283
|
-
|
|
251
|
+
}
|
|
252
|
+
function sarifContext(cwd) {
|
|
253
|
+
let version;
|
|
254
|
+
try {
|
|
255
|
+
version = readVersion();
|
|
284
256
|
}
|
|
285
|
-
|
|
286
|
-
|
|
257
|
+
catch {
|
|
258
|
+
version = undefined;
|
|
287
259
|
}
|
|
260
|
+
return { root: findProjectRoot(cwd), version };
|
|
288
261
|
}
|
|
289
|
-
function
|
|
290
|
-
return sarifDocument([
|
|
291
|
-
{
|
|
292
|
-
id: 'color-typo',
|
|
293
|
-
name: 'ColorTypo',
|
|
294
|
-
shortDescription: {
|
|
295
|
-
text: 'Class color name is a likely typo of a Tailwind color',
|
|
296
|
-
},
|
|
297
|
-
},
|
|
298
|
-
], findings.map((f) => ({
|
|
299
|
-
ruleId: 'color-typo',
|
|
300
|
-
message: { text: `${f.original} → ${f.suggestion}` },
|
|
301
|
-
locations: [
|
|
302
|
-
{
|
|
303
|
-
physicalLocation: {
|
|
304
|
-
artifactLocation: { uri: f.file },
|
|
305
|
-
region: { startLine: f.line, startColumn: f.col },
|
|
306
|
-
},
|
|
307
|
-
},
|
|
308
|
-
],
|
|
309
|
-
})));
|
|
310
|
-
}
|
|
311
|
-
function runTypos(files, config, reporter, sink) {
|
|
262
|
+
function runTypos(files, config, reporter, sink, sarif) {
|
|
312
263
|
const { findings, hadError } = collectTypos(files, config, sink);
|
|
313
264
|
if (reporter === 'json') {
|
|
314
265
|
const report = {
|
|
315
266
|
total: findings.length,
|
|
316
267
|
typos: findings,
|
|
317
268
|
};
|
|
318
|
-
sink
|
|
319
|
-
return { exitCode: findings.length > 0 || hadError ? 1 : 0 };
|
|
269
|
+
writeJson(sink, report);
|
|
320
270
|
}
|
|
321
|
-
if (reporter === 'sarif') {
|
|
322
|
-
sink
|
|
323
|
-
|
|
271
|
+
else if (reporter === 'sarif') {
|
|
272
|
+
writeJson(sink, typoSarifDocument(findings, sarif));
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
logTyposText(findings, sink);
|
|
324
276
|
}
|
|
325
|
-
|
|
326
|
-
return { exitCode: findings.length > 0 || hadError ? 1 : 0 };
|
|
277
|
+
return exitWith(findings.length > 0 || hadError);
|
|
327
278
|
}
|
|
328
279
|
function applyTransforms(content, flags, config, twMerge) {
|
|
329
280
|
const opts = toClassStringOpts(config);
|
|
330
|
-
const counts =
|
|
281
|
+
const counts = emptyCounts();
|
|
331
282
|
let result = content;
|
|
332
283
|
if (flags.fix) {
|
|
333
284
|
const fixed = fixContent(result, config);
|
|
@@ -364,47 +315,23 @@ function processFile(file, flags, config, twMerge) {
|
|
|
364
315
|
writeFileSync(file, result, 'utf8');
|
|
365
316
|
return counts;
|
|
366
317
|
}
|
|
367
|
-
const TRANSFORM_LABELS = [
|
|
368
|
-
{
|
|
369
|
-
key: 'fixed',
|
|
370
|
-
applied: 'fixed ',
|
|
371
|
-
pending: 'would fix',
|
|
372
|
-
unit: 'replacement',
|
|
373
|
-
},
|
|
374
|
-
{
|
|
375
|
-
key: 'deduped',
|
|
376
|
-
applied: 'deduped',
|
|
377
|
-
pending: 'would dedup',
|
|
378
|
-
unit: 'class string',
|
|
379
|
-
},
|
|
380
|
-
{
|
|
381
|
-
key: 'merged',
|
|
382
|
-
applied: 'merged ',
|
|
383
|
-
pending: 'would merge',
|
|
384
|
-
unit: 'conflict',
|
|
385
|
-
},
|
|
386
|
-
{
|
|
387
|
-
key: 'sorted',
|
|
388
|
-
applied: 'sorted ',
|
|
389
|
-
pending: 'would sort',
|
|
390
|
-
unit: 'class string',
|
|
391
|
-
},
|
|
392
|
-
];
|
|
393
|
-
function logTransformCounts(counts, file, check, sink) {
|
|
394
|
-
for (const { key, applied, pending, unit } of TRANSFORM_LABELS) {
|
|
395
|
-
const count = counts[key];
|
|
396
|
-
if (count > 0) {
|
|
397
|
-
sink.log(` ${check ? pending : applied} ${file} (${pluralize(count, unit)})`);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
318
|
function totalOf(c) {
|
|
402
319
|
return c.fixed + c.deduped + c.merged + c.sorted;
|
|
403
320
|
}
|
|
404
|
-
function
|
|
405
|
-
return
|
|
406
|
-
|
|
407
|
-
|
|
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 };
|
|
408
335
|
}
|
|
409
336
|
function findUnconfiguredClassFunctions(files, config) {
|
|
410
337
|
const configured = new Set(config.functionNames ?? []);
|
|
@@ -427,93 +354,7 @@ function findUnconfiguredClassFunctions(files, config) {
|
|
|
427
354
|
}
|
|
428
355
|
return [...found].sort();
|
|
429
356
|
}
|
|
430
|
-
function
|
|
431
|
-
const normalized = file.replace(/\\/g, '/');
|
|
432
|
-
const parts = normalized.split('/');
|
|
433
|
-
if (parts.length <= 4)
|
|
434
|
-
return normalized;
|
|
435
|
-
return `.../${parts.slice(-4).join('/')}`;
|
|
436
|
-
}
|
|
437
|
-
function withMore(values, limit, format) {
|
|
438
|
-
const shown = values.slice(0, limit).map(format);
|
|
439
|
-
const remaining = values.length - shown.length;
|
|
440
|
-
if (remaining > 0)
|
|
441
|
-
shown.push(`+${remaining} more`);
|
|
442
|
-
return shown.join(', ');
|
|
443
|
-
}
|
|
444
|
-
function scaleValueSummary(property, value) {
|
|
445
|
-
return `${scaleClass(property, value.value)} (${pluralize(value.count, 'use')}, ${pluralize(value.files.length, 'file')})`;
|
|
446
|
-
}
|
|
447
|
-
function scaleTotalUses(scale) {
|
|
448
|
-
return scale.values.reduce((sum, value) => sum + value.count, 0);
|
|
449
|
-
}
|
|
450
|
-
function logAnalyzeText(report, issueCount, config, sink) {
|
|
451
|
-
const textOptions = {
|
|
452
|
-
maxScaleGroups: config.analyze?.maxScaleGroups ??
|
|
453
|
-
DEFAULT_ANALYZE_TEXT_OPTIONS.maxScaleGroups,
|
|
454
|
-
maxScaleValues: config.analyze?.maxScaleValues ??
|
|
455
|
-
DEFAULT_ANALYZE_TEXT_OPTIONS.maxScaleValues,
|
|
456
|
-
maxRareValues: config.analyze?.maxRareValues ??
|
|
457
|
-
DEFAULT_ANALYZE_TEXT_OPTIONS.maxRareValues,
|
|
458
|
-
maxPatterns: config.analyze?.maxPatterns ?? DEFAULT_ANALYZE_TEXT_OPTIONS.maxPatterns,
|
|
459
|
-
};
|
|
460
|
-
sink.log('tailwind-canonical analyze');
|
|
461
|
-
sink.log(`Files analyzed: ${report.filesAnalyzed}`);
|
|
462
|
-
sink.log(`Issue groups: ${issueCount} (${pluralize(report.colorVariants.length, 'color')}, ${pluralize(report.scaleInconsistencies.length, 'scale')}, ${pluralize(report.combinations.length, 'pattern')})`);
|
|
463
|
-
if (report.rareScaleValues.length > 0) {
|
|
464
|
-
sink.log(`Rare values: ${report.rareScaleValues.length}`);
|
|
465
|
-
}
|
|
466
|
-
if (issueCount === 0) {
|
|
467
|
-
sink.log('\nNo cross-file inconsistencies found');
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
if (report.colorVariants.length > 0) {
|
|
471
|
-
sink.log('\nColor variants');
|
|
472
|
-
for (const group of report.colorVariants) {
|
|
473
|
-
const tokens = group.variants
|
|
474
|
-
.map((v) => `${group.property}-${v.token} x${v.count}`)
|
|
475
|
-
.join(', ');
|
|
476
|
-
sink.log(` - ${group.property}/${group.family}: ${tokens}`);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
if (report.scaleInconsistencies.length > 0) {
|
|
480
|
-
sink.log('\nScale inconsistency groups');
|
|
481
|
-
const scales = [...report.scaleInconsistencies].sort((a, b) => scaleTotalUses(b) - scaleTotalUses(a) ||
|
|
482
|
-
a.property.localeCompare(b.property));
|
|
483
|
-
for (const scale of scales.slice(0, textOptions.maxScaleGroups)) {
|
|
484
|
-
const totalUses = scaleTotalUses(scale);
|
|
485
|
-
const files = new Set(scale.values.flatMap((value) => value.files));
|
|
486
|
-
sink.log(` - ${scale.property}: ${scale.values.length} values, ${totalUses} uses, ${pluralize(files.size, 'file')}`);
|
|
487
|
-
sink.log(` Top: ${withMore(scale.values, textOptions.maxScaleValues, (value) => scaleValueSummary(scale.property, value))}`);
|
|
488
|
-
}
|
|
489
|
-
const remaining = scales.length - textOptions.maxScaleGroups;
|
|
490
|
-
if (remaining > 0)
|
|
491
|
-
sink.log(` - +${remaining} more scale groups`);
|
|
492
|
-
}
|
|
493
|
-
if (report.rareScaleValues.length > 0) {
|
|
494
|
-
sink.log('\nRare scale values');
|
|
495
|
-
for (const rare of report.rareScaleValues.slice(0, textOptions.maxRareValues)) {
|
|
496
|
-
const example = rare.files[0]
|
|
497
|
-
? `; e.g. ${compactPath(rare.files[0])}`
|
|
498
|
-
: '';
|
|
499
|
-
sink.log(` - ${rare.className}: ${pluralize(rare.count, 'use')} in ${pluralize(rare.files.length, 'file')} (${rare.propertyCount} ${rare.property} uses total)${example}`);
|
|
500
|
-
}
|
|
501
|
-
const remaining = report.rareScaleValues.length - textOptions.maxRareValues;
|
|
502
|
-
if (remaining > 0)
|
|
503
|
-
sink.log(` - +${remaining} more rare values`);
|
|
504
|
-
}
|
|
505
|
-
if (report.combinations.length > 0) {
|
|
506
|
-
sink.log('\nRepeated patterns');
|
|
507
|
-
for (const combo of report.combinations.slice(0, textOptions.maxPatterns)) {
|
|
508
|
-
sink.log(` - Pattern: "${combo.classes.join(' ')}" repeated in ${pluralize(combo.files.length, 'file')}`);
|
|
509
|
-
}
|
|
510
|
-
const remaining = report.combinations.length - textOptions.maxPatterns;
|
|
511
|
-
if (remaining > 0)
|
|
512
|
-
sink.log(` - +${remaining} more repeated patterns`);
|
|
513
|
-
}
|
|
514
|
-
sink.log(`\nFound ${pluralize(issueCount, 'consistency issue')} across ${pluralize(report.filesAnalyzed, 'file')}`);
|
|
515
|
-
}
|
|
516
|
-
function runAnalyze(files, config, reporter, sink) {
|
|
357
|
+
function runAnalyze(files, config, reporter, sink, sarif) {
|
|
517
358
|
let hadError = false;
|
|
518
359
|
const options = toConsistencyOptions(config);
|
|
519
360
|
const report = analyzeConsistencyFiles(files, config, options, (file, err) => {
|
|
@@ -531,131 +372,15 @@ function runAnalyze(files, config, reporter, sink) {
|
|
|
531
372
|
sink.error(`Warning: detected ${calls} calls but functionNames does not include them; --analyze may miss class strings.`);
|
|
532
373
|
}
|
|
533
374
|
if (reporter === 'json') {
|
|
534
|
-
sink
|
|
535
|
-
return { exitCode: issueCount > 0 || hadError ? 1 : 0 };
|
|
375
|
+
writeJson(sink, report);
|
|
536
376
|
}
|
|
537
|
-
if (reporter === 'sarif') {
|
|
538
|
-
|
|
539
|
-
for (const group of report.colorVariants) {
|
|
540
|
-
const tokens = group.variants.map((v) => v.token).join(', ');
|
|
541
|
-
const files = [...new Set(group.variants.flatMap((v) => v.files))];
|
|
542
|
-
results.push({
|
|
543
|
-
ruleId: 'color-variant-inconsistency',
|
|
544
|
-
message: {
|
|
545
|
-
text: `${group.variants.length} ${group.family} color variants for ${group.property}: ${tokens}`,
|
|
546
|
-
},
|
|
547
|
-
locations: fileLocations(files),
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
for (const scale of report.scaleInconsistencies) {
|
|
551
|
-
const values = scale.values
|
|
552
|
-
.map((v) => scaleClass(scale.property, v.value))
|
|
553
|
-
.join(' vs ');
|
|
554
|
-
const files = [...new Set(scale.values.flatMap((v) => v.files))];
|
|
555
|
-
results.push({
|
|
556
|
-
ruleId: 'scale-inconsistency',
|
|
557
|
-
message: { text: `${scale.property} inconsistency: ${values}` },
|
|
558
|
-
locations: fileLocations(files),
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
for (const rare of report.rareScaleValues) {
|
|
562
|
-
results.push({
|
|
563
|
-
ruleId: 'rare-scale-value',
|
|
564
|
-
message: {
|
|
565
|
-
text: `${rare.className} is rare for ${rare.property}: ${rare.count} occurrence(s) in ${rare.files.length} file(s), within ${rare.propertyCount} ${rare.property} uses`,
|
|
566
|
-
},
|
|
567
|
-
locations: fileLocations(rare.files),
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
for (const combo of report.combinations) {
|
|
571
|
-
results.push({
|
|
572
|
-
ruleId: 'repeated-combination',
|
|
573
|
-
message: {
|
|
574
|
-
text: `Repeated class combination: ${combo.classes.join(' ')}`,
|
|
575
|
-
},
|
|
576
|
-
locations: fileLocations(combo.files),
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
const sarifOutput = sarifDocument([
|
|
580
|
-
{
|
|
581
|
-
id: 'color-variant-inconsistency',
|
|
582
|
-
name: 'ColorVariantInconsistency',
|
|
583
|
-
shortDescription: {
|
|
584
|
-
text: 'Multiple color variants of the same family used for one property',
|
|
585
|
-
},
|
|
586
|
-
},
|
|
587
|
-
{
|
|
588
|
-
id: 'scale-inconsistency',
|
|
589
|
-
name: 'ScaleInconsistency',
|
|
590
|
-
shortDescription: {
|
|
591
|
-
text: 'Inconsistent scale values used for the same property',
|
|
592
|
-
},
|
|
593
|
-
},
|
|
594
|
-
{
|
|
595
|
-
id: 'rare-scale-value',
|
|
596
|
-
name: 'RareScaleValue',
|
|
597
|
-
shortDescription: {
|
|
598
|
-
text: 'A scale value appears rarely within an otherwise common property',
|
|
599
|
-
},
|
|
600
|
-
},
|
|
601
|
-
{
|
|
602
|
-
id: 'repeated-combination',
|
|
603
|
-
name: 'RepeatedCombination',
|
|
604
|
-
shortDescription: {
|
|
605
|
-
text: 'Identical class combination repeated across files',
|
|
606
|
-
},
|
|
607
|
-
},
|
|
608
|
-
], results);
|
|
609
|
-
sink.write(`${JSON.stringify(sarifOutput, null, 2)}\n`);
|
|
610
|
-
return { exitCode: issueCount > 0 || hadError ? 1 : 0 };
|
|
611
|
-
}
|
|
612
|
-
logAnalyzeText(report, issueCount, config, sink);
|
|
613
|
-
return { exitCode: issueCount > 0 || hadError ? 1 : 0 };
|
|
614
|
-
}
|
|
615
|
-
function startWatch(files, flags, config, twMerge, sink) {
|
|
616
|
-
const transforming = flags.fix || flags.dedup || flags.merge || flags.sort;
|
|
617
|
-
const fileSet = new Set(files);
|
|
618
|
-
const timers = new Map();
|
|
619
|
-
const dirs = new Set(files.map((f) => dirname(f)));
|
|
620
|
-
sink.log(`Watching ${pluralize(files.length, 'file')}... (Ctrl+C to stop)`);
|
|
621
|
-
for (const dir of dirs) {
|
|
622
|
-
fsWatch(dir, { recursive: true }, (_, filename) => {
|
|
623
|
-
if (!filename)
|
|
624
|
-
return;
|
|
625
|
-
const full = resolve(dir, filename);
|
|
626
|
-
if (!fileSet.has(full))
|
|
627
|
-
return;
|
|
628
|
-
clearTimeout(timers.get(full));
|
|
629
|
-
timers.set(full, setTimeout(() => {
|
|
630
|
-
if (transforming) {
|
|
631
|
-
try {
|
|
632
|
-
const total = totalOf(processFile(full, flags, config, twMerge));
|
|
633
|
-
if (total > 0) {
|
|
634
|
-
sink.log(`${timestamp()} ${full} — ${pluralize(total, 'change')} applied`);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
catch (err) {
|
|
638
|
-
sink.error(`${timestamp()} ${full} — error: ${errMsg(err)}`);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
else {
|
|
642
|
-
const findings = analyzeFile(full, config);
|
|
643
|
-
if (findings.length > 0) {
|
|
644
|
-
sink.log(`${timestamp()} ${full} — ${pluralize(findings.length, 'finding')}`);
|
|
645
|
-
for (const f of findings) {
|
|
646
|
-
const tag = f.suggestion.isCustomToken ? ' [custom token]' : '';
|
|
647
|
-
sink.log(` ${f.line}:${f.col} ${f.suggestion.original} → ${f.suggestion.canonical}${tag}`);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
}, 50));
|
|
652
|
-
});
|
|
377
|
+
else if (reporter === 'sarif') {
|
|
378
|
+
writeJson(sink, analyzeSarifDocument(report, sarif));
|
|
653
379
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
return { exitCode: 0, watching: true };
|
|
380
|
+
else {
|
|
381
|
+
logAnalyzeText(report, issueCount, config, sink);
|
|
382
|
+
}
|
|
383
|
+
return exitWith(issueCount > 0 || hadError);
|
|
659
384
|
}
|
|
660
385
|
function runInit(cwd, sink) {
|
|
661
386
|
const existing = CONFIG_FILENAMES.find((name) => existsSync(join(cwd, name)));
|
|
@@ -679,8 +404,7 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
679
404
|
}
|
|
680
405
|
if (flags.version) {
|
|
681
406
|
try {
|
|
682
|
-
|
|
683
|
-
sink.log(pkg.version);
|
|
407
|
+
sink.log(readVersion());
|
|
684
408
|
return { exitCode: 0 };
|
|
685
409
|
}
|
|
686
410
|
catch (err) {
|
|
@@ -712,7 +436,7 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
712
436
|
config = await loadConfig(cwd);
|
|
713
437
|
}
|
|
714
438
|
catch (err) {
|
|
715
|
-
sink.error(`tailwind-canonical: ${
|
|
439
|
+
sink.error(`tailwind-canonical: ${errMsg(err)}`);
|
|
716
440
|
return { exitCode: 1 };
|
|
717
441
|
}
|
|
718
442
|
flags = applyDefaultCommand(flags, config);
|
|
@@ -720,12 +444,7 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
720
444
|
sink.error(USAGE);
|
|
721
445
|
return { exitCode: 1 };
|
|
722
446
|
}
|
|
723
|
-
if (flags.check &&
|
|
724
|
-
!flags.analyze &&
|
|
725
|
-
!flags.fix &&
|
|
726
|
-
!flags.dedup &&
|
|
727
|
-
!flags.merge &&
|
|
728
|
-
!flags.sort) {
|
|
447
|
+
if (flags.check && !flags.analyze && !isTransforming(flags)) {
|
|
729
448
|
sink.error('--check requires at least one of --fix, --dedup, --merge, --sort');
|
|
730
449
|
return { exitCode: 1 };
|
|
731
450
|
}
|
|
@@ -748,118 +467,126 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
748
467
|
sink.error(`No files matched: ${flags.targets.join(' ')}`);
|
|
749
468
|
return { exitCode: 1 };
|
|
750
469
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
470
|
+
const sarif = sarifContext(cwd);
|
|
471
|
+
if (flags.analyze) {
|
|
472
|
+
return runAnalyze(files, config, flags.reporter, sink, sarif);
|
|
473
|
+
}
|
|
474
|
+
const transforming = isTransforming(flags);
|
|
754
475
|
if (flags.typos && !transforming) {
|
|
755
|
-
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
|
+
});
|
|
756
486
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function runTransforms(files, flags, config, sink, sarif, twMerge) {
|
|
498
|
+
const totals = emptyCounts();
|
|
760
499
|
const changedFiles = [];
|
|
761
500
|
let hadError = false;
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
if (totalOf(counts) > 0)
|
|
774
|
-
changedFiles.push(file);
|
|
775
|
-
if (flags.reporter === 'text') {
|
|
776
|
-
logTransformCounts(counts, file, flags.check, sink);
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
else {
|
|
780
|
-
const findings = analyzeFile(file, config);
|
|
781
|
-
allFindings.push(...findings);
|
|
782
|
-
if (flags.reporter === 'text') {
|
|
783
|
-
for (const f of findings) {
|
|
784
|
-
const tag = f.suggestion.isCustomToken ? ' [custom token]' : '';
|
|
785
|
-
sink.log(` ${f.file}:${f.line}:${f.col} ${f.suggestion.original} → ${f.suggestion.canonical}${tag}`);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
catch (err) {
|
|
791
|
-
hadError = true;
|
|
792
|
-
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);
|
|
793
512
|
}
|
|
794
513
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
if (transforming) {
|
|
799
|
-
const typoResult = flags.typos
|
|
800
|
-
? collectTypos(files, config, sink)
|
|
801
|
-
: undefined;
|
|
802
|
-
const totalChanges = totalOf(totals);
|
|
803
|
-
const exitCode = hadError ||
|
|
804
|
-
(flags.check && totalChanges > 0) ||
|
|
805
|
-
(typoResult && (typoResult.findings.length > 0 || typoResult.hadError))
|
|
806
|
-
? 1
|
|
807
|
-
: 0;
|
|
808
|
-
if (flags.reporter === 'json') {
|
|
809
|
-
const report = {
|
|
810
|
-
files: files.length,
|
|
811
|
-
changedFiles,
|
|
812
|
-
fixed: totals.fixed,
|
|
813
|
-
deduped: totals.deduped,
|
|
814
|
-
merged: totals.merged,
|
|
815
|
-
sorted: totals.sorted,
|
|
816
|
-
};
|
|
817
|
-
if (flags.check)
|
|
818
|
-
report.check = true;
|
|
819
|
-
if (typoResult) {
|
|
820
|
-
report.typoTotal = typoResult.findings.length;
|
|
821
|
-
report.typos = typoResult.findings;
|
|
822
|
-
}
|
|
823
|
-
sink.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
824
|
-
return { exitCode };
|
|
514
|
+
catch (err) {
|
|
515
|
+
hadError = true;
|
|
516
|
+
sink.error(`${file}: ${errMsg(err)}`);
|
|
825
517
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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;
|
|
832
538
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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);
|
|
840
579
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
parts.push(pluralize(totals.fixed, 'replacement'));
|
|
845
|
-
if (flags.dedup)
|
|
846
|
-
parts.push(pluralize(totals.deduped, 'dedup'));
|
|
847
|
-
if (flags.merge)
|
|
848
|
-
parts.push(`${pluralize(totals.merged, 'conflict')} merged`);
|
|
849
|
-
if (flags.sort)
|
|
850
|
-
parts.push(`${totals.sorted} sorted`);
|
|
851
|
-
sink.log(`\n✓ Fixed ${parts.join(', ')} across ${pluralize(files.length, 'file')}`);
|
|
580
|
+
catch (err) {
|
|
581
|
+
hadError = true;
|
|
582
|
+
sink.error(`${file}: ${errMsg(err)}`);
|
|
852
583
|
}
|
|
853
|
-
if (typoResult)
|
|
854
|
-
logTyposText(typoResult.findings, sink);
|
|
855
|
-
return { exitCode };
|
|
856
584
|
}
|
|
857
|
-
|
|
858
|
-
if (flags.reporter === 'json') {
|
|
585
|
+
if (reporter === 'json') {
|
|
859
586
|
const report = {
|
|
860
587
|
files: files.length,
|
|
861
|
-
total:
|
|
862
|
-
findings:
|
|
588
|
+
total: findings.length,
|
|
589
|
+
findings: findings.map((f) => ({
|
|
863
590
|
file: f.file,
|
|
864
591
|
line: f.line,
|
|
865
592
|
col: f.col,
|
|
@@ -868,40 +595,18 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
868
595
|
isCustomToken: f.suggestion.isCustomToken,
|
|
869
596
|
})),
|
|
870
597
|
};
|
|
871
|
-
sink
|
|
872
|
-
return
|
|
598
|
+
writeJson(sink, report);
|
|
599
|
+
return exitWith(findings.length > 0 || hadError);
|
|
873
600
|
}
|
|
874
|
-
if (
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
text: 'Arbitrary value has a canonical Tailwind equivalent',
|
|
881
|
-
},
|
|
882
|
-
},
|
|
883
|
-
], allFindings.map((f) => ({
|
|
884
|
-
ruleId: 'no-arbitrary-canonical',
|
|
885
|
-
message: {
|
|
886
|
-
text: `${f.suggestion.original} → ${f.suggestion.canonical}`,
|
|
887
|
-
},
|
|
888
|
-
locations: [
|
|
889
|
-
{
|
|
890
|
-
physicalLocation: {
|
|
891
|
-
artifactLocation: { uri: f.file },
|
|
892
|
-
region: { startLine: f.line, startColumn: f.col },
|
|
893
|
-
},
|
|
894
|
-
},
|
|
895
|
-
],
|
|
896
|
-
})));
|
|
897
|
-
sink.write(`${JSON.stringify(sarifOutput, null, 2)}\n`);
|
|
898
|
-
return { exitCode: totalFindings > 0 || hadError ? 1 : 0 };
|
|
899
|
-
}
|
|
900
|
-
if (totalFindings > 0) {
|
|
901
|
-
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' : ''}`);
|
|
902
607
|
sink.log(' Run with --fix to auto-replace\n');
|
|
903
|
-
return
|
|
608
|
+
return exitWith(true);
|
|
904
609
|
}
|
|
905
610
|
sink.log('✓ No non-canonical classes found');
|
|
906
|
-
return
|
|
611
|
+
return exitWith(hadError);
|
|
907
612
|
}
|