@csszyx/cli 0.9.8 → 0.9.10
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/dist/bin.d.mts +1 -0
- package/dist/bin.mjs +1418 -0
- package/dist/index.d.mts +0 -1
- package/dist/index.mjs +8 -4831
- package/dist/shared/cli.dDkXlxD_.mjs +3428 -0
- package/package.json +4 -4
package/dist/bin.mjs
ADDED
|
@@ -0,0 +1,1418 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs$1, { readFileSync } from 'node:fs';
|
|
3
|
+
import cac from 'cac';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import path__default from 'node:path';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
import { i as transformHtmlSourceSimple, t as transformSource, h as generateTypes } from './shared/cli.dDkXlxD_.mjs';
|
|
10
|
+
import { execa } from 'execa';
|
|
11
|
+
import prompts from 'prompts';
|
|
12
|
+
import readline from 'node:readline';
|
|
13
|
+
import fg from 'fast-glob';
|
|
14
|
+
import { runNextPrebuild } from '@csszyx/unplugin/next-prebuild';
|
|
15
|
+
import { NextSafelistWatcher } from '@csszyx/unplugin/next-watcher';
|
|
16
|
+
import { watch } from 'chokidar';
|
|
17
|
+
import { Minimatch } from 'minimatch';
|
|
18
|
+
import 'node:fs/promises';
|
|
19
|
+
import 'node:url';
|
|
20
|
+
import 'tailwindcss/resolveConfig.js';
|
|
21
|
+
import '@babel/parser';
|
|
22
|
+
import '@babel/types';
|
|
23
|
+
|
|
24
|
+
const colors = {
|
|
25
|
+
success: pc.green,
|
|
26
|
+
error: pc.red,
|
|
27
|
+
warn: pc.yellow,
|
|
28
|
+
info: pc.cyan,
|
|
29
|
+
dim: pc.dim,
|
|
30
|
+
bold: pc.bold
|
|
31
|
+
};
|
|
32
|
+
const icons = {
|
|
33
|
+
success: "\u2713",
|
|
34
|
+
error: "\u2717",
|
|
35
|
+
warn: "\u26A0",
|
|
36
|
+
info: "\u2139"
|
|
37
|
+
};
|
|
38
|
+
function printHeader(title) {
|
|
39
|
+
const width = 48;
|
|
40
|
+
const padding = Math.max(0, width - title.length - 4);
|
|
41
|
+
console.log(pc.cyan(`\u250C${"\u2500".repeat(width - 2)}\u2510`));
|
|
42
|
+
console.log(`${pc.cyan("\u2502")} ${pc.bold(title)}${" ".repeat(padding)}${pc.cyan("\u2502")}`);
|
|
43
|
+
console.log(pc.cyan(`\u2514${"\u2500".repeat(width - 2)}\u2518`));
|
|
44
|
+
console.log();
|
|
45
|
+
}
|
|
46
|
+
function printSection(title) {
|
|
47
|
+
console.log();
|
|
48
|
+
console.log(pc.bold(title));
|
|
49
|
+
console.log(pc.dim("\u2501".repeat(48)));
|
|
50
|
+
}
|
|
51
|
+
function printSuccess(message) {
|
|
52
|
+
console.log(colors.success(`${icons.success} ${message}`));
|
|
53
|
+
}
|
|
54
|
+
function printError(message) {
|
|
55
|
+
console.log(colors.error(`${icons.error} ${message}`));
|
|
56
|
+
}
|
|
57
|
+
function printWarn(message) {
|
|
58
|
+
console.log(colors.warn(`${icons.warn} ${message}`));
|
|
59
|
+
}
|
|
60
|
+
function printInfo(message) {
|
|
61
|
+
console.log(colors.info(`${icons.info} ${message}`));
|
|
62
|
+
}
|
|
63
|
+
const spinner = {
|
|
64
|
+
start(text) {
|
|
65
|
+
return ora(text).start();
|
|
66
|
+
},
|
|
67
|
+
succeed(spinner2, text) {
|
|
68
|
+
spinner2.succeed(colors.success(text));
|
|
69
|
+
},
|
|
70
|
+
fail(spinner2, text) {
|
|
71
|
+
spinner2.fail(colors.error(text));
|
|
72
|
+
},
|
|
73
|
+
warn(spinner2, text) {
|
|
74
|
+
spinner2.warn(colors.warn(text));
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
function printBar(values, max, width = 20) {
|
|
78
|
+
const filled = Math.round(values.reduce((a, b) => a + b, 0) / max * width);
|
|
79
|
+
return "\u25A0".repeat(filled) + "\u25A1".repeat(width - filled);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function audit(options = {}) {
|
|
83
|
+
const cwd = options.cwd || process.cwd();
|
|
84
|
+
const stats = await collectStats(cwd);
|
|
85
|
+
if (options.json) {
|
|
86
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
printHeader("csszyx Audit Report");
|
|
90
|
+
printSection("\u{1F4CA} Mangle Statistics");
|
|
91
|
+
if (stats.totalClasses === 0) {
|
|
92
|
+
console.log(" Tier distribution not yet available.");
|
|
93
|
+
console.log(" Run a production build first, then re-run csszyx audit.");
|
|
94
|
+
} else {
|
|
95
|
+
console.log(` Total Classes: ${stats.totalClasses}`);
|
|
96
|
+
console.log(` Mangled Classes: ${stats.totalClasses} (100%)`);
|
|
97
|
+
console.log(" Unmangled Classes: 0");
|
|
98
|
+
console.log();
|
|
99
|
+
console.log(" Tier Distribution:");
|
|
100
|
+
const tierNames = [
|
|
101
|
+
"Tier 1 (a-Z)",
|
|
102
|
+
"Tier 2 (a0-Z9)",
|
|
103
|
+
"Tier 3 (aa-ZZ)",
|
|
104
|
+
"Tier 4 (a00-Z99)",
|
|
105
|
+
"Tier 5 (aaa+)"
|
|
106
|
+
];
|
|
107
|
+
for (let i = 1; i <= 5; i++) {
|
|
108
|
+
const count = stats.tierDistribution[i] || 0;
|
|
109
|
+
const percent = stats.totalClasses ? Math.round(count / stats.totalClasses * 100) : 0;
|
|
110
|
+
const bar = printBar([count], stats.totalClasses, 20);
|
|
111
|
+
console.log(
|
|
112
|
+
` \u2022 ${tierNames[i - 1].padEnd(18)} ${String(count).padStart(3)} (${String(percent).padStart(2)}%) ${colors.dim(bar)}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
printSection("\u{1F4BE} Bundle Size Impact");
|
|
117
|
+
if (stats.bundleSavings.originalHTML > 0) {
|
|
118
|
+
const htmlSavings = stats.bundleSavings.originalHTML - stats.bundleSavings.mangledHTML;
|
|
119
|
+
const htmlPercent = Math.round(htmlSavings / stats.bundleSavings.originalHTML * 100);
|
|
120
|
+
console.log(` Original HTML: ${formatBytes(stats.bundleSavings.originalHTML)}`);
|
|
121
|
+
console.log(
|
|
122
|
+
` Mangled HTML: ${formatBytes(stats.bundleSavings.mangledHTML)} \u2193 ${htmlPercent}% (-${formatBytes(htmlSavings)})`
|
|
123
|
+
);
|
|
124
|
+
console.log();
|
|
125
|
+
}
|
|
126
|
+
if (stats.bundleSavings.originalCSS > 0) {
|
|
127
|
+
const cssSavings = stats.bundleSavings.originalCSS - stats.bundleSavings.mangledCSS;
|
|
128
|
+
const cssPercent = Math.round(cssSavings / stats.bundleSavings.originalCSS * 100);
|
|
129
|
+
console.log(` Original CSS: ${formatBytes(stats.bundleSavings.originalCSS)}`);
|
|
130
|
+
console.log(
|
|
131
|
+
` Mangled CSS: ${formatBytes(stats.bundleSavings.mangledCSS)} \u2193 ${cssPercent}% (-${formatBytes(cssSavings)})`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
console.log();
|
|
135
|
+
printInfo("\u{1F4A1} Tip: Enable runtime lite bundle for -1.1KB");
|
|
136
|
+
console.log(" \u2192 import { _sz } from 'csszyx/lite'");
|
|
137
|
+
}
|
|
138
|
+
async function collectStats(cwd) {
|
|
139
|
+
const stats = {
|
|
140
|
+
totalClasses: 0,
|
|
141
|
+
tierDistribution: {},
|
|
142
|
+
bundleSavings: {
|
|
143
|
+
originalHTML: 0,
|
|
144
|
+
mangledHTML: 0,
|
|
145
|
+
originalCSS: 0,
|
|
146
|
+
mangledCSS: 0
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const distDir = path__default.join(cwd, "dist");
|
|
150
|
+
if (!fs.existsSync(distDir)) {
|
|
151
|
+
return stats;
|
|
152
|
+
}
|
|
153
|
+
const htmlFiles = fs.readdirSync(distDir, { recursive: true }).filter((f) => String(f).endsWith(".html"));
|
|
154
|
+
const cssFiles = fs.readdirSync(distDir, { recursive: true }).filter((f) => String(f).endsWith(".css"));
|
|
155
|
+
if (htmlFiles.length > 0) {
|
|
156
|
+
const htmlContent = fs.readFileSync(path__default.join(distDir, String(htmlFiles[0])), "utf-8");
|
|
157
|
+
stats.bundleSavings.mangledHTML = Buffer.byteLength(htmlContent);
|
|
158
|
+
stats.bundleSavings.originalHTML = Math.round(stats.bundleSavings.mangledHTML * 1.67);
|
|
159
|
+
}
|
|
160
|
+
if (cssFiles.length > 0) {
|
|
161
|
+
const cssContent = fs.readFileSync(path__default.join(distDir, String(cssFiles[0])), "utf-8");
|
|
162
|
+
stats.bundleSavings.mangledCSS = Buffer.byteLength(cssContent);
|
|
163
|
+
stats.bundleSavings.originalCSS = Math.round(stats.bundleSavings.mangledCSS * 1.71);
|
|
164
|
+
}
|
|
165
|
+
return stats;
|
|
166
|
+
}
|
|
167
|
+
function formatBytes(bytes) {
|
|
168
|
+
if (bytes === 0) {
|
|
169
|
+
return "0 B";
|
|
170
|
+
}
|
|
171
|
+
if (bytes < 1024) {
|
|
172
|
+
return `${bytes} B`;
|
|
173
|
+
}
|
|
174
|
+
if (bytes < 1024 * 1024) {
|
|
175
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
176
|
+
}
|
|
177
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function detectFramework(cwd) {
|
|
181
|
+
try {
|
|
182
|
+
const pkgPath = path__default.join(cwd, "package.json");
|
|
183
|
+
if (!fs.existsSync(pkgPath)) {
|
|
184
|
+
return "unknown";
|
|
185
|
+
}
|
|
186
|
+
const pkg = fs.readJSONSync(pkgPath);
|
|
187
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
188
|
+
if (deps.next) {
|
|
189
|
+
const hasAppDir = fs.existsSync(path__default.join(cwd, "app"));
|
|
190
|
+
return hasAppDir ? "nextjs-app" : "nextjs-pages";
|
|
191
|
+
}
|
|
192
|
+
if (deps.nuxt) {
|
|
193
|
+
return "nuxt";
|
|
194
|
+
}
|
|
195
|
+
if (deps["@sveltejs/kit"]) {
|
|
196
|
+
return "sveltekit";
|
|
197
|
+
}
|
|
198
|
+
if (deps.astro) {
|
|
199
|
+
return "astro";
|
|
200
|
+
}
|
|
201
|
+
if (deps.vite) {
|
|
202
|
+
if (deps.react || deps["react-dom"]) {
|
|
203
|
+
return "vite-react";
|
|
204
|
+
}
|
|
205
|
+
if (deps.vue) {
|
|
206
|
+
return "vite-vue";
|
|
207
|
+
}
|
|
208
|
+
if (deps.svelte) {
|
|
209
|
+
return "vite-svelte";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return "unknown";
|
|
213
|
+
} catch {
|
|
214
|
+
return "unknown";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function detectPackageManager(cwd) {
|
|
218
|
+
if (fs.existsSync(path__default.join(cwd, "pnpm-lock.yaml"))) {
|
|
219
|
+
return "pnpm";
|
|
220
|
+
}
|
|
221
|
+
if (fs.existsSync(path__default.join(cwd, "yarn.lock"))) {
|
|
222
|
+
return "yarn";
|
|
223
|
+
}
|
|
224
|
+
if (fs.existsSync(path__default.join(cwd, "bun.lockb"))) {
|
|
225
|
+
return "bun";
|
|
226
|
+
}
|
|
227
|
+
return "npm";
|
|
228
|
+
}
|
|
229
|
+
function hasTailwindInstalled(cwd) {
|
|
230
|
+
try {
|
|
231
|
+
const pkgPath = path__default.join(cwd, "package.json");
|
|
232
|
+
if (!fs.existsSync(pkgPath)) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
const pkg = fs.readJSONSync(pkgPath);
|
|
236
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
237
|
+
return !!deps.tailwindcss;
|
|
238
|
+
} catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function hasTypeScript(cwd) {
|
|
243
|
+
return fs.existsSync(path__default.join(cwd, "tsconfig.json")) || fs.existsSync(path__default.join(cwd, "jsconfig.json"));
|
|
244
|
+
}
|
|
245
|
+
function getProjectInfo(cwd = process.cwd()) {
|
|
246
|
+
return {
|
|
247
|
+
framework: detectFramework(cwd),
|
|
248
|
+
packageManager: detectPackageManager(cwd),
|
|
249
|
+
hasTailwind: hasTailwindInstalled(cwd),
|
|
250
|
+
hasTypeScript: hasTypeScript(cwd),
|
|
251
|
+
rootDir: cwd
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function getFrameworkName(framework) {
|
|
255
|
+
const names = {
|
|
256
|
+
"vite-react": "Vite + React",
|
|
257
|
+
"vite-vue": "Vite + Vue",
|
|
258
|
+
"vite-svelte": "Vite + Svelte",
|
|
259
|
+
"nextjs-app": "Next.js (App Router)",
|
|
260
|
+
"nextjs-pages": "Next.js (Pages Router)",
|
|
261
|
+
nuxt: "Nuxt 3",
|
|
262
|
+
sveltekit: "SvelteKit",
|
|
263
|
+
astro: "Astro",
|
|
264
|
+
unknown: "Unknown"
|
|
265
|
+
};
|
|
266
|
+
return names[framework];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function doctor(options = {}) {
|
|
270
|
+
const cwd = options.cwd || process.cwd();
|
|
271
|
+
const projectInfo = getProjectInfo(cwd);
|
|
272
|
+
printHeader("csszyx Doctor");
|
|
273
|
+
let issueCount = 0;
|
|
274
|
+
printSection("\u{1F4CB} Configuration Health");
|
|
275
|
+
const hasConfig = fs.existsSync(path__default.join(cwd, "csszyx.config.ts")) || fs.existsSync(path__default.join(cwd, "csszyx.config.js"));
|
|
276
|
+
if (hasConfig) {
|
|
277
|
+
printSuccess("csszyx configuration found");
|
|
278
|
+
} else {
|
|
279
|
+
printWarn("No csszyx.config found - using defaults");
|
|
280
|
+
}
|
|
281
|
+
if (projectInfo.hasTailwind) {
|
|
282
|
+
printSuccess("Tailwind CSS installed");
|
|
283
|
+
} else {
|
|
284
|
+
printError("Tailwind CSS not found");
|
|
285
|
+
issueCount++;
|
|
286
|
+
if (options.verbose) {
|
|
287
|
+
console.log(" \u2192 Run: npm install -D tailwindcss");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
printSection("\u{1F4E6} Package Versions");
|
|
291
|
+
try {
|
|
292
|
+
const pkgPath = path__default.join(cwd, "package.json");
|
|
293
|
+
const pkg = fs.readJSONSync(pkgPath);
|
|
294
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
295
|
+
if (deps.csszyx) {
|
|
296
|
+
printSuccess(`csszyx: ${deps.csszyx}`);
|
|
297
|
+
} else {
|
|
298
|
+
printError("csszyx package not installed");
|
|
299
|
+
issueCount++;
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
printError("Failed to read package.json");
|
|
303
|
+
issueCount++;
|
|
304
|
+
}
|
|
305
|
+
printSection("\u{1F528} Build Output");
|
|
306
|
+
const distDir = path__default.join(cwd, "dist");
|
|
307
|
+
if (fs.existsSync(distDir)) {
|
|
308
|
+
const htmlFiles = fs.readdirSync(distDir, { recursive: true }).filter((f) => String(f).endsWith(".html"));
|
|
309
|
+
if (htmlFiles.length > 0) {
|
|
310
|
+
printSuccess(`Found ${htmlFiles.length} HTML file(s)`);
|
|
311
|
+
const htmlContent = fs.readFileSync(path__default.join(distDir, String(htmlFiles[0])), "utf-8");
|
|
312
|
+
if (htmlContent.includes("data-sz-checksum")) {
|
|
313
|
+
printSuccess("Checksum injection working");
|
|
314
|
+
} else {
|
|
315
|
+
printWarn("Checksum not found in HTML");
|
|
316
|
+
if (options.verbose) {
|
|
317
|
+
console.log(" \u2192 Enable injectChecksum in production config");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
printWarn("No build output found - run build first");
|
|
323
|
+
}
|
|
324
|
+
console.log();
|
|
325
|
+
if (issueCount === 0) {
|
|
326
|
+
printSuccess("\u2728 No issues found! Your setup looks good.");
|
|
327
|
+
} else {
|
|
328
|
+
printWarn(`Found ${issueCount} issue(s)`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const VITE_FRAMEWORKS = /* @__PURE__ */ new Set(["vite-react", "vite-vue", "vite-svelte"]);
|
|
333
|
+
async function readFileOrNull(filePath) {
|
|
334
|
+
try {
|
|
335
|
+
return await fs.readFile(filePath, "utf8");
|
|
336
|
+
} catch (err) {
|
|
337
|
+
if (err?.code === "ENOENT") {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
throw err;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const NEXTJS_FRAMEWORKS = /* @__PURE__ */ new Set(["nextjs-app", "nextjs-pages"]);
|
|
344
|
+
const CSS_ENTRY_CANDIDATES = [
|
|
345
|
+
"src/index.css",
|
|
346
|
+
"src/app.css",
|
|
347
|
+
"src/globals.css",
|
|
348
|
+
"app/globals.css",
|
|
349
|
+
"src/styles/index.css",
|
|
350
|
+
"styles/globals.css"
|
|
351
|
+
];
|
|
352
|
+
async function init(options = {}) {
|
|
353
|
+
const cwd = options.cwd || process.cwd();
|
|
354
|
+
const projectInfo = getProjectInfo(cwd);
|
|
355
|
+
printHeader("csszyx Setup Wizard");
|
|
356
|
+
if (projectInfo.framework !== "unknown") {
|
|
357
|
+
printSuccess(`Detected: ${getFrameworkName(projectInfo.framework)}`);
|
|
358
|
+
printInfo(`Package Manager: ${projectInfo.packageManager}`);
|
|
359
|
+
}
|
|
360
|
+
let config = {
|
|
361
|
+
enableSSR: true,
|
|
362
|
+
enableRecovery: true,
|
|
363
|
+
installTailwind: !projectInfo.hasTailwind,
|
|
364
|
+
setupGitignore: true,
|
|
365
|
+
setupTsconfig: !!projectInfo.hasTypeScript
|
|
366
|
+
};
|
|
367
|
+
if (!options.yes) {
|
|
368
|
+
const answers = await prompts([
|
|
369
|
+
{
|
|
370
|
+
type: projectInfo.hasTailwind ? null : "confirm",
|
|
371
|
+
name: "installTailwind",
|
|
372
|
+
message: "Install Tailwind CSS v4?",
|
|
373
|
+
initial: true
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
type: "confirm",
|
|
377
|
+
name: "enableSSR",
|
|
378
|
+
message: "Enable SSR Hydration Guard?",
|
|
379
|
+
initial: true
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
type: "confirm",
|
|
383
|
+
name: "enableRecovery",
|
|
384
|
+
message: "Enable development mode recovery?",
|
|
385
|
+
initial: true
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
type: "confirm",
|
|
389
|
+
name: "setupGitignore",
|
|
390
|
+
message: "Add .csszyx to .gitignore?",
|
|
391
|
+
initial: true
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
type: projectInfo.hasTypeScript ? "confirm" : null,
|
|
395
|
+
name: "setupTsconfig",
|
|
396
|
+
message: "Add .csszyx/theme.d.ts to tsconfig.json (Theme Auto-Scan)?",
|
|
397
|
+
initial: true
|
|
398
|
+
}
|
|
399
|
+
]);
|
|
400
|
+
config = { ...config, ...answers };
|
|
401
|
+
}
|
|
402
|
+
const spin = spinner.start("Installing csszyx...");
|
|
403
|
+
try {
|
|
404
|
+
await execa(projectInfo.packageManager, ["add", "csszyx", "@csszyx/runtime"], { cwd });
|
|
405
|
+
if (projectInfo.hasTypeScript) {
|
|
406
|
+
await execa(projectInfo.packageManager, ["add", "-D", "@csszyx/types"], { cwd });
|
|
407
|
+
}
|
|
408
|
+
if (config.installTailwind) {
|
|
409
|
+
const twPackage = VITE_FRAMEWORKS.has(projectInfo.framework) ? "@tailwindcss/vite" : "@tailwindcss/postcss";
|
|
410
|
+
await execa(projectInfo.packageManager, ["add", "-D", "tailwindcss", twPackage], {
|
|
411
|
+
cwd
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
spinner.succeed(spin, "Installed csszyx");
|
|
415
|
+
} catch (error) {
|
|
416
|
+
spinner.fail(spin, "Failed to install packages");
|
|
417
|
+
printError(String(error));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const spin2 = spinner.start("Creating config files...");
|
|
421
|
+
try {
|
|
422
|
+
const configContent = generateConfigFile(config);
|
|
423
|
+
const configPath = path__default.join(
|
|
424
|
+
cwd,
|
|
425
|
+
projectInfo.hasTypeScript ? "csszyx.config.ts" : "csszyx.config.js"
|
|
426
|
+
);
|
|
427
|
+
await fs.writeFile(configPath, configContent);
|
|
428
|
+
if (config.installTailwind) {
|
|
429
|
+
await setupTailwindCss(cwd, projectInfo.framework);
|
|
430
|
+
}
|
|
431
|
+
await injectPlugin(cwd, projectInfo.framework);
|
|
432
|
+
if (config.setupGitignore) {
|
|
433
|
+
const gitignorePath = path__default.join(cwd, ".gitignore");
|
|
434
|
+
const ignoreEntry = "\n# csszyx generated theme types\n.csszyx\n";
|
|
435
|
+
const existing = await readFileOrNull(gitignorePath);
|
|
436
|
+
if (existing !== null) {
|
|
437
|
+
if (!existing.includes(".csszyx")) {
|
|
438
|
+
await fs.appendFile(gitignorePath, ignoreEntry);
|
|
439
|
+
}
|
|
440
|
+
} else {
|
|
441
|
+
await fs.writeFile(gitignorePath, "node_modules\n.csszyx\n");
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (config.setupTsconfig) {
|
|
445
|
+
await setupTsconfig(cwd);
|
|
446
|
+
}
|
|
447
|
+
if (projectInfo.hasTypeScript) {
|
|
448
|
+
await setupSzTypes(cwd);
|
|
449
|
+
}
|
|
450
|
+
spinner.succeed(spin2, "Created configuration files");
|
|
451
|
+
} catch (error) {
|
|
452
|
+
spinner.fail(spin2, "Failed to create config files");
|
|
453
|
+
printError(String(error));
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
console.log();
|
|
457
|
+
printSuccess("\u{1F389} All done!");
|
|
458
|
+
console.log();
|
|
459
|
+
printInfo("Next steps:");
|
|
460
|
+
console.log(` \u2022 Run '${projectInfo.packageManager} run dev' to start`);
|
|
461
|
+
if (projectInfo.hasTypeScript) {
|
|
462
|
+
console.log(" \u2022 The `sz` prop is typed via csszyx-env.d.ts");
|
|
463
|
+
}
|
|
464
|
+
if (NEXTJS_FRAMEWORKS.has(projectInfo.framework)) {
|
|
465
|
+
console.log(" \u2022 Using Turbopack? Production builds fail-closed until the safelist");
|
|
466
|
+
console.log(" is seeded \u2014 wire the build script as:");
|
|
467
|
+
console.log(` "build": "csszyx next prebuild 'app/**/*.tsx' && next build"`);
|
|
468
|
+
console.log(" and run `csszyx next watch` alongside `next dev`.");
|
|
469
|
+
console.log(" Setup guide: https://csszyx.com/docs/installation#nextjs-turbopack-setup");
|
|
470
|
+
}
|
|
471
|
+
console.log(" \u2022 Check the docs at https://csszyx.com");
|
|
472
|
+
}
|
|
473
|
+
async function setupTailwindCss(cwd, framework) {
|
|
474
|
+
let cssPath;
|
|
475
|
+
let content = null;
|
|
476
|
+
for (const candidate of CSS_ENTRY_CANDIDATES) {
|
|
477
|
+
const full = path__default.join(cwd, candidate);
|
|
478
|
+
const existing = await readFileOrNull(full);
|
|
479
|
+
if (existing !== null) {
|
|
480
|
+
cssPath = full;
|
|
481
|
+
content = existing;
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (!cssPath || content === null) {
|
|
486
|
+
cssPath = path__default.join(cwd, "src/index.css");
|
|
487
|
+
await fs.ensureDir(path__default.dirname(cssPath));
|
|
488
|
+
await fs.writeFile(cssPath, '@import "tailwindcss";\n');
|
|
489
|
+
printInfo(`Created ${path__default.relative(cwd, cssPath)} with Tailwind v4 import`);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (!content.includes('@import "tailwindcss"') && !content.includes("@import 'tailwindcss'")) {
|
|
493
|
+
await fs.writeFile(cssPath, `@import "tailwindcss";
|
|
494
|
+
|
|
495
|
+
${content}`);
|
|
496
|
+
printInfo(`Added Tailwind v4 import to ${path__default.relative(cwd, cssPath)}`);
|
|
497
|
+
}
|
|
498
|
+
if (NEXTJS_FRAMEWORKS.has(framework)) {
|
|
499
|
+
const postcssMjs = path__default.join(cwd, "postcss.config.mjs");
|
|
500
|
+
const postcssJs = path__default.join(cwd, "postcss.config.js");
|
|
501
|
+
const postcssTs = path__default.join(cwd, "postcss.config.ts");
|
|
502
|
+
const hasExisting = await readFileOrNull(postcssMjs) !== null || await readFileOrNull(postcssJs) !== null || await readFileOrNull(postcssTs) !== null;
|
|
503
|
+
if (!hasExisting) {
|
|
504
|
+
await fs.writeFile(postcssMjs, generatePostcssConfig());
|
|
505
|
+
printInfo("Created postcss.config.mjs for Tailwind v4");
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async function injectPlugin(cwd, framework) {
|
|
510
|
+
if (VITE_FRAMEWORKS.has(framework)) {
|
|
511
|
+
const injected = await injectVitePlugin(cwd);
|
|
512
|
+
if (!injected) {
|
|
513
|
+
printWarn("Could not auto-inject csszyx plugin. Add manually to vite.config.ts:");
|
|
514
|
+
console.log(generateVitePluginInstructions());
|
|
515
|
+
}
|
|
516
|
+
} else if (NEXTJS_FRAMEWORKS.has(framework)) {
|
|
517
|
+
const injected = await injectNextPlugin(cwd);
|
|
518
|
+
if (!injected) {
|
|
519
|
+
printWarn("Could not auto-inject csszyx plugin. Add manually to next.config.js:");
|
|
520
|
+
console.log(generateNextPluginInstructions());
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
printInfo("Add csszyx plugin to your bundler config:");
|
|
524
|
+
console.log(generateVitePluginInstructions());
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async function injectVitePlugin(cwd) {
|
|
528
|
+
const candidates = [
|
|
529
|
+
path__default.join(cwd, "vite.config.ts"),
|
|
530
|
+
path__default.join(cwd, "vite.config.js"),
|
|
531
|
+
path__default.join(cwd, "vite.config.mts"),
|
|
532
|
+
path__default.join(cwd, "vite.config.mjs")
|
|
533
|
+
];
|
|
534
|
+
let configPath;
|
|
535
|
+
let content = null;
|
|
536
|
+
for (const c of candidates) {
|
|
537
|
+
const existing = await readFileOrNull(c);
|
|
538
|
+
if (existing !== null) {
|
|
539
|
+
configPath = c;
|
|
540
|
+
content = existing;
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (!configPath || content === null) {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
if (content.includes("csszyx")) {
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
const importBlock = [
|
|
551
|
+
"import csszyx from 'csszyx/vite';",
|
|
552
|
+
content.includes("@tailwindcss/vite") ? null : "import tailwindcss from '@tailwindcss/vite';"
|
|
553
|
+
].filter(Boolean).join("\n");
|
|
554
|
+
const lastImportMatch = [...content.matchAll(/^import .+$/gm)].pop();
|
|
555
|
+
if (!lastImportMatch || lastImportMatch.index === void 0) {
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
const insertAt = lastImportMatch.index + lastImportMatch[0].length;
|
|
559
|
+
content = `${content.slice(0, insertAt)}
|
|
560
|
+
${importBlock}${content.slice(insertAt)}`;
|
|
561
|
+
const pluginsMatch = content.match(/plugins\s*:\s*\[/);
|
|
562
|
+
if (!pluginsMatch || pluginsMatch.index === void 0) {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
const pluginsInsertAt = pluginsMatch.index + pluginsMatch[0].length;
|
|
566
|
+
const twEntry = content.includes("tailwindcss()") ? "" : "\n tailwindcss(),";
|
|
567
|
+
content = content.slice(0, pluginsInsertAt) + `
|
|
568
|
+
...csszyx(), // csszyx MUST come before tailwindcss${twEntry}` + content.slice(pluginsInsertAt);
|
|
569
|
+
await fs.writeFile(configPath, content);
|
|
570
|
+
printInfo(`Injected csszyx plugin into ${path__default.basename(configPath)}`);
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
async function injectNextPlugin(cwd) {
|
|
574
|
+
const candidates = [
|
|
575
|
+
path__default.join(cwd, "next.config.ts"),
|
|
576
|
+
path__default.join(cwd, "next.config.mjs"),
|
|
577
|
+
path__default.join(cwd, "next.config.js")
|
|
578
|
+
];
|
|
579
|
+
let configPath;
|
|
580
|
+
let content = null;
|
|
581
|
+
for (const c of candidates) {
|
|
582
|
+
const existing = await readFileOrNull(c);
|
|
583
|
+
if (existing !== null) {
|
|
584
|
+
configPath = c;
|
|
585
|
+
content = existing;
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (!configPath) {
|
|
590
|
+
configPath = path__default.join(cwd, "next.config.js");
|
|
591
|
+
await fs.writeFile(configPath, generateNextConfig());
|
|
592
|
+
printInfo("Created next.config.js with csszyx plugin");
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
if (content?.includes("csszyx")) {
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
async function setupTsconfig(cwd) {
|
|
601
|
+
const primary = path__default.join(cwd, "tsconfig.json");
|
|
602
|
+
const viteTsConfig = path__default.join(cwd, "tsconfig.app.json");
|
|
603
|
+
let tsconfigPath = primary;
|
|
604
|
+
let content = await readFileOrNull(tsconfigPath);
|
|
605
|
+
if (content === null) {
|
|
606
|
+
tsconfigPath = viteTsConfig;
|
|
607
|
+
content = await readFileOrNull(tsconfigPath);
|
|
608
|
+
}
|
|
609
|
+
if (content === null) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (content.includes(".csszyx")) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const includeMatch = content.match(/"include"\s*:\s*\[/);
|
|
616
|
+
if (includeMatch && includeMatch.index !== void 0) {
|
|
617
|
+
const insertPos = includeMatch.index + includeMatch[0].length;
|
|
618
|
+
content = content.slice(0, insertPos) + '\n "./.csszyx/theme.d.ts",\n "./.csszyx",' + content.slice(insertPos);
|
|
619
|
+
await fs.writeFile(tsconfigPath, content);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async function setupSzTypes(cwd) {
|
|
623
|
+
const envPath = path__default.join(cwd, "csszyx-env.d.ts");
|
|
624
|
+
const reference = '/// <reference types="@csszyx/types/jsx" />';
|
|
625
|
+
const existing = await readFileOrNull(envPath);
|
|
626
|
+
if (existing === null) {
|
|
627
|
+
await fs.writeFile(
|
|
628
|
+
envPath,
|
|
629
|
+
`// Generated by csszyx. Enables the \`sz\` JSX prop types.
|
|
630
|
+
${reference}
|
|
631
|
+
`
|
|
632
|
+
);
|
|
633
|
+
printInfo("Created csszyx-env.d.ts (sz prop types)");
|
|
634
|
+
} else if (!existing.includes("@csszyx/types/jsx")) {
|
|
635
|
+
await fs.writeFile(envPath, `${existing.trimEnd()}
|
|
636
|
+
${reference}
|
|
637
|
+
`);
|
|
638
|
+
printInfo("Added the sz type reference to csszyx-env.d.ts");
|
|
639
|
+
}
|
|
640
|
+
await ensureTsconfigInclude(cwd, "csszyx-env.d.ts");
|
|
641
|
+
}
|
|
642
|
+
async function ensureTsconfigInclude(cwd, entry) {
|
|
643
|
+
for (const name of ["tsconfig.json", "tsconfig.app.json"]) {
|
|
644
|
+
const tsconfigPath = path__default.join(cwd, name);
|
|
645
|
+
const content = await readFileOrNull(tsconfigPath);
|
|
646
|
+
if (content === null) {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (content.includes(entry)) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const includeMatch = content.match(/"include"\s*:\s*\[/);
|
|
653
|
+
if (includeMatch && includeMatch.index !== void 0) {
|
|
654
|
+
const insertPos = includeMatch.index + includeMatch[0].length;
|
|
655
|
+
await fs.writeFile(
|
|
656
|
+
tsconfigPath,
|
|
657
|
+
`${content.slice(0, insertPos)}
|
|
658
|
+
"${entry}",${content.slice(insertPos)}`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
function generateConfigFile(config) {
|
|
665
|
+
return `import type { CsszyxConfig } from 'csszyx';
|
|
666
|
+
|
|
667
|
+
const config: CsszyxConfig = {
|
|
668
|
+
development: {
|
|
669
|
+
debug: true,
|
|
670
|
+
},
|
|
671
|
+
production: {
|
|
672
|
+
injectChecksum: ${config.enableSSR},
|
|
673
|
+
},
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
export default config;
|
|
677
|
+
`;
|
|
678
|
+
}
|
|
679
|
+
function generatePostcssConfig() {
|
|
680
|
+
return `export default {
|
|
681
|
+
plugins: {
|
|
682
|
+
'@tailwindcss/postcss': {},
|
|
683
|
+
},
|
|
684
|
+
};
|
|
685
|
+
`;
|
|
686
|
+
}
|
|
687
|
+
function generateNextConfig() {
|
|
688
|
+
return `const csszyxWebpack = require('@csszyx/unplugin/webpack').default;
|
|
689
|
+
|
|
690
|
+
/** @type {import('next').NextConfig} */
|
|
691
|
+
const nextConfig = {
|
|
692
|
+
webpack(config) {
|
|
693
|
+
config.plugins.unshift(...csszyxWebpack());
|
|
694
|
+
return config;
|
|
695
|
+
},
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
module.exports = nextConfig;
|
|
699
|
+
`;
|
|
700
|
+
}
|
|
701
|
+
function generateVitePluginInstructions() {
|
|
702
|
+
return `
|
|
703
|
+
import csszyx from 'csszyx/vite';
|
|
704
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
705
|
+
|
|
706
|
+
export default defineConfig({
|
|
707
|
+
plugins: [
|
|
708
|
+
...csszyx(), // csszyx MUST come before tailwindcss
|
|
709
|
+
tailwindcss(),
|
|
710
|
+
// ...your other plugins
|
|
711
|
+
],
|
|
712
|
+
});
|
|
713
|
+
`;
|
|
714
|
+
}
|
|
715
|
+
function generateNextPluginInstructions() {
|
|
716
|
+
return `
|
|
717
|
+
const csszyxWebpack = require('@csszyx/unplugin/webpack').default;
|
|
718
|
+
|
|
719
|
+
module.exports = {
|
|
720
|
+
webpack(config) {
|
|
721
|
+
config.plugins.unshift(...csszyxWebpack());
|
|
722
|
+
return config;
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
`;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function createLogFile(cwd) {
|
|
729
|
+
const now = /* @__PURE__ */ new Date();
|
|
730
|
+
const ts = now.toISOString().slice(0, 19).replace("T", "_").replace(/:/g, "-");
|
|
731
|
+
const logDir = path__default.join(cwd, ".csszyx", "logs");
|
|
732
|
+
fs$1.mkdirSync(logDir, { recursive: true });
|
|
733
|
+
const filePath = path__default.join(logDir, `migrate-${ts}.log`);
|
|
734
|
+
const lines = [`csszyx migrate \u2014 ${now.toISOString()}`, ""];
|
|
735
|
+
return {
|
|
736
|
+
filePath,
|
|
737
|
+
writeLine: (line) => lines.push(line),
|
|
738
|
+
flush: () => fs$1.writeFileSync(filePath, `${lines.join("\n")}
|
|
739
|
+
`, "utf-8")
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
function isGitignored(cwd, pattern) {
|
|
743
|
+
try {
|
|
744
|
+
const content = fs$1.readFileSync(path__default.join(cwd, ".gitignore"), "utf-8");
|
|
745
|
+
return content.split("\n").some((l) => {
|
|
746
|
+
const t = l.trim();
|
|
747
|
+
return t === pattern || t === `${pattern}/` || t === `/${pattern}`;
|
|
748
|
+
});
|
|
749
|
+
} catch {
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
async function askYesNo(question) {
|
|
754
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
755
|
+
return new Promise((resolve) => {
|
|
756
|
+
rl.question(question, (answer) => {
|
|
757
|
+
rl.close();
|
|
758
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
async function migrate(options = {}) {
|
|
763
|
+
const cwd = options.cwd || process.cwd();
|
|
764
|
+
let dryRun = options.dryRun || false;
|
|
765
|
+
const ignorePatterns = options.ignore || [];
|
|
766
|
+
const audit = options.audit || false;
|
|
767
|
+
const resolveTodosPath = options.resolveTodos;
|
|
768
|
+
let customMap, files;
|
|
769
|
+
if (audit) {
|
|
770
|
+
dryRun = true;
|
|
771
|
+
}
|
|
772
|
+
let injectTodos = options.injectTodos || false;
|
|
773
|
+
if (resolveTodosPath && !injectTodos) {
|
|
774
|
+
injectTodos = true;
|
|
775
|
+
}
|
|
776
|
+
printHeader("csszyx Migration Tool");
|
|
777
|
+
if (process.stdout.isTTY && !injectTodos && !audit && !resolveTodosPath) {
|
|
778
|
+
const answer = await askYesNo(
|
|
779
|
+
"Add {/* @sz-todo */} comments above elements with unrecognized classes? [y/N] "
|
|
780
|
+
);
|
|
781
|
+
if (answer) {
|
|
782
|
+
injectTodos = true;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (resolveTodosPath) {
|
|
786
|
+
try {
|
|
787
|
+
const absolutePath = path__default.resolve(cwd, resolveTodosPath);
|
|
788
|
+
const content = fs$1.readFileSync(absolutePath, "utf-8");
|
|
789
|
+
customMap = JSON.parse(content);
|
|
790
|
+
printInfo(`Loaded resolution map from ${resolveTodosPath}`);
|
|
791
|
+
} catch {
|
|
792
|
+
printWarn(
|
|
793
|
+
`Could not load resolve map from ${resolveTodosPath}. Ensure the file exists and is valid JSON.`
|
|
794
|
+
);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (audit) {
|
|
799
|
+
printInfo("Audit mode \u2014 scanning for unrecognized classes to generate a mapping file...");
|
|
800
|
+
} else if (dryRun) {
|
|
801
|
+
printInfo("Dry run mode \u2014 no files will be modified");
|
|
802
|
+
}
|
|
803
|
+
const log = createLogFile(cwd);
|
|
804
|
+
log.writeLine(
|
|
805
|
+
`Mode: ${audit ? "audit" : dryRun ? "dry-run" : "migrate"}${resolveTodosPath ? ` (resolve-todos: ${resolveTodosPath})` : ""}`
|
|
806
|
+
);
|
|
807
|
+
log.writeLine(`injectTodos: ${injectTodos}`);
|
|
808
|
+
log.writeLine("");
|
|
809
|
+
if (!isGitignored(cwd, ".csszyx")) {
|
|
810
|
+
printWarn(
|
|
811
|
+
"Tip: add .csszyx/ to your .gitignore to exclude migration logs from version control."
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
const patterns = options.pattern ? [options.pattern] : ["**/*.{jsx,tsx,html}"];
|
|
815
|
+
const ignore = [
|
|
816
|
+
"**/node_modules/**",
|
|
817
|
+
"**/dist/**",
|
|
818
|
+
"**/build/**",
|
|
819
|
+
"**/.next/**",
|
|
820
|
+
"**/.nuxt/**",
|
|
821
|
+
...ignorePatterns
|
|
822
|
+
];
|
|
823
|
+
const s = spinner.start("Scanning for files...");
|
|
824
|
+
try {
|
|
825
|
+
files = await fg(patterns, { cwd, ignore, absolute: true });
|
|
826
|
+
} catch (err) {
|
|
827
|
+
s.fail("File scan failed");
|
|
828
|
+
printWarn(`Could not scan files: ${err instanceof Error ? err.message : String(err)}`);
|
|
829
|
+
log.flush();
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
s.succeed(`Found ${files.length} files`);
|
|
833
|
+
if (files.length === 0) {
|
|
834
|
+
printWarn(
|
|
835
|
+
options.pattern ? `No files found matching pattern: ${options.pattern}` : "No JSX/TSX/HTML files found"
|
|
836
|
+
);
|
|
837
|
+
log.writeLine("No files found.");
|
|
838
|
+
log.flush();
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
let totalTransformed = 0;
|
|
842
|
+
let totalSkipped = 0;
|
|
843
|
+
let totalSkippedComponent = 0;
|
|
844
|
+
let totalFiles = 0;
|
|
845
|
+
const allUnrecognized = [];
|
|
846
|
+
const allWarnings = [];
|
|
847
|
+
const unusedImportFiles = [];
|
|
848
|
+
const s2 = spinner.start("Migrating...");
|
|
849
|
+
for (const filePath of files) {
|
|
850
|
+
const source = fs$1.readFileSync(filePath, "utf-8");
|
|
851
|
+
const isHtml = filePath.endsWith(".html");
|
|
852
|
+
const hasRelevantAttr = isHtml ? source.includes("class=") : source.includes("className=");
|
|
853
|
+
if (!hasRelevantAttr) {
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
let processSource = source;
|
|
857
|
+
if (resolveTodosPath && !isHtml) {
|
|
858
|
+
processSource = processSource.replace(
|
|
859
|
+
/\{\/\*\s*@sz-todo:\s*(\S(?:.*\S)?)\s*\*\/\}\n?/g,
|
|
860
|
+
""
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
const result = isHtml ? transformHtmlSourceSimple(processSource, filePath, {
|
|
864
|
+
braces: options.braces,
|
|
865
|
+
injectFouc: options.injectFouc,
|
|
866
|
+
injectRuntime: options.injectRuntime,
|
|
867
|
+
cdnUrl: options.cdnUrl,
|
|
868
|
+
localPath: options.localPath
|
|
869
|
+
}) : transformSource(processSource, filePath, { injectTodos, customMap });
|
|
870
|
+
allWarnings.push(...result.warnings);
|
|
871
|
+
if (result.changed) {
|
|
872
|
+
totalFiles++;
|
|
873
|
+
totalTransformed += result.stats.classNamesTransformed;
|
|
874
|
+
totalSkipped += result.stats.classNamesSkipped;
|
|
875
|
+
totalSkippedComponent += result.stats.classNamesSkippedComponent;
|
|
876
|
+
allUnrecognized.push(...result.stats.classesUnrecognized);
|
|
877
|
+
if (result.potentiallyUnusedImports.length > 0) {
|
|
878
|
+
const rel2 = path__default.relative(cwd, filePath);
|
|
879
|
+
unusedImportFiles.push({ file: rel2, imports: result.potentiallyUnusedImports });
|
|
880
|
+
}
|
|
881
|
+
if (!dryRun) {
|
|
882
|
+
try {
|
|
883
|
+
fs$1.writeFileSync(filePath, result.code, "utf-8");
|
|
884
|
+
} catch (err) {
|
|
885
|
+
const rel2 = path__default.relative(cwd, filePath);
|
|
886
|
+
printWarn(
|
|
887
|
+
`Could not write ${rel2}: ${err instanceof Error ? err.message : String(err)}`
|
|
888
|
+
);
|
|
889
|
+
log.writeLine(` Write error: ${rel2}`);
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const rel = path__default.relative(cwd, filePath);
|
|
894
|
+
if (dryRun) {
|
|
895
|
+
printInfo(` ${rel}: ${result.stats.classNamesTransformed} className(s) \u2192 sz`);
|
|
896
|
+
log.writeLine(` ${rel}: ${result.stats.classNamesTransformed} className(s) \u2192 sz`);
|
|
897
|
+
} else {
|
|
898
|
+
log.writeLine(` ${rel}: ${result.stats.classNamesTransformed} className(s) \u2192 sz`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
s2.succeed("Migration complete");
|
|
903
|
+
console.info();
|
|
904
|
+
printSuccess(`Files modified: ${totalFiles}`);
|
|
905
|
+
printSuccess(`classNames converted: ${totalTransformed}`);
|
|
906
|
+
log.writeLine(`Files modified: ${totalFiles}`);
|
|
907
|
+
log.writeLine(`classNames converted: ${totalTransformed}`);
|
|
908
|
+
if (totalSkipped > 0) {
|
|
909
|
+
printWarn(`classNames skipped (dynamic): ${totalSkipped}`);
|
|
910
|
+
log.writeLine(`classNames skipped (dynamic): ${totalSkipped}`);
|
|
911
|
+
}
|
|
912
|
+
if (totalSkippedComponent > 0) {
|
|
913
|
+
printWarn(`classNames kept on components (no sz support): ${totalSkippedComponent}`);
|
|
914
|
+
log.writeLine(`classNames kept on components (no sz support): ${totalSkippedComponent}`);
|
|
915
|
+
}
|
|
916
|
+
if (allUnrecognized.length > 0) {
|
|
917
|
+
const unique = [...new Set(allUnrecognized)];
|
|
918
|
+
printWarn(
|
|
919
|
+
`Unrecognized classes (${unique.length}): ${unique.slice(0, 10).join(", ")}${unique.length > 10 ? "..." : ""}`
|
|
920
|
+
);
|
|
921
|
+
log.writeLine(`Unrecognized classes (${unique.length}): ${unique.join(", ")}`);
|
|
922
|
+
}
|
|
923
|
+
if (allWarnings.length > 0) {
|
|
924
|
+
console.info();
|
|
925
|
+
for (const w of allWarnings.slice(0, 5)) {
|
|
926
|
+
printWarn(w);
|
|
927
|
+
}
|
|
928
|
+
if (allWarnings.length > 5) {
|
|
929
|
+
printWarn(`... and ${allWarnings.length - 5} more warnings`);
|
|
930
|
+
}
|
|
931
|
+
log.writeLine("");
|
|
932
|
+
log.writeLine("Warnings:");
|
|
933
|
+
for (const w of allWarnings) {
|
|
934
|
+
log.writeLine(` ${w}`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (audit) {
|
|
938
|
+
const todoPath = path__default.join(cwd, ".csszyx-todo.json");
|
|
939
|
+
const unique = [...new Set(allUnrecognized)];
|
|
940
|
+
console.info();
|
|
941
|
+
if (unique.length === 0) {
|
|
942
|
+
printSuccess(
|
|
943
|
+
"Audit complete. 100% of your classes are perfectly recognized by csszyx!"
|
|
944
|
+
);
|
|
945
|
+
log.writeLine("Audit: 100% recognized.");
|
|
946
|
+
} else {
|
|
947
|
+
const todoObj = {};
|
|
948
|
+
for (const u of unique) {
|
|
949
|
+
todoObj[u] = "sz:todo";
|
|
950
|
+
}
|
|
951
|
+
try {
|
|
952
|
+
fs$1.writeFileSync(todoPath, JSON.stringify(todoObj, null, 2));
|
|
953
|
+
} catch (err) {
|
|
954
|
+
printWarn(
|
|
955
|
+
`Could not write ${path__default.relative(cwd, todoPath)}: ${err instanceof Error ? err.message : String(err)}`
|
|
956
|
+
);
|
|
957
|
+
log.flush();
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
printSuccess(
|
|
961
|
+
`Audit complete. Exported ${unique.length} unrecognized classes to ${path__default.relative(cwd, todoPath)}.`
|
|
962
|
+
);
|
|
963
|
+
printInfo(
|
|
964
|
+
"Edit this file to map custom classes, then run: npx @csszyx/cli migrate --resolve-todos .csszyx-todo.json"
|
|
965
|
+
);
|
|
966
|
+
log.writeLine(
|
|
967
|
+
`Audit: ${unique.length} unrecognized classes written to ${path__default.relative(cwd, todoPath)}`
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (resolveTodosPath) {
|
|
972
|
+
const unique = [...new Set(allUnrecognized)];
|
|
973
|
+
if (unique.length > 0) {
|
|
974
|
+
console.info();
|
|
975
|
+
printWarn(
|
|
976
|
+
`Still unresolved after this pass (${unique.length}): ${unique.slice(0, 10).join(", ")}${unique.length > 10 ? "..." : ""}`
|
|
977
|
+
);
|
|
978
|
+
printInfo("Re-run --audit to generate a fresh snapshot when ready.");
|
|
979
|
+
log.writeLine(`Still unresolved (${unique.length}): ${unique.join(", ")}`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (unusedImportFiles.length > 0) {
|
|
983
|
+
console.info();
|
|
984
|
+
printWarn("Potentially unused imports (run ESLint to clean up):");
|
|
985
|
+
for (const { file, imports } of unusedImportFiles) {
|
|
986
|
+
printInfo(` ${file}: ${imports.map((i) => `import { ${i} }`).join(", ")}`);
|
|
987
|
+
log.writeLine(` Unused import in ${file}: ${imports.join(", ")}`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
try {
|
|
991
|
+
log.flush();
|
|
992
|
+
printInfo(`Migration log saved to ${path__default.relative(cwd, log.filePath)}`);
|
|
993
|
+
} catch {
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const DEFAULT_NEXT_SOURCE_PATTERN = "{app,pages,src}/**/*.{ts,tsx,js,jsx,mjs,cjs}";
|
|
998
|
+
const DEFAULT_NEXT_SOURCE_IGNORE = [
|
|
999
|
+
"node_modules/**",
|
|
1000
|
+
".git/**",
|
|
1001
|
+
".next/**",
|
|
1002
|
+
".next-turbo-*/**",
|
|
1003
|
+
".csszyx/**",
|
|
1004
|
+
"dist/**",
|
|
1005
|
+
"build/**"
|
|
1006
|
+
];
|
|
1007
|
+
|
|
1008
|
+
async function nextPrebuild(options = {}) {
|
|
1009
|
+
const cwd = path__default.resolve(options.cwd ?? process.cwd());
|
|
1010
|
+
const root = path__default.resolve(options.root ?? cwd);
|
|
1011
|
+
const pattern = options.pattern ?? DEFAULT_NEXT_SOURCE_PATTERN;
|
|
1012
|
+
try {
|
|
1013
|
+
const mode = normalizeMode(options.mode);
|
|
1014
|
+
const parserMode = normalizeParserMode$1(options.parserMode);
|
|
1015
|
+
const matches = await fg(pattern, {
|
|
1016
|
+
cwd: root,
|
|
1017
|
+
absolute: true,
|
|
1018
|
+
ignore: [...DEFAULT_NEXT_SOURCE_IGNORE, ...options.extraIgnore ?? []],
|
|
1019
|
+
dot: false,
|
|
1020
|
+
onlyFiles: true
|
|
1021
|
+
});
|
|
1022
|
+
if (matches.length === 0) {
|
|
1023
|
+
const message = `No source files matched pattern \`${pattern}\` under ${root}.`;
|
|
1024
|
+
if (options.json) {
|
|
1025
|
+
console.log(
|
|
1026
|
+
JSON.stringify(
|
|
1027
|
+
{ ok: false, reason: "no-files-matched", root, pattern, mode },
|
|
1028
|
+
null,
|
|
1029
|
+
2
|
|
1030
|
+
)
|
|
1031
|
+
);
|
|
1032
|
+
} else {
|
|
1033
|
+
console.error(`${colors.error(icons.error)} ${message}`);
|
|
1034
|
+
}
|
|
1035
|
+
return 1;
|
|
1036
|
+
}
|
|
1037
|
+
const result = runNextPrebuild({
|
|
1038
|
+
files: matches,
|
|
1039
|
+
explicitRoot: root,
|
|
1040
|
+
cwd,
|
|
1041
|
+
mode,
|
|
1042
|
+
parserMode,
|
|
1043
|
+
safelistOutputFile: options.outputFile,
|
|
1044
|
+
cacheDir: options.cacheDir,
|
|
1045
|
+
config: { mangleVars: false }
|
|
1046
|
+
// Versions intentionally omitted: runNextPrebuild's package.json
|
|
1047
|
+
// fallback reads the real installed @csszyx/unplugin and
|
|
1048
|
+
// @csszyx/compiler versions so the manifest's generation identity
|
|
1049
|
+
// tracks the engine that actually runs the transform.
|
|
1050
|
+
});
|
|
1051
|
+
if (options.json) {
|
|
1052
|
+
console.log(
|
|
1053
|
+
JSON.stringify(
|
|
1054
|
+
{
|
|
1055
|
+
ok: true,
|
|
1056
|
+
root,
|
|
1057
|
+
mode,
|
|
1058
|
+
scannedCount: result.scannedCount,
|
|
1059
|
+
transformedCount: result.transformedCount,
|
|
1060
|
+
skippedMissingCount: result.skippedMissingCount,
|
|
1061
|
+
sourceCount: result.sourceCount,
|
|
1062
|
+
classCount: result.classCount,
|
|
1063
|
+
manifestPath: result.manifestPath,
|
|
1064
|
+
safelistOutputPath: result.safelistOutputPath
|
|
1065
|
+
},
|
|
1066
|
+
null,
|
|
1067
|
+
2
|
|
1068
|
+
)
|
|
1069
|
+
);
|
|
1070
|
+
} else {
|
|
1071
|
+
console.log(`${colors.success(icons.success)} csszyx next prebuild done`);
|
|
1072
|
+
console.log(` root: ${root}`);
|
|
1073
|
+
console.log(` mode: ${mode}`);
|
|
1074
|
+
console.log(` scanned: ${result.scannedCount}`);
|
|
1075
|
+
console.log(` transformed: ${result.transformedCount}`);
|
|
1076
|
+
console.log(` skipped: ${result.skippedMissingCount}`);
|
|
1077
|
+
console.log(` sources: ${result.sourceCount}`);
|
|
1078
|
+
console.log(` classes: ${result.classCount}`);
|
|
1079
|
+
console.log(` safelist: ${result.safelistOutputPath}`);
|
|
1080
|
+
console.log(` manifest: ${result.manifestPath}`);
|
|
1081
|
+
}
|
|
1082
|
+
return 0;
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1085
|
+
if (options.json) {
|
|
1086
|
+
console.log(JSON.stringify({ ok: false, reason: message }, null, 2));
|
|
1087
|
+
} else {
|
|
1088
|
+
console.error(`${colors.error(icons.error)} ${message}`);
|
|
1089
|
+
}
|
|
1090
|
+
return 1;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
function normalizeMode(mode) {
|
|
1094
|
+
if (mode === void 0) {
|
|
1095
|
+
return "production";
|
|
1096
|
+
}
|
|
1097
|
+
if (mode === "development" || mode === "production") {
|
|
1098
|
+
return mode;
|
|
1099
|
+
}
|
|
1100
|
+
throw new Error(`Invalid --mode "${mode}". Expected "development" or "production".`);
|
|
1101
|
+
}
|
|
1102
|
+
function normalizeParserMode$1(parserMode) {
|
|
1103
|
+
if (parserMode === void 0) {
|
|
1104
|
+
return void 0;
|
|
1105
|
+
}
|
|
1106
|
+
if (parserMode === "rust" || parserMode === "oxc" || parserMode === "babel") {
|
|
1107
|
+
return parserMode;
|
|
1108
|
+
}
|
|
1109
|
+
throw new Error(`Invalid --parser-mode "${parserMode}". Expected "rust", "oxc", or "babel".`);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const SOURCE_EXTENSION = /\.[cm]?[jt]sx?$/i;
|
|
1113
|
+
async function startNextWatch(options = {}, dependencies = {}) {
|
|
1114
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
1115
|
+
const root = path.resolve(options.root ?? cwd);
|
|
1116
|
+
const pattern = options.pattern ?? DEFAULT_NEXT_SOURCE_PATTERN;
|
|
1117
|
+
const ignore = [...DEFAULT_NEXT_SOURCE_IGNORE, ...options.extraIgnore ?? []];
|
|
1118
|
+
const parserMode = normalizeParserMode(options.parserMode);
|
|
1119
|
+
const debounceMs = normalizeDebounceMs(options.debounceMs);
|
|
1120
|
+
const files = await fg(pattern, {
|
|
1121
|
+
cwd: root,
|
|
1122
|
+
absolute: true,
|
|
1123
|
+
ignore,
|
|
1124
|
+
dot: false,
|
|
1125
|
+
onlyFiles: true
|
|
1126
|
+
});
|
|
1127
|
+
if (files.length === 0) {
|
|
1128
|
+
throw new Error(`No source files matched pattern \`${pattern}\` under ${root}.`);
|
|
1129
|
+
}
|
|
1130
|
+
const prebuild = runNextPrebuild({
|
|
1131
|
+
files,
|
|
1132
|
+
explicitRoot: root,
|
|
1133
|
+
cwd,
|
|
1134
|
+
mode: "development",
|
|
1135
|
+
parserMode,
|
|
1136
|
+
safelistOutputFile: options.outputFile,
|
|
1137
|
+
cacheDir: options.cacheDir,
|
|
1138
|
+
config: { mangleVars: false }
|
|
1139
|
+
});
|
|
1140
|
+
let resolveFailure = () => {
|
|
1141
|
+
};
|
|
1142
|
+
let failed = false;
|
|
1143
|
+
const failure = new Promise((resolve) => {
|
|
1144
|
+
resolveFailure = resolve;
|
|
1145
|
+
});
|
|
1146
|
+
const reportFailure = (error) => {
|
|
1147
|
+
if (failed) {
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
failed = true;
|
|
1151
|
+
resolveFailure(error instanceof Error ? error : new Error(String(error)));
|
|
1152
|
+
};
|
|
1153
|
+
const controller = new NextSafelistWatcher({
|
|
1154
|
+
context: prebuild.context,
|
|
1155
|
+
debounceMs,
|
|
1156
|
+
onError: reportFailure
|
|
1157
|
+
});
|
|
1158
|
+
const watchFactory = dependencies.watch ?? watch;
|
|
1159
|
+
const fsWatcher = watchFactory(root, {
|
|
1160
|
+
ignoreInitial: true,
|
|
1161
|
+
persistent: true,
|
|
1162
|
+
atomic: true,
|
|
1163
|
+
awaitWriteFinish: {
|
|
1164
|
+
stabilityThreshold: 25,
|
|
1165
|
+
pollInterval: 10
|
|
1166
|
+
},
|
|
1167
|
+
ignored: createIgnoredMatcher(root, prebuild.context.safelist.shardsDir, ignore)
|
|
1168
|
+
});
|
|
1169
|
+
fsWatcher.on("all", (event, filePath) => {
|
|
1170
|
+
const absolutePath = path.resolve(filePath);
|
|
1171
|
+
if (event === "add" || event === "change" || event === "unlink") {
|
|
1172
|
+
if (controller.notify(event, absolutePath) || event !== "unlink" || !SOURCE_EXTENSION.test(absolutePath)) {
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
controller.notifySourceRemoval(absolutePath);
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
fsWatcher.on("error", reportFailure);
|
|
1179
|
+
try {
|
|
1180
|
+
await waitForWatcherReady(fsWatcher);
|
|
1181
|
+
controller.start();
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
await fsWatcher.close();
|
|
1184
|
+
controller.close();
|
|
1185
|
+
throw error;
|
|
1186
|
+
}
|
|
1187
|
+
let closed = false;
|
|
1188
|
+
return {
|
|
1189
|
+
root,
|
|
1190
|
+
sourcePattern: pattern,
|
|
1191
|
+
safelistOutputPath: prebuild.safelistOutputPath,
|
|
1192
|
+
manifestPath: prebuild.manifestPath,
|
|
1193
|
+
failure,
|
|
1194
|
+
close: async () => {
|
|
1195
|
+
if (closed) {
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
closed = true;
|
|
1199
|
+
await fsWatcher.close();
|
|
1200
|
+
controller.close();
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
async function nextWatch(options = {}) {
|
|
1205
|
+
let session;
|
|
1206
|
+
let exitCode = 0;
|
|
1207
|
+
try {
|
|
1208
|
+
session = await startNextWatch(options);
|
|
1209
|
+
if (!options.silent) {
|
|
1210
|
+
console.log(`${colors.success(icons.success)} csszyx next watch ready`);
|
|
1211
|
+
console.log(` root: ${session.root}`);
|
|
1212
|
+
console.log(` pattern: ${session.sourcePattern}`);
|
|
1213
|
+
console.log(` safelist: ${session.safelistOutputPath}`);
|
|
1214
|
+
console.log(` manifest: ${session.manifestPath}`);
|
|
1215
|
+
}
|
|
1216
|
+
const outcome = await waitForShutdown(session.failure);
|
|
1217
|
+
if (outcome) {
|
|
1218
|
+
console.error(`${colors.error(icons.error)} ${outcome.message}`);
|
|
1219
|
+
exitCode = 1;
|
|
1220
|
+
}
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1223
|
+
console.error(`${colors.error(icons.error)} ${message}`);
|
|
1224
|
+
exitCode = 1;
|
|
1225
|
+
}
|
|
1226
|
+
try {
|
|
1227
|
+
await session?.close();
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1230
|
+
console.error(`${colors.error(icons.error)} Failed to close Next watcher: ${message}`);
|
|
1231
|
+
exitCode = 1;
|
|
1232
|
+
}
|
|
1233
|
+
return exitCode;
|
|
1234
|
+
}
|
|
1235
|
+
function waitForWatcherReady(watcher) {
|
|
1236
|
+
return new Promise((resolve, reject) => {
|
|
1237
|
+
const onReady = () => {
|
|
1238
|
+
watcher.off("error", onStartupError);
|
|
1239
|
+
resolve();
|
|
1240
|
+
};
|
|
1241
|
+
const onStartupError = (error) => {
|
|
1242
|
+
watcher.off("ready", onReady);
|
|
1243
|
+
reject(error);
|
|
1244
|
+
};
|
|
1245
|
+
watcher.once("ready", onReady);
|
|
1246
|
+
watcher.once("error", onStartupError);
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
function waitForShutdown(failure) {
|
|
1250
|
+
return new Promise((resolve) => {
|
|
1251
|
+
const cleanup = () => {
|
|
1252
|
+
process.off("SIGINT", onSignal);
|
|
1253
|
+
process.off("SIGTERM", onSignal);
|
|
1254
|
+
};
|
|
1255
|
+
const onSignal = () => {
|
|
1256
|
+
cleanup();
|
|
1257
|
+
resolve(void 0);
|
|
1258
|
+
};
|
|
1259
|
+
process.once("SIGINT", onSignal);
|
|
1260
|
+
process.once("SIGTERM", onSignal);
|
|
1261
|
+
failure.then((error) => {
|
|
1262
|
+
cleanup();
|
|
1263
|
+
resolve(error);
|
|
1264
|
+
});
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
function createIgnoredMatcher(root, shardsDir, ignore) {
|
|
1268
|
+
const normalizedShardsDir = path.resolve(shardsDir);
|
|
1269
|
+
const matchers = ignore.flatMap((pattern) => {
|
|
1270
|
+
const normalized = pattern.replace(/\\/g, "/");
|
|
1271
|
+
const variants = normalized.endsWith("/**") ? [normalized, normalized.slice(0, -3)] : [normalized];
|
|
1272
|
+
return variants.map((variant) => new Minimatch(variant, { dot: true }));
|
|
1273
|
+
});
|
|
1274
|
+
return (candidate) => {
|
|
1275
|
+
const absolute = path.resolve(candidate);
|
|
1276
|
+
const relativeToShards = path.relative(absolute, normalizedShardsDir);
|
|
1277
|
+
if (absolute === normalizedShardsDir || absolute.startsWith(`${normalizedShardsDir}${path.sep}`) || relativeToShards !== ".." && !relativeToShards.startsWith(`..${path.sep}`) && !path.isAbsolute(relativeToShards)) {
|
|
1278
|
+
return false;
|
|
1279
|
+
}
|
|
1280
|
+
const relative = path.relative(root, absolute).replace(/\\/g, "/");
|
|
1281
|
+
if (!relative || relative.startsWith("../") || path.isAbsolute(relative)) {
|
|
1282
|
+
return false;
|
|
1283
|
+
}
|
|
1284
|
+
return matchers.some((matcher) => matcher.match(relative));
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
function normalizeParserMode(parserMode) {
|
|
1288
|
+
if (parserMode === void 0) {
|
|
1289
|
+
return void 0;
|
|
1290
|
+
}
|
|
1291
|
+
if (parserMode === "rust" || parserMode === "oxc" || parserMode === "babel") {
|
|
1292
|
+
return parserMode;
|
|
1293
|
+
}
|
|
1294
|
+
throw new Error(`Invalid --parser-mode "${parserMode}". Expected "rust", "oxc", or "babel".`);
|
|
1295
|
+
}
|
|
1296
|
+
function normalizeDebounceMs(debounceMs) {
|
|
1297
|
+
if (debounceMs === void 0) {
|
|
1298
|
+
return void 0;
|
|
1299
|
+
}
|
|
1300
|
+
const parsed = typeof debounceMs === "number" ? debounceMs : Number(debounceMs);
|
|
1301
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 6e4) {
|
|
1302
|
+
throw new Error("Invalid --debounce-ms. Expected an integer between 0 and 60000.");
|
|
1303
|
+
}
|
|
1304
|
+
return parsed;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const cli = cac("csszyx");
|
|
1308
|
+
normalizeNextCommandAlias(process.argv);
|
|
1309
|
+
function readCliVersion() {
|
|
1310
|
+
try {
|
|
1311
|
+
const manifest = JSON.parse(
|
|
1312
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8")
|
|
1313
|
+
);
|
|
1314
|
+
return typeof manifest.version === "string" ? manifest.version : "0.0.0";
|
|
1315
|
+
} catch {
|
|
1316
|
+
return "0.0.0";
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
const VERSION = readCliVersion();
|
|
1320
|
+
async function runNextPrebuildCommand(pattern, options) {
|
|
1321
|
+
const code = await nextPrebuild({
|
|
1322
|
+
cwd: options.cwd,
|
|
1323
|
+
root: options.root,
|
|
1324
|
+
mode: options.mode,
|
|
1325
|
+
parserMode: options.parserMode,
|
|
1326
|
+
outputFile: options.outputFile,
|
|
1327
|
+
cacheDir: options.cacheDir,
|
|
1328
|
+
pattern,
|
|
1329
|
+
extraIgnore: options.ignore ? String(options.ignore).split(",") : void 0,
|
|
1330
|
+
json: options.json
|
|
1331
|
+
});
|
|
1332
|
+
if (code !== 0) {
|
|
1333
|
+
process.exit(code);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
async function runNextWatchCommand(pattern, options) {
|
|
1337
|
+
const code = await nextWatch({
|
|
1338
|
+
cwd: options.cwd,
|
|
1339
|
+
root: options.root,
|
|
1340
|
+
parserMode: options.parserMode,
|
|
1341
|
+
outputFile: options.outputFile,
|
|
1342
|
+
cacheDir: options.cacheDir,
|
|
1343
|
+
pattern,
|
|
1344
|
+
extraIgnore: options.ignore ? String(options.ignore).split(",") : void 0,
|
|
1345
|
+
debounceMs: options.debounceMs
|
|
1346
|
+
});
|
|
1347
|
+
process.exitCode = code;
|
|
1348
|
+
}
|
|
1349
|
+
cli.command("init", "Setup csszyx in your project").option("--framework <name>", "Specify framework").option("--yes", "Skip prompts (use defaults)").option("--cwd <dir>", "Current working directory").action(async (options) => {
|
|
1350
|
+
await init({
|
|
1351
|
+
framework: options.framework,
|
|
1352
|
+
yes: options.yes,
|
|
1353
|
+
cwd: options.cwd
|
|
1354
|
+
});
|
|
1355
|
+
});
|
|
1356
|
+
cli.command("doctor", "Diagnose mangling issues").option("--verbose", "Show detailed output").option("--cwd <dir>", "Current working directory").action(async (options) => {
|
|
1357
|
+
await doctor({
|
|
1358
|
+
verbose: options.verbose,
|
|
1359
|
+
cwd: options.cwd
|
|
1360
|
+
});
|
|
1361
|
+
});
|
|
1362
|
+
cli.command("audit", "Analyze mangling performance").option("--json", "Output as JSON").option("--watch", "Live updates").option("--compare <dir>", "Compare with previous build").option("--cwd <dir>", "Current working directory").action(async (options) => {
|
|
1363
|
+
await audit({
|
|
1364
|
+
json: options.json,
|
|
1365
|
+
watch: options.watch,
|
|
1366
|
+
compare: options.compare,
|
|
1367
|
+
cwd: options.cwd
|
|
1368
|
+
});
|
|
1369
|
+
});
|
|
1370
|
+
cli.command("generate-types", "Generate TypeScript declarations from tailwind.config.js").option("-c, --config <path>", "Path to tailwind.config.js").option("-o, --output <path>", "Output file path (default: ./csszyx.d.ts)").option("--cwd <dir>", "Current working directory").option("--silent", "Silent mode (no output)").action(async (options) => {
|
|
1371
|
+
await generateTypes({
|
|
1372
|
+
config: options.config,
|
|
1373
|
+
output: options.output,
|
|
1374
|
+
cwd: options.cwd,
|
|
1375
|
+
silent: options.silent
|
|
1376
|
+
});
|
|
1377
|
+
});
|
|
1378
|
+
cli.command("migrate [dir]", "Convert Tailwind className to sz prop").option("--dry-run", "Show changes without modifying files").option("--ignore <patterns>", "Glob patterns to ignore (comma-separated)").option("--pattern <glob>", "Custom glob pattern for file discovery").option("--cwd <dir>", "Current working directory").option("--braces", "Wrap HTML sz values in outer { } braces (default: bare)").option("--no-fouc", "Skip FOUC-prevention CSS injection into HTML files").option("--inject-runtime <mode>", "Inject runtime script into HTML: local | cdn").option("--cdn-url <url>", "Custom CDN URL for --inject-runtime cdn").option(
|
|
1379
|
+
"--local-path <path>",
|
|
1380
|
+
"Local script path for --inject-runtime local (default: csszyx-runtime.js)"
|
|
1381
|
+
).option("--audit", "Scan without modifying files and output .csszyx-todo.json").option("--inject-todos", "Inject {/* @sz-todo */} comments above unrecognized classes").option("--resolve-todos <file>", "Path to a JSON file mapping custom classes to sz properties").action(async (dir, options) => {
|
|
1382
|
+
await migrate({
|
|
1383
|
+
dryRun: options.dryRun,
|
|
1384
|
+
ignore: options.ignore ? options.ignore.split(",") : void 0,
|
|
1385
|
+
pattern: options.pattern,
|
|
1386
|
+
cwd: dir || options.cwd,
|
|
1387
|
+
braces: options.braces,
|
|
1388
|
+
injectFouc: options.fouc !== false,
|
|
1389
|
+
injectRuntime: options.injectRuntime === "local" ? "local" : options.injectRuntime === "cdn" ? "cdn" : false,
|
|
1390
|
+
cdnUrl: options.cdnUrl,
|
|
1391
|
+
localPath: options.localPath,
|
|
1392
|
+
audit: options.audit,
|
|
1393
|
+
injectTodos: options.injectTodos,
|
|
1394
|
+
resolveTodos: options.resolveTodos
|
|
1395
|
+
});
|
|
1396
|
+
});
|
|
1397
|
+
cli.command(
|
|
1398
|
+
"next-prebuild [pattern]",
|
|
1399
|
+
"Seed the Next.js Turbopack csszyx safelist and generation manifest"
|
|
1400
|
+
).option("--root <dir>", "Next app root (defaults to cwd)").option("--cwd <dir>", "Current working directory").option("--mode <mode>", "development | production (default: production)").option("--parser-mode <mode>", "rust | oxc | babel (default: rust)").option(
|
|
1401
|
+
"--output-file <path>",
|
|
1402
|
+
"Tailwind @source safelist output (default: csszyx-classes.html)"
|
|
1403
|
+
).option("--cache-dir <dir>", "Cache directory relative to root (default: .csszyx/cache)").option("--ignore <patterns>", "Extra glob patterns to ignore (comma-separated)").option("--json", "Emit a single JSON result instead of formatted text").action(runNextPrebuildCommand);
|
|
1404
|
+
cli.command("next-watch [pattern]", "Maintain the Next.js Turbopack csszyx safelist").option("--root <dir>", "Next app root (defaults to cwd)").option("--cwd <dir>", "Current working directory").option("--parser-mode <mode>", "rust | oxc | babel (default: rust)").option(
|
|
1405
|
+
"--output-file <path>",
|
|
1406
|
+
"Tailwind @source safelist output (default: csszyx-classes.html)"
|
|
1407
|
+
).option("--cache-dir <dir>", "Cache directory relative to root (default: .csszyx/cache)").option("--ignore <patterns>", "Extra glob patterns to ignore (comma-separated)").option("--debounce-ms <ms>", "Safelist materialization debounce (default: 50)").action(runNextWatchCommand);
|
|
1408
|
+
cli.command("").action(() => {
|
|
1409
|
+
cli.outputHelp();
|
|
1410
|
+
});
|
|
1411
|
+
cli.help();
|
|
1412
|
+
cli.version(VERSION);
|
|
1413
|
+
cli.parse();
|
|
1414
|
+
function normalizeNextCommandAlias(argv) {
|
|
1415
|
+
if (argv[2] === "next" && (argv[3] === "prebuild" || argv[3] === "watch")) {
|
|
1416
|
+
argv.splice(2, 2, `next-${argv[3]}`);
|
|
1417
|
+
}
|
|
1418
|
+
}
|