@csszyx/unplugin 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/webpack.cjs CHANGED
@@ -200,6 +200,194 @@ function transformIndexHtml(html, mangleMap, checksum, options = {}) {
200
200
  return injectHydrationData(html, mangleMap, checksum, options);
201
201
  }
202
202
 
203
+ // src/theme-scanner.ts
204
+ var EMPTY_THEME = { colors: [], spacings: [], fonts: [], radii: [], shadows: [] };
205
+ function stripLayerWrappers(css) {
206
+ let result = "";
207
+ let i = 0;
208
+ while (i < css.length) {
209
+ const layerIdx = css.indexOf("@layer", i);
210
+ if (layerIdx === -1) {
211
+ result += css.slice(i);
212
+ break;
213
+ }
214
+ result += css.slice(i, layerIdx);
215
+ const openBrace = css.indexOf("{", layerIdx);
216
+ if (openBrace === -1) {
217
+ result += css.slice(layerIdx);
218
+ break;
219
+ }
220
+ let depth = 0;
221
+ let j = openBrace;
222
+ while (j < css.length) {
223
+ if (css[j] === "{") {
224
+ depth++;
225
+ }
226
+ if (css[j] === "}") {
227
+ depth--;
228
+ if (depth === 0) {
229
+ result += css.slice(openBrace + 1, j);
230
+ i = j + 1;
231
+ break;
232
+ }
233
+ }
234
+ j++;
235
+ }
236
+ if (depth !== 0) {
237
+ result += css.slice(openBrace);
238
+ break;
239
+ }
240
+ }
241
+ return result;
242
+ }
243
+ function extractThemeBlocks(css) {
244
+ const blocks = [];
245
+ const themeStart = /@theme\s+(?:inline\s+)?\{|@theme\{/g;
246
+ let match;
247
+ while ((match = themeStart.exec(css)) !== null) {
248
+ const openPos = css.indexOf("{", match.index);
249
+ let depth = 0;
250
+ let j = openPos;
251
+ while (j < css.length) {
252
+ if (css[j] === "{") {
253
+ depth++;
254
+ }
255
+ if (css[j] === "}") {
256
+ depth--;
257
+ if (depth === 0) {
258
+ blocks.push(css.slice(openPos + 1, j));
259
+ break;
260
+ }
261
+ }
262
+ j++;
263
+ }
264
+ }
265
+ return blocks;
266
+ }
267
+ function categorizeProperty(prop) {
268
+ const categoryMap = [
269
+ ["color-", "colors"],
270
+ ["spacing-", "spacings"],
271
+ ["font-", "fonts"],
272
+ ["radius-", "radii"],
273
+ ["shadow-", "shadows"]
274
+ ];
275
+ for (const [prefix, category] of categoryMap) {
276
+ if (prop.startsWith(prefix)) {
277
+ let token = prop.slice(prefix.length);
278
+ token = token.replace(/-\d+$/, "");
279
+ if (token) {
280
+ return { category, token };
281
+ }
282
+ }
283
+ }
284
+ return null;
285
+ }
286
+ function parseThemeBlocks(cssContent) {
287
+ const result = {
288
+ colors: /* @__PURE__ */ new Set(),
289
+ spacings: /* @__PURE__ */ new Set(),
290
+ fonts: /* @__PURE__ */ new Set(),
291
+ radii: /* @__PURE__ */ new Set(),
292
+ shadows: /* @__PURE__ */ new Set()
293
+ };
294
+ const stripped = stripLayerWrappers(cssContent);
295
+ const blocks = extractThemeBlocks(stripped);
296
+ const propPattern = /--([a-z][a-z0-9-]*)(?:\s*:[^;]+)?;/g;
297
+ for (const block of blocks) {
298
+ let match;
299
+ while ((match = propPattern.exec(block)) !== null) {
300
+ const categorized = categorizeProperty(match[1]);
301
+ if (categorized) {
302
+ result[categorized.category].add(categorized.token);
303
+ }
304
+ }
305
+ propPattern.lastIndex = 0;
306
+ }
307
+ return {
308
+ colors: [...result.colors].sort(),
309
+ spacings: [...result.spacings].sort(),
310
+ fonts: [...result.fonts].sort(),
311
+ radii: [...result.radii].sort(),
312
+ shadows: [...result.shadows].sort()
313
+ };
314
+ }
315
+ function mergeThemes(themes) {
316
+ if (themes.length === 0) {
317
+ return { ...EMPTY_THEME };
318
+ }
319
+ const merged = {
320
+ colors: /* @__PURE__ */ new Set(),
321
+ spacings: /* @__PURE__ */ new Set(),
322
+ fonts: /* @__PURE__ */ new Set(),
323
+ radii: /* @__PURE__ */ new Set(),
324
+ shadows: /* @__PURE__ */ new Set()
325
+ };
326
+ for (const theme of themes) {
327
+ for (const cat of Object.keys(merged)) {
328
+ for (const token of theme[cat]) {
329
+ merged[cat].add(token);
330
+ }
331
+ }
332
+ }
333
+ return {
334
+ colors: [...merged.colors].sort(),
335
+ spacings: [...merged.spacings].sort(),
336
+ fonts: [...merged.fonts].sort(),
337
+ radii: [...merged.radii].sort(),
338
+ shadows: [...merged.shadows].sort()
339
+ };
340
+ }
341
+
342
+ // src/theme-type-writer.ts
343
+ var import_node_fs = require("fs");
344
+ var import_node_path = require("path");
345
+ function generateThemeDts(opts) {
346
+ const { theme, sourceFiles } = opts;
347
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
348
+ const sources = sourceFiles.join(", ");
349
+ const toUnion = (tokens) => tokens.map((t) => `'${t}'`).join(" | ");
350
+ const entries = [];
351
+ if (theme.colors.length > 0) {
352
+ entries.push(` colors: ${toUnion(theme.colors)};`);
353
+ }
354
+ if (theme.spacings.length > 0) {
355
+ entries.push(` spacings: ${toUnion(theme.spacings)};`);
356
+ }
357
+ if (theme.fonts.length > 0) {
358
+ entries.push(` fonts: ${toUnion(theme.fonts)};`);
359
+ }
360
+ if (theme.radii.length > 0) {
361
+ entries.push(` radii: ${toUnion(theme.radii)};`);
362
+ }
363
+ if (theme.shadows.length > 0) {
364
+ entries.push(` shadows: ${toUnion(theme.shadows)};`);
365
+ }
366
+ return [
367
+ "// Auto-generated by csszyx theme-scanner \u2014 DO NOT EDIT",
368
+ `// Source: ${sources}`,
369
+ `// Updated: ${timestamp}`,
370
+ "",
371
+ "declare module '@csszyx/compiler' {",
372
+ " /**",
373
+ " * Custom design tokens extracted from @theme blocks.",
374
+ " * These tokens are surfaced in sz prop IntelliSense.",
375
+ " */",
376
+ " interface CustomTheme {",
377
+ ...entries,
378
+ " }",
379
+ "}",
380
+ "",
381
+ "export {};",
382
+ ""
383
+ ].join("\n");
384
+ }
385
+ function writeThemeDts(opts) {
386
+ const content = generateThemeDts(opts);
387
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(opts.outputPath), { recursive: true });
388
+ (0, import_node_fs.writeFileSync)(opts.outputPath, content, "utf-8");
389
+ }
390
+
203
391
  // src/virtual-modules.ts
204
392
  var VIRTUAL_MODULE_ID = "virtual:csszyx/mangle-map";
205
393
  var RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID;
@@ -252,19 +440,259 @@ function resolveVirtualModule(id) {
252
440
  // src/unplugin.ts
253
441
  var CHECKSUM_PLACEHOLDER = "___CSSZYX_CHECKSUM___";
254
442
  var MANGLE_MAP_PLACEHOLDER = "___CSSZYX_MANGLE_MAP___";
443
+ var _hasWarnedTsConfig = false;
444
+ function runThemeScan(rootDir, scanCss) {
445
+ if (!scanCss) {
446
+ return;
447
+ }
448
+ const patterns = Array.isArray(scanCss) ? scanCss : [scanCss];
449
+ const sourceFiles = [];
450
+ for (const pattern of patterns) {
451
+ const resolved = path.isAbsolute(pattern) ? pattern : path.join(rootDir, pattern);
452
+ if (fs.existsSync(resolved)) {
453
+ sourceFiles.push(resolved);
454
+ }
455
+ }
456
+ if (sourceFiles.length === 0) {
457
+ return;
458
+ }
459
+ const themes = sourceFiles.map((f) => {
460
+ try {
461
+ return parseThemeBlocks(fs.readFileSync(f, "utf-8"));
462
+ } catch {
463
+ return null;
464
+ }
465
+ }).filter((t) => t !== null);
466
+ const merged = mergeThemes(themes);
467
+ const outputPath = path.join(rootDir, ".csszyx", "theme.d.ts");
468
+ writeThemeDts({ outputPath, theme: merged, sourceFiles });
469
+ if (!_hasWarnedTsConfig) {
470
+ _hasWarnedTsConfig = true;
471
+ try {
472
+ const checkFile = (cfgPath) => {
473
+ if (fs.existsSync(cfgPath)) {
474
+ const content = fs.readFileSync(cfgPath, "utf-8");
475
+ if (!content.includes(".csszyx")) {
476
+ console.warn(`
477
+ \x1B[33m\u26A0\uFE0F CSSzyx: Theme Auto-Scan enabled, but TypeScript isn't configured. Run "npx @csszyx/cli init" to fix.\x1B[0m
478
+ `);
479
+ }
480
+ return true;
481
+ }
482
+ return false;
483
+ };
484
+ if (!checkFile(path.join(rootDir, "tsconfig.json"))) {
485
+ checkFile(path.join(rootDir, "tsconfig.app.json"));
486
+ }
487
+ } catch {
488
+ }
489
+ }
490
+ }
491
+ function mangleCodeClassesSync(code, mangleMap) {
492
+ function mangleClassString(classString) {
493
+ return classString.split(/\s+/).filter(Boolean).map((cls) => {
494
+ return mangleMap[cls.replace(/\\(.)/g, "$1")] || cls;
495
+ }).join(" ");
496
+ }
497
+ let result = code.replace(/(?:class(?:Name)?|sz)[:=]\s*"((?:[^"\\]|\\.)*)"/g, (match, classes) => {
498
+ const mangled = mangleClassString(classes);
499
+ if (mangled === classes) {
500
+ return match;
501
+ }
502
+ return match.replace(classes, mangled);
503
+ }).replace(/(?:class(?:Name)?|sz)[:=]\s*'((?:[^'\\]|\\.)*)'/g, (match, classes) => {
504
+ const mangled = mangleClassString(classes);
505
+ if (mangled === classes) {
506
+ return match;
507
+ }
508
+ return match.replace(classes, mangled);
509
+ });
510
+ result = result.replace(/className:\s*`([^`]+)`/g, (fullMatch, tplContent) => {
511
+ let changed = false;
512
+ let out = "";
513
+ let i = 0;
514
+ while (i < tplContent.length) {
515
+ const interStart = tplContent.indexOf("${", i);
516
+ if (interStart === -1) {
517
+ const quasi2 = tplContent.slice(i);
518
+ const trimmed2 = quasi2.trim();
519
+ if (trimmed2) {
520
+ const m = mangleClassString(trimmed2);
521
+ if (m !== trimmed2) {
522
+ changed = true;
523
+ out += quasi2.replace(trimmed2, m);
524
+ } else {
525
+ out += quasi2;
526
+ }
527
+ } else {
528
+ out += quasi2;
529
+ }
530
+ break;
531
+ }
532
+ const quasi = tplContent.slice(i, interStart);
533
+ const trimmed = quasi.trim();
534
+ if (trimmed) {
535
+ const m = mangleClassString(trimmed);
536
+ if (m !== trimmed) {
537
+ changed = true;
538
+ out += quasi.replace(trimmed, m);
539
+ } else {
540
+ out += quasi;
541
+ }
542
+ } else {
543
+ out += quasi;
544
+ }
545
+ let j = interStart + 2;
546
+ let depth = 0;
547
+ while (j < tplContent.length) {
548
+ if (tplContent[j] === "{") {
549
+ depth++;
550
+ } else if (tplContent[j] === "}") {
551
+ if (depth === 0) {
552
+ j++;
553
+ break;
554
+ }
555
+ depth--;
556
+ }
557
+ j++;
558
+ }
559
+ const interInner = tplContent.slice(interStart + 2, j - 1);
560
+ const mangledInner = interInner.replace(/"([^"]*)"/g, (qm, inner) => {
561
+ const parts = inner.split(/\s+/).filter(Boolean);
562
+ if (parts.length === 0) {
563
+ return qm;
564
+ }
565
+ const m = parts.map((p) => mangleMap[p] || p).join(" ");
566
+ if (m === inner) {
567
+ return qm;
568
+ }
569
+ changed = true;
570
+ return '"' + m + '"';
571
+ });
572
+ out += "${" + mangledInner + "}";
573
+ i = j;
574
+ }
575
+ return changed ? "className:`" + out + "`" : fullMatch;
576
+ });
577
+ {
578
+ const marker = "className:";
579
+ let searchFrom = 0;
580
+ let out = "";
581
+ while (searchFrom < result.length) {
582
+ const idx = result.indexOf(marker, searchFrom);
583
+ if (idx === -1) {
584
+ out += result.slice(searchFrom);
585
+ break;
586
+ }
587
+ out += result.slice(searchFrom, idx + marker.length);
588
+ const afterColon = idx + marker.length;
589
+ let exprStart = afterColon;
590
+ while (exprStart < result.length && result[exprStart] === " ") {
591
+ exprStart++;
592
+ }
593
+ const firstChar = result[exprStart];
594
+ if (firstChar === '"' || firstChar === "'" || firstChar === "`") {
595
+ searchFrom = afterColon;
596
+ continue;
597
+ }
598
+ let depth = 0;
599
+ let j = afterColon;
600
+ while (j < result.length) {
601
+ const ch = result[j];
602
+ if (ch === "(" || ch === "[") {
603
+ depth++;
604
+ } else if (ch === ")" || ch === "]") {
605
+ if (depth === 0) {
606
+ break;
607
+ }
608
+ depth--;
609
+ } else if (depth === 0 && (ch === "," || ch === ";" || ch === "\n" || ch === "}")) {
610
+ break;
611
+ }
612
+ j++;
613
+ }
614
+ const expr = result.slice(afterColon, j);
615
+ const qIdx = expr.indexOf("?");
616
+ if (qIdx === -1 || !expr.slice(qIdx).includes(":")) {
617
+ out += expr;
618
+ searchFrom = j;
619
+ continue;
620
+ }
621
+ let changed = false;
622
+ const mangled = expr.replace(/"([^"]*)"/g, (qm, inner) => {
623
+ const parts = inner.split(/\s+/).filter(Boolean);
624
+ if (parts.length === 0) {
625
+ return qm;
626
+ }
627
+ const mangledStr = parts.map((p) => mangleMap[p] || p).join(" ");
628
+ if (mangledStr !== inner) {
629
+ changed = true;
630
+ return '"' + mangledStr + '"';
631
+ }
632
+ return qm;
633
+ });
634
+ out += changed ? mangled : expr;
635
+ searchFrom = j;
636
+ }
637
+ result = out;
638
+ }
639
+ result = result.replace(/(?<=(?:[,(]|&&)\s*)"([^"]+)"/g, (match, inner) => {
640
+ const tokens = inner.split(/\s+/).filter(Boolean);
641
+ if (tokens.length === 0) {
642
+ return match;
643
+ }
644
+ let changed = false;
645
+ const mangled = [];
646
+ for (const t of tokens) {
647
+ const m = mangleMap[t];
648
+ if (m === void 0) {
649
+ return match;
650
+ }
651
+ if (m !== t) {
652
+ changed = true;
653
+ }
654
+ mangled.push(m);
655
+ }
656
+ if (!changed) {
657
+ return match;
658
+ }
659
+ return '"' + mangled.join(" ") + '"';
660
+ });
661
+ return result;
662
+ }
255
663
  function createCsszyxPlugins(options = {}) {
256
664
  const manglingEnabled = options.production?.mangle !== false;
257
665
  const state = {
258
666
  classes: /* @__PURE__ */ new Set(),
259
667
  mangleMap: {},
260
668
  checksum: "",
261
- finalized: false
669
+ finalized: false,
670
+ rootDir: process.cwd()
262
671
  };
263
- const SAFELIST_FILENAME = "csszyx-classes.js";
672
+ const SAFELIST_FILENAME = "csszyx-classes.html";
264
673
  const SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js"]);
265
674
  const IGNORE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".next", ".git", "dist", "build", ".turbo"]);
266
- function prescanAndWriteClasses(rootDir) {
675
+ function writeSafelistFile(classes) {
676
+ if (classes.size === 0) {
677
+ return;
678
+ }
679
+ const safelistPath = path.join(state.rootDir, SAFELIST_FILENAME);
680
+ const classList = Array.from(classes).join(" ");
681
+ const content = `<!-- Auto-generated by csszyx \u2014 DO NOT EDIT -->
682
+ <!-- Tailwind CSS scans this file for class name detection -->
683
+ <div class="${classList}"><div class="${classList}">x</div><div class="${classList}">x</div></div>
684
+ `;
685
+ try {
686
+ const existing = fs.existsSync(safelistPath) ? fs.readFileSync(safelistPath, "utf-8") : "";
687
+ if (existing !== content) {
688
+ fs.writeFileSync(safelistPath, content);
689
+ }
690
+ } catch {
691
+ }
692
+ }
693
+ function prescanAndWriteClasses() {
267
694
  const discoveredClasses = /* @__PURE__ */ new Set();
695
+ const rawDiscoveredClasses = /* @__PURE__ */ new Set();
268
696
  function scanDir(dir) {
269
697
  let entries;
270
698
  try {
@@ -291,6 +719,9 @@ function createCsszyxPlugins(options = {}) {
291
719
  for (const cls of result.classes) {
292
720
  discoveredClasses.add(cls);
293
721
  }
722
+ for (const cls of result.rawClassNames) {
723
+ rawDiscoveredClasses.add(cls);
724
+ }
294
725
  if (result.usesRuntime) {
295
726
  const szCallRe = /_sz\(\s*\{/g;
296
727
  let szMatch;
@@ -345,21 +776,12 @@ function createCsszyxPlugins(options = {}) {
345
776
  }
346
777
  }
347
778
  }
348
- scanDir(rootDir);
779
+ scanDir(state.rootDir);
349
780
  for (const cls of discoveredClasses) {
350
781
  state.classes.add(cls);
351
782
  }
352
- if (discoveredClasses.size > 0) {
353
- const safelistPath = path.join(rootDir, SAFELIST_FILENAME);
354
- const content = '// Auto-generated by csszyx \u2014 DO NOT EDIT\n// Tailwind CSS scans this file for class name detection\nexport default "' + Array.from(discoveredClasses).join(" ") + '";\n';
355
- try {
356
- const existing = fs.existsSync(safelistPath) ? fs.readFileSync(safelistPath, "utf-8") : "";
357
- if (existing !== content) {
358
- fs.writeFileSync(safelistPath, content);
359
- }
360
- } catch {
361
- }
362
- }
783
+ const safelistClasses = /* @__PURE__ */ new Set([...discoveredClasses, ...rawDiscoveredClasses]);
784
+ writeSafelistFile(safelistClasses);
363
785
  }
364
786
  function extractClasses(code) {
365
787
  const dqPattern = /(?:class(?:Name)?|sz)[:=]\s*"([^"]*)"/g;
@@ -407,47 +829,8 @@ function createCsszyxPlugins(options = {}) {
407
829
  state.checksum = (0, import_core.compute_mangle_checksum)(state.mangleMap);
408
830
  state.finalized = true;
409
831
  }
410
- function mangleClassString(classString) {
411
- return classString.split(/\s+/).map((cls) => state.mangleMap[cls] || cls).join(" ");
412
- }
413
832
  function mangleCodeClasses(code) {
414
- let result = code.replace(/(?:class(?:Name)?|sz)[:=]\s*"([^"]*)"/g, (match, classes) => {
415
- const mangled = mangleClassString(classes);
416
- if (mangled === classes) {
417
- return match;
418
- }
419
- return match.replace(classes, mangled);
420
- }).replace(/(?:class(?:Name)?|sz)[:=]\s*'([^']*)'/g, (match, classes) => {
421
- const mangled = mangleClassString(classes);
422
- if (mangled === classes) {
423
- return match;
424
- }
425
- return match.replace(classes, mangled);
426
- });
427
- result = result.replace(/className:(?!["'])([^,;}\])\n]+)/g, (fullMatch, expr) => {
428
- const qIdx = expr.indexOf("?");
429
- if (qIdx === -1 || !expr.slice(qIdx).includes(":")) {
430
- return fullMatch;
431
- }
432
- let changed = false;
433
- const mangled = expr.replace(/"([^"]*)"/g, (qm, inner) => {
434
- const parts = inner.split(/\s+/).filter(Boolean);
435
- if (parts.length === 0) {
436
- return qm;
437
- }
438
- const mangledStr = parts.map((p) => state.mangleMap[p] || p).join(" ");
439
- if (mangledStr !== inner) {
440
- changed = true;
441
- return '"' + mangledStr + '"';
442
- }
443
- return qm;
444
- });
445
- if (changed) {
446
- return "className:" + mangled;
447
- }
448
- return fullMatch;
449
- });
450
- return result;
833
+ return mangleCodeClassesSync(code, state.mangleMap);
451
834
  }
452
835
  function replacePlaceholders(code) {
453
836
  let result = code;
@@ -455,7 +838,9 @@ function createCsszyxPlugins(options = {}) {
455
838
  result = result.split(CHECKSUM_PLACEHOLDER).join(state.checksum);
456
839
  }
457
840
  if (result.includes(MANGLE_MAP_PLACEHOLDER)) {
458
- result = result.split(MANGLE_MAP_PLACEHOLDER).join(JSON.stringify(state.mangleMap));
841
+ const jsonMap = JSON.stringify(state.mangleMap);
842
+ const escapedMap = result.includes("eval(") ? jsonMap.replace(/"/g, '\\"') : jsonMap;
843
+ result = result.split(MANGLE_MAP_PLACEHOLDER).join(escapedMap);
459
844
  }
460
845
  return result;
461
846
  }
@@ -517,12 +902,18 @@ function createCsszyxPlugins(options = {}) {
517
902
  if (hasTailwindImport && state.classes.size > 0) {
518
903
  const candidates = Array.from(state.classes).filter((c) => c.length >= 2 && /^[a-z]/.test(c)).join(" ");
519
904
  if (candidates) {
520
- const inlineDirective = `@source inline("${candidates}");
905
+ const safelistPath = path.join(state.rootDir, SAFELIST_FILENAME).replace(/\\/g, "/");
906
+ const cssDir = path.dirname(id).replace(/\\/g, "/");
907
+ let relPath = path.posix.relative(cssDir, safelistPath);
908
+ if (!relPath.startsWith(".")) {
909
+ relPath = "./" + relPath;
910
+ }
911
+ const sourceDirective = `@source "${relPath}";
521
912
  `;
522
913
  const transformed2 = code.replace(
523
914
  /(@import\s+["']tailwindcss[^"']*["'];)/,
524
915
  `$1
525
- ${inlineDirective}`
916
+ ${sourceDirective}`
526
917
  );
527
918
  if (transformed2 !== code) {
528
919
  return { code: transformed2, map: null };
@@ -533,6 +924,7 @@ ${inlineDirective}`
533
924
  }
534
925
  let transformedCode = code;
535
926
  let usesRuntime = false;
927
+ let usesMerge = false;
536
928
  let usesColorVar = false;
537
929
  let transformed = false;
538
930
  let szClasses;
@@ -554,9 +946,16 @@ ${inlineDirective}`
554
946
  const result = (0, import_compiler.transformSourceCode)(code);
555
947
  transformedCode = result.code;
556
948
  usesRuntime = result.usesRuntime;
949
+ usesMerge = result.usesMerge;
557
950
  usesColorVar = result.usesColorVar;
558
951
  transformed = result.transformed;
559
952
  szClasses = result.classes;
953
+ if (result.diagnostics.length > 0 && process.env.NODE_ENV !== "production") {
954
+ for (const msg of result.diagnostics) {
955
+ this.warn(`[csszyx] ${id}
956
+ ${msg}`);
957
+ }
958
+ }
560
959
  }
561
960
  }
562
961
  if (transformedCode.includes("<html") && /layout|Root|Document|app\\.tsx?$/i.test(id)) {
@@ -576,18 +975,32 @@ ${inlineDirective}`
576
975
  if (usesRuntime) {
577
976
  imports.push("_sz");
578
977
  }
978
+ if (usesMerge) {
979
+ imports.push("_szMerge");
980
+ }
579
981
  if (usesColorVar) {
580
982
  imports.push("__szColorVar");
581
983
  }
582
- if (imports.length > 0 && !transformedCode.includes("from 'csszyx/lite'")) {
583
- const importStmt = `import { ${imports.join(", ")} } from 'csszyx/lite';
584
- `;
585
- const directiveMatch = transformedCode.match(/^['"]use (client|server)['"];?\s*/);
586
- if (directiveMatch) {
587
- const directive = directiveMatch[0];
588
- transformedCode = transformedCode.replace(directive, `${directive}${importStmt}`);
984
+ const needed = imports.filter(
985
+ (name) => !new RegExp(`\\{[^}]*\\b${name}\\b[^}]*\\}\\s*from\\s*['"]@csszyx/runtime['"]`).test(transformedCode)
986
+ );
987
+ if (needed.length > 0) {
988
+ const existingImport = transformedCode.match(/^(import\s*\{[^}]*)\}\s*from\s*'@csszyx\/runtime'/m);
989
+ if (existingImport) {
990
+ transformedCode = transformedCode.replace(
991
+ existingImport[0],
992
+ `${existingImport[1]}, ${needed.join(", ")} } from '@csszyx/runtime'`
993
+ );
589
994
  } else {
590
- transformedCode = `${importStmt}${transformedCode}`;
995
+ const importStmt = `import { ${needed.join(", ")} } from '@csszyx/runtime';
996
+ `;
997
+ const directiveMatch = transformedCode.match(/^['"]use (client|server)['"];?\s*/);
998
+ if (directiveMatch) {
999
+ const directive = directiveMatch[0];
1000
+ transformedCode = transformedCode.replace(directive, `${directive}${importStmt}`);
1001
+ } else {
1002
+ transformedCode = `${importStmt}${transformedCode}`;
1003
+ }
591
1004
  }
592
1005
  transformed = true;
593
1006
  }
@@ -607,6 +1020,9 @@ ${inlineDirective}`
607
1020
  /** Finalizes the mangle map after all source modules have been processed. */
608
1021
  buildEnd() {
609
1022
  finalizeMangleMap();
1023
+ if (manglingEnabled && Object.keys(state.mangleMap).length > 0) {
1024
+ globalThis.__csszyx_ssr_mangle_map = state.mangleMap;
1025
+ }
610
1026
  },
611
1027
  /**
612
1028
  * Webpack hook: pre-scans source files before compilation for Tailwind class discovery.
@@ -614,23 +1030,94 @@ ${inlineDirective}`
614
1030
  */
615
1031
  webpack(compiler) {
616
1032
  compiler.hooks.beforeCompile.tap("csszyx:prescan", () => {
1033
+ const root = compiler.context || process.cwd();
1034
+ state.rootDir = root;
617
1035
  if (state.classes.size === 0) {
618
- prescanAndWriteClasses(compiler.context || process.cwd());
1036
+ prescanAndWriteClasses();
619
1037
  }
1038
+ runThemeScan(root, options.build?.scanCss);
620
1039
  });
1040
+ if (options.build?.scanCss) {
1041
+ const patterns = Array.isArray(options.build.scanCss) ? options.build.scanCss : [options.build.scanCss];
1042
+ compiler.hooks.thisCompilation.tap("csszyx:theme-deps", (compilation) => {
1043
+ const root = compiler.context || process.cwd();
1044
+ for (const pattern of patterns) {
1045
+ const resolved = path.isAbsolute(pattern) ? pattern : path.join(root, pattern);
1046
+ if (fs.existsSync(resolved)) {
1047
+ compilation.fileDependencies.add(resolved);
1048
+ }
1049
+ }
1050
+ });
1051
+ }
621
1052
  },
622
1053
  vite: {
623
1054
  /**
624
1055
  * Vite hook: pre-scans source files when config is resolved.
1056
+ * Also runs theme scan to generate .csszyx/theme.d.ts if scanCss is configured.
625
1057
  * @param config - the resolved Vite configuration object
626
1058
  */
627
1059
  configResolved(config) {
628
- prescanAndWriteClasses(config.root || process.cwd());
1060
+ const root = config.root || process.cwd();
1061
+ state.rootDir = root;
1062
+ prescanAndWriteClasses();
1063
+ runThemeScan(root, options.build?.scanCss);
1064
+ },
1065
+ /**
1066
+ * Vite HMR hook: re-runs theme scan when a watched CSS file changes,
1067
+ * and incrementally updates csszyx-classes.html when a source file gains new sz classes.
1068
+ * @param ctx - HMR context containing the changed file
1069
+ */
1070
+ handleHotUpdate(ctx) {
1071
+ const scanCss = options.build?.scanCss;
1072
+ if (scanCss) {
1073
+ const patterns = Array.isArray(scanCss) ? scanCss : [scanCss];
1074
+ const root = ctx.server.config.root || process.cwd();
1075
+ const isWatched = patterns.some((p) => {
1076
+ const resolved = path.isAbsolute(p) ? p : path.join(root, p);
1077
+ return ctx.file === resolved;
1078
+ });
1079
+ if (isWatched) {
1080
+ runThemeScan(root, scanCss);
1081
+ }
1082
+ }
1083
+ if (!SOURCE_EXTENSIONS.has(path.extname(ctx.file))) {
1084
+ return;
1085
+ }
1086
+ if (ctx.file.includes("node_modules")) {
1087
+ return;
1088
+ }
1089
+ let fileContent, result;
1090
+ try {
1091
+ fileContent = fs.readFileSync(ctx.file, "utf-8");
1092
+ } catch {
1093
+ return;
1094
+ }
1095
+ if (!fileContent.includes("sz=") && !/\bsz\s*:\s*["'{]/.test(fileContent)) {
1096
+ return;
1097
+ }
1098
+ try {
1099
+ result = (0, import_compiler.transformSourceCode)(fileContent);
1100
+ } catch {
1101
+ return;
1102
+ }
1103
+ if (!result.transformed) {
1104
+ return;
1105
+ }
1106
+ const sizeBefore = state.classes.size;
1107
+ for (const cls of result.classes) {
1108
+ state.classes.add(cls);
1109
+ }
1110
+ if (state.classes.size > sizeBefore) {
1111
+ writeSafelistFile(state.classes);
1112
+ const safelistPath = path.join(state.rootDir, SAFELIST_FILENAME);
1113
+ ctx.server.watcher.emit("change", safelistPath);
1114
+ }
629
1115
  },
630
1116
  transformIndexHtml: {
631
1117
  order: "pre",
632
1118
  /**
633
1119
  * Injects hydration data (mangle map + checksum) into the HTML document.
1120
+ * Also mangles class attributes in SSR-rendered HTML so they match mangled CSS selectors.
634
1121
  * @param html - the raw HTML string to transform
635
1122
  * @returns transformed HTML with injected hydration data
636
1123
  */
@@ -665,10 +1152,23 @@ ${inlineDirective}`
665
1152
  },
666
1153
  (assets) => {
667
1154
  finalizeMangleMap();
1155
+ const isWebpackDevMode = compiler.options.mode === "development";
1156
+ const manifestData = {
1157
+ version: "0.4.0",
1158
+ buildId: state.checksum,
1159
+ classes: Object.keys(state.mangleMap)
1160
+ };
1161
+ if (manglingEnabled && !isWebpackDevMode && Object.keys(state.mangleMap).length > 0) {
1162
+ manifestData.mangleMap = state.mangleMap;
1163
+ }
1164
+ compilation.emitAsset(
1165
+ "csszyx-manifest.json",
1166
+ new compiler.webpack.sources.RawSource(JSON.stringify(manifestData))
1167
+ );
668
1168
  for (const file in assets) {
669
1169
  const asset = assets[file];
670
1170
  const source = asset.source().toString();
671
- if (manglingEnabled && Object.keys(state.mangleMap).length > 0) {
1171
+ if (manglingEnabled && !isWebpackDevMode && Object.keys(state.mangleMap).length > 0) {
672
1172
  if (file.endsWith(".css")) {
673
1173
  try {
674
1174
  const result = mangleCSSSync(source, state.mangleMap, {
@@ -688,6 +1188,21 @@ ${inlineDirective}`
688
1188
  throw e;
689
1189
  }
690
1190
  }
1191
+ } else if (file.endsWith(".html")) {
1192
+ const mangledHtml = source.replace(/\bclass="([^"]*)"/g, (_m, cls) => {
1193
+ const out = cls.split(/\s+/).filter(Boolean).map((c) => state.mangleMap[c] || c).join(" ");
1194
+ return out !== cls ? `class="${out}"` : _m;
1195
+ }).replace(/\bclass='([^']*)'/g, (_m, cls) => {
1196
+ const out = cls.split(/\s+/).filter(Boolean).map((c) => state.mangleMap[c] || c).join(" ");
1197
+ return out !== cls ? `class='${out}'` : _m;
1198
+ });
1199
+ if (mangledHtml !== source) {
1200
+ compilation.updateAsset(
1201
+ file,
1202
+ new compiler.webpack.sources.RawSource(mangledHtml)
1203
+ );
1204
+ continue;
1205
+ }
691
1206
  } else if (file.endsWith(".js")) {
692
1207
  let mangled = mangleCodeClasses(source);
693
1208
  mangled = replacePlaceholders(mangled);
@@ -722,6 +1237,19 @@ ${inlineDirective}`
722
1237
  */
723
1238
  generateBundle(_options, bundle) {
724
1239
  finalizeMangleMap();
1240
+ const manifestData = {
1241
+ version: "0.4.0",
1242
+ buildId: state.checksum,
1243
+ classes: Object.keys(state.mangleMap)
1244
+ };
1245
+ if (manglingEnabled && Object.keys(state.mangleMap).length > 0) {
1246
+ manifestData.mangleMap = state.mangleMap;
1247
+ }
1248
+ this.emitFile({
1249
+ type: "asset",
1250
+ fileName: "csszyx-manifest.json",
1251
+ source: JSON.stringify(manifestData)
1252
+ });
725
1253
  for (const file in bundle) {
726
1254
  const chunk = bundle[file];
727
1255
  if (manglingEnabled && Object.keys(state.mangleMap).length > 0) {