tailwind-canonical 0.7.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/dist/cli/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, watch as fsWatch, readFileSync, writeFileSync, } from 'node:fs';
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
- const SARIF_SCHEMA = 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json';
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 logTyposText(findings, sink) {
279
- for (const f of findings) {
280
- sink.log(` ${f.file}:${f.line}:${f.col} ${f.original} → ${f.suggestion} [typo]`);
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
- if (findings.length === 0) {
283
- sink.log('✓ No likely typos found');
251
+ }
252
+ function sarifContext(cwd) {
253
+ let version;
254
+ try {
255
+ version = readVersion();
284
256
  }
285
- else {
286
- sink.log(`\n✖ Found ${pluralize(findings.length, 'likely typo')}`);
257
+ catch {
258
+ version = undefined;
287
259
  }
260
+ return { root: findProjectRoot(cwd), version };
288
261
  }
289
- function typoSarifDocument(findings) {
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.write(`${JSON.stringify(report, null, 2)}\n`);
319
- return { exitCode: findings.length > 0 || hadError ? 1 : 0 };
269
+ writeJson(sink, report);
320
270
  }
321
- if (reporter === 'sarif') {
322
- sink.write(`${JSON.stringify(typoSarifDocument(findings), null, 2)}\n`);
323
- return { exitCode: findings.length > 0 || hadError ? 1 : 0 };
271
+ else if (reporter === 'sarif') {
272
+ writeJson(sink, typoSarifDocument(findings, sarif));
273
+ }
274
+ else {
275
+ logTyposText(findings, sink);
324
276
  }
325
- logTyposText(findings, sink);
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 = { fixed: 0, deduped: 0, merged: 0, sorted: 0 };
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 scaleClass(property, value) {
405
- return value.startsWith('-')
406
- ? `-${property}-${value.slice(1)}`
407
- : `${property}-${value}`;
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 compactPath(file) {
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.write(`${JSON.stringify(report, null, 2)}\n`);
535
- return { exitCode: issueCount > 0 || hadError ? 1 : 0 };
375
+ writeJson(sink, report);
536
376
  }
537
- if (reporter === 'sarif') {
538
- const results = [];
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
- process.once('SIGINT', () => {
655
- sink.log('\nWatcher stopped.');
656
- process.exit(0);
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
- const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
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: ${err instanceof Error ? err.message : String(err)}`);
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
- if (flags.analyze)
752
- return runAnalyze(files, config, flags.reporter, sink);
753
- const transforming = flags.fix || flags.dedup || flags.merge || flags.sort;
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
- const watching = flags.watch && !flags.typos && !flags.check;
758
- const totals = { fixed: 0, deduped: 0, merged: 0, sorted: 0 };
759
- const allFindings = [];
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
- if (!watching) {
763
- for (const file of files) {
764
- try {
765
- if (transforming) {
766
- const counts = flags.check
767
- ? applyTransforms(readFileSync(file, 'utf8'), flags, config, twMerge).counts
768
- : processFile(file, flags, config, twMerge);
769
- totals.fixed += counts.fixed;
770
- totals.deduped += counts.deduped;
771
- totals.merged += counts.merged;
772
- totals.sorted += counts.sorted;
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
- if (watching)
797
- return startWatch(files, flags, config, twMerge, sink);
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
- if (flags.reporter === 'sarif') {
827
- const doc = typoResult
828
- ? typoSarifDocument(typoResult.findings)
829
- : sarifDocument([], []);
830
- sink.write(`${JSON.stringify(doc, null, 2)}\n`);
831
- return { exitCode };
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
- if (flags.check) {
834
- if (totalChanges > 0) {
835
- sink.log(`\n✖ ${pluralize(totalChanges, 'pending change')} across ${pluralize(changedFiles.length, 'file')} (run without --check to apply)`);
836
- }
837
- else {
838
- sink.log('✓ No pending changes');
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
- else {
842
- const parts = [];
843
- if (flags.fix)
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
- const totalFindings = allFindings.length;
858
- if (flags.reporter === 'json') {
585
+ if (reporter === 'json') {
859
586
  const report = {
860
587
  files: files.length,
861
- total: totalFindings,
862
- findings: allFindings.map((f) => ({
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.write(`${JSON.stringify(report, null, 2)}\n`);
872
- return { exitCode: totalFindings > 0 || hadError ? 1 : 0 };
598
+ writeJson(sink, report);
599
+ return exitWith(findings.length > 0 || hadError);
873
600
  }
874
- if (flags.reporter === 'sarif') {
875
- const sarifOutput = sarifDocument([
876
- {
877
- id: 'no-arbitrary-canonical',
878
- name: 'NoArbitraryCanonical',
879
- shortDescription: {
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 { exitCode: 1 };
608
+ return exitWith(true);
904
609
  }
905
610
  sink.log('✓ No non-canonical classes found');
906
- return { exitCode: hadError ? 1 : 0 };
611
+ return exitWith(hadError);
907
612
  }