|
1 | | -import type { LegacyTailwindcssPatcherOptions } from './options/legacy' |
2 | | -import type { |
3 | | - TailwindcssPatchOptions, |
4 | | - TailwindTokenByFileMap, |
5 | | - TailwindTokenLocation, |
6 | | -} from './types' |
7 | | -import process from 'node:process' |
8 | | - |
9 | | -import { CONFIG_NAME, getConfig, initConfig } from '@tailwindcss-mangle/config' |
10 | | -import { defu } from '@tailwindcss-mangle/shared' |
11 | | -import cac from 'cac' |
12 | | -import fs from 'fs-extra' |
13 | | -import path from 'pathe' |
14 | | - |
15 | | -import { TailwindcssPatcher } from './api/tailwindcss-patcher' |
16 | | -import { groupTokensByFile } from './extraction/candidate-extractor' |
17 | | -import logger from './logger' |
18 | | -import { fromLegacyOptions, fromUnifiedConfig } from './options/legacy' |
19 | | - |
20 | | -const cli = cac('tw-patch') |
21 | | - |
22 | | -async function loadPatchOptions(cwd: string, overrides?: TailwindcssPatchOptions) { |
23 | | - const { config } = await getConfig(cwd) |
24 | | - const legacyConfig = config as (typeof config) & { patch?: LegacyTailwindcssPatcherOptions['patch'] } |
25 | | - const base = config?.registry |
26 | | - ? fromUnifiedConfig(config.registry) |
27 | | - : legacyConfig?.patch |
28 | | - ? fromLegacyOptions({ patch: legacyConfig.patch }) |
29 | | - : {} |
30 | | - const merged = defu<TailwindcssPatchOptions, TailwindcssPatchOptions[]>(overrides ?? {}, base) |
31 | | - return merged |
32 | | -} |
33 | | - |
34 | | -cli |
35 | | - .command('install', 'Apply Tailwind CSS runtime patches') |
36 | | - .option('--cwd <dir>', 'Working directory', { default: process.cwd() }) |
37 | | - .action(async (args: { cwd: string }) => { |
38 | | - const options = await loadPatchOptions(args.cwd) |
39 | | - const patcher = new TailwindcssPatcher(options) |
40 | | - await patcher.patch() |
41 | | - logger.success('Tailwind CSS runtime patched successfully.') |
42 | | - }) |
43 | | - |
44 | | -cli |
45 | | - .command('extract', 'Collect generated class names into a cache file') |
46 | | - .option('--cwd <dir>', 'Working directory', { default: process.cwd() }) |
47 | | - .option('--output <file>', 'Override output file path') |
48 | | - .option('--format <format>', 'Output format (json|lines)') |
49 | | - .option('--css <file>', 'Tailwind CSS entry CSS when using v4') |
50 | | - .option('--no-write', 'Skip writing to disk') |
51 | | - .action(async (args: { cwd: string, output?: string, format?: 'json' | 'lines', css?: string, write?: boolean }) => { |
52 | | - const overrides: TailwindcssPatchOptions = {} |
53 | | - |
54 | | - if (args.output || args.format) { |
55 | | - overrides.output = { |
56 | | - file: args.output, |
57 | | - format: args.format, |
58 | | - } |
59 | | - } |
60 | | - |
61 | | - if (args.css) { |
62 | | - overrides.tailwind = { |
63 | | - v4: { |
64 | | - cssEntries: [args.css], |
65 | | - }, |
66 | | - } |
67 | | - } |
68 | | - |
69 | | - const options = await loadPatchOptions(args.cwd, overrides) |
70 | | - const patcher = new TailwindcssPatcher(options) |
71 | | - const result = await patcher.extract({ write: args.write }) |
72 | | - |
73 | | - if (result.filename) { |
74 | | - logger.success(`Collected ${result.classList.length} classes → ${result.filename}`) |
75 | | - } |
76 | | - else { |
77 | | - logger.success(`Collected ${result.classList.length} classes.`) |
78 | | - } |
79 | | - }) |
80 | | - |
81 | | -type TokenOutputFormat = 'json' | 'lines' | 'grouped-json' |
82 | | -type TokenGroupKey = 'relative' | 'absolute' |
83 | | -const TOKEN_FORMATS: TokenOutputFormat[] = ['json', 'lines', 'grouped-json'] |
84 | | - |
85 | | -cli |
86 | | - .command('tokens', 'Extract Tailwind tokens with file/position metadata') |
87 | | - .option('--cwd <dir>', 'Working directory', { default: process.cwd() }) |
88 | | - .option('--output <file>', 'Override output file path', { default: '.tw-patch/tw-token-report.json' }) |
89 | | - .option('--format <format>', 'Output format (json|lines|grouped-json)', { default: 'json' }) |
90 | | - .option('--group-key <key>', 'Grouping key for grouped-json output (relative|absolute)', { default: 'relative' }) |
91 | | - .option('--no-write', 'Skip writing to disk') |
92 | | - .action(async (args: { |
93 | | - cwd: string |
94 | | - output?: string |
95 | | - format?: TokenOutputFormat |
96 | | - groupKey?: TokenGroupKey |
97 | | - write?: boolean |
98 | | - }) => { |
99 | | - const options = await loadPatchOptions(args.cwd) |
100 | | - const patcher = new TailwindcssPatcher(options) |
101 | | - const report = await patcher.collectContentTokens() |
102 | | - |
103 | | - const shouldWrite = args.write ?? true |
104 | | - let format: TokenOutputFormat = args.format ?? 'json' |
105 | | - if (!TOKEN_FORMATS.includes(format)) { |
106 | | - format = 'json' |
107 | | - } |
108 | | - const targetFile = args.output ?? '.tw-patch/tw-token-report.json' |
109 | | - const groupKey: TokenGroupKey = args.groupKey === 'absolute' ? 'absolute' : 'relative' |
110 | | - const buildGrouped = () => |
111 | | - groupTokensByFile(report, { |
112 | | - key: groupKey, |
113 | | - stripAbsolutePaths: groupKey !== 'absolute', |
114 | | - }) |
115 | | - const grouped = format === 'grouped-json' ? buildGrouped() : null |
116 | | - const resolveGrouped = () => grouped ?? buildGrouped() |
117 | | - |
118 | | - if (shouldWrite) { |
119 | | - const target = path.resolve(targetFile) |
120 | | - await fs.ensureDir(path.dirname(target)) |
121 | | - if (format === 'json') { |
122 | | - await fs.writeJSON(target, report, { spaces: 2 }) |
123 | | - } |
124 | | - else if (format === 'grouped-json') { |
125 | | - await fs.writeJSON(target, resolveGrouped(), { spaces: 2 }) |
126 | | - } |
127 | | - else { |
128 | | - const lines = report.entries.map(formatTokenLine) |
129 | | - await fs.writeFile(target, `${lines.join('\n')}\n`, 'utf8') |
130 | | - } |
131 | | - logger.success( |
132 | | - `Collected ${report.entries.length} tokens (${format}) → ${target.replace(process.cwd(), '.')}`, |
133 | | - ) |
134 | | - } |
135 | | - else { |
136 | | - logger.success(`Collected ${report.entries.length} tokens from ${report.filesScanned} files.`) |
137 | | - if (format === 'lines') { |
138 | | - const preview = report.entries.slice(0, 5).map(formatTokenLine).join('\n') |
139 | | - if (preview) { |
140 | | - logger.log('') |
141 | | - logger.info(preview) |
142 | | - if (report.entries.length > 5) { |
143 | | - logger.info(`…and ${report.entries.length - 5} more.`) |
144 | | - } |
145 | | - } |
146 | | - } |
147 | | - else if (format === 'grouped-json') { |
148 | | - const map = resolveGrouped() |
149 | | - const { preview, moreFiles } = formatGroupedPreview(map) |
150 | | - if (preview) { |
151 | | - logger.log('') |
152 | | - logger.info(preview) |
153 | | - if (moreFiles > 0) { |
154 | | - logger.info(`…and ${moreFiles} more files.`) |
155 | | - } |
156 | | - } |
157 | | - } |
158 | | - else { |
159 | | - const previewEntries = report.entries.slice(0, 3) |
160 | | - if (previewEntries.length) { |
161 | | - logger.log('') |
162 | | - logger.info(JSON.stringify(previewEntries, null, 2)) |
163 | | - } |
164 | | - } |
165 | | - } |
166 | | - |
167 | | - if (report.skippedFiles.length) { |
168 | | - logger.warn('Skipped files:') |
169 | | - for (const skipped of report.skippedFiles) { |
170 | | - logger.warn(` • ${skipped.file} (${skipped.reason})`) |
171 | | - } |
172 | | - } |
173 | | - }) |
174 | | - |
175 | | -cli |
176 | | - .command('init', 'Generate a tailwindcss-patch config file') |
177 | | - .option('--cwd <dir>', 'Working directory', { default: process.cwd() }) |
178 | | - .action(async (args: { cwd: string }) => { |
179 | | - await initConfig(args.cwd) |
180 | | - logger.success(`✨ ${CONFIG_NAME}.config.ts initialized!`) |
181 | | - }) |
| 1 | +import { createTailwindcssPatchCli } from './cli/commands' |
182 | 2 |
|
| 3 | +const cli = createTailwindcssPatchCli() |
183 | 4 | cli.help() |
184 | 5 | cli.parse() |
185 | | - |
186 | | -function formatTokenLine(entry: TailwindTokenLocation) { |
187 | | - return `${entry.relativeFile}:${entry.line}:${entry.column} ${entry.rawCandidate} (${entry.start}-${entry.end})` |
188 | | -} |
189 | | - |
190 | | -function formatGroupedPreview(map: TailwindTokenByFileMap, limit: number = 3) { |
191 | | - const files = Object.keys(map) |
192 | | - if (!files.length) { |
193 | | - return { preview: '', moreFiles: 0 } |
194 | | - } |
195 | | - |
196 | | - const lines = files.slice(0, limit).map((file) => { |
197 | | - const tokens = map[file] |
198 | | - const sample = tokens.slice(0, 3).map(token => token.rawCandidate).join(', ') |
199 | | - const suffix = tokens.length > 3 ? ', …' : '' |
200 | | - return `${file}: ${tokens.length} tokens (${sample}${suffix})` |
201 | | - }) |
202 | | - |
203 | | - return { |
204 | | - preview: lines.join('\n'), |
205 | | - moreFiles: Math.max(0, files.length - limit), |
206 | | - } |
207 | | -} |
0 commit comments