tailwind-canonical 0.9.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 +80 -33
- package/dist/cli/cli.js +33 -5
- package/dist/cli/watch.d.ts +4 -2
- package/dist/cli/watch.js +15 -6
- package/dist/core/class-strings.d.ts +2 -0
- package/dist/core/class-strings.js +158 -130
- package/dist/core/config.js +11 -3
- package/dist/core/consistency.js +4 -4
- package/dist/core/deduplicator.d.ts +1 -1
- package/dist/core/deduplicator.js +32 -8
- package/dist/core/lexicon.d.ts +7 -1
- package/dist/core/lexicon.js +43 -6
- package/dist/core/merger.js +7 -5
- package/dist/core/rules.d.ts +2 -0
- package/dist/core/rules.js +5 -1
- package/dist/core/scanner.d.ts +4 -0
- package/dist/core/typos.js +4 -5
- package/dist/eslint/plugin.js +9 -2
- package/dist/io/config.js +3 -0
- package/dist/io/scanner.js +48 -17
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -94,7 +94,7 @@ 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
98
|
Directory and glob discovery skip symbolic links. An explicitly named file symlink remains supported and reads or changes its target.
|
|
99
99
|
|
|
100
100
|
### Watch behavior
|
|
@@ -111,7 +111,7 @@ with a nonzero exit code. Watch output is text only.
|
|
|
111
111
|
|
|
112
112
|
| Flag | What it fixes | Example |
|
|
113
113
|
|---|---|---|
|
|
114
|
-
| `--fix` | Arbitrary values → canonical tokens | `
|
|
114
|
+
| `--fix` | Arbitrary values → canonical tokens | `p-[16px]` → `p-4` |
|
|
115
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` |
|
|
116
116
|
| `--sort` | Canonical class order | `text-sm flex p-4` → `flex p-4 text-sm` |
|
|
117
117
|
| `--merge` | tailwind-merge conflict resolution | `bg-red-500 bg-blue-500` → `bg-blue-500` |
|
|
@@ -131,9 +131,9 @@ npx tailwind-canonical --reporter json ./src
|
|
|
131
131
|
```json
|
|
132
132
|
{
|
|
133
133
|
"files": 3,
|
|
134
|
-
"total":
|
|
134
|
+
"total": 1,
|
|
135
135
|
"findings": [
|
|
136
|
-
{ "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 }
|
|
137
137
|
]
|
|
138
138
|
}
|
|
139
139
|
```
|
|
@@ -214,11 +214,11 @@ Skip specific lines or blocks from check and all transforms (`--fix`, `--dedup`,
|
|
|
214
214
|
|
|
215
215
|
```jsx
|
|
216
216
|
{/* tailwind-canonical-disable-next-line */}
|
|
217
|
-
<div className="
|
|
217
|
+
<div className="p-[16px]" /> {/* left untouched */}
|
|
218
218
|
|
|
219
219
|
{/* tailwind-canonical-disable */}
|
|
220
|
-
<div className="
|
|
221
|
-
<div className="m-[
|
|
220
|
+
<div className="p-[16px] h-[24px]" /> {/* whole block untouched */}
|
|
221
|
+
<div className="m-[8px]" />
|
|
222
222
|
{/* tailwind-canonical-enable */}
|
|
223
223
|
```
|
|
224
224
|
|
|
@@ -335,13 +335,17 @@ flex block → block (last wins)
|
|
|
335
335
|
relative absolute → absolute
|
|
336
336
|
```
|
|
337
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
|
+
|
|
338
342
|
### Padding / margin shorthand collapse
|
|
339
343
|
|
|
340
344
|
```
|
|
341
345
|
px-4 py-4 → p-4
|
|
342
346
|
p-4 px-2 → py-4 px-2
|
|
343
|
-
pt-2 pb-2 → py-2
|
|
344
|
-
pl-4 pr-4 → px-4
|
|
347
|
+
pt-2 pb-2 → py-2 (v3 only)
|
|
348
|
+
pl-4 pr-4 → px-4 (v3 only)
|
|
345
349
|
mx-4 my-4 → m-4
|
|
346
350
|
```
|
|
347
351
|
|
|
@@ -349,24 +353,31 @@ mx-4 my-4 → m-4
|
|
|
349
353
|
|
|
350
354
|
```
|
|
351
355
|
border-t-2 border-b-2 border-l-2 border-r-2 → border-2
|
|
352
|
-
border-t-2 border-b-2 → border-y-2
|
|
353
|
-
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)
|
|
354
358
|
```
|
|
355
359
|
|
|
356
360
|
### Inset shorthand collapse
|
|
357
361
|
|
|
358
362
|
```
|
|
359
363
|
top-4 right-4 bottom-4 left-4 → inset-4
|
|
360
|
-
top-4 bottom-4 → inset-y-4
|
|
361
|
-
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)
|
|
362
366
|
```
|
|
363
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
|
+
|
|
364
375
|
## Arbitrary value detection (`--fix`)
|
|
365
376
|
|
|
366
377
|
| Arbitrary | Canonical | Notes |
|
|
367
378
|
|---|---|---|
|
|
368
|
-
| `text-[12px]` | `text-xs` |
|
|
369
|
-
| `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 |
|
|
370
381
|
| `h-[64px]` | `h-16` | Spacing scale ÷4 |
|
|
371
382
|
| `p-[1px]` / `gap-[1px]` | `p-px` / `gap-px` | 1px spacing step |
|
|
372
383
|
| `w-[50%]` | `w-1/2` | Percentage fractions |
|
|
@@ -393,16 +404,19 @@ left-2 right-2 → inset-x-2
|
|
|
393
404
|
| `order-[1]` / `order-[0]` | `order-1` / `order-none` | Any integer on v4 |
|
|
394
405
|
| `grow-[1]` / `shrink-[0]` | `grow` / `shrink-0` | |
|
|
395
406
|
| `line-clamp-[3]`, `columns-[3]` | `line-clamp-3`, `columns-3` | Any integer ≥ 1 on v4 |
|
|
396
|
-
| `text-[11px]` | `text-2xs` | Requires `customTextTokens` |
|
|
397
|
-
| `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 |
|
|
398
409
|
| `hover:p-[16px]` | `hover:p-4` | Variants are preserved |
|
|
399
410
|
| `!p-[16px]` / `p-[16px]!` | `!p-4` / `p-4!` | Important modifier kept in place |
|
|
400
411
|
| `-mt-[8px]` / `mt-[-8px]` | `-mt-2` | Negative margin / inset / translate / space / scale / rotate / z / order only |
|
|
401
412
|
|
|
402
|
-
`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:
|
|
403
414
|
|
|
404
415
|
```ts
|
|
405
|
-
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
|
+
}
|
|
406
420
|
```
|
|
407
421
|
|
|
408
422
|
Non-divisible values (`h-[22px]`, `px-[7px]`) are left untouched.
|
|
@@ -488,11 +502,40 @@ export default {
|
|
|
488
502
|
}
|
|
489
503
|
```
|
|
490
504
|
|
|
491
|
-
Only explicitly configured pixel mappings are
|
|
492
|
-
`
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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.
|
|
496
539
|
|
|
497
540
|
## Config
|
|
498
541
|
|
|
@@ -500,10 +543,9 @@ Create `tailwind-canonical.config.ts` at the root:
|
|
|
500
543
|
|
|
501
544
|
```ts
|
|
502
545
|
export default {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
},
|
|
546
|
+
// Text mappings are inactive unless the line-height change is intentional:
|
|
547
|
+
// allowTextSizeLineHeightChanges: true,
|
|
548
|
+
// customTextTokens: { 10: '3xs', 11: '2xs' },
|
|
507
549
|
customSpacingTokens: {
|
|
508
550
|
14: '3.5',
|
|
509
551
|
},
|
|
@@ -537,7 +579,7 @@ export default {
|
|
|
537
579
|
}
|
|
538
580
|
```
|
|
539
581
|
|
|
540
|
-
`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.
|
|
541
583
|
|
|
542
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.
|
|
543
585
|
|
|
@@ -612,13 +654,14 @@ ESLint without schema errors:
|
|
|
612
654
|
```js
|
|
613
655
|
'tailwind-canonical/no-arbitrary-canonical': [
|
|
614
656
|
'warn',
|
|
615
|
-
{
|
|
657
|
+
{ customSpacingTokens: { 14: '3.5' }, ignorePatterns: [/^font-/] },
|
|
616
658
|
],
|
|
617
659
|
```
|
|
618
660
|
|
|
619
661
|
The rule **honors**: `customTextTokens`, `customSpacingTokens`, `ignorePatterns`
|
|
620
662
|
and `tailwindVersion` (all applied via `suggestCanonical`), plus
|
|
621
|
-
`useDefaultTheme` to disable inferred mappings
|
|
663
|
+
`useDefaultTheme` to disable inferred mappings and
|
|
664
|
+
`allowTextSizeLineHeightChanges` for the explicit typography opt-in. It also uses
|
|
622
665
|
`attributeNames` (default: `['className']`) and `functionNames` (default:
|
|
623
666
|
`['cn', 'clsx', 'cva']`) to identify class strings. Other `Config` fields are
|
|
624
667
|
accepted for shared configuration and have no effect on this rule.
|
|
@@ -630,13 +673,13 @@ from an interpolation by whitespace are transformed; partial tokens such as
|
|
|
630
673
|
`text-[12px]${suffix}` are preserved. `no-conflicting-classes` accepts the same
|
|
631
674
|
shared configuration and uses its attribute/function names and custom text tokens. Enabling this rule without
|
|
632
675
|
`tailwind-merge` installed raises an installation error. In `cn`/`clsx` object arguments,
|
|
633
|
-
only keys contain classes; predicate values are preserved. For `cva`, base
|
|
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
|
|
634
677
|
classes, variant classes and compound variant `class`/`className` values are checked.
|
|
635
678
|
|
|
636
679
|
## Known limits
|
|
637
680
|
|
|
638
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.
|
|
639
|
-
- **CLI and content APIs extract static class positions** — strings, arrays, conditional result branches, `cn`/`clsx` object keys, and `cva` variant classes are supported. Predicates, variant identifiers, `defaultVariants
|
|
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.
|
|
640
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.
|
|
641
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.
|
|
642
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`.
|
|
@@ -684,9 +727,13 @@ are exercised rather than relying only on source tests.
|
|
|
684
727
|
|
|
685
728
|
```bash
|
|
686
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
|
|
687
732
|
pnpm benchmark
|
|
688
733
|
# Tune the fixed JSX/Vue/Svelte corpus and emit machine-readable measurements:
|
|
689
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
|
|
690
737
|
```
|
|
691
738
|
|
|
692
739
|
The benchmark checks expected findings and transform idempotence, then measures
|
package/dist/cli/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { analyzeConsistencyFiles, toConsistencyOptions, } from '../core/consiste
|
|
|
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';
|
|
@@ -18,6 +18,8 @@ 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:',
|
|
@@ -471,7 +473,27 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
471
473
|
sink.error(`Warning: ${warning}`);
|
|
472
474
|
}
|
|
473
475
|
const targetPaths = flags.targets.map((target) => resolveTarget(cwd, target));
|
|
474
|
-
const
|
|
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
|
+
}
|
|
475
497
|
if (files.length === 0 &&
|
|
476
498
|
!(flags.watch && !flags.analyze && !flags.typos && !flags.check)) {
|
|
477
499
|
sink.error(`No files matched: ${flags.targets.join(' ')}`);
|
|
@@ -490,9 +512,12 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
490
512
|
const path = findConfigPath(cwd);
|
|
491
513
|
return path ? dirname(path) : cwd;
|
|
492
514
|
};
|
|
515
|
+
const explicitTargets = (paths) => paths.filter((path) => !path.startsWith('!') &&
|
|
516
|
+
targetKinds.get(path.replace(/\\/g, '/')) === 'literal');
|
|
493
517
|
return startWatch({
|
|
494
518
|
files,
|
|
495
|
-
|
|
519
|
+
explicitTargets: explicitTargets(targetPaths),
|
|
520
|
+
roots: watchRoots(targetPaths, [cwd, configDirectory()], targetKinds),
|
|
496
521
|
sink,
|
|
497
522
|
refresh: async (changed) => {
|
|
498
523
|
const nextConfig = await loadConfig(cwd, { reload: true });
|
|
@@ -507,8 +532,10 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
507
532
|
if (nextFlags.merge && !nextMerge)
|
|
508
533
|
({ twMerge: nextMerge } = await import('tailwind-merge'));
|
|
509
534
|
const paths = nextFlags.targets.map((target) => resolveTarget(cwd, target));
|
|
510
|
-
const nextFiles = await resolveTargets(paths
|
|
511
|
-
|
|
535
|
+
const nextFiles = await resolveTargets(paths, {
|
|
536
|
+
targetKinds: rememberTargetKinds(paths),
|
|
537
|
+
});
|
|
538
|
+
const nextRoots = watchRoots(paths, [cwd, configDirectory()], targetKinds);
|
|
512
539
|
const configChanged = JSON.stringify(config, (_key, value) => value instanceof RegExp ? value.toString() : value) !==
|
|
513
540
|
JSON.stringify(nextConfig, (_key, value) => value instanceof RegExp ? value.toString() : value);
|
|
514
541
|
config = nextConfig;
|
|
@@ -517,6 +544,7 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
517
544
|
return {
|
|
518
545
|
files: nextFiles,
|
|
519
546
|
roots: nextRoots,
|
|
547
|
+
explicitTargets: explicitTargets(paths),
|
|
520
548
|
reprocessAll: configChanged ||
|
|
521
549
|
[...changed].some((file) => CONFIG_FILENAMES.some((name) => file.endsWith(`/${name}`) || file.endsWith(`\\${name}`))),
|
|
522
550
|
};
|
package/dist/cli/watch.d.ts
CHANGED
|
@@ -2,18 +2,20 @@ import type { Sink } from './types.js';
|
|
|
2
2
|
type WatchRefresh = {
|
|
3
3
|
files: string[];
|
|
4
4
|
roots: string[];
|
|
5
|
+
explicitTargets?: string[];
|
|
5
6
|
reprocessAll?: boolean;
|
|
6
7
|
};
|
|
7
8
|
type WatchOptions = {
|
|
8
9
|
files: string[];
|
|
9
10
|
roots: string[];
|
|
11
|
+
explicitTargets?: string[];
|
|
10
12
|
sink: Sink;
|
|
11
13
|
refresh: (changed: ReadonlySet<string>) => Promise<WatchRefresh>;
|
|
12
14
|
processFile: (file: string) => void;
|
|
13
15
|
};
|
|
14
16
|
/** Watch the existing ancestor of each target, including currently empty globs. */
|
|
15
|
-
export declare function watchRoots(targets: string[], configDirs: string[]): string[];
|
|
16
|
-
export declare function startWatch({ files, roots, sink, refresh, processFile, }: WatchOptions): {
|
|
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): {
|
|
17
19
|
exitCode: number;
|
|
18
20
|
watching: boolean;
|
|
19
21
|
close: () => void;
|
package/dist/cli/watch.js
CHANGED
|
@@ -8,12 +8,14 @@ function contains(root, path) {
|
|
|
8
8
|
(!isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`)));
|
|
9
9
|
}
|
|
10
10
|
/** Watch the existing ancestor of each target, including currently empty globs. */
|
|
11
|
-
export function watchRoots(targets, configDirs) {
|
|
11
|
+
export function watchRoots(targets, configDirs, targetKinds) {
|
|
12
12
|
const dirs = [...configDirs];
|
|
13
13
|
for (const target of targets) {
|
|
14
14
|
if (target.startsWith('!'))
|
|
15
15
|
continue;
|
|
16
|
-
const
|
|
16
|
+
const kind = targetKinds?.get(target.replace(/\\/g, '/'));
|
|
17
|
+
const literal = kind ? kind === 'literal' : existsSync(target);
|
|
18
|
+
const magic = literal ? -1 : target.search(/[*?{[(]/);
|
|
17
19
|
const prefix = target.slice(0, magic);
|
|
18
20
|
let dir = magic < 0 ? target : prefix.endsWith(sep) ? prefix : dirname(prefix);
|
|
19
21
|
if (existsSync(dir) && !statSync(dir).isDirectory())
|
|
@@ -29,7 +31,7 @@ export function watchRoots(targets, configDirs) {
|
|
|
29
31
|
const roots = [...new Set(dirs.map((dir) => resolve(dir)))];
|
|
30
32
|
return roots.filter((dir) => !roots.some((other) => other !== dir && contains(other, dir)));
|
|
31
33
|
}
|
|
32
|
-
export function startWatch({ files, roots, sink, refresh, processFile, }) {
|
|
34
|
+
export function startWatch({ files, roots, explicitTargets = [], sink, refresh, processFile, }) {
|
|
33
35
|
let fileSet = new Set(files);
|
|
34
36
|
const watchers = new Map();
|
|
35
37
|
const changed = new Set();
|
|
@@ -72,12 +74,18 @@ export function startWatch({ files, roots, sink, refresh, processFile, }) {
|
|
|
72
74
|
const watcher = fsWatch(root, { recursive: true }, (_, filename) => {
|
|
73
75
|
if (closed)
|
|
74
76
|
return;
|
|
75
|
-
|
|
77
|
+
const path = filename ? resolve(root, String(filename)) : root;
|
|
78
|
+
const ignored = filename &&
|
|
76
79
|
String(filename)
|
|
77
80
|
.split(/[\\/]/)
|
|
78
|
-
.some((part) => DEFAULT_IGNORE.includes(part))
|
|
81
|
+
.some((part) => DEFAULT_IGNORE.includes(part));
|
|
82
|
+
const explicitlyIncluded = explicitTargets.some((target) => contains(target, path) &&
|
|
83
|
+
!relative(target, path)
|
|
84
|
+
.split(/[\\/]/)
|
|
85
|
+
.some((part) => DEFAULT_IGNORE.includes(part)));
|
|
86
|
+
if (ignored && !explicitlyIncluded)
|
|
79
87
|
return;
|
|
80
|
-
changed.add(
|
|
88
|
+
changed.add(path);
|
|
81
89
|
schedule();
|
|
82
90
|
});
|
|
83
91
|
watchers.set(root, watcher);
|
|
@@ -113,6 +121,7 @@ export function startWatch({ files, roots, sink, refresh, processFile, }) {
|
|
|
113
121
|
const next = await refresh(batch);
|
|
114
122
|
if (closed)
|
|
115
123
|
return;
|
|
124
|
+
explicitTargets = next.explicitTargets ?? [];
|
|
116
125
|
synchronize(next.roots);
|
|
117
126
|
if (closed)
|
|
118
127
|
return;
|
|
@@ -2,6 +2,7 @@ export type ClassStringOpts = {
|
|
|
2
2
|
functionNames?: string[];
|
|
3
3
|
attributeNames?: string[];
|
|
4
4
|
customTextTokens?: Record<number, string>;
|
|
5
|
+
tailwindVersion?: 3 | 4;
|
|
5
6
|
isSuppressed?: (line: number) => boolean;
|
|
6
7
|
};
|
|
7
8
|
/** Builds the shared {functionNames, attributeNames} extraction options from a config. */
|
|
@@ -9,6 +10,7 @@ export declare function toClassStringOpts(config: {
|
|
|
9
10
|
functionNames?: string[];
|
|
10
11
|
attributeNames?: string[];
|
|
11
12
|
customTextTokens?: Record<number, string>;
|
|
13
|
+
tailwindVersion?: 3 | 4;
|
|
12
14
|
}): ClassStringOpts;
|
|
13
15
|
export declare const SINGLE_CLASS_REGEX: RegExp;
|
|
14
16
|
export declare function replaceClassStrings(content: string, transform: (s: string) => string, opts?: ClassStringOpts): {
|