tailwind-canonical 0.8.0 → 0.10.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 +152 -33
- package/dist/cli/cli.js +77 -7
- package/dist/cli/index.js +1 -1
- package/dist/cli/watch.d.ts +14 -2
- package/dist/cli/watch.js +153 -24
- package/dist/core/class-strings.d.ts +2 -0
- package/dist/core/class-strings.js +622 -96
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.js +15 -4
- package/dist/core/consistency.d.ts +5 -0
- package/dist/core/consistency.js +26 -19
- package/dist/core/deduplicator.d.ts +1 -1
- package/dist/core/deduplicator.js +35 -7
- package/dist/core/lexicon.d.ts +7 -1
- package/dist/core/lexicon.js +43 -6
- package/dist/core/merger.d.ts +1 -0
- package/dist/core/merger.js +12 -7
- package/dist/core/rules.d.ts +4 -0
- package/dist/core/rules.js +29 -3
- package/dist/core/scanner.d.ts +4 -1
- package/dist/core/scanner.js +1 -50
- package/dist/core/typos.js +4 -5
- package/dist/eslint/plugin.d.ts +1 -1
- package/dist/eslint/plugin.js +114 -30
- package/dist/io/config.d.ts +4 -1
- package/dist/io/config.js +48 -4
- package/dist/io/consistency.js +4 -6
- package/dist/io/scanner.js +68 -21
- package/package.json +10 -7
package/README.md
CHANGED
|
@@ -94,19 +94,30 @@ npx tailwind-canonical --help
|
|
|
94
94
|
npx tailwind-canonical --version
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
If none of the targets match any supported files, the command exits `1` with `No files matched: <targets>`. A missing target does not fail a run when other targets match. Unreadable targets or subdirectories fail discovery before any file is changed.
|
|
98
|
+
Directory and glob discovery skip symbolic links. An explicitly named file symlink remains supported and reads or changes its target.
|
|
99
|
+
|
|
100
|
+
### Watch behavior
|
|
101
|
+
|
|
102
|
+
Watch mode discovers new and recreated files, including targets from initially
|
|
103
|
+
empty globs. It applies the same exclusions and symlink policy as single-pass
|
|
104
|
+
runs. Configuration entry-file changes reload mappings, defaults and targets;
|
|
105
|
+
invalid configuration pauses processing until it is corrected. Watch roots are
|
|
106
|
+
collapsed to avoid overlapping recursive watchers. SIGINT/SIGTERM close watchers
|
|
107
|
+
and pending timers; filesystem watcher failures are reported and stop the session
|
|
108
|
+
with a nonzero exit code. Watch output is text only.
|
|
98
109
|
|
|
99
110
|
## What each flag does
|
|
100
111
|
|
|
101
112
|
| Flag | What it fixes | Example |
|
|
102
113
|
|---|---|---|
|
|
103
|
-
| `--fix` | Arbitrary values → canonical tokens | `
|
|
114
|
+
| `--fix` | Arbitrary values → canonical tokens | `p-[16px]` → `p-4` |
|
|
104
115
|
| `--dedup` | Redundant classes, conflicts, shorthand collapse | `flex block` → `block`, `px-4 py-4` → `p-4`, `border-t-2 border-b-2` → `border-y-2` |
|
|
105
116
|
| `--sort` | Canonical class order | `text-sm flex p-4` → `flex p-4 text-sm` |
|
|
106
117
|
| `--merge` | tailwind-merge conflict resolution | `bg-red-500 bg-blue-500` → `bg-blue-500` |
|
|
107
118
|
| `--check` | Dry-run: report what the combined transforms would change, write nothing (exit 1 on pending changes) | `would fix src/Button.tsx (1 replacement)` |
|
|
108
119
|
| `--analyze` | Cross-file consistency (color variants, scale drift, repeated patterns) | `Warning: 3 red color variants used for text: text-red-500, text-rose-500, text-red-600` |
|
|
109
|
-
| `--watch` | Re-run on every file save (debounced 50ms) | `[12:34:01] src/Button.tsx — 2 changes applied` |
|
|
120
|
+
| `--watch` | Re-run on every file save (debounced 50ms; text reporter only) | `[12:34:01] src/Button.tsx — 2 changes applied` |
|
|
110
121
|
| `--typos` | Likely misspelled color names (read-only; chains after transforms) | `text-gry-500` → `text-gray-500` [typo] |
|
|
111
122
|
| `--reporter json` | JSON output (check mode) or fix summary | machine-readable for CI pipelines |
|
|
112
123
|
| `--reporter sarif` | SARIF 2.1.0 output (findings in check/typos modes; transform mode emits an empty document unless `--typos` is chained) | GitHub Code Scanning / VS Code |
|
|
@@ -120,9 +131,9 @@ npx tailwind-canonical --reporter json ./src
|
|
|
120
131
|
```json
|
|
121
132
|
{
|
|
122
133
|
"files": 3,
|
|
123
|
-
"total":
|
|
134
|
+
"total": 1,
|
|
124
135
|
"findings": [
|
|
125
|
-
{ "file": "src/Button.tsx", "line": 12, "col": 17, "original": "
|
|
136
|
+
{ "file": "src/Button.tsx", "line": 12, "col": 17, "original": "p-[16px]", "canonical": "p-4", "isCustomToken": false }
|
|
126
137
|
]
|
|
127
138
|
}
|
|
128
139
|
```
|
|
@@ -203,11 +214,11 @@ Skip specific lines or blocks from check and all transforms (`--fix`, `--dedup`,
|
|
|
203
214
|
|
|
204
215
|
```jsx
|
|
205
216
|
{/* tailwind-canonical-disable-next-line */}
|
|
206
|
-
<div className="
|
|
217
|
+
<div className="p-[16px]" /> {/* left untouched */}
|
|
207
218
|
|
|
208
219
|
{/* tailwind-canonical-disable */}
|
|
209
|
-
<div className="
|
|
210
|
-
<div className="m-[
|
|
220
|
+
<div className="p-[16px] h-[24px]" /> {/* whole block untouched */}
|
|
221
|
+
<div className="m-[8px]" />
|
|
211
222
|
{/* tailwind-canonical-enable */}
|
|
212
223
|
```
|
|
213
224
|
|
|
@@ -234,7 +245,7 @@ npx tailwind-canonical --analyze ./src
|
|
|
234
245
|
# - px-11: 3 uses in 1 file (608 px uses total); e.g. src/Dialog.tsx
|
|
235
246
|
```
|
|
236
247
|
|
|
237
|
-
|
|
248
|
+
Four detectors:
|
|
238
249
|
|
|
239
250
|
| Detector | What it flags |
|
|
240
251
|
|---|---|
|
|
@@ -243,6 +254,11 @@ Three detectors:
|
|
|
243
254
|
| Rare scale values | Low-frequency scale values inside otherwise common properties (`ml-13`, `gap-24`, `px-11`, …) |
|
|
244
255
|
| Repeated patterns | Identical class combinations recurring across 3+ files |
|
|
245
256
|
|
|
257
|
+
Repeated patterns compare complete class strings, ignoring class order; they do not
|
|
258
|
+
mine shared subsets inside larger strings. The programmatic `FileClasses` input
|
|
259
|
+
can provide `classLists` to preserve those string boundaries. If omitted,
|
|
260
|
+
`classes` is treated as one class string per file for backward compatibility.
|
|
261
|
+
|
|
246
262
|
Pairs with `--reporter json` for machine-readable output. Exits `1` when any inconsistency is found.
|
|
247
263
|
|
|
248
264
|
```bash
|
|
@@ -319,13 +335,17 @@ flex block → block (last wins)
|
|
|
319
335
|
relative absolute → absolute
|
|
320
336
|
```
|
|
321
337
|
|
|
338
|
+
Conflict resolution follows the last-token convention, which can change the
|
|
339
|
+
result of Tailwind's stylesheet precedence. Important and normal utilities keep
|
|
340
|
+
separate priorities.
|
|
341
|
+
|
|
322
342
|
### Padding / margin shorthand collapse
|
|
323
343
|
|
|
324
344
|
```
|
|
325
345
|
px-4 py-4 → p-4
|
|
326
346
|
p-4 px-2 → py-4 px-2
|
|
327
|
-
pt-2 pb-2 → py-2
|
|
328
|
-
pl-4 pr-4 → px-4
|
|
347
|
+
pt-2 pb-2 → py-2 (v3 only)
|
|
348
|
+
pl-4 pr-4 → px-4 (v3 only)
|
|
329
349
|
mx-4 my-4 → m-4
|
|
330
350
|
```
|
|
331
351
|
|
|
@@ -333,24 +353,31 @@ mx-4 my-4 → m-4
|
|
|
333
353
|
|
|
334
354
|
```
|
|
335
355
|
border-t-2 border-b-2 border-l-2 border-r-2 → border-2
|
|
336
|
-
border-t-2 border-b-2 → border-y-2
|
|
337
|
-
border-l-4 border-r-4 → border-x-4
|
|
356
|
+
border-t-2 border-b-2 → border-y-2 (v3 only)
|
|
357
|
+
border-l-4 border-r-4 → border-x-4 (v3 only)
|
|
338
358
|
```
|
|
339
359
|
|
|
340
360
|
### Inset shorthand collapse
|
|
341
361
|
|
|
342
362
|
```
|
|
343
363
|
top-4 right-4 bottom-4 left-4 → inset-4
|
|
344
|
-
top-4 bottom-4 → inset-y-4
|
|
345
|
-
left-2 right-2 → inset-x-2
|
|
364
|
+
top-4 bottom-4 → inset-y-4 (v3 only)
|
|
365
|
+
left-2 right-2 → inset-x-2 (v3 only)
|
|
346
366
|
```
|
|
347
367
|
|
|
368
|
+
With Tailwind v4 (the default), axis utilities use logical inline/block properties.
|
|
369
|
+
Physical sides such as `pl-*` and `pr-*` are therefore not collapsed into `px-*`,
|
|
370
|
+
and mixed physical/logical groups are preserved. Equal values covering all four
|
|
371
|
+
sides can still collapse to the global utility. Set `tailwindVersion: 3` to enable
|
|
372
|
+
v3's physical-side to axis shorthand rules. The pure API also accepts this option:
|
|
373
|
+
`deduplicateClasses(classes, { tailwindVersion: 3 })`.
|
|
374
|
+
|
|
348
375
|
## Arbitrary value detection (`--fix`)
|
|
349
376
|
|
|
350
377
|
| Arbitrary | Canonical | Notes |
|
|
351
378
|
|---|---|---|
|
|
352
|
-
| `text-[12px]` | `text-xs` |
|
|
353
|
-
| `text-[0.75rem]` | `text-xs` |
|
|
379
|
+
| `text-[12px]` | `text-xs` | Explicit `allowTextSizeLineHeightChanges: true` only |
|
|
380
|
+
| `text-[0.75rem]` | `text-xs` | Explicit opt-in only; default leaves the class unchanged |
|
|
354
381
|
| `h-[64px]` | `h-16` | Spacing scale ÷4 |
|
|
355
382
|
| `p-[1px]` / `gap-[1px]` | `p-px` / `gap-px` | 1px spacing step |
|
|
356
383
|
| `w-[50%]` | `w-1/2` | Percentage fractions |
|
|
@@ -377,16 +404,19 @@ left-2 right-2 → inset-x-2
|
|
|
377
404
|
| `order-[1]` / `order-[0]` | `order-1` / `order-none` | Any integer on v4 |
|
|
378
405
|
| `grow-[1]` / `shrink-[0]` | `grow` / `shrink-0` | |
|
|
379
406
|
| `line-clamp-[3]`, `columns-[3]` | `line-clamp-3`, `columns-3` | Any integer ≥ 1 on v4 |
|
|
380
|
-
| `text-[11px]` | `text-2xs` | Requires `customTextTokens` |
|
|
381
|
-
| `text-[8px]`–`text-[10px]` | `text-3xs` | Requires `customTextTokens` |
|
|
407
|
+
| `text-[11px]` | `text-2xs` | Requires `customTextTokens` and explicit text-size opt-in |
|
|
408
|
+
| `text-[8px]`–`text-[10px]` | `text-3xs` | Requires `customTextTokens` and explicit text-size opt-in |
|
|
382
409
|
| `hover:p-[16px]` | `hover:p-4` | Variants are preserved |
|
|
383
410
|
| `!p-[16px]` / `p-[16px]!` | `!p-4` / `p-4!` | Important modifier kept in place |
|
|
384
411
|
| `-mt-[8px]` / `mt-[-8px]` | `-mt-2` | Negative margin / inset / translate / space / scale / rotate / z / order only |
|
|
385
412
|
|
|
386
|
-
`text-2xs` and `text-3xs` are not stock Tailwind tokens, so they are not in the built-in text size map — without config, `text-[11px]` and `text-[8px]`–`text-[10px]` are left untouched. Enable them
|
|
413
|
+
`text-2xs` and `text-3xs` are not stock Tailwind tokens, so they are not in the built-in text size map — without config, `text-[11px]` and `text-[8px]`–`text-[10px]` are left untouched. Enable them only if the typography change is intended, using `customTextTokens` and the explicit opt-in:
|
|
387
414
|
|
|
388
415
|
```ts
|
|
389
|
-
export default {
|
|
416
|
+
export default {
|
|
417
|
+
allowTextSizeLineHeightChanges: true, // named sizes may also set line-height
|
|
418
|
+
customTextTokens: { 8: '3xs', 9: '3xs', 10: '3xs', 11: '2xs' },
|
|
419
|
+
}
|
|
390
420
|
```
|
|
391
421
|
|
|
392
422
|
Non-divisible values (`h-[22px]`, `px-[7px]`) are left untouched.
|
|
@@ -459,16 +489,63 @@ discrete — `p-13` does not exist there either. So v3 mode is the safe setting
|
|
|
459
489
|
and merely leaves a few v3.4-valid suggestions on the table; `tailwindVersion:
|
|
460
490
|
4` would emit steps v3.4 cannot generate.
|
|
461
491
|
|
|
492
|
+
### Custom themes (`useDefaultTheme`)
|
|
493
|
+
|
|
494
|
+
The default mappings assume Tailwind's stock theme and a 16px root font size for
|
|
495
|
+
px/rem equivalences. If your theme changes these values, disable inferred mappings:
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
export default {
|
|
499
|
+
useDefaultTheme: false,
|
|
500
|
+
customTextTokens: { 12: 'tiny' },
|
|
501
|
+
customSpacingTokens: { 16: 'gutter' },
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
Only explicitly configured pixel mappings are considered in this mode:
|
|
506
|
+
`p-[16px]` becomes `p-gutter`, while other arbitrary values (including rem values)
|
|
507
|
+
stay unchanged. Text-size mappings are also subject to the separate opt-in below:
|
|
508
|
+
`customTextTokens` alone never enables them. The options are shared by the CLI,
|
|
509
|
+
library and ESLint canonicalization rule. They do not load your Tailwind theme
|
|
510
|
+
or customize the separate `tailwind-merge` conflict resolver.
|
|
511
|
+
|
|
512
|
+
### Preserve text line-height (default)
|
|
513
|
+
|
|
514
|
+
`text-[12px]` sets only font-size; `text-xs` also sets line-height. Equal font sizes
|
|
515
|
+
therefore do not guarantee the same rendering. By default, pixel and rem text-size
|
|
516
|
+
conversions are neither reported nor applied, including custom text tokens.
|
|
517
|
+
Variants, important modifiers and nearby `leading-*` utilities do not bypass this
|
|
518
|
+
protection: without the complete stylesheet, their presence cannot prove that the
|
|
519
|
+
conversion preserves the cascade. No `leading-[inherit]` utility is inserted.
|
|
520
|
+
|
|
521
|
+
```ts
|
|
522
|
+
suggestCanonical('text-[12px]') // null
|
|
523
|
+
suggestCanonical('p-[16px]') // { original: 'p-[16px]', canonical: 'p-4', ... }
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
To deliberately adopt the named typography token, including its line-height,
|
|
527
|
+
opt in explicitly in shared configuration:
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
export default {
|
|
531
|
+
allowTextSizeLineHeightChanges: true, // default: false
|
|
532
|
+
}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
With this option, `text-[12px]` becomes `text-xs` and custom text mappings become
|
|
536
|
+
active. This restores the earlier behavior and can change rendering. With the
|
|
537
|
+
option absent or `false`, text classes stay unchanged in lint, `--fix`, `--check`,
|
|
538
|
+
watch, the programmatic API and ESLint; other utility families remain enabled.
|
|
539
|
+
|
|
462
540
|
## Config
|
|
463
541
|
|
|
464
542
|
Create `tailwind-canonical.config.ts` at the root:
|
|
465
543
|
|
|
466
544
|
```ts
|
|
467
545
|
export default {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
},
|
|
546
|
+
// Text mappings are inactive unless the line-height change is intentional:
|
|
547
|
+
// allowTextSizeLineHeightChanges: true,
|
|
548
|
+
// customTextTokens: { 10: '3xs', 11: '2xs' },
|
|
472
549
|
customSpacingTokens: {
|
|
473
550
|
14: '3.5',
|
|
474
551
|
},
|
|
@@ -502,7 +579,7 @@ export default {
|
|
|
502
579
|
}
|
|
503
580
|
```
|
|
504
581
|
|
|
505
|
-
`customTextTokens` merges with the built-in text size map. `customSpacingTokens` supplements the default ÷4 spacing logic. `ignorePatterns` makes `suggestCanonical` skip matching classes everywhere — CLI, analyzer, and the ESLint plugin.
|
|
582
|
+
`customTextTokens` merges with the built-in text size map only when `allowTextSizeLineHeightChanges: true`; otherwise text-size suggestions stay disabled. `customSpacingTokens` supplements the default ÷4 spacing logic. `ignorePatterns` makes `suggestCanonical` skip matching classes everywhere — CLI, analyzer, and the ESLint plugin.
|
|
506
583
|
|
|
507
584
|
`attributeNames` controls which HTML/JSX attributes are scanned (default: `['className']`). Use `['class']` for plain HTML/PHP/Jinja templates, `[':class']` for Vue, `['tw']` for styled-components.
|
|
508
585
|
|
|
@@ -577,20 +654,33 @@ ESLint without schema errors:
|
|
|
577
654
|
```js
|
|
578
655
|
'tailwind-canonical/no-arbitrary-canonical': [
|
|
579
656
|
'warn',
|
|
580
|
-
{
|
|
657
|
+
{ customSpacingTokens: { 14: '3.5' }, ignorePatterns: [/^font-/] },
|
|
581
658
|
],
|
|
582
659
|
```
|
|
583
660
|
|
|
584
661
|
The rule **honors**: `customTextTokens`, `customSpacingTokens`, `ignorePatterns`
|
|
585
|
-
(all applied via `suggestCanonical`)
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
662
|
+
and `tailwindVersion` (all applied via `suggestCanonical`), plus
|
|
663
|
+
`useDefaultTheme` to disable inferred mappings and
|
|
664
|
+
`allowTextSizeLineHeightChanges` for the explicit typography opt-in. It also uses
|
|
665
|
+
`attributeNames` (default: `['className']`) and `functionNames` (default:
|
|
666
|
+
`['cn', 'clsx', 'cva']`) to identify class strings. Other `Config` fields are
|
|
667
|
+
accepted for shared configuration and have no effect on this rule.
|
|
668
|
+
|
|
669
|
+
Both rules inspect class attributes and recognized utility calls, including
|
|
670
|
+
arrays, object arguments and conditional branches. Unrelated strings and tagged
|
|
671
|
+
templates are ignored. In interpolated templates, only complete tokens separated
|
|
672
|
+
from an interpolation by whitespace are transformed; partial tokens such as
|
|
673
|
+
`text-[12px]${suffix}` are preserved. `no-conflicting-classes` accepts the same
|
|
674
|
+
shared configuration and uses its attribute/function names and custom text tokens. Enabling this rule without
|
|
675
|
+
`tailwind-merge` installed raises an installation error. In `cn`/`clsx` object arguments,
|
|
676
|
+
only keys contain classes; predicate values are preserved. To prevent renaming a key onto another property, objects with multiple properties, computed keys or spreads are left unchanged. For `cva`, base
|
|
677
|
+
classes, variant classes and compound variant `class`/`className` values are checked.
|
|
590
678
|
|
|
591
679
|
## Known limits
|
|
592
680
|
|
|
593
|
-
-
|
|
681
|
+
- **Source extraction is not a complete JavaScript/JSX parser** — ambiguous regex/division expressions and some nested templates are left intact. Quoted JSX prose can hide nested markup from CLI/API extraction; use the ESLint plugin when complete JSX syntax coverage is needed.
|
|
682
|
+
- **CLI and content APIs extract static class positions** — strings, arrays, conditional result branches, single-property `cn`/`clsx` object keys, and `cva` variant classes are supported. Objects with multiple properties, computed keys or spreads are kept unchanged to prevent key collisions. Vue `:class`/`v-bind:class` bindings are read as expressions; Svelte interpolations inside quoted attributes are kept intact. Predicates, variant identifiers, `defaultVariants` and nested calls to unconfigured functions are preserved. Dynamic values are not evaluated.
|
|
683
|
+
- **Watch reloads the configuration entry file** — imported helper modules keep Node's normal module cache. Restart the watcher after changing a helper imported by your config.
|
|
594
684
|
- **`--typos` covers Tailwind color names only** — color-name detection uses Levenshtein distance 1 against the built-in Tailwind color palette. Custom colors from your config are supported via `extraColors` in the config, but typo detection does not extend to properties, function names, or arbitrary modifiers.
|
|
595
685
|
- **`--merge` requires `tailwind-merge` as a peer dependency** — the plugin is optional, so the CLI will error if you use `--merge` without installing `tailwind-merge`. Install with `pnpm add -D tailwind-merge`.
|
|
596
686
|
|
|
@@ -627,6 +717,30 @@ npx tailwind-canonical --fix --dedup --sort ./src ./app
|
|
|
627
717
|
}
|
|
628
718
|
```
|
|
629
719
|
|
|
720
|
+
## Compatibility and performance checks
|
|
721
|
+
|
|
722
|
+
The supported ESLint range is `>=8.57.1 <11` (flat configuration), and the optional
|
|
723
|
+
`tailwind-merge` range is `>=2.0.0 <4`. CI tests the built tarball in isolated
|
|
724
|
+
consumer projects across ESLint 8/9/10 and tailwind-merge 2/3, including minimum
|
|
725
|
+
versions. Imports, the installed binary, ESLint fixes and TypeScript consumers
|
|
726
|
+
are exercised rather than relying only on source tests.
|
|
727
|
+
|
|
728
|
+
```bash
|
|
729
|
+
pnpm test:compat 8.57.1 2.0.0
|
|
730
|
+
# Compile regression cases against Tailwind v3 and v4 in temporary consumers:
|
|
731
|
+
pnpm test:css
|
|
732
|
+
pnpm benchmark
|
|
733
|
+
# Tune the fixed JSX/Vue/Svelte corpus and emit machine-readable measurements:
|
|
734
|
+
pnpm benchmark --files 300 --lines 40 --runs 7 --json
|
|
735
|
+
# Helper-heavy corpus, including large files:
|
|
736
|
+
pnpm benchmark --corpus helpers --files 1 --lines 8000 --runs 3 --json
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
The benchmark checks expected findings and transform idempotence, then measures
|
|
740
|
+
file discovery, file analysis, fix/dedup/sort and cross-file consistency. Results
|
|
741
|
+
include Node/CPU/platform, corpus size, median and p95 after warmup. Compare on the
|
|
742
|
+
same machine; this synthetic corpus is not a promise about every project.
|
|
743
|
+
|
|
630
744
|
## Dead code detection (knip)
|
|
631
745
|
|
|
632
746
|
```bash
|
|
@@ -637,6 +751,11 @@ Detects unused exports, files, and dependencies. Config in `knip.json`. Enforced
|
|
|
637
751
|
|
|
638
752
|
## Programmatic API
|
|
639
753
|
|
|
754
|
+
This package targets Node.js. Some exported functions are pure transformations,
|
|
755
|
+
but the package entry and `core` re-exports also load Node I/O modules. They are
|
|
756
|
+
not browser entry points. “Pure” describes the function's behavior, not the
|
|
757
|
+
entire import graph.
|
|
758
|
+
|
|
640
759
|
```ts
|
|
641
760
|
import {
|
|
642
761
|
suggestCanonical, // pure: class string → suggestion or null
|
package/dist/cli/cli.js
CHANGED
|
@@ -2,22 +2,24 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { analyzeFile } from '../core/analyzer.js';
|
|
4
4
|
import { toClassStringOpts } from '../core/class-strings.js';
|
|
5
|
-
import { CONFIG_FILENAMES, loadConfig, resolveInitFilename, } from '../core/config.js';
|
|
5
|
+
import { CONFIG_FILENAMES, findConfigPath, loadConfig, resolveInitFilename, } from '../core/config.js';
|
|
6
6
|
import { analyzeConsistencyFiles, toConsistencyOptions, } from '../core/consistency.js';
|
|
7
7
|
import { dedupeContent } from '../core/deduplicator.js';
|
|
8
8
|
import { fixContent } from '../core/fixer.js';
|
|
9
9
|
import { mergeContent } from '../core/merger.js';
|
|
10
|
-
import { resolveTargets } from '../core/scanner.js';
|
|
10
|
+
import { isGlob, resolveTargets } from '../core/scanner.js';
|
|
11
11
|
import { sortContent } from '../core/sorter.js';
|
|
12
12
|
import { analyzeTyposFile } from '../core/typos.js';
|
|
13
13
|
import { errMsg, pluralize, timestamp } from './format.js';
|
|
14
14
|
import { analyzeSarifDocument, findingsSarifDocument, logAnalyzeText, logFindings, logTransformCounts, logTyposText, logWatchFindings, sarifDocument, typoSarifDocument, writeJson, } from './reporters.js';
|
|
15
|
-
import { startWatch } from './watch.js';
|
|
15
|
+
import { startWatch, watchRoots } from './watch.js';
|
|
16
16
|
const USAGE = 'Usage: tailwind-canonical [--fix] [--merge] [--dedup] [--sort] [--check] [--analyze] [--typos] [--watch] [--reporter json|sarif] <dir|file> [dir|file...]\n tailwind-canonical init';
|
|
17
17
|
const INIT_CONFIG_BODY = [
|
|
18
18
|
' // Run `tailwind-canonical` with no flags using these defaults:',
|
|
19
19
|
" // defaultCommand: { fix: true, dedup: true, sort: true, typos: true, targets: ['./src'] },",
|
|
20
20
|
' // px → token additions/overrides:',
|
|
21
|
+
' // Text mappings can change line-height; enable only when intentional:',
|
|
22
|
+
' // allowTextSizeLineHeightChanges: true,',
|
|
21
23
|
" // customTextTokens: { 11: '2xs' },",
|
|
22
24
|
" // customSpacingTokens: { 14: '3.5' },",
|
|
23
25
|
' // Never suggest replacements for classes matching:',
|
|
@@ -49,7 +51,7 @@ const HELP_TEXT = [
|
|
|
49
51
|
' --check Dry-run: report what transforms would change, write nothing (exit 1 on pending changes)',
|
|
50
52
|
' --analyze Cross-file consistency analysis (read-only)',
|
|
51
53
|
' --typos Flag likely misspelled color names (read-only)',
|
|
52
|
-
' --watch Re-run on every file save (
|
|
54
|
+
' --watch Re-run on every file save (text reporter only; ignored with --typos/--check)',
|
|
53
55
|
' --reporter <type> Output format: text|json|sarif',
|
|
54
56
|
' --help, -h Show this help message',
|
|
55
57
|
' --version, -V Show the installed version',
|
|
@@ -439,6 +441,7 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
439
441
|
sink.error(`tailwind-canonical: ${errMsg(err)}`);
|
|
440
442
|
return { exitCode: 1 };
|
|
441
443
|
}
|
|
444
|
+
const cliFlags = flags;
|
|
442
445
|
flags = applyDefaultCommand(flags, config);
|
|
443
446
|
if (flags.targets.length === 0) {
|
|
444
447
|
sink.error(USAGE);
|
|
@@ -448,6 +451,14 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
448
451
|
sink.error('--check requires at least one of --fix, --dedup, --merge, --sort');
|
|
449
452
|
return { exitCode: 1 };
|
|
450
453
|
}
|
|
454
|
+
if (flags.watch &&
|
|
455
|
+
!flags.analyze &&
|
|
456
|
+
!flags.typos &&
|
|
457
|
+
!flags.check &&
|
|
458
|
+
flags.reporter !== 'text') {
|
|
459
|
+
sink.error('--watch requires --reporter text; JSON and SARIF support single-pass runs only');
|
|
460
|
+
return { exitCode: 1 };
|
|
461
|
+
}
|
|
451
462
|
let twMerge;
|
|
452
463
|
if (flags.merge && !flags.analyze) {
|
|
453
464
|
try {
|
|
@@ -462,8 +473,29 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
462
473
|
sink.error(`Warning: ${warning}`);
|
|
463
474
|
}
|
|
464
475
|
const targetPaths = flags.targets.map((target) => resolveTarget(cwd, target));
|
|
465
|
-
const
|
|
466
|
-
|
|
476
|
+
const targetKinds = new Map();
|
|
477
|
+
const rememberTargetKinds = (paths) => {
|
|
478
|
+
for (const path of paths) {
|
|
479
|
+
const target = path.startsWith('!') ? path.slice(1) : path;
|
|
480
|
+
const key = target.replace(/\\/g, '/');
|
|
481
|
+
if (!targetKinds.has(key))
|
|
482
|
+
targetKinds.set(key, existsSync(target) || !(path.startsWith('!') || isGlob(target))
|
|
483
|
+
? 'literal'
|
|
484
|
+
: 'glob');
|
|
485
|
+
}
|
|
486
|
+
return targetKinds;
|
|
487
|
+
};
|
|
488
|
+
rememberTargetKinds(targetPaths);
|
|
489
|
+
let files;
|
|
490
|
+
try {
|
|
491
|
+
files = await resolveTargets(targetPaths, { targetKinds });
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
sink.error(`tailwind-canonical: could not resolve targets: ${errMsg(error)}`);
|
|
495
|
+
return { exitCode: 1 };
|
|
496
|
+
}
|
|
497
|
+
if (files.length === 0 &&
|
|
498
|
+
!(flags.watch && !flags.analyze && !flags.typos && !flags.check)) {
|
|
467
499
|
sink.error(`No files matched: ${flags.targets.join(' ')}`);
|
|
468
500
|
return { exitCode: 1 };
|
|
469
501
|
}
|
|
@@ -476,10 +508,48 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
476
508
|
return runTypos(files, config, flags.reporter, sink, sarif);
|
|
477
509
|
}
|
|
478
510
|
if (flags.watch && !flags.typos && !flags.check) {
|
|
511
|
+
const configDirectory = () => {
|
|
512
|
+
const path = findConfigPath(cwd);
|
|
513
|
+
return path ? dirname(path) : cwd;
|
|
514
|
+
};
|
|
515
|
+
const explicitTargets = (paths) => paths.filter((path) => !path.startsWith('!') &&
|
|
516
|
+
targetKinds.get(path.replace(/\\/g, '/')) === 'literal');
|
|
479
517
|
return startWatch({
|
|
480
518
|
files,
|
|
519
|
+
explicitTargets: explicitTargets(targetPaths),
|
|
520
|
+
roots: watchRoots(targetPaths, [cwd, configDirectory()], targetKinds),
|
|
481
521
|
sink,
|
|
482
|
-
|
|
522
|
+
refresh: async (changed) => {
|
|
523
|
+
const nextConfig = await loadConfig(cwd, { reload: true });
|
|
524
|
+
const nextFlags = applyDefaultCommand(cliFlags, nextConfig);
|
|
525
|
+
if (nextFlags.analyze ||
|
|
526
|
+
nextFlags.typos ||
|
|
527
|
+
nextFlags.check ||
|
|
528
|
+
nextFlags.reporter !== 'text') {
|
|
529
|
+
throw new Error('Watch configuration must use text output without --analyze, --typos or --check');
|
|
530
|
+
}
|
|
531
|
+
let nextMerge = twMerge;
|
|
532
|
+
if (nextFlags.merge && !nextMerge)
|
|
533
|
+
({ twMerge: nextMerge } = await import('tailwind-merge'));
|
|
534
|
+
const paths = nextFlags.targets.map((target) => resolveTarget(cwd, target));
|
|
535
|
+
const nextFiles = await resolveTargets(paths, {
|
|
536
|
+
targetKinds: rememberTargetKinds(paths),
|
|
537
|
+
});
|
|
538
|
+
const nextRoots = watchRoots(paths, [cwd, configDirectory()], targetKinds);
|
|
539
|
+
const configChanged = JSON.stringify(config, (_key, value) => value instanceof RegExp ? value.toString() : value) !==
|
|
540
|
+
JSON.stringify(nextConfig, (_key, value) => value instanceof RegExp ? value.toString() : value);
|
|
541
|
+
config = nextConfig;
|
|
542
|
+
flags = nextFlags;
|
|
543
|
+
twMerge = nextMerge;
|
|
544
|
+
return {
|
|
545
|
+
files: nextFiles,
|
|
546
|
+
roots: nextRoots,
|
|
547
|
+
explicitTargets: explicitTargets(paths),
|
|
548
|
+
reprocessAll: configChanged ||
|
|
549
|
+
[...changed].some((file) => CONFIG_FILENAMES.some((name) => file.endsWith(`/${name}`) || file.endsWith(`\\${name}`))),
|
|
550
|
+
};
|
|
551
|
+
},
|
|
552
|
+
processFile: (file) => isTransforming(flags)
|
|
483
553
|
? watchTransform(file, flags, config, sink, twMerge)
|
|
484
554
|
: logWatchFindings(file, analyzeFile(file, config), timestamp(), sink),
|
|
485
555
|
});
|
package/dist/cli/index.js
CHANGED
package/dist/cli/watch.d.ts
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import type { Sink } from './types.js';
|
|
2
|
+
type WatchRefresh = {
|
|
3
|
+
files: string[];
|
|
4
|
+
roots: string[];
|
|
5
|
+
explicitTargets?: string[];
|
|
6
|
+
reprocessAll?: boolean;
|
|
7
|
+
};
|
|
2
8
|
type WatchOptions = {
|
|
3
9
|
files: string[];
|
|
10
|
+
roots: string[];
|
|
11
|
+
explicitTargets?: string[];
|
|
4
12
|
sink: Sink;
|
|
13
|
+
refresh: (changed: ReadonlySet<string>) => Promise<WatchRefresh>;
|
|
5
14
|
processFile: (file: string) => void;
|
|
6
15
|
};
|
|
7
|
-
|
|
16
|
+
/** Watch the existing ancestor of each target, including currently empty globs. */
|
|
17
|
+
export declare function watchRoots(targets: string[], configDirs: string[], targetKinds?: ReadonlyMap<string, 'literal' | 'glob'>): string[];
|
|
18
|
+
export declare function startWatch({ files, roots, explicitTargets, sink, refresh, processFile, }: WatchOptions): {
|
|
8
19
|
exitCode: number;
|
|
9
|
-
watching:
|
|
20
|
+
watching: boolean;
|
|
21
|
+
close: () => void;
|
|
10
22
|
};
|
|
11
23
|
export {};
|