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.
- package/README.md +91 -18
- package/dist/cli/cli.d.ts +0 -1
- package/dist/cli/cli.js +111 -87
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +0 -1
- package/dist/core/analyzer.d.ts +0 -1
- package/dist/core/analyzer.js +0 -1
- package/dist/core/class-strings.d.ts +0 -1
- package/dist/core/class-strings.js +0 -1
- package/dist/core/config.d.ts +2 -3
- package/dist/core/config.js +9 -2
- package/dist/core/consistency.d.ts +0 -1
- package/dist/core/consistency.js +1 -6
- package/dist/core/deduplicator.d.ts +0 -1
- package/dist/core/deduplicator.js +10 -6
- package/dist/core/fixer.d.ts +0 -1
- package/dist/core/fixer.js +0 -1
- package/dist/core/lexicon.d.ts +1 -1
- package/dist/core/lexicon.js +1 -2
- package/dist/core/merger.d.ts +0 -1
- package/dist/core/merger.js +35 -29
- package/dist/core/rules.d.ts +2 -1
- package/dist/core/rules.js +56 -1
- package/dist/core/scanner.d.ts +0 -1
- package/dist/core/scanner.js +0 -1
- package/dist/core/sorter.d.ts +0 -1
- package/dist/core/sorter.js +5 -3
- package/dist/core/suppressions.d.ts +0 -1
- package/dist/core/suppressions.js +0 -1
- package/dist/core/typos.d.ts +0 -1
- package/dist/core/typos.js +0 -1
- package/dist/eslint/plugin.d.ts +0 -1
- package/dist/eslint/plugin.js +2 -2
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/io/analyzer.d.ts +0 -1
- package/dist/io/analyzer.js +0 -1
- package/dist/io/config.d.ts +9 -1
- package/dist/io/config.js +66 -5
- package/dist/io/consistency.d.ts +0 -1
- package/dist/io/consistency.js +0 -1
- package/dist/io/deduplicator.d.ts +0 -1
- package/dist/io/deduplicator.js +0 -1
- package/dist/io/fixer.d.ts +0 -1
- package/dist/io/fixer.js +0 -1
- package/dist/io/merger.d.ts +0 -1
- package/dist/io/merger.js +0 -1
- package/dist/io/scanner.d.ts +0 -1
- package/dist/io/scanner.js +0 -1
- package/dist/io/sorter.d.ts +0 -1
- package/dist/io/sorter.js +0 -1
- package/dist/io/typos.d.ts +0 -1
- package/dist/io/typos.js +0 -1
- package/package.json +10 -9
- package/dist/cli/cli.d.ts.map +0 -1
- package/dist/cli/cli.js.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/core/analyzer.d.ts.map +0 -1
- package/dist/core/analyzer.js.map +0 -1
- package/dist/core/class-strings.d.ts.map +0 -1
- package/dist/core/class-strings.js.map +0 -1
- package/dist/core/config.d.ts.map +0 -1
- package/dist/core/config.js.map +0 -1
- package/dist/core/consistency.d.ts.map +0 -1
- package/dist/core/consistency.js.map +0 -1
- package/dist/core/deduplicator.d.ts.map +0 -1
- package/dist/core/deduplicator.js.map +0 -1
- package/dist/core/fixer.d.ts.map +0 -1
- package/dist/core/fixer.js.map +0 -1
- package/dist/core/lexicon.d.ts.map +0 -1
- package/dist/core/lexicon.js.map +0 -1
- package/dist/core/merger.d.ts.map +0 -1
- package/dist/core/merger.js.map +0 -1
- package/dist/core/rules.d.ts.map +0 -1
- package/dist/core/rules.js.map +0 -1
- package/dist/core/scanner.d.ts.map +0 -1
- package/dist/core/scanner.js.map +0 -1
- package/dist/core/sorter.d.ts.map +0 -1
- package/dist/core/sorter.js.map +0 -1
- package/dist/core/suppressions.d.ts.map +0 -1
- package/dist/core/suppressions.js.map +0 -1
- package/dist/core/typos.d.ts.map +0 -1
- package/dist/core/typos.js.map +0 -1
- package/dist/eslint/plugin.d.ts.map +0 -1
- package/dist/eslint/plugin.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/io/analyzer.d.ts.map +0 -1
- package/dist/io/analyzer.js.map +0 -1
- package/dist/io/config.d.ts.map +0 -1
- package/dist/io/config.js.map +0 -1
- package/dist/io/consistency.d.ts.map +0 -1
- package/dist/io/consistency.js.map +0 -1
- package/dist/io/deduplicator.d.ts.map +0 -1
- package/dist/io/deduplicator.js.map +0 -1
- package/dist/io/fixer.d.ts.map +0 -1
- package/dist/io/fixer.js.map +0 -1
- package/dist/io/merger.d.ts.map +0 -1
- package/dist/io/merger.js.map +0 -1
- package/dist/io/scanner.d.ts.map +0 -1
- package/dist/io/scanner.js.map +0 -1
- package/dist/io/sorter.d.ts.map +0 -1
- package/dist/io/sorter.js.map +0 -1
- package/dist/io/typos.d.ts.map +0 -1
- package/dist/io/typos.js.map +0 -1
package/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+
|
|
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
|
|
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.
|
|
424
|
-
|
|
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,
|
|
535
|
-
analyzeFile,
|
|
536
|
-
fixFile,
|
|
537
|
-
deduplicateClasses,
|
|
538
|
-
dedupeFile,
|
|
539
|
-
sortClasses,
|
|
540
|
-
sortFile,
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
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
|
|
8
|
-
import { fixContent
|
|
9
|
-
import { mergeContent
|
|
7
|
+
import { dedupeContent } from '../core/deduplicator.js';
|
|
8
|
+
import { fixContent } from '../core/fixer.js';
|
|
9
|
+
import { mergeContent } from '../core/merger.js';
|
|
10
10
|
import { resolveTargets } from '../core/scanner.js';
|
|
11
|
-
import { sortContent
|
|
11
|
+
import { sortContent } from '../core/sorter.js';
|
|
12
12
|
import { analyzeTyposFile } from '../core/typos.js';
|
|
13
13
|
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
|
|
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
|
|
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:
|
|
195
|
-
merge: flags.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
? flags.analyze
|
|
204
|
-
: (defaults.analyze ?? flags.analyze),
|
|
205
|
-
typos: flags.hasExplicitMode
|
|
206
|
-
? flags.typos
|
|
207
|
-
: (defaults.typos ?? flags.typos),
|
|
208
|
-
watch: flags.hasExplicitWatch
|
|
209
|
-
? flags.watch
|
|
210
|
-
: (defaults.watch ?? flags.watch),
|
|
211
|
-
check: flags.hasExplicitCheck
|
|
212
|
-
? flags.check
|
|
213
|
-
: (defaults.check ?? flags.check),
|
|
214
|
-
reporter: flags.hasExplicitReporter
|
|
215
|
-
? flags.reporter
|
|
216
|
-
: (defaults.reporter ?? flags.reporter),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
346
|
-
counts.fixed = count;
|
|
347
|
-
|
|
333
|
+
const fixed = fixContent(result, config);
|
|
334
|
+
counts.fixed = fixed.count;
|
|
335
|
+
result = fixed.result;
|
|
348
336
|
}
|
|
349
337
|
if (flags.dedup) {
|
|
350
|
-
const
|
|
351
|
-
counts.deduped = count;
|
|
352
|
-
|
|
338
|
+
const deduped = dedupeContent(result, opts);
|
|
339
|
+
counts.deduped = deduped.count;
|
|
340
|
+
result = deduped.result;
|
|
353
341
|
}
|
|
354
|
-
if (flags.merge
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
|
361
|
-
counts.sorted = count;
|
|
362
|
-
|
|
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)
|
|
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
|
-
|
|
598
|
-
|
|
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
|
-
|
|
606
|
-
sink.error(`${timestamp()} ${full} — error: ${
|
|
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 =
|
|
636
|
-
|
|
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
|
-
?
|
|
736
|
-
:
|
|
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
|
-
|
|
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
|
package/dist/cli/index.d.ts
CHANGED
package/dist/cli/index.js
CHANGED
package/dist/core/analyzer.d.ts
CHANGED
package/dist/core/analyzer.js
CHANGED
package/dist/core/config.d.ts
CHANGED
|
@@ -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';
|
package/dist/core/config.js
CHANGED
|
@@ -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
|
package/dist/core/consistency.js
CHANGED
|
@@ -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
|
|
@@ -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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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) =>
|
|
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
|
package/dist/core/fixer.d.ts
CHANGED
package/dist/core/fixer.js
CHANGED
package/dist/core/lexicon.d.ts
CHANGED
|
@@ -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
|
package/dist/core/lexicon.js
CHANGED
|
@@ -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
|
package/dist/core/merger.d.ts
CHANGED