@csszyx/cli 0.9.0 → 0.9.2

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/index.mjs CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import fs$1, { existsSync, writeFileSync, readFileSync } from 'node:fs';
2
3
  import cac from 'cac';
3
- import path, { resolve, dirname } from 'node:path';
4
+ import * as path from 'node:path';
5
+ import path__default, { resolve, dirname } from 'node:path';
4
6
  import fs from 'fs-extra';
5
7
  import ora from 'ora';
6
8
  import pc from 'picocolors';
7
- import fs$1, { existsSync, writeFileSync } from 'node:fs';
8
9
  import { mkdir } from 'node:fs/promises';
9
10
  import { pathToFileURL } from 'node:url';
10
11
  import resolveConfig from 'tailwindcss/resolveConfig.js';
@@ -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
  }
@@ -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") {
@@ -1934,6 +1955,11 @@ const REVERSE_BOOLEAN_MAP = {
1934
1955
  isolate: "isolate",
1935
1956
  ordinal: "ordinal",
1936
1957
  "slashed-zero": "slashedZero",
1958
+ // Bare `transition` (common transition property) and the `group`/`peer`
1959
+ // marker classes round-trip through the compiler as boolean sugar.
1960
+ transition: "transition",
1961
+ group: "group",
1962
+ peer: "peer",
1937
1963
  // Divide/Space reverse
1938
1964
  "divide-x-reverse": "divideXReverse",
1939
1965
  "divide-y-reverse": "divideYReverse",
@@ -2351,7 +2377,7 @@ const KNOWN_SIMPLE_VARIANTS = /* @__PURE__ */ new Set([
2351
2377
  "pointer-none"
2352
2378
  ]);
2353
2379
 
2354
- function parseClass(cls) {
2380
+ function parseClass(cls, options = {}) {
2355
2381
  let important = false;
2356
2382
  let input = cls;
2357
2383
  if (input.endsWith("!")) {
@@ -2364,7 +2390,7 @@ function parseClass(cls) {
2364
2390
  negative = true;
2365
2391
  negInput = input.slice(1);
2366
2392
  }
2367
- const boolResult = tryBooleanMatch(input);
2393
+ const boolResult = tryBooleanMatch(input, options);
2368
2394
  if (boolResult) {
2369
2395
  return applyImportant(boolResult, important);
2370
2396
  }
@@ -2380,10 +2406,7 @@ function parseClass(cls) {
2380
2406
  continue;
2381
2407
  }
2382
2408
  if (REVERSE_BOOLEAN_MAP[source]) {
2383
- return applyImportant(
2384
- { prop: REVERSE_BOOLEAN_MAP[source], value: true },
2385
- important
2386
- );
2409
+ return applyImportant(booleanClassToParsed(source, options), important);
2387
2410
  }
2388
2411
  if (prefix === "divide-x" || prefix === "divide-y") {
2389
2412
  return applyImportant({ prop, value: true }, important);
@@ -2422,7 +2445,7 @@ function parseClass(cls) {
2422
2445
  }
2423
2446
  }
2424
2447
  }
2425
- const displayResult = tryDisplay(input);
2448
+ const displayResult = tryDisplay(input, options);
2426
2449
  if (displayResult) {
2427
2450
  return applyImportant(displayResult, important);
2428
2451
  }
@@ -2456,17 +2479,17 @@ function applyImportant(result, important) {
2456
2479
  }
2457
2480
  return result;
2458
2481
  }
2459
- function tryBooleanMatch(cls) {
2482
+ function tryBooleanMatch(cls, options) {
2460
2483
  if (BOOLEAN_VALUE_MAP[cls]) {
2461
2484
  const { prop, value } = BOOLEAN_VALUE_MAP[cls];
2462
2485
  return { prop, value };
2463
2486
  }
2464
2487
  if (REVERSE_BOOLEAN_MAP[cls]) {
2465
- return { prop: REVERSE_BOOLEAN_MAP[cls], value: true };
2488
+ return booleanClassToParsed(cls, options);
2466
2489
  }
2467
2490
  return null;
2468
2491
  }
2469
- function tryDisplay(cls) {
2492
+ function tryDisplay(cls, options) {
2470
2493
  const displayValues = /* @__PURE__ */ new Set([
2471
2494
  "block",
2472
2495
  "inline",
@@ -2484,10 +2507,33 @@ function tryDisplay(cls) {
2484
2507
  "list-item"
2485
2508
  ]);
2486
2509
  if (displayValues.has(cls)) {
2487
- return REVERSE_BOOLEAN_MAP[cls] ? { prop: REVERSE_BOOLEAN_MAP[cls], value: true } : null;
2510
+ return REVERSE_BOOLEAN_MAP[cls] ? booleanClassToParsed(cls, options) : null;
2488
2511
  }
2489
2512
  return null;
2490
2513
  }
2514
+ function booleanClassToParsed(cls, options) {
2515
+ const displayValue = DISPLAY_CLASS_VALUES[cls];
2516
+ if (options.display === "canonical" && displayValue) {
2517
+ return { prop: "display", value: displayValue, cssProperty: "display" };
2518
+ }
2519
+ return { prop: REVERSE_BOOLEAN_MAP[cls], value: true };
2520
+ }
2521
+ const DISPLAY_CLASS_VALUES = {
2522
+ block: "block",
2523
+ inline: "inline",
2524
+ "inline-block": "inline-block",
2525
+ flex: "flex",
2526
+ "inline-flex": "inline-flex",
2527
+ grid: "grid",
2528
+ "inline-grid": "inline-grid",
2529
+ hidden: "none",
2530
+ contents: "contents",
2531
+ table: "table",
2532
+ "table-row": "table-row",
2533
+ "table-cell": "table-cell",
2534
+ "flow-root": "flow-root",
2535
+ "list-item": "list-item"
2536
+ };
2491
2537
  function tryGradient(cls, negative) {
2492
2538
  let input = cls;
2493
2539
  let type = null;
@@ -3133,6 +3179,8 @@ function normalizeVariantKey(variant) {
3133
3179
  const TODO_KEEP = "sz:keep";
3134
3180
  const TODO_REMOVE = "sz:remove";
3135
3181
  const TODO_PENDING = "sz:todo";
3182
+ const MAX_TOKEN_CACHE_SIZE = 4096;
3183
+ const parsedTokenCache = /* @__PURE__ */ new Map();
3136
3184
  function resolveCustomMapEntry(token, customMap, resolveString) {
3137
3185
  if (!(token in customMap)) {
3138
3186
  return null;
@@ -3164,6 +3212,8 @@ function classNameToSzObject(className, customMap) {
3164
3212
  const szObject = {};
3165
3213
  const unrecognized = [];
3166
3214
  const keepInClassName = [];
3215
+ const seenCssPropertiesByPath = /* @__PURE__ */ new Map();
3216
+ const conflictedCssPropertiesByPath = /* @__PURE__ */ new Map();
3167
3217
  for (const token of tokens) {
3168
3218
  if (customMap && token in customMap) {
3169
3219
  const entry = resolveCustomMapEntry(
@@ -3201,21 +3251,79 @@ function classNameToSzObject(className, customMap) {
3201
3251
  }
3202
3252
  }
3203
3253
  }
3204
- const { variantParts, baseClass } = extractVariants(token);
3205
- const parsed = parseClass(baseClass);
3206
- if (!parsed) {
3254
+ const parsedToken = parseClassTokenCached(token);
3255
+ if (!parsedToken) {
3207
3256
  unrecognized.push(token);
3208
3257
  continue;
3209
3258
  }
3210
- const variantKeys = variantParts.map((v) => mapVariant(v));
3211
- const keyPath = [];
3212
- for (const vk of variantKeys) {
3213
- keyPath.push(...vk);
3259
+ if (isCssPropertyConflicted(conflictedCssPropertiesByPath, parsedToken)) {
3260
+ unrecognized.push(token);
3261
+ continue;
3214
3262
  }
3215
- setNestedValue(szObject, keyPath, parsed.prop, parsed.value);
3263
+ const conflict = findCssPropertyConflict(seenCssPropertiesByPath, parsedToken, token);
3264
+ if (conflict) {
3265
+ rememberCssPropertyConflict(conflictedCssPropertiesByPath, parsedToken);
3266
+ unrecognized.push(conflict, token);
3267
+ removeNestedValue(szObject, parsedToken.keyPath, parsedToken.prop);
3268
+ continue;
3269
+ }
3270
+ rememberCssProperty(seenCssPropertiesByPath, parsedToken, token);
3271
+ setNestedValue(
3272
+ szObject,
3273
+ parsedToken.keyPath,
3274
+ parsedToken.prop,
3275
+ cloneParsedValue(parsedToken.value)
3276
+ );
3216
3277
  }
3217
3278
  return { szObject, unrecognized, keepInClassName };
3218
3279
  }
3280
+ function parseClassTokenCached(token) {
3281
+ if (parsedTokenCache.has(token)) {
3282
+ return parsedTokenCache.get(token) ?? null;
3283
+ }
3284
+ const parsed = parseClassToken(token);
3285
+ rememberParsedToken(token, parsed);
3286
+ return parsed;
3287
+ }
3288
+ function parseClassToken(token) {
3289
+ const { variantParts, baseClass } = extractVariants(token);
3290
+ const parsed = parseClass(baseClass, { display: "canonical" });
3291
+ if (!parsed) {
3292
+ return null;
3293
+ }
3294
+ const keyPath = [];
3295
+ for (const variant of variantParts) {
3296
+ keyPath.push(...mapVariant(variant));
3297
+ }
3298
+ return {
3299
+ keyPath,
3300
+ prop: parsed.prop,
3301
+ value: parsed.value,
3302
+ cssProperty: parsed.cssProperty
3303
+ };
3304
+ }
3305
+ function rememberParsedToken(token, parsed) {
3306
+ if (parsedTokenCache.size >= MAX_TOKEN_CACHE_SIZE) {
3307
+ const oldest = parsedTokenCache.keys().next().value;
3308
+ if (oldest !== void 0) {
3309
+ parsedTokenCache.delete(oldest);
3310
+ }
3311
+ }
3312
+ parsedTokenCache.set(token, parsed);
3313
+ }
3314
+ function cloneParsedValue(value) {
3315
+ if (Array.isArray(value)) {
3316
+ return value.map(cloneParsedValue);
3317
+ }
3318
+ if (value && typeof value === "object") {
3319
+ const clone = {};
3320
+ for (const [key, nested] of Object.entries(value)) {
3321
+ clone[key] = cloneParsedValue(nested);
3322
+ }
3323
+ return clone;
3324
+ }
3325
+ return value;
3326
+ }
3219
3327
  function setNestedValue(obj, keyPath, prop, value) {
3220
3328
  let current = obj;
3221
3329
  for (const key of keyPath) {
@@ -3226,6 +3334,64 @@ function setNestedValue(obj, keyPath, prop, value) {
3226
3334
  }
3227
3335
  current[prop] = value;
3228
3336
  }
3337
+ function findCssPropertyConflict(seen, parsed, token) {
3338
+ if (!parsed.cssProperty) {
3339
+ return null;
3340
+ }
3341
+ const scope = parsed.keyPath.join("\0");
3342
+ const previous = seen.get(scope)?.get(parsed.cssProperty);
3343
+ return previous && previous !== token ? previous : null;
3344
+ }
3345
+ function isCssPropertyConflicted(conflicted, parsed) {
3346
+ if (!parsed.cssProperty) {
3347
+ return false;
3348
+ }
3349
+ return conflicted.get(parsed.keyPath.join("\0"))?.has(parsed.cssProperty) === true;
3350
+ }
3351
+ function rememberCssPropertyConflict(conflicted, parsed) {
3352
+ if (!parsed.cssProperty) {
3353
+ return;
3354
+ }
3355
+ const scope = parsed.keyPath.join("\0");
3356
+ let properties = conflicted.get(scope);
3357
+ if (!properties) {
3358
+ properties = /* @__PURE__ */ new Set();
3359
+ conflicted.set(scope, properties);
3360
+ }
3361
+ properties.add(parsed.cssProperty);
3362
+ }
3363
+ function rememberCssProperty(seen, parsed, token) {
3364
+ if (!parsed.cssProperty) {
3365
+ return;
3366
+ }
3367
+ const scope = parsed.keyPath.join("\0");
3368
+ let properties = seen.get(scope);
3369
+ if (!properties) {
3370
+ properties = /* @__PURE__ */ new Map();
3371
+ seen.set(scope, properties);
3372
+ }
3373
+ properties.set(parsed.cssProperty, token);
3374
+ }
3375
+ function removeNestedValue(obj, keyPath, prop) {
3376
+ let current = obj;
3377
+ const parents = [];
3378
+ for (const key of keyPath) {
3379
+ const next = current[key];
3380
+ if (!next || typeof next !== "object" || Array.isArray(next)) {
3381
+ return;
3382
+ }
3383
+ parents.push([current, key]);
3384
+ current = next;
3385
+ }
3386
+ delete current[prop];
3387
+ for (let index = parents.length - 1; index >= 0; index--) {
3388
+ const [parent, key] = parents[index];
3389
+ const child = parent[key];
3390
+ if (child && typeof child === "object" && Object.keys(child).length === 0) {
3391
+ delete parent[key];
3392
+ }
3393
+ }
3394
+ }
3229
3395
 
3230
3396
  const CLSX_LIKE_NAMES = /* @__PURE__ */ new Set(["clsx", "cn", "cx", "twMerge", "classNames", "classnames"]);
3231
3397
  function isClsxLikeName(name) {
@@ -3532,7 +3698,7 @@ function isExpression(node, t) {
3532
3698
  return t.isExpression(node);
3533
3699
  }
3534
3700
 
3535
- const traverse = typeof _traverse === "function" ? _traverse : _traverse.default;
3701
+ const VISITOR_KEYS = t.VISITOR_KEYS;
3536
3702
  function injectTodoComment(unrecognized, parent, options, replacements) {
3537
3703
  if (!options.injectTodos || unrecognized.length === 0) {
3538
3704
  return;
@@ -3548,16 +3714,64 @@ function injectTodoComment(unrecognized, parent, options, replacements) {
3548
3714
  `
3549
3715
  });
3550
3716
  }
3717
+ function walkAst(node, visitors, ancestors = []) {
3718
+ if (t.isImportDeclaration(node)) {
3719
+ visitors.ImportDeclaration?.(node);
3720
+ } else if (t.isCallExpression(node)) {
3721
+ visitors.CallExpression?.(node, ancestors);
3722
+ } else if (t.isJSXAttribute(node)) {
3723
+ visitors.JSXAttribute?.(node, ancestors[ancestors.length - 1] ?? null);
3724
+ }
3725
+ const keys = VISITOR_KEYS[node.type];
3726
+ if (!keys) {
3727
+ return;
3728
+ }
3729
+ ancestors.push(node);
3730
+ for (const key of keys) {
3731
+ const child = node[key];
3732
+ if (Array.isArray(child)) {
3733
+ for (const item of child) {
3734
+ if (isAstNode(item)) {
3735
+ walkAst(item, visitors, ancestors);
3736
+ }
3737
+ }
3738
+ } else if (isAstNode(child)) {
3739
+ walkAst(child, visitors, ancestors);
3740
+ }
3741
+ }
3742
+ ancestors.pop();
3743
+ }
3744
+ function isAstNode(value) {
3745
+ return Boolean(value && typeof value === "object" && "type" in value);
3746
+ }
3747
+ function isClassNameJsxAttribute(node) {
3748
+ return t.isJSXAttribute(node) && t.isJSXIdentifier(node.name) && node.name.name === "className";
3749
+ }
3551
3750
  function transformSource(source, filePath, options = {}) {
3552
3751
  const warnings = [];
3553
3752
  let classNamesTransformed = 0;
3554
3753
  let classNamesSkipped = 0;
3754
+ let classNamesSkippedComponent = 0;
3555
3755
  const classesUnrecognized = [];
3556
3756
  const replacements = [];
3557
3757
  const clsxImportNames = /* @__PURE__ */ new Set();
3558
3758
  let clsxUsedOutsideClassName = false;
3559
3759
  const clsxCallsitesMigrated = /* @__PURE__ */ new Set();
3560
3760
  let hasCvaImport = false;
3761
+ if (source.indexOf("className") === -1 && source.indexOf("cva") === -1) {
3762
+ return {
3763
+ code: source,
3764
+ changed: false,
3765
+ warnings: [],
3766
+ stats: {
3767
+ classNamesTransformed: 0,
3768
+ classNamesSkipped: 0,
3769
+ classNamesSkippedComponent: 0,
3770
+ classesUnrecognized: []
3771
+ },
3772
+ potentiallyUnusedImports: []
3773
+ };
3774
+ }
3561
3775
  let ast;
3562
3776
  try {
3563
3777
  ast = parse(source, {
@@ -3571,50 +3785,49 @@ function transformSource(source, filePath, options = {}) {
3571
3785
  code: source,
3572
3786
  changed: false,
3573
3787
  warnings: [`Parse error in ${filePath}: ${msg}`],
3574
- stats: { classNamesTransformed: 0, classNamesSkipped: 0, classesUnrecognized: [] },
3788
+ stats: {
3789
+ classNamesTransformed: 0,
3790
+ classNamesSkipped: 0,
3791
+ classNamesSkippedComponent: 0,
3792
+ classesUnrecognized: []
3793
+ },
3575
3794
  potentiallyUnusedImports: []
3576
3795
  };
3577
3796
  }
3578
- traverse(ast, {
3579
- // Track clsx-like and CVA imports
3580
- ImportDeclaration(path) {
3581
- const src = path.node.source.value;
3797
+ walkAst(ast, {
3798
+ ImportDeclaration(node) {
3799
+ const src = node.source.value;
3582
3800
  const clsxPackages = ["clsx", "clsx/lite", "classnames", "tailwind-merge"];
3583
3801
  const isClsxPkg = clsxPackages.some((p) => src === p || src.startsWith(`${p}/`));
3584
3802
  const cvaPkgs = ["cva", "class-variance-authority"];
3585
3803
  if (cvaPkgs.some((p) => src === p || src.startsWith(`${p}/`))) {
3586
3804
  hasCvaImport = true;
3587
3805
  }
3588
- for (const spec of path.node.specifiers) {
3806
+ for (const spec of node.specifiers) {
3589
3807
  const localName = spec.local.name;
3590
3808
  if (isClsxPkg || isClsxLikeName(localName)) {
3591
3809
  clsxImportNames.add(localName);
3592
3810
  }
3593
3811
  }
3594
3812
  },
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
- );
3813
+ CallExpression(node, ancestors) {
3814
+ if (t.isIdentifier(node.callee) && clsxImportNames.has(node.callee.name)) {
3815
+ const inClassName = ancestors.some(isClassNameJsxAttribute);
3601
3816
  if (!inClassName) {
3602
3817
  clsxUsedOutsideClassName = true;
3603
3818
  }
3604
3819
  }
3605
3820
  },
3606
- // Main transformation: className → sz
3607
- JSXAttribute(path) {
3608
- const attrName = path.node.name;
3821
+ JSXAttribute(node, parent) {
3822
+ const attrName = node.name;
3609
3823
  if (!t.isJSXIdentifier(attrName) || attrName.name !== "className") {
3610
3824
  return;
3611
3825
  }
3612
- const parent = path.parent;
3613
3826
  if (t.isJSXOpeningElement(parent)) {
3614
3827
  const elementName = parent.name;
3615
3828
  const isCapitalized = t.isJSXIdentifier(elementName) && /^[A-Z]/.test(elementName.name) || t.isJSXMemberExpression(elementName);
3616
3829
  if (isCapitalized) {
3617
- classNamesSkipped++;
3830
+ classNamesSkippedComponent++;
3618
3831
  return;
3619
3832
  }
3620
3833
  }
@@ -3627,9 +3840,9 @@ function transformSource(source, filePath, options = {}) {
3627
3840
  return;
3628
3841
  }
3629
3842
  }
3630
- const value = path.node.value;
3631
- const attrStart = path.node.start;
3632
- const attrEnd = path.node.end;
3843
+ const value = node.value;
3844
+ const attrStart = node.start;
3845
+ const attrEnd = node.end;
3633
3846
  if (attrStart === null || attrStart === void 0 || attrEnd === null || attrEnd === void 0) {
3634
3847
  return;
3635
3848
  }
@@ -3639,7 +3852,7 @@ function transformSource(source, filePath, options = {}) {
3639
3852
  replacements.push({ start: attrStart, end: attrEnd, text: result.replacement });
3640
3853
  classNamesTransformed++;
3641
3854
  classesUnrecognized.push(...result.unrecognized);
3642
- injectTodoComment(result.unrecognized, path.parent, options, replacements);
3855
+ injectTodoComment(result.unrecognized, parent, options, replacements);
3643
3856
  } else {
3644
3857
  classNamesSkipped++;
3645
3858
  }
@@ -3657,7 +3870,7 @@ function transformSource(source, filePath, options = {}) {
3657
3870
  });
3658
3871
  classNamesTransformed++;
3659
3872
  classesUnrecognized.push(...result.unrecognized);
3660
- injectTodoComment(result.unrecognized, path.parent, options, replacements);
3873
+ injectTodoComment(result.unrecognized, parent, options, replacements);
3661
3874
  } else {
3662
3875
  classNamesSkipped++;
3663
3876
  }
@@ -3676,8 +3889,9 @@ function transformSource(source, filePath, options = {}) {
3676
3889
  } else {
3677
3890
  classNamesSkipped++;
3678
3891
  warnings.push(...result.warnings.map((w) => `[${filePath}] ${w}`));
3892
+ classesUnrecognized.push(...result.unrecognized);
3679
3893
  }
3680
- injectTodoComment(result.unrecognized, path.parent, options, replacements);
3894
+ injectTodoComment(result.unrecognized, parent, options, replacements);
3681
3895
  return;
3682
3896
  }
3683
3897
  if (t.isCallExpression(expr) && t.isIdentifier(expr.callee) && isClsxLikeName(expr.callee.name)) {
@@ -3696,8 +3910,9 @@ function transformSource(source, filePath, options = {}) {
3696
3910
  } else {
3697
3911
  classNamesSkipped++;
3698
3912
  warnings.push(...result.warnings.map((w) => `[${filePath}] ${w}`));
3913
+ classesUnrecognized.push(...result.unrecognized);
3699
3914
  }
3700
- injectTodoComment(result.unrecognized, path.parent, options, replacements);
3915
+ injectTodoComment(result.unrecognized, parent, options, replacements);
3701
3916
  return;
3702
3917
  }
3703
3918
  if (t.isConditionalExpression(expr)) {
@@ -3713,8 +3928,9 @@ function transformSource(source, filePath, options = {}) {
3713
3928
  } else {
3714
3929
  classNamesSkipped++;
3715
3930
  warnings.push(...result.warnings.map((w) => `[${filePath}] ${w}`));
3931
+ classesUnrecognized.push(...result.unrecognized);
3716
3932
  }
3717
- injectTodoComment(result.unrecognized, path.parent, options, replacements);
3933
+ injectTodoComment(result.unrecognized, parent, options, replacements);
3718
3934
  return;
3719
3935
  }
3720
3936
  if (t.isLogicalExpression(expr) && expr.operator === "&&") {
@@ -3730,8 +3946,9 @@ function transformSource(source, filePath, options = {}) {
3730
3946
  } else {
3731
3947
  classNamesSkipped++;
3732
3948
  warnings.push(...result.warnings.map((w) => `[${filePath}] ${w}`));
3949
+ classesUnrecognized.push(...result.unrecognized);
3733
3950
  }
3734
- injectTodoComment(result.unrecognized, path.parent, options, replacements);
3951
+ injectTodoComment(result.unrecognized, parent, options, replacements);
3735
3952
  return;
3736
3953
  }
3737
3954
  classNamesSkipped++;
@@ -3763,7 +3980,12 @@ function transformSource(source, filePath, options = {}) {
3763
3980
  code: output,
3764
3981
  changed: replacements.length > 0,
3765
3982
  warnings,
3766
- stats: { classNamesTransformed, classNamesSkipped, classesUnrecognized },
3983
+ stats: {
3984
+ classNamesTransformed,
3985
+ classNamesSkipped,
3986
+ classNamesSkippedComponent,
3987
+ classesUnrecognized
3988
+ },
3767
3989
  potentiallyUnusedImports
3768
3990
  };
3769
3991
  }
@@ -3805,6 +4027,7 @@ function transformHtmlSourceSimple(source, filePath, options = {}) {
3805
4027
  const warnings = [];
3806
4028
  let classNamesTransformed = 0;
3807
4029
  let classNamesSkipped = 0;
4030
+ const classNamesSkippedComponent = 0;
3808
4031
  const classesUnrecognized = [];
3809
4032
  let changed = false;
3810
4033
  let output = source.replace(/\bclass="([^"]*)"/g, (match, classStr) => {
@@ -3852,7 +4075,12 @@ function transformHtmlSourceSimple(source, filePath, options = {}) {
3852
4075
  code: output,
3853
4076
  changed,
3854
4077
  warnings,
3855
- stats: { classNamesTransformed, classNamesSkipped, classesUnrecognized },
4078
+ stats: {
4079
+ classNamesTransformed,
4080
+ classNamesSkipped,
4081
+ classNamesSkippedComponent,
4082
+ classesUnrecognized
4083
+ },
3856
4084
  potentiallyUnusedImports: []
3857
4085
  };
3858
4086
  }
@@ -3860,9 +4088,9 @@ function transformHtmlSourceSimple(source, filePath, options = {}) {
3860
4088
  function createLogFile(cwd) {
3861
4089
  const now = /* @__PURE__ */ new Date();
3862
4090
  const ts = now.toISOString().slice(0, 19).replace("T", "_").replace(/:/g, "-");
3863
- const logDir = path.join(cwd, ".csszyx", "logs");
4091
+ const logDir = path__default.join(cwd, ".csszyx", "logs");
3864
4092
  fs$1.mkdirSync(logDir, { recursive: true });
3865
- const filePath = path.join(logDir, `migrate-${ts}.log`);
4093
+ const filePath = path__default.join(logDir, `migrate-${ts}.log`);
3866
4094
  const lines = [`csszyx migrate \u2014 ${now.toISOString()}`, ""];
3867
4095
  return {
3868
4096
  filePath,
@@ -3873,7 +4101,7 @@ function createLogFile(cwd) {
3873
4101
  }
3874
4102
  function isGitignored(cwd, pattern) {
3875
4103
  try {
3876
- const content = fs$1.readFileSync(path.join(cwd, ".gitignore"), "utf-8");
4104
+ const content = fs$1.readFileSync(path__default.join(cwd, ".gitignore"), "utf-8");
3877
4105
  return content.split("\n").some((l) => {
3878
4106
  const t = l.trim();
3879
4107
  return t === pattern || t === `${pattern}/` || t === `/${pattern}`;
@@ -3916,7 +4144,7 @@ async function migrate(options = {}) {
3916
4144
  }
3917
4145
  if (resolveTodosPath) {
3918
4146
  try {
3919
- const absolutePath = path.resolve(cwd, resolveTodosPath);
4147
+ const absolutePath = path__default.resolve(cwd, resolveTodosPath);
3920
4148
  const content = fs$1.readFileSync(absolutePath, "utf-8");
3921
4149
  customMap = JSON.parse(content);
3922
4150
  printInfo(`Loaded resolution map from ${resolveTodosPath}`);
@@ -3972,6 +4200,7 @@ async function migrate(options = {}) {
3972
4200
  }
3973
4201
  let totalTransformed = 0;
3974
4202
  let totalSkipped = 0;
4203
+ let totalSkippedComponent = 0;
3975
4204
  let totalFiles = 0;
3976
4205
  const allUnrecognized = [];
3977
4206
  const allWarnings = [];
@@ -4003,16 +4232,17 @@ async function migrate(options = {}) {
4003
4232
  totalFiles++;
4004
4233
  totalTransformed += result.stats.classNamesTransformed;
4005
4234
  totalSkipped += result.stats.classNamesSkipped;
4235
+ totalSkippedComponent += result.stats.classNamesSkippedComponent;
4006
4236
  allUnrecognized.push(...result.stats.classesUnrecognized);
4007
4237
  if (result.potentiallyUnusedImports.length > 0) {
4008
- const rel2 = path.relative(cwd, filePath);
4238
+ const rel2 = path__default.relative(cwd, filePath);
4009
4239
  unusedImportFiles.push({ file: rel2, imports: result.potentiallyUnusedImports });
4010
4240
  }
4011
4241
  if (!dryRun) {
4012
4242
  try {
4013
4243
  fs$1.writeFileSync(filePath, result.code, "utf-8");
4014
4244
  } catch (err) {
4015
- const rel2 = path.relative(cwd, filePath);
4245
+ const rel2 = path__default.relative(cwd, filePath);
4016
4246
  printWarn(
4017
4247
  `Could not write ${rel2}: ${err instanceof Error ? err.message : String(err)}`
4018
4248
  );
@@ -4020,7 +4250,7 @@ async function migrate(options = {}) {
4020
4250
  continue;
4021
4251
  }
4022
4252
  }
4023
- const rel = path.relative(cwd, filePath);
4253
+ const rel = path__default.relative(cwd, filePath);
4024
4254
  if (dryRun) {
4025
4255
  printInfo(` ${rel}: ${result.stats.classNamesTransformed} className(s) \u2192 sz`);
4026
4256
  log.writeLine(` ${rel}: ${result.stats.classNamesTransformed} className(s) \u2192 sz`);
@@ -4039,6 +4269,10 @@ async function migrate(options = {}) {
4039
4269
  printWarn(`classNames skipped (dynamic): ${totalSkipped}`);
4040
4270
  log.writeLine(`classNames skipped (dynamic): ${totalSkipped}`);
4041
4271
  }
4272
+ if (totalSkippedComponent > 0) {
4273
+ printWarn(`classNames kept on components (no sz support): ${totalSkippedComponent}`);
4274
+ log.writeLine(`classNames kept on components (no sz support): ${totalSkippedComponent}`);
4275
+ }
4042
4276
  if (allUnrecognized.length > 0) {
4043
4277
  const unique = [...new Set(allUnrecognized)];
4044
4278
  printWarn(
@@ -4061,7 +4295,7 @@ async function migrate(options = {}) {
4061
4295
  }
4062
4296
  }
4063
4297
  if (audit) {
4064
- const todoPath = path.join(cwd, ".csszyx-todo.json");
4298
+ const todoPath = path__default.join(cwd, ".csszyx-todo.json");
4065
4299
  const unique = [...new Set(allUnrecognized)];
4066
4300
  console.info();
4067
4301
  if (unique.length === 0) {
@@ -4078,19 +4312,19 @@ async function migrate(options = {}) {
4078
4312
  fs$1.writeFileSync(todoPath, JSON.stringify(todoObj, null, 2));
4079
4313
  } catch (err) {
4080
4314
  printWarn(
4081
- `Could not write ${path.relative(cwd, todoPath)}: ${err instanceof Error ? err.message : String(err)}`
4315
+ `Could not write ${path__default.relative(cwd, todoPath)}: ${err instanceof Error ? err.message : String(err)}`
4082
4316
  );
4083
4317
  log.flush();
4084
4318
  return;
4085
4319
  }
4086
4320
  printSuccess(
4087
- `Audit complete. Exported ${unique.length} unrecognized classes to ${path.relative(cwd, todoPath)}.`
4321
+ `Audit complete. Exported ${unique.length} unrecognized classes to ${path__default.relative(cwd, todoPath)}.`
4088
4322
  );
4089
4323
  printInfo(
4090
4324
  "Edit this file to map custom classes, then run: npx @csszyx/cli migrate --resolve-todos .csszyx-todo.json"
4091
4325
  );
4092
4326
  log.writeLine(
4093
- `Audit: ${unique.length} unrecognized classes written to ${path.relative(cwd, todoPath)}`
4327
+ `Audit: ${unique.length} unrecognized classes written to ${path__default.relative(cwd, todoPath)}`
4094
4328
  );
4095
4329
  }
4096
4330
  }
@@ -4115,13 +4349,363 @@ async function migrate(options = {}) {
4115
4349
  }
4116
4350
  try {
4117
4351
  log.flush();
4118
- printInfo(`Migration log saved to ${path.relative(cwd, log.filePath)}`);
4352
+ printInfo(`Migration log saved to ${path__default.relative(cwd, log.filePath)}`);
4119
4353
  } catch {
4120
4354
  }
4121
4355
  }
4122
4356
 
4357
+ const DEFAULT_NEXT_SOURCE_PATTERN = "{app,pages,src}/**/*.{ts,tsx,js,jsx,mjs,cjs}";
4358
+ const DEFAULT_NEXT_SOURCE_IGNORE = [
4359
+ "node_modules/**",
4360
+ ".git/**",
4361
+ ".next/**",
4362
+ ".next-turbo-*/**",
4363
+ ".csszyx/**",
4364
+ "dist/**",
4365
+ "build/**"
4366
+ ];
4367
+
4368
+ async function nextPrebuild(options = {}) {
4369
+ const cwd = path__default.resolve(options.cwd ?? process.cwd());
4370
+ const root = path__default.resolve(options.root ?? cwd);
4371
+ const pattern = options.pattern ?? DEFAULT_NEXT_SOURCE_PATTERN;
4372
+ try {
4373
+ const mode = normalizeMode(options.mode);
4374
+ const parserMode = normalizeParserMode$1(options.parserMode);
4375
+ const matches = await fg(pattern, {
4376
+ cwd: root,
4377
+ absolute: true,
4378
+ ignore: [...DEFAULT_NEXT_SOURCE_IGNORE, ...options.extraIgnore ?? []],
4379
+ dot: false,
4380
+ onlyFiles: true
4381
+ });
4382
+ if (matches.length === 0) {
4383
+ const message = `No source files matched pattern \`${pattern}\` under ${root}.`;
4384
+ if (options.json) {
4385
+ console.log(
4386
+ JSON.stringify(
4387
+ { ok: false, reason: "no-files-matched", root, pattern, mode },
4388
+ null,
4389
+ 2
4390
+ )
4391
+ );
4392
+ } else {
4393
+ console.error(`${colors.error(icons.error)} ${message}`);
4394
+ }
4395
+ return 1;
4396
+ }
4397
+ const result = runNextPrebuild({
4398
+ files: matches,
4399
+ explicitRoot: root,
4400
+ cwd,
4401
+ mode,
4402
+ parserMode,
4403
+ safelistOutputFile: options.outputFile,
4404
+ cacheDir: options.cacheDir,
4405
+ config: { mangleVars: false }
4406
+ // Versions intentionally omitted: runNextPrebuild's package.json
4407
+ // fallback reads the real installed @csszyx/unplugin and
4408
+ // @csszyx/compiler versions so the manifest's generation identity
4409
+ // tracks the engine that actually runs the transform.
4410
+ });
4411
+ if (options.json) {
4412
+ console.log(
4413
+ JSON.stringify(
4414
+ {
4415
+ ok: true,
4416
+ root,
4417
+ mode,
4418
+ scannedCount: result.scannedCount,
4419
+ transformedCount: result.transformedCount,
4420
+ skippedMissingCount: result.skippedMissingCount,
4421
+ sourceCount: result.sourceCount,
4422
+ classCount: result.classCount,
4423
+ manifestPath: result.manifestPath,
4424
+ safelistOutputPath: result.safelistOutputPath
4425
+ },
4426
+ null,
4427
+ 2
4428
+ )
4429
+ );
4430
+ } else {
4431
+ console.log(`${colors.success(icons.success)} csszyx next prebuild done`);
4432
+ console.log(` root: ${root}`);
4433
+ console.log(` mode: ${mode}`);
4434
+ console.log(` scanned: ${result.scannedCount}`);
4435
+ console.log(` transformed: ${result.transformedCount}`);
4436
+ console.log(` skipped: ${result.skippedMissingCount}`);
4437
+ console.log(` sources: ${result.sourceCount}`);
4438
+ console.log(` classes: ${result.classCount}`);
4439
+ console.log(` safelist: ${result.safelistOutputPath}`);
4440
+ console.log(` manifest: ${result.manifestPath}`);
4441
+ }
4442
+ return 0;
4443
+ } catch (error) {
4444
+ const message = error instanceof Error ? error.message : String(error);
4445
+ if (options.json) {
4446
+ console.log(JSON.stringify({ ok: false, reason: message }, null, 2));
4447
+ } else {
4448
+ console.error(`${colors.error(icons.error)} ${message}`);
4449
+ }
4450
+ return 1;
4451
+ }
4452
+ }
4453
+ function normalizeMode(mode) {
4454
+ if (mode === void 0) {
4455
+ return "production";
4456
+ }
4457
+ if (mode === "development" || mode === "production") {
4458
+ return mode;
4459
+ }
4460
+ throw new Error(`Invalid --mode "${mode}". Expected "development" or "production".`);
4461
+ }
4462
+ function normalizeParserMode$1(parserMode) {
4463
+ if (parserMode === void 0) {
4464
+ return void 0;
4465
+ }
4466
+ if (parserMode === "rust" || parserMode === "oxc" || parserMode === "babel") {
4467
+ return parserMode;
4468
+ }
4469
+ throw new Error(`Invalid --parser-mode "${parserMode}". Expected "rust", "oxc", or "babel".`);
4470
+ }
4471
+
4472
+ const SOURCE_EXTENSION = /\.[cm]?[jt]sx?$/i;
4473
+ async function startNextWatch(options = {}, dependencies = {}) {
4474
+ const cwd = path.resolve(options.cwd ?? process.cwd());
4475
+ const root = path.resolve(options.root ?? cwd);
4476
+ const pattern = options.pattern ?? DEFAULT_NEXT_SOURCE_PATTERN;
4477
+ const ignore = [...DEFAULT_NEXT_SOURCE_IGNORE, ...options.extraIgnore ?? []];
4478
+ const parserMode = normalizeParserMode(options.parserMode);
4479
+ const debounceMs = normalizeDebounceMs(options.debounceMs);
4480
+ const files = await fg(pattern, {
4481
+ cwd: root,
4482
+ absolute: true,
4483
+ ignore,
4484
+ dot: false,
4485
+ onlyFiles: true
4486
+ });
4487
+ if (files.length === 0) {
4488
+ throw new Error(`No source files matched pattern \`${pattern}\` under ${root}.`);
4489
+ }
4490
+ const prebuild = runNextPrebuild({
4491
+ files,
4492
+ explicitRoot: root,
4493
+ cwd,
4494
+ mode: "development",
4495
+ parserMode,
4496
+ safelistOutputFile: options.outputFile,
4497
+ cacheDir: options.cacheDir,
4498
+ config: { mangleVars: false }
4499
+ });
4500
+ let resolveFailure = () => {
4501
+ };
4502
+ let failed = false;
4503
+ const failure = new Promise((resolve) => {
4504
+ resolveFailure = resolve;
4505
+ });
4506
+ const reportFailure = (error) => {
4507
+ if (failed) {
4508
+ return;
4509
+ }
4510
+ failed = true;
4511
+ resolveFailure(error instanceof Error ? error : new Error(String(error)));
4512
+ };
4513
+ const controller = new NextSafelistWatcher({
4514
+ context: prebuild.context,
4515
+ debounceMs,
4516
+ onError: reportFailure
4517
+ });
4518
+ const watchFactory = dependencies.watch ?? watch;
4519
+ const fsWatcher = watchFactory(root, {
4520
+ ignoreInitial: true,
4521
+ persistent: true,
4522
+ atomic: true,
4523
+ awaitWriteFinish: {
4524
+ stabilityThreshold: 25,
4525
+ pollInterval: 10
4526
+ },
4527
+ ignored: createIgnoredMatcher(root, prebuild.context.safelist.shardsDir, ignore)
4528
+ });
4529
+ fsWatcher.on("all", (event, filePath) => {
4530
+ const absolutePath = path.resolve(filePath);
4531
+ if (event === "add" || event === "change" || event === "unlink") {
4532
+ if (controller.notify(event, absolutePath) || event !== "unlink" || !SOURCE_EXTENSION.test(absolutePath)) {
4533
+ return;
4534
+ }
4535
+ controller.notifySourceRemoval(absolutePath);
4536
+ }
4537
+ });
4538
+ fsWatcher.on("error", reportFailure);
4539
+ try {
4540
+ await waitForWatcherReady(fsWatcher);
4541
+ controller.start();
4542
+ } catch (error) {
4543
+ await fsWatcher.close();
4544
+ controller.close();
4545
+ throw error;
4546
+ }
4547
+ let closed = false;
4548
+ return {
4549
+ root,
4550
+ sourcePattern: pattern,
4551
+ safelistOutputPath: prebuild.safelistOutputPath,
4552
+ manifestPath: prebuild.manifestPath,
4553
+ failure,
4554
+ close: async () => {
4555
+ if (closed) {
4556
+ return;
4557
+ }
4558
+ closed = true;
4559
+ await fsWatcher.close();
4560
+ controller.close();
4561
+ }
4562
+ };
4563
+ }
4564
+ async function nextWatch(options = {}) {
4565
+ let session;
4566
+ let exitCode = 0;
4567
+ try {
4568
+ session = await startNextWatch(options);
4569
+ if (!options.silent) {
4570
+ console.log(`${colors.success(icons.success)} csszyx next watch ready`);
4571
+ console.log(` root: ${session.root}`);
4572
+ console.log(` pattern: ${session.sourcePattern}`);
4573
+ console.log(` safelist: ${session.safelistOutputPath}`);
4574
+ console.log(` manifest: ${session.manifestPath}`);
4575
+ }
4576
+ const outcome = await waitForShutdown(session.failure);
4577
+ if (outcome) {
4578
+ console.error(`${colors.error(icons.error)} ${outcome.message}`);
4579
+ exitCode = 1;
4580
+ }
4581
+ } catch (error) {
4582
+ const message = error instanceof Error ? error.message : String(error);
4583
+ console.error(`${colors.error(icons.error)} ${message}`);
4584
+ exitCode = 1;
4585
+ }
4586
+ try {
4587
+ await session?.close();
4588
+ } catch (error) {
4589
+ const message = error instanceof Error ? error.message : String(error);
4590
+ console.error(`${colors.error(icons.error)} Failed to close Next watcher: ${message}`);
4591
+ exitCode = 1;
4592
+ }
4593
+ return exitCode;
4594
+ }
4595
+ function waitForWatcherReady(watcher) {
4596
+ return new Promise((resolve, reject) => {
4597
+ const onReady = () => {
4598
+ watcher.off("error", onStartupError);
4599
+ resolve();
4600
+ };
4601
+ const onStartupError = (error) => {
4602
+ watcher.off("ready", onReady);
4603
+ reject(error);
4604
+ };
4605
+ watcher.once("ready", onReady);
4606
+ watcher.once("error", onStartupError);
4607
+ });
4608
+ }
4609
+ function waitForShutdown(failure) {
4610
+ return new Promise((resolve) => {
4611
+ const cleanup = () => {
4612
+ process.off("SIGINT", onSignal);
4613
+ process.off("SIGTERM", onSignal);
4614
+ };
4615
+ const onSignal = () => {
4616
+ cleanup();
4617
+ resolve(void 0);
4618
+ };
4619
+ process.once("SIGINT", onSignal);
4620
+ process.once("SIGTERM", onSignal);
4621
+ failure.then((error) => {
4622
+ cleanup();
4623
+ resolve(error);
4624
+ });
4625
+ });
4626
+ }
4627
+ function createIgnoredMatcher(root, shardsDir, ignore) {
4628
+ const normalizedShardsDir = path.resolve(shardsDir);
4629
+ const matchers = ignore.flatMap((pattern) => {
4630
+ const normalized = pattern.replace(/\\/g, "/");
4631
+ const variants = normalized.endsWith("/**") ? [normalized, normalized.slice(0, -3)] : [normalized];
4632
+ return variants.map((variant) => new Minimatch(variant, { dot: true }));
4633
+ });
4634
+ return (candidate) => {
4635
+ const absolute = path.resolve(candidate);
4636
+ const relativeToShards = path.relative(absolute, normalizedShardsDir);
4637
+ if (absolute === normalizedShardsDir || absolute.startsWith(`${normalizedShardsDir}${path.sep}`) || relativeToShards !== ".." && !relativeToShards.startsWith(`..${path.sep}`) && !path.isAbsolute(relativeToShards)) {
4638
+ return false;
4639
+ }
4640
+ const relative = path.relative(root, absolute).replace(/\\/g, "/");
4641
+ if (!relative || relative.startsWith("../") || path.isAbsolute(relative)) {
4642
+ return false;
4643
+ }
4644
+ return matchers.some((matcher) => matcher.match(relative));
4645
+ };
4646
+ }
4647
+ function normalizeParserMode(parserMode) {
4648
+ if (parserMode === void 0) {
4649
+ return void 0;
4650
+ }
4651
+ if (parserMode === "rust" || parserMode === "oxc" || parserMode === "babel") {
4652
+ return parserMode;
4653
+ }
4654
+ throw new Error(`Invalid --parser-mode "${parserMode}". Expected "rust", "oxc", or "babel".`);
4655
+ }
4656
+ function normalizeDebounceMs(debounceMs) {
4657
+ if (debounceMs === void 0) {
4658
+ return void 0;
4659
+ }
4660
+ const parsed = typeof debounceMs === "number" ? debounceMs : Number(debounceMs);
4661
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 6e4) {
4662
+ throw new Error("Invalid --debounce-ms. Expected an integer between 0 and 60000.");
4663
+ }
4664
+ return parsed;
4665
+ }
4666
+
4123
4667
  const cli = cac("csszyx");
4124
- const VERSION = "0.0.0";
4668
+ normalizeNextCommandAlias(process.argv);
4669
+ function readCliVersion() {
4670
+ try {
4671
+ const manifest = JSON.parse(
4672
+ readFileSync(new URL("../package.json", import.meta.url), "utf8")
4673
+ );
4674
+ return typeof manifest.version === "string" ? manifest.version : "0.0.0";
4675
+ } catch {
4676
+ return "0.0.0";
4677
+ }
4678
+ }
4679
+ const VERSION = readCliVersion();
4680
+ async function runNextPrebuildCommand(pattern, options) {
4681
+ const code = await nextPrebuild({
4682
+ cwd: options.cwd,
4683
+ root: options.root,
4684
+ mode: options.mode,
4685
+ parserMode: options.parserMode,
4686
+ outputFile: options.outputFile,
4687
+ cacheDir: options.cacheDir,
4688
+ pattern,
4689
+ extraIgnore: options.ignore ? String(options.ignore).split(",") : void 0,
4690
+ json: options.json
4691
+ });
4692
+ if (code !== 0) {
4693
+ process.exit(code);
4694
+ }
4695
+ }
4696
+ async function runNextWatchCommand(pattern, options) {
4697
+ const code = await nextWatch({
4698
+ cwd: options.cwd,
4699
+ root: options.root,
4700
+ parserMode: options.parserMode,
4701
+ outputFile: options.outputFile,
4702
+ cacheDir: options.cacheDir,
4703
+ pattern,
4704
+ extraIgnore: options.ignore ? String(options.ignore).split(",") : void 0,
4705
+ debounceMs: options.debounceMs
4706
+ });
4707
+ process.exitCode = code;
4708
+ }
4125
4709
  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) => {
4126
4710
  await init({
4127
4711
  framework: options.framework,
@@ -4170,11 +4754,27 @@ cli.command("migrate [dir]", "Convert Tailwind className to sz prop").option("--
4170
4754
  resolveTodos: options.resolveTodos
4171
4755
  });
4172
4756
  });
4757
+ cli.command(
4758
+ "next-prebuild [pattern]",
4759
+ "Seed the Next.js Turbopack csszyx safelist and generation manifest"
4760
+ ).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(
4761
+ "--output-file <path>",
4762
+ "Tailwind @source safelist output (default: csszyx-classes.html)"
4763
+ ).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);
4764
+ 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(
4765
+ "--output-file <path>",
4766
+ "Tailwind @source safelist output (default: csszyx-classes.html)"
4767
+ ).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);
4173
4768
  cli.command("").action(() => {
4174
4769
  cli.outputHelp();
4175
4770
  });
4176
4771
  cli.help();
4177
4772
  cli.version(VERSION);
4178
4773
  cli.parse();
4774
+ function normalizeNextCommandAlias(argv) {
4775
+ if (argv[2] === "next" && (argv[3] === "prebuild" || argv[3] === "watch")) {
4776
+ argv.splice(2, 2, `next-${argv[3]}`);
4777
+ }
4778
+ }
4179
4779
 
4180
4780
  export { classNameToSzObject, extractScreenKeys, extractSpacingKeys, findConfigFile, flattenColors, generateAndWriteTypes, generateTypeDeclarations, generateTypes, transformSource as migrateSource, scanTailwindConfig, writeDeclarationFile };