tailwind-canonical 0.5.0 → 0.7.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.
Files changed (106) hide show
  1. package/README.md +183 -14
  2. package/dist/cli/cli.d.ts +2 -1
  3. package/dist/cli/cli.js +208 -75
  4. package/dist/cli/index.d.ts +0 -1
  5. package/dist/cli/index.js +0 -1
  6. package/dist/core/analyzer.d.ts +0 -1
  7. package/dist/core/analyzer.js +0 -1
  8. package/dist/core/class-strings.d.ts +0 -1
  9. package/dist/core/class-strings.js +0 -1
  10. package/dist/core/config.d.ts +2 -3
  11. package/dist/core/config.js +11 -2
  12. package/dist/core/consistency.d.ts +0 -1
  13. package/dist/core/consistency.js +1 -6
  14. package/dist/core/deduplicator.d.ts +0 -1
  15. package/dist/core/deduplicator.js +10 -6
  16. package/dist/core/fixer.d.ts +0 -1
  17. package/dist/core/fixer.js +0 -1
  18. package/dist/core/lexicon.d.ts +1 -1
  19. package/dist/core/lexicon.js +1 -2
  20. package/dist/core/merger.d.ts +0 -1
  21. package/dist/core/merger.js +35 -29
  22. package/dist/core/rules.d.ts +3 -1
  23. package/dist/core/rules.js +56 -1
  24. package/dist/core/scanner.d.ts +0 -1
  25. package/dist/core/scanner.js +0 -1
  26. package/dist/core/sorter.d.ts +0 -1
  27. package/dist/core/sorter.js +5 -3
  28. package/dist/core/suppressions.d.ts +0 -1
  29. package/dist/core/suppressions.js +0 -1
  30. package/dist/core/typos.d.ts +0 -1
  31. package/dist/core/typos.js +0 -1
  32. package/dist/eslint/plugin.d.ts +0 -1
  33. package/dist/eslint/plugin.js +2 -2
  34. package/dist/index.d.ts +0 -1
  35. package/dist/index.js +0 -1
  36. package/dist/io/analyzer.d.ts +0 -1
  37. package/dist/io/analyzer.js +0 -1
  38. package/dist/io/config.d.ts +9 -1
  39. package/dist/io/config.js +93 -7
  40. package/dist/io/consistency.d.ts +0 -1
  41. package/dist/io/consistency.js +0 -1
  42. package/dist/io/deduplicator.d.ts +0 -1
  43. package/dist/io/deduplicator.js +0 -1
  44. package/dist/io/fixer.d.ts +0 -1
  45. package/dist/io/fixer.js +0 -1
  46. package/dist/io/merger.d.ts +0 -1
  47. package/dist/io/merger.js +0 -1
  48. package/dist/io/scanner.d.ts +0 -1
  49. package/dist/io/scanner.js +0 -1
  50. package/dist/io/sorter.d.ts +0 -1
  51. package/dist/io/sorter.js +0 -1
  52. package/dist/io/typos.d.ts +0 -1
  53. package/dist/io/typos.js +0 -1
  54. package/package.json +11 -10
  55. package/dist/cli/cli.d.ts.map +0 -1
  56. package/dist/cli/cli.js.map +0 -1
  57. package/dist/cli/index.d.ts.map +0 -1
  58. package/dist/cli/index.js.map +0 -1
  59. package/dist/core/analyzer.d.ts.map +0 -1
  60. package/dist/core/analyzer.js.map +0 -1
  61. package/dist/core/class-strings.d.ts.map +0 -1
  62. package/dist/core/class-strings.js.map +0 -1
  63. package/dist/core/config.d.ts.map +0 -1
  64. package/dist/core/config.js.map +0 -1
  65. package/dist/core/consistency.d.ts.map +0 -1
  66. package/dist/core/consistency.js.map +0 -1
  67. package/dist/core/deduplicator.d.ts.map +0 -1
  68. package/dist/core/deduplicator.js.map +0 -1
  69. package/dist/core/fixer.d.ts.map +0 -1
  70. package/dist/core/fixer.js.map +0 -1
  71. package/dist/core/lexicon.d.ts.map +0 -1
  72. package/dist/core/lexicon.js.map +0 -1
  73. package/dist/core/merger.d.ts.map +0 -1
  74. package/dist/core/merger.js.map +0 -1
  75. package/dist/core/rules.d.ts.map +0 -1
  76. package/dist/core/rules.js.map +0 -1
  77. package/dist/core/scanner.d.ts.map +0 -1
  78. package/dist/core/scanner.js.map +0 -1
  79. package/dist/core/sorter.d.ts.map +0 -1
  80. package/dist/core/sorter.js.map +0 -1
  81. package/dist/core/suppressions.d.ts.map +0 -1
  82. package/dist/core/suppressions.js.map +0 -1
  83. package/dist/core/typos.d.ts.map +0 -1
  84. package/dist/core/typos.js.map +0 -1
  85. package/dist/eslint/plugin.d.ts.map +0 -1
  86. package/dist/eslint/plugin.js.map +0 -1
  87. package/dist/index.d.ts.map +0 -1
  88. package/dist/index.js.map +0 -1
  89. package/dist/io/analyzer.d.ts.map +0 -1
  90. package/dist/io/analyzer.js.map +0 -1
  91. package/dist/io/config.d.ts.map +0 -1
  92. package/dist/io/config.js.map +0 -1
  93. package/dist/io/consistency.d.ts.map +0 -1
  94. package/dist/io/consistency.js.map +0 -1
  95. package/dist/io/deduplicator.d.ts.map +0 -1
  96. package/dist/io/deduplicator.js.map +0 -1
  97. package/dist/io/fixer.d.ts.map +0 -1
  98. package/dist/io/fixer.js.map +0 -1
  99. package/dist/io/merger.d.ts.map +0 -1
  100. package/dist/io/merger.js.map +0 -1
  101. package/dist/io/scanner.d.ts.map +0 -1
  102. package/dist/io/scanner.js.map +0 -1
  103. package/dist/io/sorter.d.ts.map +0 -1
  104. package/dist/io/sorter.js.map +0 -1
  105. package/dist/io/typos.d.ts.map +0 -1
  106. package/dist/io/typos.js.map +0 -1
package/dist/cli/cli.js CHANGED
@@ -1,24 +1,50 @@
1
- import { watch as fsWatch, readFileSync } from 'node:fs';
2
- import { dirname, resolve } from 'node:path';
1
+ import { existsSync, watch as fsWatch, readFileSync, writeFileSync, } from 'node:fs';
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 { 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 { dedupeFile } from '../core/deduplicator.js';
8
- import { fixFile } from '../core/fixer.js';
9
- import { mergeFile } from '../core/merger.js';
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 { sortFile } from '../core/sorter.js';
11
+ import { sortContent } from '../core/sorter.js';
12
12
  import { analyzeTyposFile } from '../core/typos.js';
13
13
  const SARIF_SCHEMA = 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json';
14
- const USAGE = 'Usage: tailwind-canonical [--fix] [--merge] [--dedup] [--sort] [--analyze] [--typos] [--watch] [--reporter json|sarif] <dir|file> [dir|file...]';
14
+ 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 INIT_CONFIG_BODY = [
16
+ ' // Run `tailwind-canonical` with no flags using these defaults:',
17
+ " // defaultCommand: { fix: true, dedup: true, sort: true, typos: true, targets: ['./src'] },",
18
+ ' // px → token additions/overrides:',
19
+ " // customTextTokens: { 11: '2xs' },",
20
+ " // customSpacingTokens: { 14: '3.5' },",
21
+ ' // Never suggest replacements for classes matching:',
22
+ ' // ignorePatterns: [/^font-/],',
23
+ ];
24
+ const INIT_CONFIG_TS = [
25
+ "import type { Config } from 'tailwind-canonical'",
26
+ '',
27
+ 'export default {',
28
+ ...INIT_CONFIG_BODY,
29
+ '} satisfies Config',
30
+ '',
31
+ ].join('\n');
32
+ const INIT_CONFIG_JS = [
33
+ "/** @type {import('tailwind-canonical').Config} */",
34
+ 'export default {',
35
+ ...INIT_CONFIG_BODY,
36
+ '}',
37
+ '',
38
+ ].join('\n');
15
39
  const HELP_TEXT = [
16
40
  USAGE,
17
41
  '',
42
+ ' init Create a tailwind-canonical config scaffold in the current directory',
18
43
  ' --fix Auto-replace arbitrary values with canonical Tailwind classes',
19
44
  ' --dedup Remove redundant classes and collapse shorthands',
20
45
  ' --merge Resolve conflicting classes via tailwind-merge',
21
46
  ' --sort Sort classes into canonical order',
47
+ ' --check Dry-run: report what transforms would change, write nothing (exit 1 on pending changes)',
22
48
  ' --analyze Cross-file consistency analysis (read-only)',
23
49
  ' --typos Flag likely misspelled color names (read-only)',
24
50
  ' --watch Re-run on every file save (transform/check mode; ignored with --typos)',
@@ -92,6 +118,7 @@ const KNOWN_FLAGS = new Set([
92
118
  '--merge',
93
119
  '--dedup',
94
120
  '--sort',
121
+ '--check',
95
122
  '--watch',
96
123
  '--analyze',
97
124
  '--typos',
@@ -156,6 +183,7 @@ export function parseArgs(argv) {
156
183
  watch: argv.includes('--watch'),
157
184
  analyze: argv.includes('--analyze'),
158
185
  typos: argv.includes('--typos'),
186
+ check: argv.includes('--check'),
159
187
  help: argv.includes('--help') || argv.includes('-h'),
160
188
  version: argv.includes('--version') || argv.includes('-V'),
161
189
  reporter,
@@ -164,34 +192,29 @@ export function parseArgs(argv) {
164
192
  hasExplicitMode: MODE_FLAGS.some((flag) => argv.includes(flag)),
165
193
  hasExplicitReporter: reporterRaw !== undefined,
166
194
  hasExplicitWatch: argv.includes('--watch'),
195
+ hasExplicitCheck: argv.includes('--check'),
167
196
  };
168
197
  }
198
+ /** Explicit CLI flags win; otherwise `defaultCommand` fills the gap. */
199
+ function fallback(explicit, flag, configured) {
200
+ return explicit ? flag : (configured ?? flag);
201
+ }
169
202
  function applyDefaultCommand(flags, config) {
170
203
  const defaults = config.defaultCommand;
171
204
  if (!defaults)
172
205
  return flags;
206
+ const mode = flags.hasExplicitMode;
173
207
  return {
174
208
  ...flags,
175
- fix: flags.hasExplicitMode ? flags.fix : (defaults.fix ?? flags.fix),
176
- merge: flags.hasExplicitMode
177
- ? flags.merge
178
- : (defaults.merge ?? flags.merge),
179
- dedup: flags.hasExplicitMode
180
- ? flags.dedup
181
- : (defaults.dedup ?? flags.dedup),
182
- sort: flags.hasExplicitMode ? flags.sort : (defaults.sort ?? flags.sort),
183
- analyze: flags.hasExplicitMode
184
- ? flags.analyze
185
- : (defaults.analyze ?? flags.analyze),
186
- typos: flags.hasExplicitMode
187
- ? flags.typos
188
- : (defaults.typos ?? flags.typos),
189
- watch: flags.hasExplicitWatch
190
- ? flags.watch
191
- : (defaults.watch ?? flags.watch),
192
- reporter: flags.hasExplicitReporter
193
- ? flags.reporter
194
- : (defaults.reporter ?? flags.reporter),
209
+ fix: fallback(mode, flags.fix, defaults.fix),
210
+ merge: fallback(mode, flags.merge, defaults.merge),
211
+ dedup: fallback(mode, flags.dedup, defaults.dedup),
212
+ sort: fallback(mode, flags.sort, defaults.sort),
213
+ analyze: fallback(mode, flags.analyze, defaults.analyze),
214
+ typos: fallback(mode, flags.typos, defaults.typos),
215
+ watch: fallback(flags.hasExplicitWatch, flags.watch, defaults.watch),
216
+ check: fallback(flags.hasExplicitCheck, flags.check, defaults.check),
217
+ reporter: fallback(flags.hasExplicitReporter, flags.reporter, defaults.reporter),
195
218
  targets: flags.targets.length > 0 ? flags.targets : (defaults.targets ?? []),
196
219
  };
197
220
  }
@@ -222,12 +245,19 @@ export function flagWarnings(flags) {
222
245
  }
223
246
  if (flags.typos)
224
247
  warnings.push('--typos ignored: --analyze takes priority');
248
+ if (flags.check)
249
+ warnings.push('--check ignored: --analyze takes priority');
225
250
  if (flags.watch)
226
251
  warnings.push('--watch ignored: not supported with --analyze');
227
252
  return warnings;
228
253
  }
229
- if (flags.typos && flags.watch) {
230
- warnings.push('--watch ignored: not supported with --typos');
254
+ if (flags.watch && (flags.typos || flags.check)) {
255
+ const unsupported = [];
256
+ if (flags.typos)
257
+ unsupported.push('--typos');
258
+ if (flags.check)
259
+ unsupported.push('--check');
260
+ warnings.push(`--watch ignored: not supported with ${unsupported.join('/')}`);
231
261
  }
232
262
  return warnings;
233
263
  }
@@ -295,19 +325,79 @@ function runTypos(files, config, reporter, sink) {
295
325
  logTyposText(findings, sink);
296
326
  return { exitCode: findings.length > 0 || hadError ? 1 : 0 };
297
327
  }
298
- async function processFile(file, flags, config) {
328
+ function applyTransforms(content, flags, config, twMerge) {
299
329
  const opts = toClassStringOpts(config);
300
330
  const counts = { fixed: 0, deduped: 0, merged: 0, sorted: 0 };
301
- if (flags.fix)
302
- counts.fixed = fixFile(file, config);
303
- if (flags.dedup)
304
- counts.deduped = dedupeFile(file, opts);
305
- if (flags.merge)
306
- counts.merged = await mergeFile(file, opts);
307
- if (flags.sort)
308
- counts.sorted = sortFile(file, opts, config.sortOrder);
331
+ let result = content;
332
+ if (flags.fix) {
333
+ const fixed = fixContent(result, config);
334
+ counts.fixed = fixed.count;
335
+ result = fixed.result;
336
+ }
337
+ if (flags.dedup) {
338
+ const deduped = dedupeContent(result, opts);
339
+ counts.deduped = deduped.count;
340
+ result = deduped.result;
341
+ }
342
+ if (flags.merge) {
343
+ if (!twMerge) {
344
+ throw new Error('--merge requires the optional peer dependency "tailwind-merge". Install it with: pnpm add -D tailwind-merge');
345
+ }
346
+ const merged = mergeContent(result, twMerge, opts);
347
+ counts.merged = merged.count;
348
+ result = merged.result;
349
+ }
350
+ if (flags.sort) {
351
+ const sorted = sortContent(result, opts, config.sortOrder);
352
+ counts.sorted = sorted.count;
353
+ result = sorted.result;
354
+ }
355
+ return { counts, result };
356
+ }
357
+ function processFile(file, flags, config, twMerge) {
358
+ const content = readFileSync(file, 'utf8');
359
+ const { counts, result } = applyTransforms(content, flags, config, twMerge);
360
+ // Counts, not text equality: replaceClassStrings also normalizes attribute
361
+ // spacing, which no transform counts. Writing on any textual difference
362
+ // would rewrite files that --check truthfully reports as clean.
363
+ if (totalOf(counts) > 0)
364
+ writeFileSync(file, result, 'utf8');
309
365
  return counts;
310
366
  }
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
+ }
311
401
  function totalOf(c) {
312
402
  return c.fixed + c.deduped + c.merged + c.sorted;
313
403
  }
@@ -522,10 +612,9 @@ function runAnalyze(files, config, reporter, sink) {
522
612
  logAnalyzeText(report, issueCount, config, sink);
523
613
  return { exitCode: issueCount > 0 || hadError ? 1 : 0 };
524
614
  }
525
- function startWatch(files, flags, config, sink) {
615
+ function startWatch(files, flags, config, twMerge, sink) {
526
616
  const transforming = flags.fix || flags.dedup || flags.merge || flags.sort;
527
617
  const fileSet = new Set(files);
528
- const inFlight = new Set();
529
618
  const timers = new Map();
530
619
  const dirs = new Set(files.map((f) => dirname(f)));
531
620
  sink.log(`Watching ${pluralize(files.length, 'file')}... (Ctrl+C to stop)`);
@@ -534,23 +623,20 @@ function startWatch(files, flags, config, sink) {
534
623
  if (!filename)
535
624
  return;
536
625
  const full = resolve(dir, filename);
537
- if (!fileSet.has(full) || inFlight.has(full))
626
+ if (!fileSet.has(full))
538
627
  return;
539
628
  clearTimeout(timers.get(full));
540
629
  timers.set(full, setTimeout(() => {
541
630
  if (transforming) {
542
- inFlight.add(full);
543
- processFile(full, flags, config)
544
- .then((counts) => {
545
- const total = totalOf(counts);
631
+ try {
632
+ const total = totalOf(processFile(full, flags, config, twMerge));
546
633
  if (total > 0) {
547
634
  sink.log(`${timestamp()} ${full} — ${pluralize(total, 'change')} applied`);
548
635
  }
549
- })
550
- .catch((err) => {
551
- sink.error(`${timestamp()} ${full} — error: ${err instanceof Error ? err.message : String(err)}`);
552
- })
553
- .finally(() => inFlight.delete(full));
636
+ }
637
+ catch (err) {
638
+ sink.error(`${timestamp()} ${full} — error: ${errMsg(err)}`);
639
+ }
554
640
  }
555
641
  else {
556
642
  const findings = analyzeFile(full, config);
@@ -571,6 +657,20 @@ function startWatch(files, flags, config, sink) {
571
657
  });
572
658
  return { exitCode: 0, watching: true };
573
659
  }
660
+ function runInit(cwd, sink) {
661
+ const existing = CONFIG_FILENAMES.find((name) => existsSync(join(cwd, name)));
662
+ if (existing) {
663
+ sink.error(`${existing} already exists`);
664
+ return { exitCode: 1 };
665
+ }
666
+ const filename = resolveInitFilename(cwd);
667
+ const content = /\.m?ts$/.test(filename) ? INIT_CONFIG_TS : INIT_CONFIG_JS;
668
+ writeFileSync(join(cwd, filename), content, 'utf8');
669
+ sink.log(`Created ${filename}`);
670
+ sink.log('Uncomment defaultCommand to set your CLI defaults.');
671
+ sink.log('Then run `npx tailwind-canonical` to lint your project.');
672
+ return { exitCode: 0 };
673
+ }
574
674
  export async function run(argv, cwd, sink = defaultSink) {
575
675
  let flags = parseArgs(argv);
576
676
  if (flags.help) {
@@ -588,6 +688,21 @@ export async function run(argv, cwd, sink = defaultSink) {
588
688
  return { exitCode: 1 };
589
689
  }
590
690
  }
691
+ if (flags.targets[0] === 'init') {
692
+ if (flags.error ||
693
+ flags.hasExplicitMode ||
694
+ flags.hasExplicitWatch ||
695
+ flags.hasExplicitCheck ||
696
+ flags.hasExplicitReporter) {
697
+ sink.error('init takes no flags');
698
+ return { exitCode: 1 };
699
+ }
700
+ if (flags.targets.length > 1) {
701
+ sink.error('init takes no extra arguments');
702
+ return { exitCode: 1 };
703
+ }
704
+ return runInit(cwd, sink);
705
+ }
591
706
  if (flags.error) {
592
707
  sink.error(flags.error);
593
708
  return { exitCode: 1 };
@@ -605,9 +720,19 @@ export async function run(argv, cwd, sink = defaultSink) {
605
720
  sink.error(USAGE);
606
721
  return { exitCode: 1 };
607
722
  }
608
- if (flags.merge && !flags.analyze && !flags.typos) {
723
+ if (flags.check &&
724
+ !flags.analyze &&
725
+ !flags.fix &&
726
+ !flags.dedup &&
727
+ !flags.merge &&
728
+ !flags.sort) {
729
+ sink.error('--check requires at least one of --fix, --dedup, --merge, --sort');
730
+ return { exitCode: 1 };
731
+ }
732
+ let twMerge;
733
+ if (flags.merge && !flags.analyze) {
609
734
  try {
610
- await import('tailwind-merge');
735
+ ({ twMerge } = await import('tailwind-merge'));
611
736
  }
612
737
  catch {
613
738
  sink.error('--merge requires tailwind-merge: pnpm add -D tailwind-merge');
@@ -629,7 +754,7 @@ export async function run(argv, cwd, sink = defaultSink) {
629
754
  if (flags.typos && !transforming) {
630
755
  return runTypos(files, config, flags.reporter, sink);
631
756
  }
632
- const watching = flags.watch && !flags.typos;
757
+ const watching = flags.watch && !flags.typos && !flags.check;
633
758
  const totals = { fixed: 0, deduped: 0, merged: 0, sorted: 0 };
634
759
  const allFindings = [];
635
760
  const changedFiles = [];
@@ -638,7 +763,9 @@ export async function run(argv, cwd, sink = defaultSink) {
638
763
  for (const file of files) {
639
764
  try {
640
765
  if (transforming) {
641
- const counts = await processFile(file, flags, config);
766
+ const counts = flags.check
767
+ ? applyTransforms(readFileSync(file, 'utf8'), flags, config, twMerge).counts
768
+ : processFile(file, flags, config, twMerge);
642
769
  totals.fixed += counts.fixed;
643
770
  totals.deduped += counts.deduped;
644
771
  totals.merged += counts.merged;
@@ -646,14 +773,7 @@ export async function run(argv, cwd, sink = defaultSink) {
646
773
  if (totalOf(counts) > 0)
647
774
  changedFiles.push(file);
648
775
  if (flags.reporter === 'text') {
649
- if (counts.fixed > 0)
650
- sink.log(` fixed ${file} (${pluralize(counts.fixed, 'replacement')})`);
651
- if (counts.deduped > 0)
652
- sink.log(` deduped ${file} (${pluralize(counts.deduped, 'class string')})`);
653
- if (counts.merged > 0)
654
- sink.log(` merged ${file} (${pluralize(counts.merged, 'conflict')})`);
655
- if (counts.sorted > 0)
656
- sink.log(` sorted ${file} (${pluralize(counts.sorted, 'class string')})`);
776
+ logTransformCounts(counts, file, flags.check, sink);
657
777
  }
658
778
  }
659
779
  else {
@@ -674,12 +794,14 @@ export async function run(argv, cwd, sink = defaultSink) {
674
794
  }
675
795
  }
676
796
  if (watching)
677
- return startWatch(files, flags, config, sink);
797
+ return startWatch(files, flags, config, twMerge, sink);
678
798
  if (transforming) {
679
799
  const typoResult = flags.typos
680
800
  ? collectTypos(files, config, sink)
681
801
  : undefined;
802
+ const totalChanges = totalOf(totals);
682
803
  const exitCode = hadError ||
804
+ (flags.check && totalChanges > 0) ||
683
805
  (typoResult && (typoResult.findings.length > 0 || typoResult.hadError))
684
806
  ? 1
685
807
  : 0;
@@ -692,6 +814,8 @@ export async function run(argv, cwd, sink = defaultSink) {
692
814
  merged: totals.merged,
693
815
  sorted: totals.sorted,
694
816
  };
817
+ if (flags.check)
818
+ report.check = true;
695
819
  if (typoResult) {
696
820
  report.typoTotal = typoResult.findings.length;
697
821
  report.typos = typoResult.findings;
@@ -706,16 +830,26 @@ export async function run(argv, cwd, sink = defaultSink) {
706
830
  sink.write(`${JSON.stringify(doc, null, 2)}\n`);
707
831
  return { exitCode };
708
832
  }
709
- const parts = [];
710
- if (flags.fix)
711
- parts.push(pluralize(totals.fixed, 'replacement'));
712
- if (flags.dedup)
713
- parts.push(pluralize(totals.deduped, 'dedup'));
714
- if (flags.merge)
715
- parts.push(`${pluralize(totals.merged, 'conflict')} merged`);
716
- if (flags.sort)
717
- parts.push(`${totals.sorted} sorted`);
718
- sink.log(`\n✓ Fixed ${parts.join(', ')} across ${pluralize(files.length, 'file')}`);
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
+ }
840
+ }
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')}`);
852
+ }
719
853
  if (typoResult)
720
854
  logTyposText(typoResult.findings, sink);
721
855
  return { exitCode };
@@ -771,4 +905,3 @@ export async function run(argv, cwd, sink = defaultSink) {
771
905
  sink.log('✓ No non-canonical classes found');
772
906
  return { exitCode: hadError ? 1 : 0 };
773
907
  }
774
- //# sourceMappingURL=cli.js.map
@@ -1,3 +1,2 @@
1
1
  #!/usr/bin/env node
2
2
  export {};
3
- //# sourceMappingURL=index.d.ts.map
package/dist/cli/index.js CHANGED
@@ -3,4 +3,3 @@ import { run } from './cli.js';
3
3
  const { exitCode, watching } = await run(process.argv.slice(2), process.cwd());
4
4
  if (!watching)
5
5
  process.exit(exitCode);
6
- //# sourceMappingURL=index.js.map
@@ -7,4 +7,3 @@ export type Finding = {
7
7
  };
8
8
  export declare function analyzeContent(filePath: string, content: string, config?: Config): Finding[];
9
9
  export { analyzeFile } from '../io/analyzer.js';
10
- //# sourceMappingURL=analyzer.d.ts.map
@@ -20,4 +20,3 @@ export function analyzeContent(filePath, content, config = {}) {
20
20
  return findings;
21
21
  }
22
22
  export { analyzeFile } from '../io/analyzer.js';
23
- //# sourceMappingURL=analyzer.js.map
@@ -20,4 +20,3 @@ export declare function extractClassStrings(content: string, opts?: ClassStringO
20
20
  start: number;
21
21
  end: number;
22
22
  }>;
23
- //# sourceMappingURL=class-strings.d.ts.map
@@ -149,4 +149,3 @@ export function extractClassStrings(content, opts = {}) {
149
149
  }
150
150
  return results;
151
151
  }
152
- //# sourceMappingURL=class-strings.js.map
@@ -1,5 +1,4 @@
1
1
  import type { Config } from './rules.js';
2
- export declare const CONFIG_FILENAMES: readonly ["tailwind-canonical.config.ts", "tailwind-canonical.config.js"];
2
+ export declare const CONFIG_FILENAMES: readonly ["tailwind-canonical.config.ts", "tailwind-canonical.config.mts", "tailwind-canonical.config.js", "tailwind-canonical.config.mjs"];
3
3
  export declare function validateConfig(input: unknown, filename?: string): Config;
4
- export { loadConfig } from '../io/config.js';
5
- //# sourceMappingURL=config.d.ts.map
4
+ export { loadConfig, resolveInitFilename } from '../io/config.js';
@@ -2,7 +2,9 @@ import { DEFAULT_SORT_ORDER } from './sorter.js';
2
2
  const CONFIG_FILENAME = 'tailwind-canonical.config.ts';
3
3
  export const CONFIG_FILENAMES = [
4
4
  CONFIG_FILENAME,
5
+ 'tailwind-canonical.config.mts',
5
6
  'tailwind-canonical.config.js',
7
+ 'tailwind-canonical.config.mjs',
6
8
  ];
7
9
  const KNOWN_KEYS = [
8
10
  'customTextTokens',
@@ -19,6 +21,7 @@ const KNOWN_KEYS = [
19
21
  'rareScaleMaxFiles',
20
22
  'rareScaleMaxCount',
21
23
  'defaultCommand',
24
+ 'tailwindVersion',
22
25
  ];
23
26
  const ANALYZE_KEYS = [
24
27
  'minRareScalePropertyOccurrences',
@@ -35,6 +38,7 @@ const DEFAULT_COMMAND_KEYS = [
35
38
  'merge',
36
39
  'dedup',
37
40
  'sort',
41
+ 'check',
38
42
  'analyze',
39
43
  'typos',
40
44
  'watch',
@@ -116,6 +120,7 @@ function assertDefaultCommandConfig(value, filename) {
116
120
  'merge',
117
121
  'dedup',
118
122
  'sort',
123
+ 'check',
119
124
  'analyze',
120
125
  'typos',
121
126
  'watch',
@@ -181,7 +186,11 @@ export function validateConfig(input, filename = CONFIG_FILENAME) {
181
186
  assertPositiveInteger(cfg.rareScaleMaxCount, 'rareScaleMaxCount', filename);
182
187
  if ('defaultCommand' in cfg)
183
188
  assertDefaultCommandConfig(cfg.defaultCommand, filename);
189
+ if ('tailwindVersion' in cfg &&
190
+ cfg.tailwindVersion !== 3 &&
191
+ cfg.tailwindVersion !== 4) {
192
+ fail(filename, 'tailwindVersion must be 3 or 4');
193
+ }
184
194
  return cfg;
185
195
  }
186
- export { loadConfig } from '../io/config.js';
187
- //# sourceMappingURL=config.js.map
196
+ export { loadConfig, resolveInitFilename } from '../io/config.js';
@@ -60,4 +60,3 @@ export declare function analyzeConsistency(input: FileClasses[], options?: Consi
60
60
  export declare function collectClasses(content: string, opts?: ClassStringOpts): string[];
61
61
  export declare function toConsistencyOptions(config?: Config): ConsistencyOptions;
62
62
  export { analyzeConsistencyFiles } from '../io/consistency.js';
63
- //# sourceMappingURL=consistency.d.ts.map
@@ -1,9 +1,5 @@
1
1
  import { extractClassStrings, SINGLE_CLASS_REGEX, } from './class-strings.js';
2
- import { COLOR_FAMILIES, parseColorClass, SCALE_PROPERTIES, TAILWIND_COLORS, } from './lexicon.js';
3
- function stripVariants(cls) {
4
- const idx = cls.lastIndexOf(':');
5
- return idx === -1 ? cls : cls.slice(idx + 1);
6
- }
2
+ import { COLOR_FAMILIES, parseColorClass, SCALE_PROPERTIES, stripVariants, TAILWIND_COLORS, } from './lexicon.js';
7
3
  function parseColor(cls, families, knownColors) {
8
4
  const parsed = parseColorClass(cls);
9
5
  if (!parsed)
@@ -223,4 +219,3 @@ export function toConsistencyOptions(config = {}) {
223
219
  };
224
220
  }
225
221
  export { analyzeConsistencyFiles } from '../io/consistency.js';
226
- //# sourceMappingURL=consistency.js.map
@@ -5,4 +5,3 @@ export declare function dedupeContent(content: string, opts?: ClassStringOpts):
5
5
  count: number;
6
6
  };
7
7
  export { dedupeFile } from '../io/deduplicator.js';
8
- //# sourceMappingURL=deduplicator.d.ts.map
@@ -300,10 +300,15 @@ function collapseCorners(c) {
300
300
  }
301
301
  // ─── Responsive cascade collapse ─────────────────────────────────────────────
302
302
  const RESPONSIVE_BREAKPOINTS = ['sm', 'md', 'lg', 'xl', '2xl'];
303
- const BP_ORDER = [
304
- '',
305
- ...RESPONSIVE_BREAKPOINTS,
306
- ];
303
+ // Exhaustive by type: adding a breakpoint above fails the build until ranked.
304
+ const BP_RANK = {
305
+ '': 0,
306
+ sm: 1,
307
+ md: 2,
308
+ lg: 3,
309
+ xl: 4,
310
+ '2xl': 5,
311
+ };
307
312
  function collapseResponsiveCascade(classes) {
308
313
  // Last-wins keyword utilities (display, position) share a single CSS property
309
314
  // even though their class names differ, so they must group by property — not
@@ -339,7 +344,7 @@ function collapseResponsiveCascade(classes) {
339
344
  for (const group of groups.values()) {
340
345
  if (group.length < 2 || !group.some((e) => e.bp !== ''))
341
346
  continue;
342
- const sorted = [...group].sort((a, b) => BP_ORDER.indexOf(a.bp) - BP_ORDER.indexOf(b.bp));
347
+ const sorted = [...group].sort((a, b) => BP_RANK[a.bp] - BP_RANK[b.bp]);
343
348
  let prev = null;
344
349
  for (const entry of sorted) {
345
350
  if (entry.base === prev) {
@@ -439,4 +444,3 @@ export function dedupeContent(content, opts = {}) {
439
444
  });
440
445
  }
441
446
  export { dedupeFile } from '../io/deduplicator.js';
442
- //# sourceMappingURL=deduplicator.js.map
@@ -4,4 +4,3 @@ export declare function fixContent(content: string, config?: Config): {
4
4
  count: number;
5
5
  };
6
6
  export { fixFile } from '../io/fixer.js';
7
- //# sourceMappingURL=fixer.d.ts.map
@@ -18,4 +18,3 @@ export function fixContent(content, config = {}) {
18
18
  return { result, count };
19
19
  }
20
20
  export { fixFile } from '../io/fixer.js';
21
- //# sourceMappingURL=fixer.js.map
@@ -1,5 +1,6 @@
1
1
  export declare const TAILWIND_COLORS: Set<string>;
2
2
  export declare const COLOR_FAMILIES: Record<string, string>;
3
+ export declare function stripVariants(cls: string): string;
3
4
  /**
4
5
  * Parses a color utility class into its property, color name and (optional)
5
6
  * numeric shade. Strips leading variants (`hover:` …). Returns `null` when the
@@ -13,4 +14,3 @@ export declare function parseColorClass(cls: string): {
13
14
  shade: string;
14
15
  } | null;
15
16
  export declare const SCALE_PROPERTIES: Set<string>;
16
- //# sourceMappingURL=lexicon.d.ts.map
@@ -70,7 +70,7 @@ export const COLOR_FAMILIES = {
70
70
  neutral: 'gray',
71
71
  stone: 'gray',
72
72
  };
73
- function stripVariants(cls) {
73
+ export function stripVariants(cls) {
74
74
  const idx = cls.lastIndexOf(':');
75
75
  return idx === -1 ? cls : cls.slice(idx + 1);
76
76
  }
@@ -136,4 +136,3 @@ export const SCALE_PROPERTIES = new Set([
136
136
  'space-y',
137
137
  'z',
138
138
  ]);
139
- //# sourceMappingURL=lexicon.js.map
@@ -4,4 +4,3 @@ export declare function mergeContent(content: string, twMerge: (classes: string)
4
4
  count: number;
5
5
  };
6
6
  export { mergeFile } from '../io/merger.js';
7
- //# sourceMappingURL=merger.d.ts.map