tailwind-canonical 0.6.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 +91 -18
  2. package/dist/cli/cli.d.ts +0 -1
  3. package/dist/cli/cli.js +111 -87
  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 +9 -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 +2 -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 +66 -5
  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 +10 -9
  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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Lint and auto-fix Tailwind CSS classes: arbitrary values → canonical, deduplication, shorthand collapsing, and class sorting.
4
4
 
5
- > Compatible with Tailwind CSS v3.3+ and v4 — zero `tailwindcss` dependency (conventions are built in). Custom theme tokens are supported via config.
5
+ > Compatible with Tailwind CSS v4 out of the box, and with v3.3+ via [`tailwindVersion: 3`](#tailwind-v3-vs-v4-tailwindversion) — the ÷4 spacing rule assumes v4's dynamic scale, so v3 projects must set it. Zero `tailwindcss` dependency (conventions are built in). Custom theme tokens are supported via config.
6
6
 
7
7
  ## Install
8
8
 
@@ -50,7 +50,7 @@ For pre-commit automation, see the [Pre-commit hook](#pre-commit-hook-husky--lef
50
50
  ## CLI
51
51
 
52
52
  ```bash
53
- # Scaffold a tailwind-canonical.config.ts in the current directory
53
+ # Scaffold a config in the current directory (.ts, or .js/.mjs on older Node)
54
54
  npx tailwind-canonical init
55
55
 
56
56
  # Check for non-canonical arbitrary values
@@ -356,6 +356,52 @@ left-2 right-2 → inset-x-2
356
356
 
357
357
  Non-divisible values (`h-[22px]`, `px-[7px]`) are left untouched.
358
358
 
359
+ ### Tailwind v3 vs v4 (`tailwindVersion`)
360
+
361
+ The ÷4 spacing rule assumes the **v4** scale, where every step is derived from
362
+ `calc(var(--spacing) * n)` and any integer works. v3 ships a discrete scale, so
363
+ `p-[52px]` → `p-13` compiles to nothing at all on v3 and the padding silently
364
+ disappears. Set `tailwindVersion: 3` on a v3 project:
365
+
366
+ ```ts
367
+ export default {
368
+ tailwindVersion: 3, // default: 4
369
+ }
370
+ ```
371
+
372
+ In v3 mode, suggestions are restricted to what v3.3 actually generates — the
373
+ scale differs per utility, not just per family:
374
+
375
+ | Class | `tailwindVersion: 4` | `tailwindVersion: 3` |
376
+ |---|---|---|
377
+ | `p-[16px]` | `p-4` | `p-4` |
378
+ | `p-[52px]` | `p-13` | untouched — not on the v3 scale |
379
+ | `max-w-[16px]` | `max-w-4` | untouched — scale arrived after v3.3 |
380
+ | `max-w-[0px]` | `max-w-0` | `max-w-0` — the zero step does exist |
381
+ | `size-[16px]` | `size-4` | untouched — no scale at all in v3.3 |
382
+ | `max-h-[16px]` | `max-h-4` | `max-h-4` |
383
+ | `max-w-[50%]` | `max-w-1/2` | untouched — no fractions in v3.3 |
384
+ | `top-[20%]` | `top-1/5` | untouched — v3.3 stops at quarters here |
385
+ | `top-[25%]` | `top-1/4` | `top-1/4` |
386
+ | `w-[8.333333%]` | `w-1/12` | `w-1/12` — `w` carries the full set |
387
+
388
+ Spacing steps: `size-*` has no numeric scale in v3.3, and `min-w-*`, `max-w-*`
389
+ and `min-h-*` only ship the zero step — they gained the scale afterwards.
390
+ Fractions: `w` carries all thirteen, `h` stops at sixths, `inset`/`top`/`left`/
391
+ `right`/`bottom`/`translate-x`/`translate-y` only have halves, thirds and
392
+ quarters, and `min-w`/`max-w`/`min-h`/`max-h` have none.
393
+
394
+ `text-*`, `rounded-*` and `opacity-*` are identical in both majors and are
395
+ never gated. `customSpacingTokens` stays your decision and is applied in both
396
+ modes. All of the above was measured against `tailwindcss@3.3.7`, the
397
+ advertised floor.
398
+
399
+ **On v3.4**, keep `tailwindVersion: 3`. v3.4 added the numeric scale to
400
+ `size-*`, `min-w-*`, `max-w-*` and `min-h-*`, but its spacing scale is still
401
+ discrete — `p-13` does not exist there either. So v3 mode is the safe setting
402
+ and merely leaves a few v3.4-valid suggestions on the table; `tailwindVersion:
403
+ 4` would emit steps v3.4 cannot generate.
404
+
359
405
  ## Config
360
406
 
361
407
  Create `tailwind-canonical.config.ts` at the root:
@@ -369,6 +415,8 @@ export default {
369
415
  customSpacingTokens: {
370
416
  14: '3.5',
371
417
  },
418
+ // Target Tailwind major: 3 restricts suggestions to the v3 scale (default: 4)
419
+ tailwindVersion: 4,
372
420
  // Support non-React attribute patterns (default: ['className'])
373
421
  attributeNames: ['className', 'class', ':class', 'tw'],
374
422
  // Support utility function wrappers
@@ -420,8 +468,15 @@ files and at most 3 times.
420
468
  The older top-level rare-value keys are still accepted for compatibility, but
421
469
  new configs should prefer `analyze`.
422
470
 
423
- `tailwind-canonical.config.js` is still loaded as a fallback when no TypeScript
424
- config exists.
471
+ Accepted filenames, in resolution order: `tailwind-canonical.config.ts`,
472
+ `.mts`, `.js`, `.mjs`.
473
+
474
+ A `.ts` or `.mts` config needs Node's type stripping, which is on by default
475
+ from Node 22.18 and 23.6, and available behind `--experimental-strip-types`
476
+ from 22.6. Short of that use `.mjs`, or `.js` when your nearest `package.json`
477
+ declares `"type": "module"`; both load on every supported Node. `tailwind-
478
+ canonical init` scaffolds a filename that loads on a bare `node` — it will not
479
+ write `.ts` just because the process it runs in happens to carry the flag.
425
480
 
426
481
  The config is resolved upward from the current directory: if no config file is
427
482
  found in `cwd`, tailwind-canonical checks each parent directory in turn, until
@@ -429,10 +484,6 @@ it finds one, reaches a `.git` directory, or reaches the filesystem root. This
429
484
  makes the CLI monorepo-friendly — a single config at the repo root applies to
430
485
  every package.
431
486
 
432
- On Node 22.6–23.5, loading a `.ts` config requires `--experimental-strip-types`;
433
- from Node 23.6 onward it works natively; the `.js` fallback works on every
434
- supported Node version.
435
-
436
487
  ## ESLint plugin
437
488
 
438
489
  ```js
@@ -531,15 +582,37 @@ Detects unused exports, files, and dependencies. Config in `knip.json`. Enforced
531
582
 
532
583
  ```ts
533
584
  import {
534
- suggestCanonical, // pure: class string → suggestion or null
535
- analyzeFile, // find non-canonical classes in a file
536
- fixFile, // apply --fix in-place
537
- deduplicateClasses, // pure: deduplicate a class string
538
- dedupeFile, // apply --dedup in-place
539
- sortClasses, // pure: sort a class string
540
- sortFile, // apply --sort in-place
541
- mergeFile, // apply --merge in-place (requires tailwind-merge)
542
- scanFiles, // recursive file scanner (sync, dir/file targets)
543
- resolveTargets, // async glob resolver with negation + brace expansion
585
+ suggestCanonical, // pure: class string → suggestion or null
586
+ analyzeFile, // find non-canonical classes in a file
587
+ fixFile, // apply --fix in-place
588
+ deduplicateClasses, // pure: deduplicate a class string
589
+ dedupeFile, // apply --dedup in-place
590
+ sortClasses, // pure: sort a class string
591
+ sortFile, // apply --sort in-place
592
+ DEFAULT_SORT_ORDER, // default sort category order (override via config sortOrder)
593
+ mergeFile, // apply --merge in-place (requires tailwind-merge)
594
+ scanFiles, // recursive file scanner (sync, dir/file targets)
595
+ resolveTargets, // async glob resolver with negation + brace expansion
596
+ loadConfig, // load + validate tailwind-canonical.config.{ts,mts,js,mjs} for a cwd
597
+ analyzeConsistency, // pure: cross-file consistency analysis (--analyze)
598
+ analyzeTyposFile, // find likely color-name typos in a file
599
+ detectTypo, // pure: check one class for a likely color-name typo
600
+ } from 'tailwind-canonical'
601
+
602
+ import type {
603
+ Config, // shared config object accepted across the API
604
+ Suggestion, // suggestCanonical's non-null return shape
605
+ Finding, // one entry returned by analyzeFile
606
+ ScanOptions, // extensions/ignore options for scanFiles/resolveTargets
607
+ SortCategory, // one entry of DEFAULT_SORT_ORDER / config.sortOrder
608
+ TypoFinding, // one entry returned by analyzeTyposFile
609
+ ConsistencyOptions, // options accepted by analyzeConsistency
610
+ ConsistencyReport, // analyzeConsistency's return shape
611
+ FileClasses, // per-file input shape for analyzeConsistency
612
+ ColorVariant, // one variant inside a ConsistencyReport color group
613
+ ColorVariantGroup, // one color-family group in ConsistencyReport.colorVariants
614
+ ScaleInconsistency, // one entry in ConsistencyReport.scaleInconsistencies
615
+ ScaleValue, // one value inside a ScaleInconsistency
616
+ ClassCombination, // one repeated-pattern entry in ConsistencyReport.combinations
544
617
  } from 'tailwind-canonical'
545
618
  ```
package/dist/cli/cli.d.ts CHANGED
@@ -38,4 +38,3 @@ export declare function parseArgs(argv: string[]): Flags;
38
38
  export declare function flagWarnings(flags: Flags): string[];
39
39
  export declare function run(argv: string[], cwd: string, sink?: Sink): Promise<RunResult>;
40
40
  export {};
41
- //# sourceMappingURL=cli.d.ts.map
package/dist/cli/cli.js CHANGED
@@ -2,20 +2,17 @@ import { existsSync, watch as fsWatch, readFileSync, writeFileSync, } from 'node
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, dedupeFile } from '../core/deduplicator.js';
8
- import { fixContent, fixFile } from '../core/fixer.js';
9
- import { mergeContent, 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 { sortContent, 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
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_CONTENT = [
16
- "import type { Config } from 'tailwind-canonical'",
17
- '',
18
- 'export default {',
15
+ const INIT_CONFIG_BODY = [
19
16
  ' // Run `tailwind-canonical` with no flags using these defaults:',
20
17
  " // defaultCommand: { fix: true, dedup: true, sort: true, typos: true, targets: ['./src'] },",
21
18
  ' // px → token additions/overrides:',
@@ -23,13 +20,26 @@ const INIT_CONFIG_CONTENT = [
23
20
  " // customSpacingTokens: { 14: '3.5' },",
24
21
  ' // Never suggest replacements for classes matching:',
25
22
  ' // ignorePatterns: [/^font-/],',
23
+ ];
24
+ const INIT_CONFIG_TS = [
25
+ "import type { Config } from 'tailwind-canonical'",
26
+ '',
27
+ 'export default {',
28
+ ...INIT_CONFIG_BODY,
26
29
  '} satisfies Config',
27
30
  '',
28
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');
29
39
  const HELP_TEXT = [
30
40
  USAGE,
31
41
  '',
32
- ' init Create a tailwind-canonical.config.ts scaffold in the current directory',
42
+ ' init Create a tailwind-canonical config scaffold in the current directory',
33
43
  ' --fix Auto-replace arbitrary values with canonical Tailwind classes',
34
44
  ' --dedup Remove redundant classes and collapse shorthands',
35
45
  ' --merge Resolve conflicting classes via tailwind-merge',
@@ -185,35 +195,26 @@ export function parseArgs(argv) {
185
195
  hasExplicitCheck: argv.includes('--check'),
186
196
  };
187
197
  }
198
+ /** Explicit CLI flags win; otherwise `defaultCommand` fills the gap. */
199
+ function fallback(explicit, flag, configured) {
200
+ return explicit ? flag : (configured ?? flag);
201
+ }
188
202
  function applyDefaultCommand(flags, config) {
189
203
  const defaults = config.defaultCommand;
190
204
  if (!defaults)
191
205
  return flags;
206
+ const mode = flags.hasExplicitMode;
192
207
  return {
193
208
  ...flags,
194
- fix: flags.hasExplicitMode ? flags.fix : (defaults.fix ?? flags.fix),
195
- merge: flags.hasExplicitMode
196
- ? flags.merge
197
- : (defaults.merge ?? flags.merge),
198
- dedup: flags.hasExplicitMode
199
- ? flags.dedup
200
- : (defaults.dedup ?? flags.dedup),
201
- sort: flags.hasExplicitMode ? flags.sort : (defaults.sort ?? flags.sort),
202
- analyze: flags.hasExplicitMode
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),
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),
217
218
  targets: flags.targets.length > 0 ? flags.targets : (defaults.targets ?? []),
218
219
  };
219
220
  }
@@ -324,45 +325,79 @@ function runTypos(files, config, reporter, sink) {
324
325
  logTyposText(findings, sink);
325
326
  return { exitCode: findings.length > 0 || hadError ? 1 : 0 };
326
327
  }
327
- async function processFile(file, flags, config) {
328
+ function applyTransforms(content, flags, config, twMerge) {
328
329
  const opts = toClassStringOpts(config);
329
330
  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;
339
- }
340
- async function checkFile(file, flags, config, twMerge) {
341
- const opts = toClassStringOpts(config);
342
- const counts = { fixed: 0, deduped: 0, merged: 0, sorted: 0 };
343
- let content = readFileSync(file, 'utf8');
331
+ let result = content;
344
332
  if (flags.fix) {
345
- const { result, count } = fixContent(content, config);
346
- counts.fixed = count;
347
- content = result;
333
+ const fixed = fixContent(result, config);
334
+ counts.fixed = fixed.count;
335
+ result = fixed.result;
348
336
  }
349
337
  if (flags.dedup) {
350
- const { result, count } = dedupeContent(content, opts);
351
- counts.deduped = count;
352
- content = result;
338
+ const deduped = dedupeContent(result, opts);
339
+ counts.deduped = deduped.count;
340
+ result = deduped.result;
353
341
  }
354
- if (flags.merge && twMerge) {
355
- const { result, count } = mergeContent(content, twMerge, opts);
356
- counts.merged = count;
357
- content = result;
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;
358
349
  }
359
350
  if (flags.sort) {
360
- const { result, count } = sortContent(content, opts, config.sortOrder);
361
- counts.sorted = count;
362
- content = result;
351
+ const sorted = sortContent(result, opts, config.sortOrder);
352
+ counts.sorted = sorted.count;
353
+ result = sorted.result;
363
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');
364
365
  return counts;
365
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
+ }
366
401
  function totalOf(c) {
367
402
  return c.fixed + c.deduped + c.merged + c.sorted;
368
403
  }
@@ -577,10 +612,9 @@ function runAnalyze(files, config, reporter, sink) {
577
612
  logAnalyzeText(report, issueCount, config, sink);
578
613
  return { exitCode: issueCount > 0 || hadError ? 1 : 0 };
579
614
  }
580
- function startWatch(files, flags, config, sink) {
615
+ function startWatch(files, flags, config, twMerge, sink) {
581
616
  const transforming = flags.fix || flags.dedup || flags.merge || flags.sort;
582
617
  const fileSet = new Set(files);
583
- const inFlight = new Set();
584
618
  const timers = new Map();
585
619
  const dirs = new Set(files.map((f) => dirname(f)));
586
620
  sink.log(`Watching ${pluralize(files.length, 'file')}... (Ctrl+C to stop)`);
@@ -589,23 +623,20 @@ function startWatch(files, flags, config, sink) {
589
623
  if (!filename)
590
624
  return;
591
625
  const full = resolve(dir, filename);
592
- if (!fileSet.has(full) || inFlight.has(full))
626
+ if (!fileSet.has(full))
593
627
  return;
594
628
  clearTimeout(timers.get(full));
595
629
  timers.set(full, setTimeout(() => {
596
630
  if (transforming) {
597
- inFlight.add(full);
598
- processFile(full, flags, config)
599
- .then((counts) => {
600
- const total = totalOf(counts);
631
+ try {
632
+ const total = totalOf(processFile(full, flags, config, twMerge));
601
633
  if (total > 0) {
602
634
  sink.log(`${timestamp()} ${full} — ${pluralize(total, 'change')} applied`);
603
635
  }
604
- })
605
- .catch((err) => {
606
- sink.error(`${timestamp()} ${full} — error: ${err instanceof Error ? err.message : String(err)}`);
607
- })
608
- .finally(() => inFlight.delete(full));
636
+ }
637
+ catch (err) {
638
+ sink.error(`${timestamp()} ${full} — error: ${errMsg(err)}`);
639
+ }
609
640
  }
610
641
  else {
611
642
  const findings = analyzeFile(full, config);
@@ -632,8 +663,9 @@ function runInit(cwd, sink) {
632
663
  sink.error(`${existing} already exists`);
633
664
  return { exitCode: 1 };
634
665
  }
635
- const filename = CONFIG_FILENAMES[0];
636
- writeFileSync(join(cwd, filename), INIT_CONFIG_CONTENT, 'utf8');
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');
637
669
  sink.log(`Created ${filename}`);
638
670
  sink.log('Uncomment defaultCommand to set your CLI defaults.');
639
671
  sink.log('Then run `npx tailwind-canonical` to lint your project.');
@@ -732,8 +764,8 @@ export async function run(argv, cwd, sink = defaultSink) {
732
764
  try {
733
765
  if (transforming) {
734
766
  const counts = flags.check
735
- ? await checkFile(file, flags, config, twMerge)
736
- : await processFile(file, flags, config);
767
+ ? applyTransforms(readFileSync(file, 'utf8'), flags, config, twMerge).counts
768
+ : processFile(file, flags, config, twMerge);
737
769
  totals.fixed += counts.fixed;
738
770
  totals.deduped += counts.deduped;
739
771
  totals.merged += counts.merged;
@@ -741,14 +773,7 @@ export async function run(argv, cwd, sink = defaultSink) {
741
773
  if (totalOf(counts) > 0)
742
774
  changedFiles.push(file);
743
775
  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')})`);
776
+ logTransformCounts(counts, file, flags.check, sink);
752
777
  }
753
778
  }
754
779
  else {
@@ -769,7 +794,7 @@ export async function run(argv, cwd, sink = defaultSink) {
769
794
  }
770
795
  }
771
796
  if (watching)
772
- return startWatch(files, flags, config, sink);
797
+ return startWatch(files, flags, config, twMerge, sink);
773
798
  if (transforming) {
774
799
  const typoResult = flags.typos
775
800
  ? collectTypos(files, config, sink)
@@ -880,4 +905,3 @@ export async function run(argv, cwd, sink = defaultSink) {
880
905
  sink.log('✓ No non-canonical classes found');
881
906
  return { exitCode: hadError ? 1 : 0 };
882
907
  }
883
- //# 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',
@@ -183,7 +186,11 @@ export function validateConfig(input, filename = CONFIG_FILENAME) {
183
186
  assertPositiveInteger(cfg.rareScaleMaxCount, 'rareScaleMaxCount', filename);
184
187
  if ('defaultCommand' in cfg)
185
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
+ }
186
194
  return cfg;
187
195
  }
188
- export { loadConfig } from '../io/config.js';
189
- //# 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