@csszyx/cli 0.9.9 → 0.10.0

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