@csszyx/cli 0.8.0 → 0.9.1

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.
Files changed (3) hide show
  1. package/README.md +3 -0
  2. package/dist/index.mjs +672 -116
  3. package/package.json +17 -16
package/README.md CHANGED
@@ -61,6 +61,9 @@ npx csszyx generate-types --output ./src/csszyx.d.ts
61
61
  ### `migrate`
62
62
 
63
63
  Convert Tailwind `className="..."` to CSSzyx `sz={...}` props. Phase 1 supports static string classNames.
64
+ Display utilities migrate to canonical `display` props (`flex` →
65
+ `{ display: 'flex' }`) instead of boolean sugar, and conflicting display
66
+ utilities in the same variant scope stay unresolved for manual review.
64
67
 
65
68
  ```bash
66
69
  npx csszyx migrate src/
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import cac from 'cac';
3
- import path, { resolve, dirname } from 'node:path';
3
+ import * as path from 'node:path';
4
+ import path__default, { resolve, dirname } from 'node:path';
4
5
  import fs from 'fs-extra';
5
6
  import ora from 'ora';
6
7
  import pc from 'picocolors';
@@ -13,8 +14,11 @@ import prompts from 'prompts';
13
14
  import readline from 'node:readline';
14
15
  import fg from 'fast-glob';
15
16
  import { parse } from '@babel/parser';
16
- import _traverse from '@babel/traverse';
17
17
  import * as t from '@babel/types';
18
+ import { runNextPrebuild } from '@csszyx/unplugin/next-prebuild';
19
+ import { NextSafelistWatcher } from '@csszyx/unplugin/next-watcher';
20
+ import { watch } from 'chokidar';
21
+ import { Minimatch } from 'minimatch';
18
22
 
19
23
  const colors = {
20
24
  success: pc.green,
@@ -141,19 +145,19 @@ async function collectStats(cwd) {
141
145
  mangledCSS: 0
142
146
  }
143
147
  };
144
- const distDir = path.join(cwd, "dist");
148
+ const distDir = path__default.join(cwd, "dist");
145
149
  if (!fs.existsSync(distDir)) {
146
150
  return stats;
147
151
  }
148
152
  const htmlFiles = fs.readdirSync(distDir, { recursive: true }).filter((f) => String(f).endsWith(".html"));
149
153
  const cssFiles = fs.readdirSync(distDir, { recursive: true }).filter((f) => String(f).endsWith(".css"));
150
154
  if (htmlFiles.length > 0) {
151
- const htmlContent = fs.readFileSync(path.join(distDir, String(htmlFiles[0])), "utf-8");
155
+ const htmlContent = fs.readFileSync(path__default.join(distDir, String(htmlFiles[0])), "utf-8");
152
156
  stats.bundleSavings.mangledHTML = Buffer.byteLength(htmlContent);
153
157
  stats.bundleSavings.originalHTML = Math.round(stats.bundleSavings.mangledHTML * 1.67);
154
158
  }
155
159
  if (cssFiles.length > 0) {
156
- const cssContent = fs.readFileSync(path.join(distDir, String(cssFiles[0])), "utf-8");
160
+ const cssContent = fs.readFileSync(path__default.join(distDir, String(cssFiles[0])), "utf-8");
157
161
  stats.bundleSavings.mangledCSS = Buffer.byteLength(cssContent);
158
162
  stats.bundleSavings.originalCSS = Math.round(stats.bundleSavings.mangledCSS * 1.71);
159
163
  }
@@ -174,14 +178,14 @@ function formatBytes(bytes) {
174
178
 
175
179
  function detectFramework(cwd) {
176
180
  try {
177
- const pkgPath = path.join(cwd, "package.json");
181
+ const pkgPath = path__default.join(cwd, "package.json");
178
182
  if (!fs.existsSync(pkgPath)) {
179
183
  return "unknown";
180
184
  }
181
185
  const pkg = fs.readJSONSync(pkgPath);
182
186
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
183
187
  if (deps.next) {
184
- const hasAppDir = fs.existsSync(path.join(cwd, "app"));
188
+ const hasAppDir = fs.existsSync(path__default.join(cwd, "app"));
185
189
  return hasAppDir ? "nextjs-app" : "nextjs-pages";
186
190
  }
187
191
  if (deps.nuxt) {
@@ -210,20 +214,20 @@ function detectFramework(cwd) {
210
214
  }
211
215
  }
212
216
  function detectPackageManager(cwd) {
213
- if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) {
217
+ if (fs.existsSync(path__default.join(cwd, "pnpm-lock.yaml"))) {
214
218
  return "pnpm";
215
219
  }
216
- if (fs.existsSync(path.join(cwd, "yarn.lock"))) {
220
+ if (fs.existsSync(path__default.join(cwd, "yarn.lock"))) {
217
221
  return "yarn";
218
222
  }
219
- if (fs.existsSync(path.join(cwd, "bun.lockb"))) {
223
+ if (fs.existsSync(path__default.join(cwd, "bun.lockb"))) {
220
224
  return "bun";
221
225
  }
222
226
  return "npm";
223
227
  }
224
228
  function hasTailwindInstalled(cwd) {
225
229
  try {
226
- const pkgPath = path.join(cwd, "package.json");
230
+ const pkgPath = path__default.join(cwd, "package.json");
227
231
  if (!fs.existsSync(pkgPath)) {
228
232
  return false;
229
233
  }
@@ -235,7 +239,7 @@ function hasTailwindInstalled(cwd) {
235
239
  }
236
240
  }
237
241
  function hasTypeScript(cwd) {
238
- return fs.existsSync(path.join(cwd, "tsconfig.json")) || fs.existsSync(path.join(cwd, "jsconfig.json"));
242
+ return fs.existsSync(path__default.join(cwd, "tsconfig.json")) || fs.existsSync(path__default.join(cwd, "jsconfig.json"));
239
243
  }
240
244
  function getProjectInfo(cwd = process.cwd()) {
241
245
  return {
@@ -267,7 +271,7 @@ async function doctor(options = {}) {
267
271
  printHeader("csszyx Doctor");
268
272
  let issueCount = 0;
269
273
  printSection("\u{1F4CB} Configuration Health");
270
- const hasConfig = fs.existsSync(path.join(cwd, "csszyx.config.ts")) || fs.existsSync(path.join(cwd, "csszyx.config.js"));
274
+ const hasConfig = fs.existsSync(path__default.join(cwd, "csszyx.config.ts")) || fs.existsSync(path__default.join(cwd, "csszyx.config.js"));
271
275
  if (hasConfig) {
272
276
  printSuccess("csszyx configuration found");
273
277
  } else {
@@ -284,7 +288,7 @@ async function doctor(options = {}) {
284
288
  }
285
289
  printSection("\u{1F4E6} Package Versions");
286
290
  try {
287
- const pkgPath = path.join(cwd, "package.json");
291
+ const pkgPath = path__default.join(cwd, "package.json");
288
292
  const pkg = fs.readJSONSync(pkgPath);
289
293
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
290
294
  if (deps.csszyx) {
@@ -298,12 +302,12 @@ async function doctor(options = {}) {
298
302
  issueCount++;
299
303
  }
300
304
  printSection("\u{1F528} Build Output");
301
- const distDir = path.join(cwd, "dist");
305
+ const distDir = path__default.join(cwd, "dist");
302
306
  if (fs.existsSync(distDir)) {
303
307
  const htmlFiles = fs.readdirSync(distDir, { recursive: true }).filter((f) => String(f).endsWith(".html"));
304
308
  if (htmlFiles.length > 0) {
305
309
  printSuccess(`Found ${htmlFiles.length} HTML file(s)`);
306
- const htmlContent = fs.readFileSync(path.join(distDir, String(htmlFiles[0])), "utf-8");
310
+ const htmlContent = fs.readFileSync(path__default.join(distDir, String(htmlFiles[0])), "utf-8");
307
311
  if (htmlContent.includes("data-sz-checksum")) {
308
312
  printSuccess("Checksum injection working");
309
313
  } else {
@@ -1196,6 +1200,16 @@ async function generateTypes(options = {}) {
1196
1200
  }
1197
1201
 
1198
1202
  const VITE_FRAMEWORKS = /* @__PURE__ */ new Set(["vite-react", "vite-vue", "vite-svelte"]);
1203
+ async function readFileOrNull(filePath) {
1204
+ try {
1205
+ return await fs.readFile(filePath, "utf8");
1206
+ } catch (err) {
1207
+ if (err?.code === "ENOENT") {
1208
+ return null;
1209
+ }
1210
+ throw err;
1211
+ }
1212
+ }
1199
1213
  const NEXTJS_FRAMEWORKS = /* @__PURE__ */ new Set(["nextjs-app", "nextjs-pages"]);
1200
1214
  const CSS_ENTRY_CANDIDATES = [
1201
1215
  "src/index.css",
@@ -1273,7 +1287,7 @@ async function init(options = {}) {
1273
1287
  const spin2 = spinner.start("Creating config files...");
1274
1288
  try {
1275
1289
  const configContent = generateConfigFile(config);
1276
- const configPath = path.join(
1290
+ const configPath = path__default.join(
1277
1291
  cwd,
1278
1292
  projectInfo.hasTypeScript ? "csszyx.config.ts" : "csszyx.config.js"
1279
1293
  );
@@ -1283,11 +1297,11 @@ async function init(options = {}) {
1283
1297
  }
1284
1298
  await injectPlugin(cwd, projectInfo.framework);
1285
1299
  if (config.setupGitignore) {
1286
- const gitignorePath = path.join(cwd, ".gitignore");
1300
+ const gitignorePath = path__default.join(cwd, ".gitignore");
1287
1301
  const ignoreEntry = "\n# csszyx generated theme types\n.csszyx\n";
1288
- if (fs.existsSync(gitignorePath)) {
1289
- const content = await fs.readFile(gitignorePath, "utf8");
1290
- if (!content.includes(".csszyx")) {
1302
+ const existing = await readFileOrNull(gitignorePath);
1303
+ if (existing !== null) {
1304
+ if (!existing.includes(".csszyx")) {
1291
1305
  await fs.appendFile(gitignorePath, ignoreEntry);
1292
1306
  }
1293
1307
  } else {
@@ -1312,32 +1326,35 @@ async function init(options = {}) {
1312
1326
  }
1313
1327
  async function setupTailwindCss(cwd, framework) {
1314
1328
  let cssPath;
1329
+ let content = null;
1315
1330
  for (const candidate of CSS_ENTRY_CANDIDATES) {
1316
- const full = path.join(cwd, candidate);
1317
- if (fs.existsSync(full)) {
1331
+ const full = path__default.join(cwd, candidate);
1332
+ const existing = await readFileOrNull(full);
1333
+ if (existing !== null) {
1318
1334
  cssPath = full;
1335
+ content = existing;
1319
1336
  break;
1320
1337
  }
1321
1338
  }
1322
- if (!cssPath) {
1323
- cssPath = path.join(cwd, "src/index.css");
1324
- await fs.ensureDir(path.dirname(cssPath));
1339
+ if (!cssPath || content === null) {
1340
+ cssPath = path__default.join(cwd, "src/index.css");
1341
+ await fs.ensureDir(path__default.dirname(cssPath));
1325
1342
  await fs.writeFile(cssPath, '@import "tailwindcss";\n');
1326
- printInfo(`Created ${path.relative(cwd, cssPath)} with Tailwind v4 import`);
1343
+ printInfo(`Created ${path__default.relative(cwd, cssPath)} with Tailwind v4 import`);
1327
1344
  return;
1328
1345
  }
1329
- const content = await fs.readFile(cssPath, "utf8");
1330
1346
  if (!content.includes('@import "tailwindcss"') && !content.includes("@import 'tailwindcss'")) {
1331
1347
  await fs.writeFile(cssPath, `@import "tailwindcss";
1332
1348
 
1333
1349
  ${content}`);
1334
- printInfo(`Added Tailwind v4 import to ${path.relative(cwd, cssPath)}`);
1350
+ printInfo(`Added Tailwind v4 import to ${path__default.relative(cwd, cssPath)}`);
1335
1351
  }
1336
1352
  if (NEXTJS_FRAMEWORKS.has(framework)) {
1337
- const postcssMjs = path.join(cwd, "postcss.config.mjs");
1338
- const postcssJs = path.join(cwd, "postcss.config.js");
1339
- const postcssTs = path.join(cwd, "postcss.config.ts");
1340
- if (!fs.existsSync(postcssMjs) && !fs.existsSync(postcssJs) && !fs.existsSync(postcssTs)) {
1353
+ const postcssMjs = path__default.join(cwd, "postcss.config.mjs");
1354
+ const postcssJs = path__default.join(cwd, "postcss.config.js");
1355
+ const postcssTs = path__default.join(cwd, "postcss.config.ts");
1356
+ const hasExisting = await readFileOrNull(postcssMjs) !== null || await readFileOrNull(postcssJs) !== null || await readFileOrNull(postcssTs) !== null;
1357
+ if (!hasExisting) {
1341
1358
  await fs.writeFile(postcssMjs, generatePostcssConfig());
1342
1359
  printInfo("Created postcss.config.mjs for Tailwind v4");
1343
1360
  }
@@ -1363,22 +1380,24 @@ async function injectPlugin(cwd, framework) {
1363
1380
  }
1364
1381
  async function injectVitePlugin(cwd) {
1365
1382
  const candidates = [
1366
- path.join(cwd, "vite.config.ts"),
1367
- path.join(cwd, "vite.config.js"),
1368
- path.join(cwd, "vite.config.mts"),
1369
- path.join(cwd, "vite.config.mjs")
1383
+ path__default.join(cwd, "vite.config.ts"),
1384
+ path__default.join(cwd, "vite.config.js"),
1385
+ path__default.join(cwd, "vite.config.mts"),
1386
+ path__default.join(cwd, "vite.config.mjs")
1370
1387
  ];
1371
1388
  let configPath;
1389
+ let content = null;
1372
1390
  for (const c of candidates) {
1373
- if (fs.existsSync(c)) {
1391
+ const existing = await readFileOrNull(c);
1392
+ if (existing !== null) {
1374
1393
  configPath = c;
1394
+ content = existing;
1375
1395
  break;
1376
1396
  }
1377
1397
  }
1378
- if (!configPath) {
1398
+ if (!configPath || content === null) {
1379
1399
  return false;
1380
1400
  }
1381
- let content = await fs.readFile(configPath, "utf8");
1382
1401
  if (content.includes("csszyx")) {
1383
1402
  return true;
1384
1403
  }
@@ -1402,46 +1421,48 @@ ${importBlock}${content.slice(insertAt)}`;
1402
1421
  content = content.slice(0, pluginsInsertAt) + `
1403
1422
  ...csszyx(), // csszyx MUST come before tailwindcss${twEntry}` + content.slice(pluginsInsertAt);
1404
1423
  await fs.writeFile(configPath, content);
1405
- printInfo(`Injected csszyx plugin into ${path.basename(configPath)}`);
1424
+ printInfo(`Injected csszyx plugin into ${path__default.basename(configPath)}`);
1406
1425
  return true;
1407
1426
  }
1408
1427
  async function injectNextPlugin(cwd) {
1409
1428
  const candidates = [
1410
- path.join(cwd, "next.config.ts"),
1411
- path.join(cwd, "next.config.mjs"),
1412
- path.join(cwd, "next.config.js")
1429
+ path__default.join(cwd, "next.config.ts"),
1430
+ path__default.join(cwd, "next.config.mjs"),
1431
+ path__default.join(cwd, "next.config.js")
1413
1432
  ];
1414
1433
  let configPath;
1434
+ let content = null;
1415
1435
  for (const c of candidates) {
1416
- if (fs.existsSync(c)) {
1436
+ const existing = await readFileOrNull(c);
1437
+ if (existing !== null) {
1417
1438
  configPath = c;
1439
+ content = existing;
1418
1440
  break;
1419
1441
  }
1420
1442
  }
1421
1443
  if (!configPath) {
1422
- configPath = path.join(cwd, "next.config.js");
1444
+ configPath = path__default.join(cwd, "next.config.js");
1423
1445
  await fs.writeFile(configPath, generateNextConfig());
1424
1446
  printInfo("Created next.config.js with csszyx plugin");
1425
1447
  return true;
1426
1448
  }
1427
- const content = await fs.readFile(configPath, "utf8");
1428
- if (content.includes("csszyx")) {
1449
+ if (content?.includes("csszyx")) {
1429
1450
  return true;
1430
1451
  }
1431
1452
  return false;
1432
1453
  }
1433
1454
  async function setupTsconfig(cwd) {
1434
- let tsconfigPath = path.join(cwd, "tsconfig.json");
1435
- if (!fs.existsSync(tsconfigPath)) {
1436
- const viteTsConfig = path.join(cwd, "tsconfig.app.json");
1437
- if (fs.existsSync(viteTsConfig)) {
1438
- tsconfigPath = viteTsConfig;
1439
- }
1440
- }
1441
- if (!fs.existsSync(tsconfigPath)) {
1455
+ const primary = path__default.join(cwd, "tsconfig.json");
1456
+ const viteTsConfig = path__default.join(cwd, "tsconfig.app.json");
1457
+ let tsconfigPath = primary;
1458
+ let content = await readFileOrNull(tsconfigPath);
1459
+ if (content === null) {
1460
+ tsconfigPath = viteTsConfig;
1461
+ content = await readFileOrNull(tsconfigPath);
1462
+ }
1463
+ if (content === null) {
1442
1464
  return;
1443
1465
  }
1444
- let content = await fs.readFile(tsconfigPath, "utf8");
1445
1466
  if (content.includes(".csszyx")) {
1446
1467
  return;
1447
1468
  }
@@ -1565,7 +1586,7 @@ function isGradientObj(v) {
1565
1586
  return typeof v === "object" && v !== null && "gradient" in v;
1566
1587
  }
1567
1588
  function formatKey(key) {
1568
- if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) {
1589
+ if (/^[a-z_$][\w$]*$/i.test(key)) {
1569
1590
  return key;
1570
1591
  }
1571
1592
  return `'${key}'`;
@@ -1590,7 +1611,7 @@ function formatValue(value, indent) {
1590
1611
  const items = value.map((v) => formatValue(v, indent));
1591
1612
  return `[${items.join(", ")}]`;
1592
1613
  }
1593
- if (typeof value === "object" && value !== null) {
1614
+ if (typeof value === "object") {
1594
1615
  if (isColorOpacityObj(value)) {
1595
1616
  const parts = [`color: '${escapeString(String(value.color))}'`];
1596
1617
  if (typeof value.op === "number") {
@@ -2351,7 +2372,7 @@ const KNOWN_SIMPLE_VARIANTS = /* @__PURE__ */ new Set([
2351
2372
  "pointer-none"
2352
2373
  ]);
2353
2374
 
2354
- function parseClass(cls) {
2375
+ function parseClass(cls, options = {}) {
2355
2376
  let important = false;
2356
2377
  let input = cls;
2357
2378
  if (input.endsWith("!")) {
@@ -2364,7 +2385,7 @@ function parseClass(cls) {
2364
2385
  negative = true;
2365
2386
  negInput = input.slice(1);
2366
2387
  }
2367
- const boolResult = tryBooleanMatch(input);
2388
+ const boolResult = tryBooleanMatch(input, options);
2368
2389
  if (boolResult) {
2369
2390
  return applyImportant(boolResult, important);
2370
2391
  }
@@ -2380,10 +2401,7 @@ function parseClass(cls) {
2380
2401
  continue;
2381
2402
  }
2382
2403
  if (REVERSE_BOOLEAN_MAP[source]) {
2383
- return applyImportant(
2384
- { prop: REVERSE_BOOLEAN_MAP[source], value: true },
2385
- important
2386
- );
2404
+ return applyImportant(booleanClassToParsed(source, options), important);
2387
2405
  }
2388
2406
  if (prefix === "divide-x" || prefix === "divide-y") {
2389
2407
  return applyImportant({ prop, value: true }, important);
@@ -2422,7 +2440,7 @@ function parseClass(cls) {
2422
2440
  }
2423
2441
  }
2424
2442
  }
2425
- const displayResult = tryDisplay(input);
2443
+ const displayResult = tryDisplay(input, options);
2426
2444
  if (displayResult) {
2427
2445
  return applyImportant(displayResult, important);
2428
2446
  }
@@ -2456,17 +2474,17 @@ function applyImportant(result, important) {
2456
2474
  }
2457
2475
  return result;
2458
2476
  }
2459
- function tryBooleanMatch(cls) {
2477
+ function tryBooleanMatch(cls, options) {
2460
2478
  if (BOOLEAN_VALUE_MAP[cls]) {
2461
2479
  const { prop, value } = BOOLEAN_VALUE_MAP[cls];
2462
2480
  return { prop, value };
2463
2481
  }
2464
2482
  if (REVERSE_BOOLEAN_MAP[cls]) {
2465
- return { prop: REVERSE_BOOLEAN_MAP[cls], value: true };
2483
+ return booleanClassToParsed(cls, options);
2466
2484
  }
2467
2485
  return null;
2468
2486
  }
2469
- function tryDisplay(cls) {
2487
+ function tryDisplay(cls, options) {
2470
2488
  const displayValues = /* @__PURE__ */ new Set([
2471
2489
  "block",
2472
2490
  "inline",
@@ -2484,10 +2502,33 @@ function tryDisplay(cls) {
2484
2502
  "list-item"
2485
2503
  ]);
2486
2504
  if (displayValues.has(cls)) {
2487
- return REVERSE_BOOLEAN_MAP[cls] ? { prop: REVERSE_BOOLEAN_MAP[cls], value: true } : null;
2505
+ return REVERSE_BOOLEAN_MAP[cls] ? booleanClassToParsed(cls, options) : null;
2488
2506
  }
2489
2507
  return null;
2490
2508
  }
2509
+ function booleanClassToParsed(cls, options) {
2510
+ const displayValue = DISPLAY_CLASS_VALUES[cls];
2511
+ if (options.display === "canonical" && displayValue) {
2512
+ return { prop: "display", value: displayValue, cssProperty: "display" };
2513
+ }
2514
+ return { prop: REVERSE_BOOLEAN_MAP[cls], value: true };
2515
+ }
2516
+ const DISPLAY_CLASS_VALUES = {
2517
+ block: "block",
2518
+ inline: "inline",
2519
+ "inline-block": "inline-block",
2520
+ flex: "flex",
2521
+ "inline-flex": "inline-flex",
2522
+ grid: "grid",
2523
+ "inline-grid": "inline-grid",
2524
+ hidden: "none",
2525
+ contents: "contents",
2526
+ table: "table",
2527
+ "table-row": "table-row",
2528
+ "table-cell": "table-cell",
2529
+ "flow-root": "flow-root",
2530
+ "list-item": "list-item"
2531
+ };
2491
2532
  function tryGradient(cls, negative) {
2492
2533
  let input = cls;
2493
2534
  let type = null;
@@ -3133,6 +3174,8 @@ function normalizeVariantKey(variant) {
3133
3174
  const TODO_KEEP = "sz:keep";
3134
3175
  const TODO_REMOVE = "sz:remove";
3135
3176
  const TODO_PENDING = "sz:todo";
3177
+ const MAX_TOKEN_CACHE_SIZE = 4096;
3178
+ const parsedTokenCache = /* @__PURE__ */ new Map();
3136
3179
  function resolveCustomMapEntry(token, customMap, resolveString) {
3137
3180
  if (!(token in customMap)) {
3138
3181
  return null;
@@ -3164,6 +3207,8 @@ function classNameToSzObject(className, customMap) {
3164
3207
  const szObject = {};
3165
3208
  const unrecognized = [];
3166
3209
  const keepInClassName = [];
3210
+ const seenCssPropertiesByPath = /* @__PURE__ */ new Map();
3211
+ const conflictedCssPropertiesByPath = /* @__PURE__ */ new Map();
3167
3212
  for (const token of tokens) {
3168
3213
  if (customMap && token in customMap) {
3169
3214
  const entry = resolveCustomMapEntry(
@@ -3201,21 +3246,79 @@ function classNameToSzObject(className, customMap) {
3201
3246
  }
3202
3247
  }
3203
3248
  }
3204
- const { variantParts, baseClass } = extractVariants(token);
3205
- const parsed = parseClass(baseClass);
3206
- if (!parsed) {
3249
+ const parsedToken = parseClassTokenCached(token);
3250
+ if (!parsedToken) {
3251
+ unrecognized.push(token);
3252
+ continue;
3253
+ }
3254
+ if (isCssPropertyConflicted(conflictedCssPropertiesByPath, parsedToken)) {
3207
3255
  unrecognized.push(token);
3208
3256
  continue;
3209
3257
  }
3210
- const variantKeys = variantParts.map((v) => mapVariant(v));
3211
- const keyPath = [];
3212
- for (const vk of variantKeys) {
3213
- keyPath.push(...vk);
3258
+ const conflict = findCssPropertyConflict(seenCssPropertiesByPath, parsedToken, token);
3259
+ if (conflict) {
3260
+ rememberCssPropertyConflict(conflictedCssPropertiesByPath, parsedToken);
3261
+ unrecognized.push(conflict, token);
3262
+ removeNestedValue(szObject, parsedToken.keyPath, parsedToken.prop);
3263
+ continue;
3214
3264
  }
3215
- setNestedValue(szObject, keyPath, parsed.prop, parsed.value);
3265
+ rememberCssProperty(seenCssPropertiesByPath, parsedToken, token);
3266
+ setNestedValue(
3267
+ szObject,
3268
+ parsedToken.keyPath,
3269
+ parsedToken.prop,
3270
+ cloneParsedValue(parsedToken.value)
3271
+ );
3216
3272
  }
3217
3273
  return { szObject, unrecognized, keepInClassName };
3218
3274
  }
3275
+ function parseClassTokenCached(token) {
3276
+ if (parsedTokenCache.has(token)) {
3277
+ return parsedTokenCache.get(token) ?? null;
3278
+ }
3279
+ const parsed = parseClassToken(token);
3280
+ rememberParsedToken(token, parsed);
3281
+ return parsed;
3282
+ }
3283
+ function parseClassToken(token) {
3284
+ const { variantParts, baseClass } = extractVariants(token);
3285
+ const parsed = parseClass(baseClass, { display: "canonical" });
3286
+ if (!parsed) {
3287
+ return null;
3288
+ }
3289
+ const keyPath = [];
3290
+ for (const variant of variantParts) {
3291
+ keyPath.push(...mapVariant(variant));
3292
+ }
3293
+ return {
3294
+ keyPath,
3295
+ prop: parsed.prop,
3296
+ value: parsed.value,
3297
+ cssProperty: parsed.cssProperty
3298
+ };
3299
+ }
3300
+ function rememberParsedToken(token, parsed) {
3301
+ if (parsedTokenCache.size >= MAX_TOKEN_CACHE_SIZE) {
3302
+ const oldest = parsedTokenCache.keys().next().value;
3303
+ if (oldest !== void 0) {
3304
+ parsedTokenCache.delete(oldest);
3305
+ }
3306
+ }
3307
+ parsedTokenCache.set(token, parsed);
3308
+ }
3309
+ function cloneParsedValue(value) {
3310
+ if (Array.isArray(value)) {
3311
+ return value.map(cloneParsedValue);
3312
+ }
3313
+ if (value && typeof value === "object") {
3314
+ const clone = {};
3315
+ for (const [key, nested] of Object.entries(value)) {
3316
+ clone[key] = cloneParsedValue(nested);
3317
+ }
3318
+ return clone;
3319
+ }
3320
+ return value;
3321
+ }
3219
3322
  function setNestedValue(obj, keyPath, prop, value) {
3220
3323
  let current = obj;
3221
3324
  for (const key of keyPath) {
@@ -3226,6 +3329,64 @@ function setNestedValue(obj, keyPath, prop, value) {
3226
3329
  }
3227
3330
  current[prop] = value;
3228
3331
  }
3332
+ function findCssPropertyConflict(seen, parsed, token) {
3333
+ if (!parsed.cssProperty) {
3334
+ return null;
3335
+ }
3336
+ const scope = parsed.keyPath.join("\0");
3337
+ const previous = seen.get(scope)?.get(parsed.cssProperty);
3338
+ return previous && previous !== token ? previous : null;
3339
+ }
3340
+ function isCssPropertyConflicted(conflicted, parsed) {
3341
+ if (!parsed.cssProperty) {
3342
+ return false;
3343
+ }
3344
+ return conflicted.get(parsed.keyPath.join("\0"))?.has(parsed.cssProperty) === true;
3345
+ }
3346
+ function rememberCssPropertyConflict(conflicted, parsed) {
3347
+ if (!parsed.cssProperty) {
3348
+ return;
3349
+ }
3350
+ const scope = parsed.keyPath.join("\0");
3351
+ let properties = conflicted.get(scope);
3352
+ if (!properties) {
3353
+ properties = /* @__PURE__ */ new Set();
3354
+ conflicted.set(scope, properties);
3355
+ }
3356
+ properties.add(parsed.cssProperty);
3357
+ }
3358
+ function rememberCssProperty(seen, parsed, token) {
3359
+ if (!parsed.cssProperty) {
3360
+ return;
3361
+ }
3362
+ const scope = parsed.keyPath.join("\0");
3363
+ let properties = seen.get(scope);
3364
+ if (!properties) {
3365
+ properties = /* @__PURE__ */ new Map();
3366
+ seen.set(scope, properties);
3367
+ }
3368
+ properties.set(parsed.cssProperty, token);
3369
+ }
3370
+ function removeNestedValue(obj, keyPath, prop) {
3371
+ let current = obj;
3372
+ const parents = [];
3373
+ for (const key of keyPath) {
3374
+ const next = current[key];
3375
+ if (!next || typeof next !== "object" || Array.isArray(next)) {
3376
+ return;
3377
+ }
3378
+ parents.push([current, key]);
3379
+ current = next;
3380
+ }
3381
+ delete current[prop];
3382
+ for (let index = parents.length - 1; index >= 0; index--) {
3383
+ const [parent, key] = parents[index];
3384
+ const child = parent[key];
3385
+ if (child && typeof child === "object" && Object.keys(child).length === 0) {
3386
+ delete parent[key];
3387
+ }
3388
+ }
3389
+ }
3229
3390
 
3230
3391
  const CLSX_LIKE_NAMES = /* @__PURE__ */ new Set(["clsx", "cn", "cx", "twMerge", "classNames", "classnames"]);
3231
3392
  function isClsxLikeName(name) {
@@ -3532,7 +3693,7 @@ function isExpression(node, t) {
3532
3693
  return t.isExpression(node);
3533
3694
  }
3534
3695
 
3535
- const traverse = typeof _traverse === "function" ? _traverse : _traverse.default;
3696
+ const VISITOR_KEYS = t.VISITOR_KEYS;
3536
3697
  function injectTodoComment(unrecognized, parent, options, replacements) {
3537
3698
  if (!options.injectTodos || unrecognized.length === 0) {
3538
3699
  return;
@@ -3548,6 +3709,39 @@ function injectTodoComment(unrecognized, parent, options, replacements) {
3548
3709
  `
3549
3710
  });
3550
3711
  }
3712
+ function walkAst(node, visitors, ancestors = []) {
3713
+ if (t.isImportDeclaration(node)) {
3714
+ visitors.ImportDeclaration?.(node);
3715
+ } else if (t.isCallExpression(node)) {
3716
+ visitors.CallExpression?.(node, ancestors);
3717
+ } else if (t.isJSXAttribute(node)) {
3718
+ visitors.JSXAttribute?.(node, ancestors[ancestors.length - 1] ?? null);
3719
+ }
3720
+ const keys = VISITOR_KEYS[node.type];
3721
+ if (!keys) {
3722
+ return;
3723
+ }
3724
+ ancestors.push(node);
3725
+ for (const key of keys) {
3726
+ const child = node[key];
3727
+ if (Array.isArray(child)) {
3728
+ for (const item of child) {
3729
+ if (isAstNode(item)) {
3730
+ walkAst(item, visitors, ancestors);
3731
+ }
3732
+ }
3733
+ } else if (isAstNode(child)) {
3734
+ walkAst(child, visitors, ancestors);
3735
+ }
3736
+ }
3737
+ ancestors.pop();
3738
+ }
3739
+ function isAstNode(value) {
3740
+ return Boolean(value && typeof value === "object" && "type" in value);
3741
+ }
3742
+ function isClassNameJsxAttribute(node) {
3743
+ return t.isJSXAttribute(node) && t.isJSXIdentifier(node.name) && node.name.name === "className";
3744
+ }
3551
3745
  function transformSource(source, filePath, options = {}) {
3552
3746
  const warnings = [];
3553
3747
  let classNamesTransformed = 0;
@@ -3558,6 +3752,15 @@ function transformSource(source, filePath, options = {}) {
3558
3752
  let clsxUsedOutsideClassName = false;
3559
3753
  const clsxCallsitesMigrated = /* @__PURE__ */ new Set();
3560
3754
  let hasCvaImport = false;
3755
+ if (source.indexOf("className") === -1 && source.indexOf("cva") === -1) {
3756
+ return {
3757
+ code: source,
3758
+ changed: false,
3759
+ warnings: [],
3760
+ stats: { classNamesTransformed: 0, classNamesSkipped: 0, classesUnrecognized: [] },
3761
+ potentiallyUnusedImports: []
3762
+ };
3763
+ }
3561
3764
  let ast;
3562
3765
  try {
3563
3766
  ast = parse(source, {
@@ -3575,41 +3778,35 @@ function transformSource(source, filePath, options = {}) {
3575
3778
  potentiallyUnusedImports: []
3576
3779
  };
3577
3780
  }
3578
- traverse(ast, {
3579
- // Track clsx-like and CVA imports
3580
- ImportDeclaration(path) {
3581
- const src = path.node.source.value;
3781
+ walkAst(ast, {
3782
+ ImportDeclaration(node) {
3783
+ const src = node.source.value;
3582
3784
  const clsxPackages = ["clsx", "clsx/lite", "classnames", "tailwind-merge"];
3583
3785
  const isClsxPkg = clsxPackages.some((p) => src === p || src.startsWith(`${p}/`));
3584
3786
  const cvaPkgs = ["cva", "class-variance-authority"];
3585
3787
  if (cvaPkgs.some((p) => src === p || src.startsWith(`${p}/`))) {
3586
3788
  hasCvaImport = true;
3587
3789
  }
3588
- for (const spec of path.node.specifiers) {
3790
+ for (const spec of node.specifiers) {
3589
3791
  const localName = spec.local.name;
3590
3792
  if (isClsxPkg || isClsxLikeName(localName)) {
3591
3793
  clsxImportNames.add(localName);
3592
3794
  }
3593
3795
  }
3594
3796
  },
3595
- // Track clsx usage outside className
3596
- CallExpression(path) {
3597
- if (t.isIdentifier(path.node.callee) && clsxImportNames.has(path.node.callee.name)) {
3598
- const inClassName = path.findParent(
3599
- (p) => t.isJSXAttribute(p.node) && t.isJSXIdentifier(p.node.name) && p.node.name.name === "className"
3600
- );
3797
+ CallExpression(node, ancestors) {
3798
+ if (t.isIdentifier(node.callee) && clsxImportNames.has(node.callee.name)) {
3799
+ const inClassName = ancestors.some(isClassNameJsxAttribute);
3601
3800
  if (!inClassName) {
3602
3801
  clsxUsedOutsideClassName = true;
3603
3802
  }
3604
3803
  }
3605
3804
  },
3606
- // Main transformation: className → sz
3607
- JSXAttribute(path) {
3608
- const attrName = path.node.name;
3805
+ JSXAttribute(node, parent) {
3806
+ const attrName = node.name;
3609
3807
  if (!t.isJSXIdentifier(attrName) || attrName.name !== "className") {
3610
3808
  return;
3611
3809
  }
3612
- const parent = path.parent;
3613
3810
  if (t.isJSXOpeningElement(parent)) {
3614
3811
  const elementName = parent.name;
3615
3812
  const isCapitalized = t.isJSXIdentifier(elementName) && /^[A-Z]/.test(elementName.name) || t.isJSXMemberExpression(elementName);
@@ -3627,9 +3824,9 @@ function transformSource(source, filePath, options = {}) {
3627
3824
  return;
3628
3825
  }
3629
3826
  }
3630
- const value = path.node.value;
3631
- const attrStart = path.node.start;
3632
- const attrEnd = path.node.end;
3827
+ const value = node.value;
3828
+ const attrStart = node.start;
3829
+ const attrEnd = node.end;
3633
3830
  if (attrStart === null || attrStart === void 0 || attrEnd === null || attrEnd === void 0) {
3634
3831
  return;
3635
3832
  }
@@ -3639,7 +3836,7 @@ function transformSource(source, filePath, options = {}) {
3639
3836
  replacements.push({ start: attrStart, end: attrEnd, text: result.replacement });
3640
3837
  classNamesTransformed++;
3641
3838
  classesUnrecognized.push(...result.unrecognized);
3642
- injectTodoComment(result.unrecognized, path.parent, options, replacements);
3839
+ injectTodoComment(result.unrecognized, parent, options, replacements);
3643
3840
  } else {
3644
3841
  classNamesSkipped++;
3645
3842
  }
@@ -3657,7 +3854,7 @@ function transformSource(source, filePath, options = {}) {
3657
3854
  });
3658
3855
  classNamesTransformed++;
3659
3856
  classesUnrecognized.push(...result.unrecognized);
3660
- injectTodoComment(result.unrecognized, path.parent, options, replacements);
3857
+ injectTodoComment(result.unrecognized, parent, options, replacements);
3661
3858
  } else {
3662
3859
  classNamesSkipped++;
3663
3860
  }
@@ -3677,7 +3874,7 @@ function transformSource(source, filePath, options = {}) {
3677
3874
  classNamesSkipped++;
3678
3875
  warnings.push(...result.warnings.map((w) => `[${filePath}] ${w}`));
3679
3876
  }
3680
- injectTodoComment(result.unrecognized, path.parent, options, replacements);
3877
+ injectTodoComment(result.unrecognized, parent, options, replacements);
3681
3878
  return;
3682
3879
  }
3683
3880
  if (t.isCallExpression(expr) && t.isIdentifier(expr.callee) && isClsxLikeName(expr.callee.name)) {
@@ -3697,7 +3894,7 @@ function transformSource(source, filePath, options = {}) {
3697
3894
  classNamesSkipped++;
3698
3895
  warnings.push(...result.warnings.map((w) => `[${filePath}] ${w}`));
3699
3896
  }
3700
- injectTodoComment(result.unrecognized, path.parent, options, replacements);
3897
+ injectTodoComment(result.unrecognized, parent, options, replacements);
3701
3898
  return;
3702
3899
  }
3703
3900
  if (t.isConditionalExpression(expr)) {
@@ -3714,7 +3911,7 @@ function transformSource(source, filePath, options = {}) {
3714
3911
  classNamesSkipped++;
3715
3912
  warnings.push(...result.warnings.map((w) => `[${filePath}] ${w}`));
3716
3913
  }
3717
- injectTodoComment(result.unrecognized, path.parent, options, replacements);
3914
+ injectTodoComment(result.unrecognized, parent, options, replacements);
3718
3915
  return;
3719
3916
  }
3720
3917
  if (t.isLogicalExpression(expr) && expr.operator === "&&") {
@@ -3731,7 +3928,7 @@ function transformSource(source, filePath, options = {}) {
3731
3928
  classNamesSkipped++;
3732
3929
  warnings.push(...result.warnings.map((w) => `[${filePath}] ${w}`));
3733
3930
  }
3734
- injectTodoComment(result.unrecognized, path.parent, options, replacements);
3931
+ injectTodoComment(result.unrecognized, parent, options, replacements);
3735
3932
  return;
3736
3933
  }
3737
3934
  classNamesSkipped++;
@@ -3860,9 +4057,9 @@ function transformHtmlSourceSimple(source, filePath, options = {}) {
3860
4057
  function createLogFile(cwd) {
3861
4058
  const now = /* @__PURE__ */ new Date();
3862
4059
  const ts = now.toISOString().slice(0, 19).replace("T", "_").replace(/:/g, "-");
3863
- const logDir = path.join(cwd, ".csszyx", "logs");
4060
+ const logDir = path__default.join(cwd, ".csszyx", "logs");
3864
4061
  fs$1.mkdirSync(logDir, { recursive: true });
3865
- const filePath = path.join(logDir, `migrate-${ts}.log`);
4062
+ const filePath = path__default.join(logDir, `migrate-${ts}.log`);
3866
4063
  const lines = [`csszyx migrate \u2014 ${now.toISOString()}`, ""];
3867
4064
  return {
3868
4065
  filePath,
@@ -3873,7 +4070,7 @@ function createLogFile(cwd) {
3873
4070
  }
3874
4071
  function isGitignored(cwd, pattern) {
3875
4072
  try {
3876
- const content = fs$1.readFileSync(path.join(cwd, ".gitignore"), "utf-8");
4073
+ const content = fs$1.readFileSync(path__default.join(cwd, ".gitignore"), "utf-8");
3877
4074
  return content.split("\n").some((l) => {
3878
4075
  const t = l.trim();
3879
4076
  return t === pattern || t === `${pattern}/` || t === `/${pattern}`;
@@ -3916,7 +4113,7 @@ async function migrate(options = {}) {
3916
4113
  }
3917
4114
  if (resolveTodosPath) {
3918
4115
  try {
3919
- const absolutePath = path.resolve(cwd, resolveTodosPath);
4116
+ const absolutePath = path__default.resolve(cwd, resolveTodosPath);
3920
4117
  const content = fs$1.readFileSync(absolutePath, "utf-8");
3921
4118
  customMap = JSON.parse(content);
3922
4119
  printInfo(`Loaded resolution map from ${resolveTodosPath}`);
@@ -3986,7 +4183,10 @@ async function migrate(options = {}) {
3986
4183
  }
3987
4184
  let processSource = source;
3988
4185
  if (resolveTodosPath && !isHtml) {
3989
- processSource = processSource.replace(/\{\/\*\s*@sz-todo:\s*(.*?)\s*\*\/\}\n?/g, "");
4186
+ processSource = processSource.replace(
4187
+ /\{\/\*\s*@sz-todo:\s*(\S(?:.*\S)?)\s*\*\/\}\n?/g,
4188
+ ""
4189
+ );
3990
4190
  }
3991
4191
  const result = isHtml ? transformHtmlSourceSimple(processSource, filePath, {
3992
4192
  braces: options.braces,
@@ -4002,14 +4202,14 @@ async function migrate(options = {}) {
4002
4202
  totalSkipped += result.stats.classNamesSkipped;
4003
4203
  allUnrecognized.push(...result.stats.classesUnrecognized);
4004
4204
  if (result.potentiallyUnusedImports.length > 0) {
4005
- const rel2 = path.relative(cwd, filePath);
4205
+ const rel2 = path__default.relative(cwd, filePath);
4006
4206
  unusedImportFiles.push({ file: rel2, imports: result.potentiallyUnusedImports });
4007
4207
  }
4008
4208
  if (!dryRun) {
4009
4209
  try {
4010
4210
  fs$1.writeFileSync(filePath, result.code, "utf-8");
4011
4211
  } catch (err) {
4012
- const rel2 = path.relative(cwd, filePath);
4212
+ const rel2 = path__default.relative(cwd, filePath);
4013
4213
  printWarn(
4014
4214
  `Could not write ${rel2}: ${err instanceof Error ? err.message : String(err)}`
4015
4215
  );
@@ -4017,7 +4217,7 @@ async function migrate(options = {}) {
4017
4217
  continue;
4018
4218
  }
4019
4219
  }
4020
- const rel = path.relative(cwd, filePath);
4220
+ const rel = path__default.relative(cwd, filePath);
4021
4221
  if (dryRun) {
4022
4222
  printInfo(` ${rel}: ${result.stats.classNamesTransformed} className(s) \u2192 sz`);
4023
4223
  log.writeLine(` ${rel}: ${result.stats.classNamesTransformed} className(s) \u2192 sz`);
@@ -4058,7 +4258,7 @@ async function migrate(options = {}) {
4058
4258
  }
4059
4259
  }
4060
4260
  if (audit) {
4061
- const todoPath = path.join(cwd, ".csszyx-todo.json");
4261
+ const todoPath = path__default.join(cwd, ".csszyx-todo.json");
4062
4262
  const unique = [...new Set(allUnrecognized)];
4063
4263
  console.info();
4064
4264
  if (unique.length === 0) {
@@ -4075,19 +4275,19 @@ async function migrate(options = {}) {
4075
4275
  fs$1.writeFileSync(todoPath, JSON.stringify(todoObj, null, 2));
4076
4276
  } catch (err) {
4077
4277
  printWarn(
4078
- `Could not write ${path.relative(cwd, todoPath)}: ${err instanceof Error ? err.message : String(err)}`
4278
+ `Could not write ${path__default.relative(cwd, todoPath)}: ${err instanceof Error ? err.message : String(err)}`
4079
4279
  );
4080
4280
  log.flush();
4081
4281
  return;
4082
4282
  }
4083
4283
  printSuccess(
4084
- `Audit complete. Exported ${unique.length} unrecognized classes to ${path.relative(cwd, todoPath)}.`
4284
+ `Audit complete. Exported ${unique.length} unrecognized classes to ${path__default.relative(cwd, todoPath)}.`
4085
4285
  );
4086
4286
  printInfo(
4087
4287
  "Edit this file to map custom classes, then run: npx @csszyx/cli migrate --resolve-todos .csszyx-todo.json"
4088
4288
  );
4089
4289
  log.writeLine(
4090
- `Audit: ${unique.length} unrecognized classes written to ${path.relative(cwd, todoPath)}`
4290
+ `Audit: ${unique.length} unrecognized classes written to ${path__default.relative(cwd, todoPath)}`
4091
4291
  );
4092
4292
  }
4093
4293
  }
@@ -4112,13 +4312,353 @@ async function migrate(options = {}) {
4112
4312
  }
4113
4313
  try {
4114
4314
  log.flush();
4115
- printInfo(`Migration log saved to ${path.relative(cwd, log.filePath)}`);
4315
+ printInfo(`Migration log saved to ${path__default.relative(cwd, log.filePath)}`);
4116
4316
  } catch {
4117
4317
  }
4118
4318
  }
4119
4319
 
4320
+ const DEFAULT_NEXT_SOURCE_PATTERN = "{app,pages,src}/**/*.{ts,tsx,js,jsx,mjs,cjs}";
4321
+ const DEFAULT_NEXT_SOURCE_IGNORE = [
4322
+ "node_modules/**",
4323
+ ".git/**",
4324
+ ".next/**",
4325
+ ".next-turbo-*/**",
4326
+ ".csszyx/**",
4327
+ "dist/**",
4328
+ "build/**"
4329
+ ];
4330
+
4331
+ async function nextPrebuild(options = {}) {
4332
+ const cwd = path__default.resolve(options.cwd ?? process.cwd());
4333
+ const root = path__default.resolve(options.root ?? cwd);
4334
+ const pattern = options.pattern ?? DEFAULT_NEXT_SOURCE_PATTERN;
4335
+ try {
4336
+ const mode = normalizeMode(options.mode);
4337
+ const parserMode = normalizeParserMode$1(options.parserMode);
4338
+ const matches = await fg(pattern, {
4339
+ cwd: root,
4340
+ absolute: true,
4341
+ ignore: [...DEFAULT_NEXT_SOURCE_IGNORE, ...options.extraIgnore ?? []],
4342
+ dot: false,
4343
+ onlyFiles: true
4344
+ });
4345
+ if (matches.length === 0) {
4346
+ const message = `No source files matched pattern \`${pattern}\` under ${root}.`;
4347
+ if (options.json) {
4348
+ console.log(
4349
+ JSON.stringify(
4350
+ { ok: false, reason: "no-files-matched", root, pattern, mode },
4351
+ null,
4352
+ 2
4353
+ )
4354
+ );
4355
+ } else {
4356
+ console.error(`${colors.error(icons.error)} ${message}`);
4357
+ }
4358
+ return 1;
4359
+ }
4360
+ const result = runNextPrebuild({
4361
+ files: matches,
4362
+ explicitRoot: root,
4363
+ cwd,
4364
+ mode,
4365
+ parserMode,
4366
+ safelistOutputFile: options.outputFile,
4367
+ cacheDir: options.cacheDir,
4368
+ config: { mangleVars: false }
4369
+ // Versions intentionally omitted: runNextPrebuild's package.json
4370
+ // fallback reads the real installed @csszyx/unplugin and
4371
+ // @csszyx/compiler versions so the manifest's generation identity
4372
+ // tracks the engine that actually runs the transform.
4373
+ });
4374
+ if (options.json) {
4375
+ console.log(
4376
+ JSON.stringify(
4377
+ {
4378
+ ok: true,
4379
+ root,
4380
+ mode,
4381
+ scannedCount: result.scannedCount,
4382
+ transformedCount: result.transformedCount,
4383
+ skippedMissingCount: result.skippedMissingCount,
4384
+ sourceCount: result.sourceCount,
4385
+ classCount: result.classCount,
4386
+ manifestPath: result.manifestPath,
4387
+ safelistOutputPath: result.safelistOutputPath
4388
+ },
4389
+ null,
4390
+ 2
4391
+ )
4392
+ );
4393
+ } else {
4394
+ console.log(`${colors.success(icons.success)} csszyx next prebuild done`);
4395
+ console.log(` root: ${root}`);
4396
+ console.log(` mode: ${mode}`);
4397
+ console.log(` scanned: ${result.scannedCount}`);
4398
+ console.log(` transformed: ${result.transformedCount}`);
4399
+ console.log(` skipped: ${result.skippedMissingCount}`);
4400
+ console.log(` sources: ${result.sourceCount}`);
4401
+ console.log(` classes: ${result.classCount}`);
4402
+ console.log(` safelist: ${result.safelistOutputPath}`);
4403
+ console.log(` manifest: ${result.manifestPath}`);
4404
+ }
4405
+ return 0;
4406
+ } catch (error) {
4407
+ const message = error instanceof Error ? error.message : String(error);
4408
+ if (options.json) {
4409
+ console.log(JSON.stringify({ ok: false, reason: message }, null, 2));
4410
+ } else {
4411
+ console.error(`${colors.error(icons.error)} ${message}`);
4412
+ }
4413
+ return 1;
4414
+ }
4415
+ }
4416
+ function normalizeMode(mode) {
4417
+ if (mode === void 0) {
4418
+ return "production";
4419
+ }
4420
+ if (mode === "development" || mode === "production") {
4421
+ return mode;
4422
+ }
4423
+ throw new Error(`Invalid --mode "${mode}". Expected "development" or "production".`);
4424
+ }
4425
+ function normalizeParserMode$1(parserMode) {
4426
+ if (parserMode === void 0) {
4427
+ return void 0;
4428
+ }
4429
+ if (parserMode === "rust" || parserMode === "oxc" || parserMode === "babel") {
4430
+ return parserMode;
4431
+ }
4432
+ throw new Error(`Invalid --parser-mode "${parserMode}". Expected "rust", "oxc", or "babel".`);
4433
+ }
4434
+
4435
+ const SOURCE_EXTENSION = /\.[cm]?[jt]sx?$/i;
4436
+ async function startNextWatch(options = {}, dependencies = {}) {
4437
+ const cwd = path.resolve(options.cwd ?? process.cwd());
4438
+ const root = path.resolve(options.root ?? cwd);
4439
+ const pattern = options.pattern ?? DEFAULT_NEXT_SOURCE_PATTERN;
4440
+ const ignore = [...DEFAULT_NEXT_SOURCE_IGNORE, ...options.extraIgnore ?? []];
4441
+ const parserMode = normalizeParserMode(options.parserMode);
4442
+ const debounceMs = normalizeDebounceMs(options.debounceMs);
4443
+ const files = await fg(pattern, {
4444
+ cwd: root,
4445
+ absolute: true,
4446
+ ignore,
4447
+ dot: false,
4448
+ onlyFiles: true
4449
+ });
4450
+ if (files.length === 0) {
4451
+ throw new Error(`No source files matched pattern \`${pattern}\` under ${root}.`);
4452
+ }
4453
+ const prebuild = runNextPrebuild({
4454
+ files,
4455
+ explicitRoot: root,
4456
+ cwd,
4457
+ mode: "development",
4458
+ parserMode,
4459
+ safelistOutputFile: options.outputFile,
4460
+ cacheDir: options.cacheDir,
4461
+ config: { mangleVars: false }
4462
+ });
4463
+ let resolveFailure = () => {
4464
+ };
4465
+ let failed = false;
4466
+ const failure = new Promise((resolve) => {
4467
+ resolveFailure = resolve;
4468
+ });
4469
+ const reportFailure = (error) => {
4470
+ if (failed) {
4471
+ return;
4472
+ }
4473
+ failed = true;
4474
+ resolveFailure(error instanceof Error ? error : new Error(String(error)));
4475
+ };
4476
+ const controller = new NextSafelistWatcher({
4477
+ context: prebuild.context,
4478
+ debounceMs,
4479
+ onError: reportFailure
4480
+ });
4481
+ const watchFactory = dependencies.watch ?? watch;
4482
+ const fsWatcher = watchFactory(root, {
4483
+ ignoreInitial: true,
4484
+ persistent: true,
4485
+ atomic: true,
4486
+ awaitWriteFinish: {
4487
+ stabilityThreshold: 25,
4488
+ pollInterval: 10
4489
+ },
4490
+ ignored: createIgnoredMatcher(root, prebuild.context.safelist.shardsDir, ignore)
4491
+ });
4492
+ fsWatcher.on("all", (event, filePath) => {
4493
+ const absolutePath = path.resolve(filePath);
4494
+ if (event === "add" || event === "change" || event === "unlink") {
4495
+ if (controller.notify(event, absolutePath) || event !== "unlink" || !SOURCE_EXTENSION.test(absolutePath)) {
4496
+ return;
4497
+ }
4498
+ controller.notifySourceRemoval(absolutePath);
4499
+ }
4500
+ });
4501
+ fsWatcher.on("error", reportFailure);
4502
+ try {
4503
+ await waitForWatcherReady(fsWatcher);
4504
+ controller.start();
4505
+ } catch (error) {
4506
+ await fsWatcher.close();
4507
+ controller.close();
4508
+ throw error;
4509
+ }
4510
+ let closed = false;
4511
+ return {
4512
+ root,
4513
+ sourcePattern: pattern,
4514
+ safelistOutputPath: prebuild.safelistOutputPath,
4515
+ manifestPath: prebuild.manifestPath,
4516
+ failure,
4517
+ close: async () => {
4518
+ if (closed) {
4519
+ return;
4520
+ }
4521
+ closed = true;
4522
+ await fsWatcher.close();
4523
+ controller.close();
4524
+ }
4525
+ };
4526
+ }
4527
+ async function nextWatch(options = {}) {
4528
+ let session;
4529
+ let exitCode = 0;
4530
+ try {
4531
+ session = await startNextWatch(options);
4532
+ if (!options.silent) {
4533
+ console.log(`${colors.success(icons.success)} csszyx next watch ready`);
4534
+ console.log(` root: ${session.root}`);
4535
+ console.log(` pattern: ${session.sourcePattern}`);
4536
+ console.log(` safelist: ${session.safelistOutputPath}`);
4537
+ console.log(` manifest: ${session.manifestPath}`);
4538
+ }
4539
+ const outcome = await waitForShutdown(session.failure);
4540
+ if (outcome) {
4541
+ console.error(`${colors.error(icons.error)} ${outcome.message}`);
4542
+ exitCode = 1;
4543
+ }
4544
+ } catch (error) {
4545
+ const message = error instanceof Error ? error.message : String(error);
4546
+ console.error(`${colors.error(icons.error)} ${message}`);
4547
+ exitCode = 1;
4548
+ }
4549
+ try {
4550
+ await session?.close();
4551
+ } catch (error) {
4552
+ const message = error instanceof Error ? error.message : String(error);
4553
+ console.error(`${colors.error(icons.error)} Failed to close Next watcher: ${message}`);
4554
+ exitCode = 1;
4555
+ }
4556
+ return exitCode;
4557
+ }
4558
+ function waitForWatcherReady(watcher) {
4559
+ return new Promise((resolve, reject) => {
4560
+ const onReady = () => {
4561
+ watcher.off("error", onStartupError);
4562
+ resolve();
4563
+ };
4564
+ const onStartupError = (error) => {
4565
+ watcher.off("ready", onReady);
4566
+ reject(error);
4567
+ };
4568
+ watcher.once("ready", onReady);
4569
+ watcher.once("error", onStartupError);
4570
+ });
4571
+ }
4572
+ function waitForShutdown(failure) {
4573
+ return new Promise((resolve) => {
4574
+ const cleanup = () => {
4575
+ process.off("SIGINT", onSignal);
4576
+ process.off("SIGTERM", onSignal);
4577
+ };
4578
+ const onSignal = () => {
4579
+ cleanup();
4580
+ resolve(void 0);
4581
+ };
4582
+ process.once("SIGINT", onSignal);
4583
+ process.once("SIGTERM", onSignal);
4584
+ failure.then((error) => {
4585
+ cleanup();
4586
+ resolve(error);
4587
+ });
4588
+ });
4589
+ }
4590
+ function createIgnoredMatcher(root, shardsDir, ignore) {
4591
+ const normalizedShardsDir = path.resolve(shardsDir);
4592
+ const matchers = ignore.flatMap((pattern) => {
4593
+ const normalized = pattern.replace(/\\/g, "/");
4594
+ const variants = normalized.endsWith("/**") ? [normalized, normalized.slice(0, -3)] : [normalized];
4595
+ return variants.map((variant) => new Minimatch(variant, { dot: true }));
4596
+ });
4597
+ return (candidate) => {
4598
+ const absolute = path.resolve(candidate);
4599
+ const relativeToShards = path.relative(absolute, normalizedShardsDir);
4600
+ if (absolute === normalizedShardsDir || absolute.startsWith(`${normalizedShardsDir}${path.sep}`) || relativeToShards !== ".." && !relativeToShards.startsWith(`..${path.sep}`) && !path.isAbsolute(relativeToShards)) {
4601
+ return false;
4602
+ }
4603
+ const relative = path.relative(root, absolute).replace(/\\/g, "/");
4604
+ if (!relative || relative.startsWith("../") || path.isAbsolute(relative)) {
4605
+ return false;
4606
+ }
4607
+ return matchers.some((matcher) => matcher.match(relative));
4608
+ };
4609
+ }
4610
+ function normalizeParserMode(parserMode) {
4611
+ if (parserMode === void 0) {
4612
+ return void 0;
4613
+ }
4614
+ if (parserMode === "rust" || parserMode === "oxc" || parserMode === "babel") {
4615
+ return parserMode;
4616
+ }
4617
+ throw new Error(`Invalid --parser-mode "${parserMode}". Expected "rust", "oxc", or "babel".`);
4618
+ }
4619
+ function normalizeDebounceMs(debounceMs) {
4620
+ if (debounceMs === void 0) {
4621
+ return void 0;
4622
+ }
4623
+ const parsed = typeof debounceMs === "number" ? debounceMs : Number(debounceMs);
4624
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 6e4) {
4625
+ throw new Error("Invalid --debounce-ms. Expected an integer between 0 and 60000.");
4626
+ }
4627
+ return parsed;
4628
+ }
4629
+
4120
4630
  const cli = cac("csszyx");
4631
+ normalizeNextCommandAlias(process.argv);
4121
4632
  const VERSION = "0.0.0";
4633
+ async function runNextPrebuildCommand(pattern, options) {
4634
+ const code = await nextPrebuild({
4635
+ cwd: options.cwd,
4636
+ root: options.root,
4637
+ mode: options.mode,
4638
+ parserMode: options.parserMode,
4639
+ outputFile: options.outputFile,
4640
+ cacheDir: options.cacheDir,
4641
+ pattern,
4642
+ extraIgnore: options.ignore ? String(options.ignore).split(",") : void 0,
4643
+ json: options.json
4644
+ });
4645
+ if (code !== 0) {
4646
+ process.exit(code);
4647
+ }
4648
+ }
4649
+ async function runNextWatchCommand(pattern, options) {
4650
+ const code = await nextWatch({
4651
+ cwd: options.cwd,
4652
+ root: options.root,
4653
+ parserMode: options.parserMode,
4654
+ outputFile: options.outputFile,
4655
+ cacheDir: options.cacheDir,
4656
+ pattern,
4657
+ extraIgnore: options.ignore ? String(options.ignore).split(",") : void 0,
4658
+ debounceMs: options.debounceMs
4659
+ });
4660
+ process.exitCode = code;
4661
+ }
4122
4662
  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) => {
4123
4663
  await init({
4124
4664
  framework: options.framework,
@@ -4167,11 +4707,27 @@ cli.command("migrate [dir]", "Convert Tailwind className to sz prop").option("--
4167
4707
  resolveTodos: options.resolveTodos
4168
4708
  });
4169
4709
  });
4710
+ cli.command(
4711
+ "next-prebuild [pattern]",
4712
+ "Seed the Next.js Turbopack csszyx safelist and generation manifest"
4713
+ ).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(
4714
+ "--output-file <path>",
4715
+ "Tailwind @source safelist output (default: csszyx-classes.html)"
4716
+ ).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);
4717
+ 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(
4718
+ "--output-file <path>",
4719
+ "Tailwind @source safelist output (default: csszyx-classes.html)"
4720
+ ).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);
4170
4721
  cli.command("").action(() => {
4171
4722
  cli.outputHelp();
4172
4723
  });
4173
4724
  cli.help();
4174
4725
  cli.version(VERSION);
4175
4726
  cli.parse();
4727
+ function normalizeNextCommandAlias(argv) {
4728
+ if (argv[2] === "next" && (argv[3] === "prebuild" || argv[3] === "watch")) {
4729
+ argv.splice(2, 2, `next-${argv[3]}`);
4730
+ }
4731
+ }
4176
4732
 
4177
4733
  export { classNameToSzObject, extractScreenKeys, extractSpacingKeys, findConfigFile, flattenColors, generateAndWriteTypes, generateTypeDeclarations, generateTypes, transformSource as migrateSource, scanTailwindConfig, writeDeclarationFile };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@csszyx/cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Command-line tools for csszyx",
5
5
  "keywords": [
6
6
  "csszyx",
@@ -27,8 +27,10 @@
27
27
  "types": "./dist/index.d.mts",
28
28
  "exports": {
29
29
  ".": {
30
- "types": "./dist/index.d.mts",
31
- "import": "./dist/index.mjs"
30
+ "import": {
31
+ "types": "./dist/index.d.mts",
32
+ "default": "./dist/index.mjs"
33
+ }
32
34
  }
33
35
  },
34
36
  "files": [
@@ -36,28 +38,27 @@
36
38
  ],
37
39
  "dependencies": {
38
40
  "@babel/parser": "^7.23.0",
39
- "@babel/traverse": "^7.23.0",
40
- "@babel/generator": "^7.23.0",
41
41
  "@babel/types": "^7.23.0",
42
42
  "cac": "^6.7.14",
43
- "fast-glob": "^3.3.2",
44
- "tailwindcss": "^3.4.1",
45
- "prompts": "^2.4.2",
46
- "picocolors": "^1.0.0",
47
- "ora": "^8.0.1",
43
+ "chokidar": "^5.0.0",
48
44
  "execa": "^8.0.1",
45
+ "fast-glob": "^3.3.2",
49
46
  "fs-extra": "^11.2.0",
50
- "@csszyx/types": "0.8.0"
47
+ "minimatch": "^10.2.4",
48
+ "ora": "^8.0.1",
49
+ "picocolors": "^1.0.0",
50
+ "prompts": "^2.4.2",
51
+ "tailwindcss": "^3.4.1",
52
+ "@csszyx/types": "0.9.1",
53
+ "@csszyx/unplugin": "0.9.1"
51
54
  },
52
55
  "devDependencies": {
53
- "@types/babel__generator": "^7.6.8",
54
- "@types/babel__traverse": "^7.20.5",
56
+ "@types/fs-extra": "^11.0.4",
55
57
  "@types/node": "^20.11.5",
56
58
  "@types/prompts": "^2.4.9",
57
- "@types/fs-extra": "^11.0.4",
58
59
  "typescript": "^6.0.3",
59
- "vitest": "^4.1.6",
60
- "unbuild": "^3.6.1"
60
+ "unbuild": "^3.6.1",
61
+ "vitest": "^4.1.6"
61
62
  },
62
63
  "sideEffects": false,
63
64
  "engines": {