tailwind-canonical 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -14
- package/dist/cli/cli.d.ts +2 -1
- package/dist/cli/cli.js +208 -75
- 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 +11 -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 +3 -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 +93 -7
- 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 +11 -10
- 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/dist/cli/cli.js
CHANGED
|
@@ -1,24 +1,50 @@
|
|
|
1
|
-
import { watch as fsWatch, readFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, resolve } from 'node:path';
|
|
1
|
+
import { existsSync, watch as fsWatch, readFileSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { analyzeFile } from '../core/analyzer.js';
|
|
4
4
|
import { toClassStringOpts } from '../core/class-strings.js';
|
|
5
|
-
import { loadConfig } from '../core/config.js';
|
|
5
|
+
import { CONFIG_FILENAMES, loadConfig, resolveInitFilename, } from '../core/config.js';
|
|
6
6
|
import { analyzeConsistencyFiles, toConsistencyOptions, } from '../core/consistency.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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 {
|
|
11
|
+
import { sortContent } from '../core/sorter.js';
|
|
12
12
|
import { analyzeTyposFile } from '../core/typos.js';
|
|
13
13
|
const SARIF_SCHEMA = 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json';
|
|
14
|
-
const USAGE = 'Usage: tailwind-canonical [--fix] [--merge] [--dedup] [--sort] [--analyze] [--typos] [--watch] [--reporter json|sarif] <dir|file> [dir|file...]';
|
|
14
|
+
const USAGE = 'Usage: tailwind-canonical [--fix] [--merge] [--dedup] [--sort] [--check] [--analyze] [--typos] [--watch] [--reporter json|sarif] <dir|file> [dir|file...]\n tailwind-canonical init';
|
|
15
|
+
const INIT_CONFIG_BODY = [
|
|
16
|
+
' // Run `tailwind-canonical` with no flags using these defaults:',
|
|
17
|
+
" // defaultCommand: { fix: true, dedup: true, sort: true, typos: true, targets: ['./src'] },",
|
|
18
|
+
' // px → token additions/overrides:',
|
|
19
|
+
" // customTextTokens: { 11: '2xs' },",
|
|
20
|
+
" // customSpacingTokens: { 14: '3.5' },",
|
|
21
|
+
' // Never suggest replacements for classes matching:',
|
|
22
|
+
' // ignorePatterns: [/^font-/],',
|
|
23
|
+
];
|
|
24
|
+
const INIT_CONFIG_TS = [
|
|
25
|
+
"import type { Config } from 'tailwind-canonical'",
|
|
26
|
+
'',
|
|
27
|
+
'export default {',
|
|
28
|
+
...INIT_CONFIG_BODY,
|
|
29
|
+
'} satisfies Config',
|
|
30
|
+
'',
|
|
31
|
+
].join('\n');
|
|
32
|
+
const INIT_CONFIG_JS = [
|
|
33
|
+
"/** @type {import('tailwind-canonical').Config} */",
|
|
34
|
+
'export default {',
|
|
35
|
+
...INIT_CONFIG_BODY,
|
|
36
|
+
'}',
|
|
37
|
+
'',
|
|
38
|
+
].join('\n');
|
|
15
39
|
const HELP_TEXT = [
|
|
16
40
|
USAGE,
|
|
17
41
|
'',
|
|
42
|
+
' init Create a tailwind-canonical config scaffold in the current directory',
|
|
18
43
|
' --fix Auto-replace arbitrary values with canonical Tailwind classes',
|
|
19
44
|
' --dedup Remove redundant classes and collapse shorthands',
|
|
20
45
|
' --merge Resolve conflicting classes via tailwind-merge',
|
|
21
46
|
' --sort Sort classes into canonical order',
|
|
47
|
+
' --check Dry-run: report what transforms would change, write nothing (exit 1 on pending changes)',
|
|
22
48
|
' --analyze Cross-file consistency analysis (read-only)',
|
|
23
49
|
' --typos Flag likely misspelled color names (read-only)',
|
|
24
50
|
' --watch Re-run on every file save (transform/check mode; ignored with --typos)',
|
|
@@ -92,6 +118,7 @@ const KNOWN_FLAGS = new Set([
|
|
|
92
118
|
'--merge',
|
|
93
119
|
'--dedup',
|
|
94
120
|
'--sort',
|
|
121
|
+
'--check',
|
|
95
122
|
'--watch',
|
|
96
123
|
'--analyze',
|
|
97
124
|
'--typos',
|
|
@@ -156,6 +183,7 @@ export function parseArgs(argv) {
|
|
|
156
183
|
watch: argv.includes('--watch'),
|
|
157
184
|
analyze: argv.includes('--analyze'),
|
|
158
185
|
typos: argv.includes('--typos'),
|
|
186
|
+
check: argv.includes('--check'),
|
|
159
187
|
help: argv.includes('--help') || argv.includes('-h'),
|
|
160
188
|
version: argv.includes('--version') || argv.includes('-V'),
|
|
161
189
|
reporter,
|
|
@@ -164,34 +192,29 @@ export function parseArgs(argv) {
|
|
|
164
192
|
hasExplicitMode: MODE_FLAGS.some((flag) => argv.includes(flag)),
|
|
165
193
|
hasExplicitReporter: reporterRaw !== undefined,
|
|
166
194
|
hasExplicitWatch: argv.includes('--watch'),
|
|
195
|
+
hasExplicitCheck: argv.includes('--check'),
|
|
167
196
|
};
|
|
168
197
|
}
|
|
198
|
+
/** Explicit CLI flags win; otherwise `defaultCommand` fills the gap. */
|
|
199
|
+
function fallback(explicit, flag, configured) {
|
|
200
|
+
return explicit ? flag : (configured ?? flag);
|
|
201
|
+
}
|
|
169
202
|
function applyDefaultCommand(flags, config) {
|
|
170
203
|
const defaults = config.defaultCommand;
|
|
171
204
|
if (!defaults)
|
|
172
205
|
return flags;
|
|
206
|
+
const mode = flags.hasExplicitMode;
|
|
173
207
|
return {
|
|
174
208
|
...flags,
|
|
175
|
-
fix:
|
|
176
|
-
merge: flags.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
? flags.analyze
|
|
185
|
-
: (defaults.analyze ?? flags.analyze),
|
|
186
|
-
typos: flags.hasExplicitMode
|
|
187
|
-
? flags.typos
|
|
188
|
-
: (defaults.typos ?? flags.typos),
|
|
189
|
-
watch: flags.hasExplicitWatch
|
|
190
|
-
? flags.watch
|
|
191
|
-
: (defaults.watch ?? flags.watch),
|
|
192
|
-
reporter: flags.hasExplicitReporter
|
|
193
|
-
? flags.reporter
|
|
194
|
-
: (defaults.reporter ?? flags.reporter),
|
|
209
|
+
fix: fallback(mode, flags.fix, defaults.fix),
|
|
210
|
+
merge: fallback(mode, flags.merge, defaults.merge),
|
|
211
|
+
dedup: fallback(mode, flags.dedup, defaults.dedup),
|
|
212
|
+
sort: fallback(mode, flags.sort, defaults.sort),
|
|
213
|
+
analyze: fallback(mode, flags.analyze, defaults.analyze),
|
|
214
|
+
typos: fallback(mode, flags.typos, defaults.typos),
|
|
215
|
+
watch: fallback(flags.hasExplicitWatch, flags.watch, defaults.watch),
|
|
216
|
+
check: fallback(flags.hasExplicitCheck, flags.check, defaults.check),
|
|
217
|
+
reporter: fallback(flags.hasExplicitReporter, flags.reporter, defaults.reporter),
|
|
195
218
|
targets: flags.targets.length > 0 ? flags.targets : (defaults.targets ?? []),
|
|
196
219
|
};
|
|
197
220
|
}
|
|
@@ -222,12 +245,19 @@ export function flagWarnings(flags) {
|
|
|
222
245
|
}
|
|
223
246
|
if (flags.typos)
|
|
224
247
|
warnings.push('--typos ignored: --analyze takes priority');
|
|
248
|
+
if (flags.check)
|
|
249
|
+
warnings.push('--check ignored: --analyze takes priority');
|
|
225
250
|
if (flags.watch)
|
|
226
251
|
warnings.push('--watch ignored: not supported with --analyze');
|
|
227
252
|
return warnings;
|
|
228
253
|
}
|
|
229
|
-
if (flags.
|
|
230
|
-
|
|
254
|
+
if (flags.watch && (flags.typos || flags.check)) {
|
|
255
|
+
const unsupported = [];
|
|
256
|
+
if (flags.typos)
|
|
257
|
+
unsupported.push('--typos');
|
|
258
|
+
if (flags.check)
|
|
259
|
+
unsupported.push('--check');
|
|
260
|
+
warnings.push(`--watch ignored: not supported with ${unsupported.join('/')}`);
|
|
231
261
|
}
|
|
232
262
|
return warnings;
|
|
233
263
|
}
|
|
@@ -295,19 +325,79 @@ function runTypos(files, config, reporter, sink) {
|
|
|
295
325
|
logTyposText(findings, sink);
|
|
296
326
|
return { exitCode: findings.length > 0 || hadError ? 1 : 0 };
|
|
297
327
|
}
|
|
298
|
-
|
|
328
|
+
function applyTransforms(content, flags, config, twMerge) {
|
|
299
329
|
const opts = toClassStringOpts(config);
|
|
300
330
|
const counts = { fixed: 0, deduped: 0, merged: 0, sorted: 0 };
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
counts.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (flags.
|
|
308
|
-
|
|
331
|
+
let result = content;
|
|
332
|
+
if (flags.fix) {
|
|
333
|
+
const fixed = fixContent(result, config);
|
|
334
|
+
counts.fixed = fixed.count;
|
|
335
|
+
result = fixed.result;
|
|
336
|
+
}
|
|
337
|
+
if (flags.dedup) {
|
|
338
|
+
const deduped = dedupeContent(result, opts);
|
|
339
|
+
counts.deduped = deduped.count;
|
|
340
|
+
result = deduped.result;
|
|
341
|
+
}
|
|
342
|
+
if (flags.merge) {
|
|
343
|
+
if (!twMerge) {
|
|
344
|
+
throw new Error('--merge requires the optional peer dependency "tailwind-merge". Install it with: pnpm add -D tailwind-merge');
|
|
345
|
+
}
|
|
346
|
+
const merged = mergeContent(result, twMerge, opts);
|
|
347
|
+
counts.merged = merged.count;
|
|
348
|
+
result = merged.result;
|
|
349
|
+
}
|
|
350
|
+
if (flags.sort) {
|
|
351
|
+
const sorted = sortContent(result, opts, config.sortOrder);
|
|
352
|
+
counts.sorted = sorted.count;
|
|
353
|
+
result = sorted.result;
|
|
354
|
+
}
|
|
355
|
+
return { counts, result };
|
|
356
|
+
}
|
|
357
|
+
function processFile(file, flags, config, twMerge) {
|
|
358
|
+
const content = readFileSync(file, 'utf8');
|
|
359
|
+
const { counts, result } = applyTransforms(content, flags, config, twMerge);
|
|
360
|
+
// Counts, not text equality: replaceClassStrings also normalizes attribute
|
|
361
|
+
// spacing, which no transform counts. Writing on any textual difference
|
|
362
|
+
// would rewrite files that --check truthfully reports as clean.
|
|
363
|
+
if (totalOf(counts) > 0)
|
|
364
|
+
writeFileSync(file, result, 'utf8');
|
|
309
365
|
return counts;
|
|
310
366
|
}
|
|
367
|
+
const TRANSFORM_LABELS = [
|
|
368
|
+
{
|
|
369
|
+
key: 'fixed',
|
|
370
|
+
applied: 'fixed ',
|
|
371
|
+
pending: 'would fix',
|
|
372
|
+
unit: 'replacement',
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
key: 'deduped',
|
|
376
|
+
applied: 'deduped',
|
|
377
|
+
pending: 'would dedup',
|
|
378
|
+
unit: 'class string',
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
key: 'merged',
|
|
382
|
+
applied: 'merged ',
|
|
383
|
+
pending: 'would merge',
|
|
384
|
+
unit: 'conflict',
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
key: 'sorted',
|
|
388
|
+
applied: 'sorted ',
|
|
389
|
+
pending: 'would sort',
|
|
390
|
+
unit: 'class string',
|
|
391
|
+
},
|
|
392
|
+
];
|
|
393
|
+
function logTransformCounts(counts, file, check, sink) {
|
|
394
|
+
for (const { key, applied, pending, unit } of TRANSFORM_LABELS) {
|
|
395
|
+
const count = counts[key];
|
|
396
|
+
if (count > 0) {
|
|
397
|
+
sink.log(` ${check ? pending : applied} ${file} (${pluralize(count, unit)})`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
311
401
|
function totalOf(c) {
|
|
312
402
|
return c.fixed + c.deduped + c.merged + c.sorted;
|
|
313
403
|
}
|
|
@@ -522,10 +612,9 @@ function runAnalyze(files, config, reporter, sink) {
|
|
|
522
612
|
logAnalyzeText(report, issueCount, config, sink);
|
|
523
613
|
return { exitCode: issueCount > 0 || hadError ? 1 : 0 };
|
|
524
614
|
}
|
|
525
|
-
function startWatch(files, flags, config, sink) {
|
|
615
|
+
function startWatch(files, flags, config, twMerge, sink) {
|
|
526
616
|
const transforming = flags.fix || flags.dedup || flags.merge || flags.sort;
|
|
527
617
|
const fileSet = new Set(files);
|
|
528
|
-
const inFlight = new Set();
|
|
529
618
|
const timers = new Map();
|
|
530
619
|
const dirs = new Set(files.map((f) => dirname(f)));
|
|
531
620
|
sink.log(`Watching ${pluralize(files.length, 'file')}... (Ctrl+C to stop)`);
|
|
@@ -534,23 +623,20 @@ function startWatch(files, flags, config, sink) {
|
|
|
534
623
|
if (!filename)
|
|
535
624
|
return;
|
|
536
625
|
const full = resolve(dir, filename);
|
|
537
|
-
if (!fileSet.has(full)
|
|
626
|
+
if (!fileSet.has(full))
|
|
538
627
|
return;
|
|
539
628
|
clearTimeout(timers.get(full));
|
|
540
629
|
timers.set(full, setTimeout(() => {
|
|
541
630
|
if (transforming) {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
.then((counts) => {
|
|
545
|
-
const total = totalOf(counts);
|
|
631
|
+
try {
|
|
632
|
+
const total = totalOf(processFile(full, flags, config, twMerge));
|
|
546
633
|
if (total > 0) {
|
|
547
634
|
sink.log(`${timestamp()} ${full} — ${pluralize(total, 'change')} applied`);
|
|
548
635
|
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
sink.error(`${timestamp()} ${full} — error: ${
|
|
552
|
-
}
|
|
553
|
-
.finally(() => inFlight.delete(full));
|
|
636
|
+
}
|
|
637
|
+
catch (err) {
|
|
638
|
+
sink.error(`${timestamp()} ${full} — error: ${errMsg(err)}`);
|
|
639
|
+
}
|
|
554
640
|
}
|
|
555
641
|
else {
|
|
556
642
|
const findings = analyzeFile(full, config);
|
|
@@ -571,6 +657,20 @@ function startWatch(files, flags, config, sink) {
|
|
|
571
657
|
});
|
|
572
658
|
return { exitCode: 0, watching: true };
|
|
573
659
|
}
|
|
660
|
+
function runInit(cwd, sink) {
|
|
661
|
+
const existing = CONFIG_FILENAMES.find((name) => existsSync(join(cwd, name)));
|
|
662
|
+
if (existing) {
|
|
663
|
+
sink.error(`${existing} already exists`);
|
|
664
|
+
return { exitCode: 1 };
|
|
665
|
+
}
|
|
666
|
+
const filename = resolveInitFilename(cwd);
|
|
667
|
+
const content = /\.m?ts$/.test(filename) ? INIT_CONFIG_TS : INIT_CONFIG_JS;
|
|
668
|
+
writeFileSync(join(cwd, filename), content, 'utf8');
|
|
669
|
+
sink.log(`Created ${filename}`);
|
|
670
|
+
sink.log('Uncomment defaultCommand to set your CLI defaults.');
|
|
671
|
+
sink.log('Then run `npx tailwind-canonical` to lint your project.');
|
|
672
|
+
return { exitCode: 0 };
|
|
673
|
+
}
|
|
574
674
|
export async function run(argv, cwd, sink = defaultSink) {
|
|
575
675
|
let flags = parseArgs(argv);
|
|
576
676
|
if (flags.help) {
|
|
@@ -588,6 +688,21 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
588
688
|
return { exitCode: 1 };
|
|
589
689
|
}
|
|
590
690
|
}
|
|
691
|
+
if (flags.targets[0] === 'init') {
|
|
692
|
+
if (flags.error ||
|
|
693
|
+
flags.hasExplicitMode ||
|
|
694
|
+
flags.hasExplicitWatch ||
|
|
695
|
+
flags.hasExplicitCheck ||
|
|
696
|
+
flags.hasExplicitReporter) {
|
|
697
|
+
sink.error('init takes no flags');
|
|
698
|
+
return { exitCode: 1 };
|
|
699
|
+
}
|
|
700
|
+
if (flags.targets.length > 1) {
|
|
701
|
+
sink.error('init takes no extra arguments');
|
|
702
|
+
return { exitCode: 1 };
|
|
703
|
+
}
|
|
704
|
+
return runInit(cwd, sink);
|
|
705
|
+
}
|
|
591
706
|
if (flags.error) {
|
|
592
707
|
sink.error(flags.error);
|
|
593
708
|
return { exitCode: 1 };
|
|
@@ -605,9 +720,19 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
605
720
|
sink.error(USAGE);
|
|
606
721
|
return { exitCode: 1 };
|
|
607
722
|
}
|
|
608
|
-
if (flags.
|
|
723
|
+
if (flags.check &&
|
|
724
|
+
!flags.analyze &&
|
|
725
|
+
!flags.fix &&
|
|
726
|
+
!flags.dedup &&
|
|
727
|
+
!flags.merge &&
|
|
728
|
+
!flags.sort) {
|
|
729
|
+
sink.error('--check requires at least one of --fix, --dedup, --merge, --sort');
|
|
730
|
+
return { exitCode: 1 };
|
|
731
|
+
}
|
|
732
|
+
let twMerge;
|
|
733
|
+
if (flags.merge && !flags.analyze) {
|
|
609
734
|
try {
|
|
610
|
-
await import('tailwind-merge');
|
|
735
|
+
({ twMerge } = await import('tailwind-merge'));
|
|
611
736
|
}
|
|
612
737
|
catch {
|
|
613
738
|
sink.error('--merge requires tailwind-merge: pnpm add -D tailwind-merge');
|
|
@@ -629,7 +754,7 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
629
754
|
if (flags.typos && !transforming) {
|
|
630
755
|
return runTypos(files, config, flags.reporter, sink);
|
|
631
756
|
}
|
|
632
|
-
const watching = flags.watch && !flags.typos;
|
|
757
|
+
const watching = flags.watch && !flags.typos && !flags.check;
|
|
633
758
|
const totals = { fixed: 0, deduped: 0, merged: 0, sorted: 0 };
|
|
634
759
|
const allFindings = [];
|
|
635
760
|
const changedFiles = [];
|
|
@@ -638,7 +763,9 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
638
763
|
for (const file of files) {
|
|
639
764
|
try {
|
|
640
765
|
if (transforming) {
|
|
641
|
-
const counts =
|
|
766
|
+
const counts = flags.check
|
|
767
|
+
? applyTransforms(readFileSync(file, 'utf8'), flags, config, twMerge).counts
|
|
768
|
+
: processFile(file, flags, config, twMerge);
|
|
642
769
|
totals.fixed += counts.fixed;
|
|
643
770
|
totals.deduped += counts.deduped;
|
|
644
771
|
totals.merged += counts.merged;
|
|
@@ -646,14 +773,7 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
646
773
|
if (totalOf(counts) > 0)
|
|
647
774
|
changedFiles.push(file);
|
|
648
775
|
if (flags.reporter === 'text') {
|
|
649
|
-
|
|
650
|
-
sink.log(` fixed ${file} (${pluralize(counts.fixed, 'replacement')})`);
|
|
651
|
-
if (counts.deduped > 0)
|
|
652
|
-
sink.log(` deduped ${file} (${pluralize(counts.deduped, 'class string')})`);
|
|
653
|
-
if (counts.merged > 0)
|
|
654
|
-
sink.log(` merged ${file} (${pluralize(counts.merged, 'conflict')})`);
|
|
655
|
-
if (counts.sorted > 0)
|
|
656
|
-
sink.log(` sorted ${file} (${pluralize(counts.sorted, 'class string')})`);
|
|
776
|
+
logTransformCounts(counts, file, flags.check, sink);
|
|
657
777
|
}
|
|
658
778
|
}
|
|
659
779
|
else {
|
|
@@ -674,12 +794,14 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
674
794
|
}
|
|
675
795
|
}
|
|
676
796
|
if (watching)
|
|
677
|
-
return startWatch(files, flags, config, sink);
|
|
797
|
+
return startWatch(files, flags, config, twMerge, sink);
|
|
678
798
|
if (transforming) {
|
|
679
799
|
const typoResult = flags.typos
|
|
680
800
|
? collectTypos(files, config, sink)
|
|
681
801
|
: undefined;
|
|
802
|
+
const totalChanges = totalOf(totals);
|
|
682
803
|
const exitCode = hadError ||
|
|
804
|
+
(flags.check && totalChanges > 0) ||
|
|
683
805
|
(typoResult && (typoResult.findings.length > 0 || typoResult.hadError))
|
|
684
806
|
? 1
|
|
685
807
|
: 0;
|
|
@@ -692,6 +814,8 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
692
814
|
merged: totals.merged,
|
|
693
815
|
sorted: totals.sorted,
|
|
694
816
|
};
|
|
817
|
+
if (flags.check)
|
|
818
|
+
report.check = true;
|
|
695
819
|
if (typoResult) {
|
|
696
820
|
report.typoTotal = typoResult.findings.length;
|
|
697
821
|
report.typos = typoResult.findings;
|
|
@@ -706,16 +830,26 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
706
830
|
sink.write(`${JSON.stringify(doc, null, 2)}\n`);
|
|
707
831
|
return { exitCode };
|
|
708
832
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
833
|
+
if (flags.check) {
|
|
834
|
+
if (totalChanges > 0) {
|
|
835
|
+
sink.log(`\n✖ ${pluralize(totalChanges, 'pending change')} across ${pluralize(changedFiles.length, 'file')} (run without --check to apply)`);
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
sink.log('✓ No pending changes');
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
const parts = [];
|
|
843
|
+
if (flags.fix)
|
|
844
|
+
parts.push(pluralize(totals.fixed, 'replacement'));
|
|
845
|
+
if (flags.dedup)
|
|
846
|
+
parts.push(pluralize(totals.deduped, 'dedup'));
|
|
847
|
+
if (flags.merge)
|
|
848
|
+
parts.push(`${pluralize(totals.merged, 'conflict')} merged`);
|
|
849
|
+
if (flags.sort)
|
|
850
|
+
parts.push(`${totals.sorted} sorted`);
|
|
851
|
+
sink.log(`\n✓ Fixed ${parts.join(', ')} across ${pluralize(files.length, 'file')}`);
|
|
852
|
+
}
|
|
719
853
|
if (typoResult)
|
|
720
854
|
logTyposText(typoResult.findings, sink);
|
|
721
855
|
return { exitCode };
|
|
@@ -771,4 +905,3 @@ export async function run(argv, cwd, sink = defaultSink) {
|
|
|
771
905
|
sink.log('✓ No non-canonical classes found');
|
|
772
906
|
return { exitCode: hadError ? 1 : 0 };
|
|
773
907
|
}
|
|
774
|
-
//# sourceMappingURL=cli.js.map
|
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',
|
|
@@ -35,6 +38,7 @@ const DEFAULT_COMMAND_KEYS = [
|
|
|
35
38
|
'merge',
|
|
36
39
|
'dedup',
|
|
37
40
|
'sort',
|
|
41
|
+
'check',
|
|
38
42
|
'analyze',
|
|
39
43
|
'typos',
|
|
40
44
|
'watch',
|
|
@@ -116,6 +120,7 @@ function assertDefaultCommandConfig(value, filename) {
|
|
|
116
120
|
'merge',
|
|
117
121
|
'dedup',
|
|
118
122
|
'sort',
|
|
123
|
+
'check',
|
|
119
124
|
'analyze',
|
|
120
125
|
'typos',
|
|
121
126
|
'watch',
|
|
@@ -181,7 +186,11 @@ export function validateConfig(input, filename = CONFIG_FILENAME) {
|
|
|
181
186
|
assertPositiveInteger(cfg.rareScaleMaxCount, 'rareScaleMaxCount', filename);
|
|
182
187
|
if ('defaultCommand' in cfg)
|
|
183
188
|
assertDefaultCommandConfig(cfg.defaultCommand, filename);
|
|
189
|
+
if ('tailwindVersion' in cfg &&
|
|
190
|
+
cfg.tailwindVersion !== 3 &&
|
|
191
|
+
cfg.tailwindVersion !== 4) {
|
|
192
|
+
fail(filename, 'tailwindVersion must be 3 or 4');
|
|
193
|
+
}
|
|
184
194
|
return cfg;
|
|
185
195
|
}
|
|
186
|
-
export { loadConfig } from '../io/config.js';
|
|
187
|
-
//# sourceMappingURL=config.js.map
|
|
196
|
+
export { loadConfig, resolveInitFilename } from '../io/config.js';
|
|
@@ -60,4 +60,3 @@ export declare function analyzeConsistency(input: FileClasses[], options?: Consi
|
|
|
60
60
|
export declare function collectClasses(content: string, opts?: ClassStringOpts): string[];
|
|
61
61
|
export declare function toConsistencyOptions(config?: Config): ConsistencyOptions;
|
|
62
62
|
export { analyzeConsistencyFiles } from '../io/consistency.js';
|
|
63
|
-
//# sourceMappingURL=consistency.d.ts.map
|
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