@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/index.cjs CHANGED
@@ -34,8 +34,12 @@ __export(index_exports, {
34
34
  default: () => unplugin,
35
35
  esbuildPlugin: () => esbuildPlugin,
36
36
  escapeCSSClassName: () => escapeCSSClassName,
37
+ hasTokens: () => hasTokens,
37
38
  mangleCSS: () => mangleCSS,
38
39
  mangleCSSSync: () => mangleCSSSync,
40
+ mangleCodeClassesSync: () => mangleCodeClassesSync,
41
+ mergeThemes: () => mergeThemes,
42
+ parseThemeBlocks: () => parseThemeBlocks,
39
43
  rollupPlugin: () => rollupPlugin,
40
44
  unescapeTailwindClass: () => unescapeTailwindClass,
41
45
  unplugin: () => unplugin,
@@ -239,6 +243,148 @@ function createPostCSSPlugin(mangleMap, options = {}) {
239
243
  };
240
244
  }
241
245
 
246
+ // src/theme-scanner.ts
247
+ var EMPTY_THEME = { colors: [], spacings: [], fonts: [], radii: [], shadows: [] };
248
+ function stripLayerWrappers(css) {
249
+ let result = "";
250
+ let i = 0;
251
+ while (i < css.length) {
252
+ const layerIdx = css.indexOf("@layer", i);
253
+ if (layerIdx === -1) {
254
+ result += css.slice(i);
255
+ break;
256
+ }
257
+ result += css.slice(i, layerIdx);
258
+ const openBrace = css.indexOf("{", layerIdx);
259
+ if (openBrace === -1) {
260
+ result += css.slice(layerIdx);
261
+ break;
262
+ }
263
+ let depth = 0;
264
+ let j = openBrace;
265
+ while (j < css.length) {
266
+ if (css[j] === "{") {
267
+ depth++;
268
+ }
269
+ if (css[j] === "}") {
270
+ depth--;
271
+ if (depth === 0) {
272
+ result += css.slice(openBrace + 1, j);
273
+ i = j + 1;
274
+ break;
275
+ }
276
+ }
277
+ j++;
278
+ }
279
+ if (depth !== 0) {
280
+ result += css.slice(openBrace);
281
+ break;
282
+ }
283
+ }
284
+ return result;
285
+ }
286
+ function extractThemeBlocks(css) {
287
+ const blocks = [];
288
+ const themeStart = /@theme\s+(?:inline\s+)?\{|@theme\{/g;
289
+ let match;
290
+ while ((match = themeStart.exec(css)) !== null) {
291
+ const openPos = css.indexOf("{", match.index);
292
+ let depth = 0;
293
+ let j = openPos;
294
+ while (j < css.length) {
295
+ if (css[j] === "{") {
296
+ depth++;
297
+ }
298
+ if (css[j] === "}") {
299
+ depth--;
300
+ if (depth === 0) {
301
+ blocks.push(css.slice(openPos + 1, j));
302
+ break;
303
+ }
304
+ }
305
+ j++;
306
+ }
307
+ }
308
+ return blocks;
309
+ }
310
+ function categorizeProperty(prop) {
311
+ const categoryMap = [
312
+ ["color-", "colors"],
313
+ ["spacing-", "spacings"],
314
+ ["font-", "fonts"],
315
+ ["radius-", "radii"],
316
+ ["shadow-", "shadows"]
317
+ ];
318
+ for (const [prefix, category] of categoryMap) {
319
+ if (prop.startsWith(prefix)) {
320
+ let token = prop.slice(prefix.length);
321
+ token = token.replace(/-\d+$/, "");
322
+ if (token) {
323
+ return { category, token };
324
+ }
325
+ }
326
+ }
327
+ return null;
328
+ }
329
+ function parseThemeBlocks(cssContent) {
330
+ const result = {
331
+ colors: /* @__PURE__ */ new Set(),
332
+ spacings: /* @__PURE__ */ new Set(),
333
+ fonts: /* @__PURE__ */ new Set(),
334
+ radii: /* @__PURE__ */ new Set(),
335
+ shadows: /* @__PURE__ */ new Set()
336
+ };
337
+ const stripped = stripLayerWrappers(cssContent);
338
+ const blocks = extractThemeBlocks(stripped);
339
+ const propPattern = /--([a-z][a-z0-9-]*)(?:\s*:[^;]+)?;/g;
340
+ for (const block of blocks) {
341
+ let match;
342
+ while ((match = propPattern.exec(block)) !== null) {
343
+ const categorized = categorizeProperty(match[1]);
344
+ if (categorized) {
345
+ result[categorized.category].add(categorized.token);
346
+ }
347
+ }
348
+ propPattern.lastIndex = 0;
349
+ }
350
+ return {
351
+ colors: [...result.colors].sort(),
352
+ spacings: [...result.spacings].sort(),
353
+ fonts: [...result.fonts].sort(),
354
+ radii: [...result.radii].sort(),
355
+ shadows: [...result.shadows].sort()
356
+ };
357
+ }
358
+ function mergeThemes(themes) {
359
+ if (themes.length === 0) {
360
+ return { ...EMPTY_THEME };
361
+ }
362
+ const merged = {
363
+ colors: /* @__PURE__ */ new Set(),
364
+ spacings: /* @__PURE__ */ new Set(),
365
+ fonts: /* @__PURE__ */ new Set(),
366
+ radii: /* @__PURE__ */ new Set(),
367
+ shadows: /* @__PURE__ */ new Set()
368
+ };
369
+ for (const theme of themes) {
370
+ for (const cat of Object.keys(merged)) {
371
+ for (const token of theme[cat]) {
372
+ merged[cat].add(token);
373
+ }
374
+ }
375
+ }
376
+ return {
377
+ colors: [...merged.colors].sort(),
378
+ spacings: [...merged.spacings].sort(),
379
+ fonts: [...merged.fonts].sort(),
380
+ radii: [...merged.radii].sort(),
381
+ shadows: [...merged.shadows].sort()
382
+ };
383
+ }
384
+ function hasTokens(theme) {
385
+ return Object.values(theme).some((arr) => arr.length > 0);
386
+ }
387
+
242
388
  // src/unplugin.ts
243
389
  var fs = __toESM(require("fs"), 1);
244
390
  var path = __toESM(require("path"), 1);
@@ -309,6 +455,55 @@ function transformIndexHtml(html, mangleMap, checksum, options = {}) {
309
455
  return injectHydrationData(html, mangleMap, checksum, options);
310
456
  }
311
457
 
458
+ // src/theme-type-writer.ts
459
+ var import_node_fs = require("fs");
460
+ var import_node_path = require("path");
461
+ function generateThemeDts(opts) {
462
+ const { theme, sourceFiles } = opts;
463
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
464
+ const sources = sourceFiles.join(", ");
465
+ const toUnion = (tokens) => tokens.map((t) => `'${t}'`).join(" | ");
466
+ const entries = [];
467
+ if (theme.colors.length > 0) {
468
+ entries.push(` colors: ${toUnion(theme.colors)};`);
469
+ }
470
+ if (theme.spacings.length > 0) {
471
+ entries.push(` spacings: ${toUnion(theme.spacings)};`);
472
+ }
473
+ if (theme.fonts.length > 0) {
474
+ entries.push(` fonts: ${toUnion(theme.fonts)};`);
475
+ }
476
+ if (theme.radii.length > 0) {
477
+ entries.push(` radii: ${toUnion(theme.radii)};`);
478
+ }
479
+ if (theme.shadows.length > 0) {
480
+ entries.push(` shadows: ${toUnion(theme.shadows)};`);
481
+ }
482
+ return [
483
+ "// Auto-generated by csszyx theme-scanner \u2014 DO NOT EDIT",
484
+ `// Source: ${sources}`,
485
+ `// Updated: ${timestamp}`,
486
+ "",
487
+ "declare module '@csszyx/compiler' {",
488
+ " /**",
489
+ " * Custom design tokens extracted from @theme blocks.",
490
+ " * These tokens are surfaced in sz prop IntelliSense.",
491
+ " */",
492
+ " interface CustomTheme {",
493
+ ...entries,
494
+ " }",
495
+ "}",
496
+ "",
497
+ "export {};",
498
+ ""
499
+ ].join("\n");
500
+ }
501
+ function writeThemeDts(opts) {
502
+ const content = generateThemeDts(opts);
503
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(opts.outputPath), { recursive: true });
504
+ (0, import_node_fs.writeFileSync)(opts.outputPath, content, "utf-8");
505
+ }
506
+
312
507
  // src/virtual-modules.ts
313
508
  var VIRTUAL_MODULE_ID = "virtual:csszyx/mangle-map";
314
509
  var RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID;
@@ -361,19 +556,259 @@ function resolveVirtualModule(id) {
361
556
  // src/unplugin.ts
362
557
  var CHECKSUM_PLACEHOLDER = "___CSSZYX_CHECKSUM___";
363
558
  var MANGLE_MAP_PLACEHOLDER = "___CSSZYX_MANGLE_MAP___";
559
+ var _hasWarnedTsConfig = false;
560
+ function runThemeScan(rootDir, scanCss) {
561
+ if (!scanCss) {
562
+ return;
563
+ }
564
+ const patterns = Array.isArray(scanCss) ? scanCss : [scanCss];
565
+ const sourceFiles = [];
566
+ for (const pattern of patterns) {
567
+ const resolved = path.isAbsolute(pattern) ? pattern : path.join(rootDir, pattern);
568
+ if (fs.existsSync(resolved)) {
569
+ sourceFiles.push(resolved);
570
+ }
571
+ }
572
+ if (sourceFiles.length === 0) {
573
+ return;
574
+ }
575
+ const themes = sourceFiles.map((f) => {
576
+ try {
577
+ return parseThemeBlocks(fs.readFileSync(f, "utf-8"));
578
+ } catch {
579
+ return null;
580
+ }
581
+ }).filter((t) => t !== null);
582
+ const merged = mergeThemes(themes);
583
+ const outputPath = path.join(rootDir, ".csszyx", "theme.d.ts");
584
+ writeThemeDts({ outputPath, theme: merged, sourceFiles });
585
+ if (!_hasWarnedTsConfig) {
586
+ _hasWarnedTsConfig = true;
587
+ try {
588
+ const checkFile = (cfgPath) => {
589
+ if (fs.existsSync(cfgPath)) {
590
+ const content = fs.readFileSync(cfgPath, "utf-8");
591
+ if (!content.includes(".csszyx")) {
592
+ console.warn(`
593
+ \x1B[33m\u26A0\uFE0F CSSzyx: Theme Auto-Scan enabled, but TypeScript isn't configured. Run "npx @csszyx/cli init" to fix.\x1B[0m
594
+ `);
595
+ }
596
+ return true;
597
+ }
598
+ return false;
599
+ };
600
+ if (!checkFile(path.join(rootDir, "tsconfig.json"))) {
601
+ checkFile(path.join(rootDir, "tsconfig.app.json"));
602
+ }
603
+ } catch {
604
+ }
605
+ }
606
+ }
607
+ function mangleCodeClassesSync(code, mangleMap) {
608
+ function mangleClassString(classString) {
609
+ return classString.split(/\s+/).filter(Boolean).map((cls) => {
610
+ return mangleMap[cls.replace(/\\(.)/g, "$1")] || cls;
611
+ }).join(" ");
612
+ }
613
+ let result = code.replace(/(?:class(?:Name)?|sz)[:=]\s*"((?:[^"\\]|\\.)*)"/g, (match, classes) => {
614
+ const mangled = mangleClassString(classes);
615
+ if (mangled === classes) {
616
+ return match;
617
+ }
618
+ return match.replace(classes, mangled);
619
+ }).replace(/(?:class(?:Name)?|sz)[:=]\s*'((?:[^'\\]|\\.)*)'/g, (match, classes) => {
620
+ const mangled = mangleClassString(classes);
621
+ if (mangled === classes) {
622
+ return match;
623
+ }
624
+ return match.replace(classes, mangled);
625
+ });
626
+ result = result.replace(/className:\s*`([^`]+)`/g, (fullMatch, tplContent) => {
627
+ let changed = false;
628
+ let out = "";
629
+ let i = 0;
630
+ while (i < tplContent.length) {
631
+ const interStart = tplContent.indexOf("${", i);
632
+ if (interStart === -1) {
633
+ const quasi2 = tplContent.slice(i);
634
+ const trimmed2 = quasi2.trim();
635
+ if (trimmed2) {
636
+ const m = mangleClassString(trimmed2);
637
+ if (m !== trimmed2) {
638
+ changed = true;
639
+ out += quasi2.replace(trimmed2, m);
640
+ } else {
641
+ out += quasi2;
642
+ }
643
+ } else {
644
+ out += quasi2;
645
+ }
646
+ break;
647
+ }
648
+ const quasi = tplContent.slice(i, interStart);
649
+ const trimmed = quasi.trim();
650
+ if (trimmed) {
651
+ const m = mangleClassString(trimmed);
652
+ if (m !== trimmed) {
653
+ changed = true;
654
+ out += quasi.replace(trimmed, m);
655
+ } else {
656
+ out += quasi;
657
+ }
658
+ } else {
659
+ out += quasi;
660
+ }
661
+ let j = interStart + 2;
662
+ let depth = 0;
663
+ while (j < tplContent.length) {
664
+ if (tplContent[j] === "{") {
665
+ depth++;
666
+ } else if (tplContent[j] === "}") {
667
+ if (depth === 0) {
668
+ j++;
669
+ break;
670
+ }
671
+ depth--;
672
+ }
673
+ j++;
674
+ }
675
+ const interInner = tplContent.slice(interStart + 2, j - 1);
676
+ const mangledInner = interInner.replace(/"([^"]*)"/g, (qm, inner) => {
677
+ const parts = inner.split(/\s+/).filter(Boolean);
678
+ if (parts.length === 0) {
679
+ return qm;
680
+ }
681
+ const m = parts.map((p) => mangleMap[p] || p).join(" ");
682
+ if (m === inner) {
683
+ return qm;
684
+ }
685
+ changed = true;
686
+ return '"' + m + '"';
687
+ });
688
+ out += "${" + mangledInner + "}";
689
+ i = j;
690
+ }
691
+ return changed ? "className:`" + out + "`" : fullMatch;
692
+ });
693
+ {
694
+ const marker = "className:";
695
+ let searchFrom = 0;
696
+ let out = "";
697
+ while (searchFrom < result.length) {
698
+ const idx = result.indexOf(marker, searchFrom);
699
+ if (idx === -1) {
700
+ out += result.slice(searchFrom);
701
+ break;
702
+ }
703
+ out += result.slice(searchFrom, idx + marker.length);
704
+ const afterColon = idx + marker.length;
705
+ let exprStart = afterColon;
706
+ while (exprStart < result.length && result[exprStart] === " ") {
707
+ exprStart++;
708
+ }
709
+ const firstChar = result[exprStart];
710
+ if (firstChar === '"' || firstChar === "'" || firstChar === "`") {
711
+ searchFrom = afterColon;
712
+ continue;
713
+ }
714
+ let depth = 0;
715
+ let j = afterColon;
716
+ while (j < result.length) {
717
+ const ch = result[j];
718
+ if (ch === "(" || ch === "[") {
719
+ depth++;
720
+ } else if (ch === ")" || ch === "]") {
721
+ if (depth === 0) {
722
+ break;
723
+ }
724
+ depth--;
725
+ } else if (depth === 0 && (ch === "," || ch === ";" || ch === "\n" || ch === "}")) {
726
+ break;
727
+ }
728
+ j++;
729
+ }
730
+ const expr = result.slice(afterColon, j);
731
+ const qIdx = expr.indexOf("?");
732
+ if (qIdx === -1 || !expr.slice(qIdx).includes(":")) {
733
+ out += expr;
734
+ searchFrom = j;
735
+ continue;
736
+ }
737
+ let changed = false;
738
+ const mangled = expr.replace(/"([^"]*)"/g, (qm, inner) => {
739
+ const parts = inner.split(/\s+/).filter(Boolean);
740
+ if (parts.length === 0) {
741
+ return qm;
742
+ }
743
+ const mangledStr = parts.map((p) => mangleMap[p] || p).join(" ");
744
+ if (mangledStr !== inner) {
745
+ changed = true;
746
+ return '"' + mangledStr + '"';
747
+ }
748
+ return qm;
749
+ });
750
+ out += changed ? mangled : expr;
751
+ searchFrom = j;
752
+ }
753
+ result = out;
754
+ }
755
+ result = result.replace(/(?<=(?:[,(]|&&)\s*)"([^"]+)"/g, (match, inner) => {
756
+ const tokens = inner.split(/\s+/).filter(Boolean);
757
+ if (tokens.length === 0) {
758
+ return match;
759
+ }
760
+ let changed = false;
761
+ const mangled = [];
762
+ for (const t of tokens) {
763
+ const m = mangleMap[t];
764
+ if (m === void 0) {
765
+ return match;
766
+ }
767
+ if (m !== t) {
768
+ changed = true;
769
+ }
770
+ mangled.push(m);
771
+ }
772
+ if (!changed) {
773
+ return match;
774
+ }
775
+ return '"' + mangled.join(" ") + '"';
776
+ });
777
+ return result;
778
+ }
364
779
  function createCsszyxPlugins(options = {}) {
365
780
  const manglingEnabled = options.production?.mangle !== false;
366
781
  const state = {
367
782
  classes: /* @__PURE__ */ new Set(),
368
783
  mangleMap: {},
369
784
  checksum: "",
370
- finalized: false
785
+ finalized: false,
786
+ rootDir: process.cwd()
371
787
  };
372
- const SAFELIST_FILENAME = "csszyx-classes.js";
788
+ const SAFELIST_FILENAME = "csszyx-classes.html";
373
789
  const SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js"]);
374
790
  const IGNORE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".next", ".git", "dist", "build", ".turbo"]);
375
- function prescanAndWriteClasses(rootDir) {
791
+ function writeSafelistFile(classes) {
792
+ if (classes.size === 0) {
793
+ return;
794
+ }
795
+ const safelistPath = path.join(state.rootDir, SAFELIST_FILENAME);
796
+ const classList = Array.from(classes).join(" ");
797
+ const content = `<!-- Auto-generated by csszyx \u2014 DO NOT EDIT -->
798
+ <!-- Tailwind CSS scans this file for class name detection -->
799
+ <div class="${classList}"><div class="${classList}">x</div><div class="${classList}">x</div></div>
800
+ `;
801
+ try {
802
+ const existing = fs.existsSync(safelistPath) ? fs.readFileSync(safelistPath, "utf-8") : "";
803
+ if (existing !== content) {
804
+ fs.writeFileSync(safelistPath, content);
805
+ }
806
+ } catch {
807
+ }
808
+ }
809
+ function prescanAndWriteClasses() {
376
810
  const discoveredClasses = /* @__PURE__ */ new Set();
811
+ const rawDiscoveredClasses = /* @__PURE__ */ new Set();
377
812
  function scanDir(dir) {
378
813
  let entries;
379
814
  try {
@@ -400,6 +835,9 @@ function createCsszyxPlugins(options = {}) {
400
835
  for (const cls of result.classes) {
401
836
  discoveredClasses.add(cls);
402
837
  }
838
+ for (const cls of result.rawClassNames) {
839
+ rawDiscoveredClasses.add(cls);
840
+ }
403
841
  if (result.usesRuntime) {
404
842
  const szCallRe = /_sz\(\s*\{/g;
405
843
  let szMatch;
@@ -454,21 +892,12 @@ function createCsszyxPlugins(options = {}) {
454
892
  }
455
893
  }
456
894
  }
457
- scanDir(rootDir);
895
+ scanDir(state.rootDir);
458
896
  for (const cls of discoveredClasses) {
459
897
  state.classes.add(cls);
460
898
  }
461
- if (discoveredClasses.size > 0) {
462
- const safelistPath = path.join(rootDir, SAFELIST_FILENAME);
463
- 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';
464
- try {
465
- const existing = fs.existsSync(safelistPath) ? fs.readFileSync(safelistPath, "utf-8") : "";
466
- if (existing !== content) {
467
- fs.writeFileSync(safelistPath, content);
468
- }
469
- } catch {
470
- }
471
- }
899
+ const safelistClasses = /* @__PURE__ */ new Set([...discoveredClasses, ...rawDiscoveredClasses]);
900
+ writeSafelistFile(safelistClasses);
472
901
  }
473
902
  function extractClasses(code) {
474
903
  const dqPattern = /(?:class(?:Name)?|sz)[:=]\s*"([^"]*)"/g;
@@ -516,47 +945,8 @@ function createCsszyxPlugins(options = {}) {
516
945
  state.checksum = (0, import_core.compute_mangle_checksum)(state.mangleMap);
517
946
  state.finalized = true;
518
947
  }
519
- function mangleClassString(classString) {
520
- return classString.split(/\s+/).map((cls) => state.mangleMap[cls] || cls).join(" ");
521
- }
522
948
  function mangleCodeClasses(code) {
523
- let result = code.replace(/(?:class(?:Name)?|sz)[:=]\s*"([^"]*)"/g, (match, classes) => {
524
- const mangled = mangleClassString(classes);
525
- if (mangled === classes) {
526
- return match;
527
- }
528
- return match.replace(classes, mangled);
529
- }).replace(/(?:class(?:Name)?|sz)[:=]\s*'([^']*)'/g, (match, classes) => {
530
- const mangled = mangleClassString(classes);
531
- if (mangled === classes) {
532
- return match;
533
- }
534
- return match.replace(classes, mangled);
535
- });
536
- result = result.replace(/className:(?!["'])([^,;}\])\n]+)/g, (fullMatch, expr) => {
537
- const qIdx = expr.indexOf("?");
538
- if (qIdx === -1 || !expr.slice(qIdx).includes(":")) {
539
- return fullMatch;
540
- }
541
- let changed = false;
542
- const mangled = expr.replace(/"([^"]*)"/g, (qm, inner) => {
543
- const parts = inner.split(/\s+/).filter(Boolean);
544
- if (parts.length === 0) {
545
- return qm;
546
- }
547
- const mangledStr = parts.map((p) => state.mangleMap[p] || p).join(" ");
548
- if (mangledStr !== inner) {
549
- changed = true;
550
- return '"' + mangledStr + '"';
551
- }
552
- return qm;
553
- });
554
- if (changed) {
555
- return "className:" + mangled;
556
- }
557
- return fullMatch;
558
- });
559
- return result;
949
+ return mangleCodeClassesSync(code, state.mangleMap);
560
950
  }
561
951
  function replacePlaceholders(code) {
562
952
  let result = code;
@@ -564,7 +954,9 @@ function createCsszyxPlugins(options = {}) {
564
954
  result = result.split(CHECKSUM_PLACEHOLDER).join(state.checksum);
565
955
  }
566
956
  if (result.includes(MANGLE_MAP_PLACEHOLDER)) {
567
- result = result.split(MANGLE_MAP_PLACEHOLDER).join(JSON.stringify(state.mangleMap));
957
+ const jsonMap = JSON.stringify(state.mangleMap);
958
+ const escapedMap = result.includes("eval(") ? jsonMap.replace(/"/g, '\\"') : jsonMap;
959
+ result = result.split(MANGLE_MAP_PLACEHOLDER).join(escapedMap);
568
960
  }
569
961
  return result;
570
962
  }
@@ -626,12 +1018,18 @@ function createCsszyxPlugins(options = {}) {
626
1018
  if (hasTailwindImport && state.classes.size > 0) {
627
1019
  const candidates = Array.from(state.classes).filter((c) => c.length >= 2 && /^[a-z]/.test(c)).join(" ");
628
1020
  if (candidates) {
629
- const inlineDirective = `@source inline("${candidates}");
1021
+ const safelistPath = path.join(state.rootDir, SAFELIST_FILENAME).replace(/\\/g, "/");
1022
+ const cssDir = path.dirname(id).replace(/\\/g, "/");
1023
+ let relPath = path.posix.relative(cssDir, safelistPath);
1024
+ if (!relPath.startsWith(".")) {
1025
+ relPath = "./" + relPath;
1026
+ }
1027
+ const sourceDirective = `@source "${relPath}";
630
1028
  `;
631
1029
  const transformed2 = code.replace(
632
1030
  /(@import\s+["']tailwindcss[^"']*["'];)/,
633
1031
  `$1
634
- ${inlineDirective}`
1032
+ ${sourceDirective}`
635
1033
  );
636
1034
  if (transformed2 !== code) {
637
1035
  return { code: transformed2, map: null };
@@ -642,6 +1040,7 @@ ${inlineDirective}`
642
1040
  }
643
1041
  let transformedCode = code;
644
1042
  let usesRuntime = false;
1043
+ let usesMerge = false;
645
1044
  let usesColorVar = false;
646
1045
  let transformed = false;
647
1046
  let szClasses;
@@ -663,9 +1062,16 @@ ${inlineDirective}`
663
1062
  const result = (0, import_compiler.transformSourceCode)(code);
664
1063
  transformedCode = result.code;
665
1064
  usesRuntime = result.usesRuntime;
1065
+ usesMerge = result.usesMerge;
666
1066
  usesColorVar = result.usesColorVar;
667
1067
  transformed = result.transformed;
668
1068
  szClasses = result.classes;
1069
+ if (result.diagnostics.length > 0 && process.env.NODE_ENV !== "production") {
1070
+ for (const msg of result.diagnostics) {
1071
+ this.warn(`[csszyx] ${id}
1072
+ ${msg}`);
1073
+ }
1074
+ }
669
1075
  }
670
1076
  }
671
1077
  if (transformedCode.includes("<html") && /layout|Root|Document|app\\.tsx?$/i.test(id)) {
@@ -685,18 +1091,32 @@ ${inlineDirective}`
685
1091
  if (usesRuntime) {
686
1092
  imports.push("_sz");
687
1093
  }
1094
+ if (usesMerge) {
1095
+ imports.push("_szMerge");
1096
+ }
688
1097
  if (usesColorVar) {
689
1098
  imports.push("__szColorVar");
690
1099
  }
691
- if (imports.length > 0 && !transformedCode.includes("from 'csszyx/lite'")) {
692
- const importStmt = `import { ${imports.join(", ")} } from 'csszyx/lite';
693
- `;
694
- const directiveMatch = transformedCode.match(/^['"]use (client|server)['"];?\s*/);
695
- if (directiveMatch) {
696
- const directive = directiveMatch[0];
697
- transformedCode = transformedCode.replace(directive, `${directive}${importStmt}`);
1100
+ const needed = imports.filter(
1101
+ (name) => !new RegExp(`\\{[^}]*\\b${name}\\b[^}]*\\}\\s*from\\s*['"]@csszyx/runtime['"]`).test(transformedCode)
1102
+ );
1103
+ if (needed.length > 0) {
1104
+ const existingImport = transformedCode.match(/^(import\s*\{[^}]*)\}\s*from\s*'@csszyx\/runtime'/m);
1105
+ if (existingImport) {
1106
+ transformedCode = transformedCode.replace(
1107
+ existingImport[0],
1108
+ `${existingImport[1]}, ${needed.join(", ")} } from '@csszyx/runtime'`
1109
+ );
698
1110
  } else {
699
- transformedCode = `${importStmt}${transformedCode}`;
1111
+ const importStmt = `import { ${needed.join(", ")} } from '@csszyx/runtime';
1112
+ `;
1113
+ const directiveMatch = transformedCode.match(/^['"]use (client|server)['"];?\s*/);
1114
+ if (directiveMatch) {
1115
+ const directive = directiveMatch[0];
1116
+ transformedCode = transformedCode.replace(directive, `${directive}${importStmt}`);
1117
+ } else {
1118
+ transformedCode = `${importStmt}${transformedCode}`;
1119
+ }
700
1120
  }
701
1121
  transformed = true;
702
1122
  }
@@ -716,6 +1136,9 @@ ${inlineDirective}`
716
1136
  /** Finalizes the mangle map after all source modules have been processed. */
717
1137
  buildEnd() {
718
1138
  finalizeMangleMap();
1139
+ if (manglingEnabled && Object.keys(state.mangleMap).length > 0) {
1140
+ globalThis.__csszyx_ssr_mangle_map = state.mangleMap;
1141
+ }
719
1142
  },
720
1143
  /**
721
1144
  * Webpack hook: pre-scans source files before compilation for Tailwind class discovery.
@@ -723,23 +1146,94 @@ ${inlineDirective}`
723
1146
  */
724
1147
  webpack(compiler) {
725
1148
  compiler.hooks.beforeCompile.tap("csszyx:prescan", () => {
1149
+ const root = compiler.context || process.cwd();
1150
+ state.rootDir = root;
726
1151
  if (state.classes.size === 0) {
727
- prescanAndWriteClasses(compiler.context || process.cwd());
1152
+ prescanAndWriteClasses();
728
1153
  }
1154
+ runThemeScan(root, options.build?.scanCss);
729
1155
  });
1156
+ if (options.build?.scanCss) {
1157
+ const patterns = Array.isArray(options.build.scanCss) ? options.build.scanCss : [options.build.scanCss];
1158
+ compiler.hooks.thisCompilation.tap("csszyx:theme-deps", (compilation) => {
1159
+ const root = compiler.context || process.cwd();
1160
+ for (const pattern of patterns) {
1161
+ const resolved = path.isAbsolute(pattern) ? pattern : path.join(root, pattern);
1162
+ if (fs.existsSync(resolved)) {
1163
+ compilation.fileDependencies.add(resolved);
1164
+ }
1165
+ }
1166
+ });
1167
+ }
730
1168
  },
731
1169
  vite: {
732
1170
  /**
733
1171
  * Vite hook: pre-scans source files when config is resolved.
1172
+ * Also runs theme scan to generate .csszyx/theme.d.ts if scanCss is configured.
734
1173
  * @param config - the resolved Vite configuration object
735
1174
  */
736
1175
  configResolved(config) {
737
- prescanAndWriteClasses(config.root || process.cwd());
1176
+ const root = config.root || process.cwd();
1177
+ state.rootDir = root;
1178
+ prescanAndWriteClasses();
1179
+ runThemeScan(root, options.build?.scanCss);
1180
+ },
1181
+ /**
1182
+ * Vite HMR hook: re-runs theme scan when a watched CSS file changes,
1183
+ * and incrementally updates csszyx-classes.html when a source file gains new sz classes.
1184
+ * @param ctx - HMR context containing the changed file
1185
+ */
1186
+ handleHotUpdate(ctx) {
1187
+ const scanCss = options.build?.scanCss;
1188
+ if (scanCss) {
1189
+ const patterns = Array.isArray(scanCss) ? scanCss : [scanCss];
1190
+ const root = ctx.server.config.root || process.cwd();
1191
+ const isWatched = patterns.some((p) => {
1192
+ const resolved = path.isAbsolute(p) ? p : path.join(root, p);
1193
+ return ctx.file === resolved;
1194
+ });
1195
+ if (isWatched) {
1196
+ runThemeScan(root, scanCss);
1197
+ }
1198
+ }
1199
+ if (!SOURCE_EXTENSIONS.has(path.extname(ctx.file))) {
1200
+ return;
1201
+ }
1202
+ if (ctx.file.includes("node_modules")) {
1203
+ return;
1204
+ }
1205
+ let fileContent, result;
1206
+ try {
1207
+ fileContent = fs.readFileSync(ctx.file, "utf-8");
1208
+ } catch {
1209
+ return;
1210
+ }
1211
+ if (!fileContent.includes("sz=") && !/\bsz\s*:\s*["'{]/.test(fileContent)) {
1212
+ return;
1213
+ }
1214
+ try {
1215
+ result = (0, import_compiler.transformSourceCode)(fileContent);
1216
+ } catch {
1217
+ return;
1218
+ }
1219
+ if (!result.transformed) {
1220
+ return;
1221
+ }
1222
+ const sizeBefore = state.classes.size;
1223
+ for (const cls of result.classes) {
1224
+ state.classes.add(cls);
1225
+ }
1226
+ if (state.classes.size > sizeBefore) {
1227
+ writeSafelistFile(state.classes);
1228
+ const safelistPath = path.join(state.rootDir, SAFELIST_FILENAME);
1229
+ ctx.server.watcher.emit("change", safelistPath);
1230
+ }
738
1231
  },
739
1232
  transformIndexHtml: {
740
1233
  order: "pre",
741
1234
  /**
742
1235
  * Injects hydration data (mangle map + checksum) into the HTML document.
1236
+ * Also mangles class attributes in SSR-rendered HTML so they match mangled CSS selectors.
743
1237
  * @param html - the raw HTML string to transform
744
1238
  * @returns transformed HTML with injected hydration data
745
1239
  */
@@ -774,10 +1268,23 @@ ${inlineDirective}`
774
1268
  },
775
1269
  (assets) => {
776
1270
  finalizeMangleMap();
1271
+ const isWebpackDevMode = compiler.options.mode === "development";
1272
+ const manifestData = {
1273
+ version: "0.4.0",
1274
+ buildId: state.checksum,
1275
+ classes: Object.keys(state.mangleMap)
1276
+ };
1277
+ if (manglingEnabled && !isWebpackDevMode && Object.keys(state.mangleMap).length > 0) {
1278
+ manifestData.mangleMap = state.mangleMap;
1279
+ }
1280
+ compilation.emitAsset(
1281
+ "csszyx-manifest.json",
1282
+ new compiler.webpack.sources.RawSource(JSON.stringify(manifestData))
1283
+ );
777
1284
  for (const file in assets) {
778
1285
  const asset = assets[file];
779
1286
  const source = asset.source().toString();
780
- if (manglingEnabled && Object.keys(state.mangleMap).length > 0) {
1287
+ if (manglingEnabled && !isWebpackDevMode && Object.keys(state.mangleMap).length > 0) {
781
1288
  if (file.endsWith(".css")) {
782
1289
  try {
783
1290
  const result = mangleCSSSync(source, state.mangleMap, {
@@ -797,6 +1304,21 @@ ${inlineDirective}`
797
1304
  throw e;
798
1305
  }
799
1306
  }
1307
+ } else if (file.endsWith(".html")) {
1308
+ const mangledHtml = source.replace(/\bclass="([^"]*)"/g, (_m, cls) => {
1309
+ const out = cls.split(/\s+/).filter(Boolean).map((c) => state.mangleMap[c] || c).join(" ");
1310
+ return out !== cls ? `class="${out}"` : _m;
1311
+ }).replace(/\bclass='([^']*)'/g, (_m, cls) => {
1312
+ const out = cls.split(/\s+/).filter(Boolean).map((c) => state.mangleMap[c] || c).join(" ");
1313
+ return out !== cls ? `class='${out}'` : _m;
1314
+ });
1315
+ if (mangledHtml !== source) {
1316
+ compilation.updateAsset(
1317
+ file,
1318
+ new compiler.webpack.sources.RawSource(mangledHtml)
1319
+ );
1320
+ continue;
1321
+ }
800
1322
  } else if (file.endsWith(".js")) {
801
1323
  let mangled = mangleCodeClasses(source);
802
1324
  mangled = replacePlaceholders(mangled);
@@ -831,6 +1353,19 @@ ${inlineDirective}`
831
1353
  */
832
1354
  generateBundle(_options, bundle) {
833
1355
  finalizeMangleMap();
1356
+ const manifestData = {
1357
+ version: "0.4.0",
1358
+ buildId: state.checksum,
1359
+ classes: Object.keys(state.mangleMap)
1360
+ };
1361
+ if (manglingEnabled && Object.keys(state.mangleMap).length > 0) {
1362
+ manifestData.mangleMap = state.mangleMap;
1363
+ }
1364
+ this.emitFile({
1365
+ type: "asset",
1366
+ fileName: "csszyx-manifest.json",
1367
+ source: JSON.stringify(manifestData)
1368
+ });
834
1369
  for (const file in bundle) {
835
1370
  const chunk = bundle[file];
836
1371
  if (manglingEnabled && Object.keys(state.mangleMap).length > 0) {
@@ -904,8 +1439,9 @@ var esbuildPlugin = (options = {}) => {
904
1439
  * @param build - the esbuild plugin build context
905
1440
  */
906
1441
  setup(build) {
907
- prePlugin.esbuild(options).setup(build);
908
- postPlugin.esbuild(options).setup(build);
1442
+ const b = build;
1443
+ prePlugin.esbuild(options).setup(b);
1444
+ postPlugin.esbuild(options).setup(b);
909
1445
  }
910
1446
  };
911
1447
  };
@@ -914,8 +1450,12 @@ var esbuildPlugin = (options = {}) => {
914
1450
  createPostCSSPlugin,
915
1451
  esbuildPlugin,
916
1452
  escapeCSSClassName,
1453
+ hasTokens,
917
1454
  mangleCSS,
918
1455
  mangleCSSSync,
1456
+ mangleCodeClassesSync,
1457
+ mergeThemes,
1458
+ parseThemeBlocks,
919
1459
  rollupPlugin,
920
1460
  unescapeTailwindClass,
921
1461
  unplugin,