@csszyx/unplugin 0.3.1 → 0.5.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.
@@ -2,6 +2,148 @@ import {
2
2
  mangleCSSSync
3
3
  } from "./chunk-4M7CPGP7.js";
4
4
 
5
+ // src/theme-scanner.ts
6
+ var EMPTY_THEME = { colors: [], spacings: [], fonts: [], radii: [], shadows: [] };
7
+ function stripLayerWrappers(css) {
8
+ let result = "";
9
+ let i = 0;
10
+ while (i < css.length) {
11
+ const layerIdx = css.indexOf("@layer", i);
12
+ if (layerIdx === -1) {
13
+ result += css.slice(i);
14
+ break;
15
+ }
16
+ result += css.slice(i, layerIdx);
17
+ const openBrace = css.indexOf("{", layerIdx);
18
+ if (openBrace === -1) {
19
+ result += css.slice(layerIdx);
20
+ break;
21
+ }
22
+ let depth = 0;
23
+ let j = openBrace;
24
+ while (j < css.length) {
25
+ if (css[j] === "{") {
26
+ depth++;
27
+ }
28
+ if (css[j] === "}") {
29
+ depth--;
30
+ if (depth === 0) {
31
+ result += css.slice(openBrace + 1, j);
32
+ i = j + 1;
33
+ break;
34
+ }
35
+ }
36
+ j++;
37
+ }
38
+ if (depth !== 0) {
39
+ result += css.slice(openBrace);
40
+ break;
41
+ }
42
+ }
43
+ return result;
44
+ }
45
+ function extractThemeBlocks(css) {
46
+ const blocks = [];
47
+ const themeStart = /@theme\s+(?:inline\s+)?\{|@theme\{/g;
48
+ let match;
49
+ while ((match = themeStart.exec(css)) !== null) {
50
+ const openPos = css.indexOf("{", match.index);
51
+ let depth = 0;
52
+ let j = openPos;
53
+ while (j < css.length) {
54
+ if (css[j] === "{") {
55
+ depth++;
56
+ }
57
+ if (css[j] === "}") {
58
+ depth--;
59
+ if (depth === 0) {
60
+ blocks.push(css.slice(openPos + 1, j));
61
+ break;
62
+ }
63
+ }
64
+ j++;
65
+ }
66
+ }
67
+ return blocks;
68
+ }
69
+ function categorizeProperty(prop) {
70
+ const categoryMap = [
71
+ ["color-", "colors"],
72
+ ["spacing-", "spacings"],
73
+ ["font-", "fonts"],
74
+ ["radius-", "radii"],
75
+ ["shadow-", "shadows"]
76
+ ];
77
+ for (const [prefix, category] of categoryMap) {
78
+ if (prop.startsWith(prefix)) {
79
+ let token = prop.slice(prefix.length);
80
+ token = token.replace(/-\d+$/, "");
81
+ if (token) {
82
+ return { category, token };
83
+ }
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+ function parseThemeBlocks(cssContent) {
89
+ const result = {
90
+ colors: /* @__PURE__ */ new Set(),
91
+ spacings: /* @__PURE__ */ new Set(),
92
+ fonts: /* @__PURE__ */ new Set(),
93
+ radii: /* @__PURE__ */ new Set(),
94
+ shadows: /* @__PURE__ */ new Set()
95
+ };
96
+ const stripped = stripLayerWrappers(cssContent);
97
+ const blocks = extractThemeBlocks(stripped);
98
+ const propPattern = /--([a-z][a-z0-9-]*)(?:\s*:[^;]+)?;/g;
99
+ for (const block of blocks) {
100
+ let match;
101
+ while ((match = propPattern.exec(block)) !== null) {
102
+ const categorized = categorizeProperty(match[1]);
103
+ if (categorized) {
104
+ result[categorized.category].add(categorized.token);
105
+ }
106
+ }
107
+ propPattern.lastIndex = 0;
108
+ }
109
+ return {
110
+ colors: [...result.colors].sort(),
111
+ spacings: [...result.spacings].sort(),
112
+ fonts: [...result.fonts].sort(),
113
+ radii: [...result.radii].sort(),
114
+ shadows: [...result.shadows].sort()
115
+ };
116
+ }
117
+ function mergeThemes(themes) {
118
+ if (themes.length === 0) {
119
+ return { ...EMPTY_THEME };
120
+ }
121
+ const merged = {
122
+ colors: /* @__PURE__ */ new Set(),
123
+ spacings: /* @__PURE__ */ new Set(),
124
+ fonts: /* @__PURE__ */ new Set(),
125
+ radii: /* @__PURE__ */ new Set(),
126
+ shadows: /* @__PURE__ */ new Set()
127
+ };
128
+ for (const theme of themes) {
129
+ for (const cat of Object.keys(merged)) {
130
+ for (const token of theme[cat]) {
131
+ merged[cat].add(token);
132
+ }
133
+ }
134
+ }
135
+ return {
136
+ colors: [...merged.colors].sort(),
137
+ spacings: [...merged.spacings].sort(),
138
+ fonts: [...merged.fonts].sort(),
139
+ radii: [...merged.radii].sort(),
140
+ shadows: [...merged.shadows].sort()
141
+ };
142
+ }
143
+ function hasTokens(theme) {
144
+ return Object.values(theme).some((arr) => arr.length > 0);
145
+ }
146
+
5
147
  // src/unplugin.ts
6
148
  import * as fs from "fs";
7
149
  import * as path from "path";
@@ -72,6 +214,55 @@ function transformIndexHtml(html, mangleMap, checksum, options = {}) {
72
214
  return injectHydrationData(html, mangleMap, checksum, options);
73
215
  }
74
216
 
217
+ // src/theme-type-writer.ts
218
+ import { mkdirSync, writeFileSync } from "fs";
219
+ import { dirname } from "path";
220
+ function generateThemeDts(opts) {
221
+ const { theme, sourceFiles } = opts;
222
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
223
+ const sources = sourceFiles.join(", ");
224
+ const toUnion = (tokens) => tokens.map((t) => `'${t}'`).join(" | ");
225
+ const entries = [];
226
+ if (theme.colors.length > 0) {
227
+ entries.push(` colors: ${toUnion(theme.colors)};`);
228
+ }
229
+ if (theme.spacings.length > 0) {
230
+ entries.push(` spacings: ${toUnion(theme.spacings)};`);
231
+ }
232
+ if (theme.fonts.length > 0) {
233
+ entries.push(` fonts: ${toUnion(theme.fonts)};`);
234
+ }
235
+ if (theme.radii.length > 0) {
236
+ entries.push(` radii: ${toUnion(theme.radii)};`);
237
+ }
238
+ if (theme.shadows.length > 0) {
239
+ entries.push(` shadows: ${toUnion(theme.shadows)};`);
240
+ }
241
+ return [
242
+ "// Auto-generated by csszyx theme-scanner \u2014 DO NOT EDIT",
243
+ `// Source: ${sources}`,
244
+ `// Updated: ${timestamp}`,
245
+ "",
246
+ "declare module '@csszyx/compiler' {",
247
+ " /**",
248
+ " * Custom design tokens extracted from @theme blocks.",
249
+ " * These tokens are surfaced in sz prop IntelliSense.",
250
+ " */",
251
+ " interface CustomTheme {",
252
+ ...entries,
253
+ " }",
254
+ "}",
255
+ "",
256
+ "export {};",
257
+ ""
258
+ ].join("\n");
259
+ }
260
+ function writeThemeDts(opts) {
261
+ const content = generateThemeDts(opts);
262
+ mkdirSync(dirname(opts.outputPath), { recursive: true });
263
+ writeFileSync(opts.outputPath, content, "utf-8");
264
+ }
265
+
75
266
  // src/virtual-modules.ts
76
267
  var VIRTUAL_MODULE_ID = "virtual:csszyx/mangle-map";
77
268
  var RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID;
@@ -124,19 +315,259 @@ function resolveVirtualModule(id) {
124
315
  // src/unplugin.ts
125
316
  var CHECKSUM_PLACEHOLDER = "___CSSZYX_CHECKSUM___";
126
317
  var MANGLE_MAP_PLACEHOLDER = "___CSSZYX_MANGLE_MAP___";
318
+ var _hasWarnedTsConfig = false;
319
+ function runThemeScan(rootDir, scanCss) {
320
+ if (!scanCss) {
321
+ return;
322
+ }
323
+ const patterns = Array.isArray(scanCss) ? scanCss : [scanCss];
324
+ const sourceFiles = [];
325
+ for (const pattern of patterns) {
326
+ const resolved = path.isAbsolute(pattern) ? pattern : path.join(rootDir, pattern);
327
+ if (fs.existsSync(resolved)) {
328
+ sourceFiles.push(resolved);
329
+ }
330
+ }
331
+ if (sourceFiles.length === 0) {
332
+ return;
333
+ }
334
+ const themes = sourceFiles.map((f) => {
335
+ try {
336
+ return parseThemeBlocks(fs.readFileSync(f, "utf-8"));
337
+ } catch {
338
+ return null;
339
+ }
340
+ }).filter((t) => t !== null);
341
+ const merged = mergeThemes(themes);
342
+ const outputPath = path.join(rootDir, ".csszyx", "theme.d.ts");
343
+ writeThemeDts({ outputPath, theme: merged, sourceFiles });
344
+ if (!_hasWarnedTsConfig) {
345
+ _hasWarnedTsConfig = true;
346
+ try {
347
+ const checkFile = (cfgPath) => {
348
+ if (fs.existsSync(cfgPath)) {
349
+ const content = fs.readFileSync(cfgPath, "utf-8");
350
+ if (!content.includes(".csszyx")) {
351
+ console.warn(`
352
+ \x1B[33m\u26A0\uFE0F CSSzyx: Theme Auto-Scan enabled, but TypeScript isn't configured. Run "npx @csszyx/cli init" to fix.\x1B[0m
353
+ `);
354
+ }
355
+ return true;
356
+ }
357
+ return false;
358
+ };
359
+ if (!checkFile(path.join(rootDir, "tsconfig.json"))) {
360
+ checkFile(path.join(rootDir, "tsconfig.app.json"));
361
+ }
362
+ } catch {
363
+ }
364
+ }
365
+ }
366
+ function mangleCodeClassesSync(code, mangleMap) {
367
+ function mangleClassString(classString) {
368
+ return classString.split(/\s+/).filter(Boolean).map((cls) => {
369
+ return mangleMap[cls.replace(/\\(.)/g, "$1")] || cls;
370
+ }).join(" ");
371
+ }
372
+ let result = code.replace(/(?:class(?:Name)?|sz)[:=]\s*"((?:[^"\\]|\\.)*)"/g, (match, classes) => {
373
+ const mangled = mangleClassString(classes);
374
+ if (mangled === classes) {
375
+ return match;
376
+ }
377
+ return match.replace(classes, mangled);
378
+ }).replace(/(?:class(?:Name)?|sz)[:=]\s*'((?:[^'\\]|\\.)*)'/g, (match, classes) => {
379
+ const mangled = mangleClassString(classes);
380
+ if (mangled === classes) {
381
+ return match;
382
+ }
383
+ return match.replace(classes, mangled);
384
+ });
385
+ result = result.replace(/className:\s*`([^`]+)`/g, (fullMatch, tplContent) => {
386
+ let changed = false;
387
+ let out = "";
388
+ let i = 0;
389
+ while (i < tplContent.length) {
390
+ const interStart = tplContent.indexOf("${", i);
391
+ if (interStart === -1) {
392
+ const quasi2 = tplContent.slice(i);
393
+ const trimmed2 = quasi2.trim();
394
+ if (trimmed2) {
395
+ const m = mangleClassString(trimmed2);
396
+ if (m !== trimmed2) {
397
+ changed = true;
398
+ out += quasi2.replace(trimmed2, m);
399
+ } else {
400
+ out += quasi2;
401
+ }
402
+ } else {
403
+ out += quasi2;
404
+ }
405
+ break;
406
+ }
407
+ const quasi = tplContent.slice(i, interStart);
408
+ const trimmed = quasi.trim();
409
+ if (trimmed) {
410
+ const m = mangleClassString(trimmed);
411
+ if (m !== trimmed) {
412
+ changed = true;
413
+ out += quasi.replace(trimmed, m);
414
+ } else {
415
+ out += quasi;
416
+ }
417
+ } else {
418
+ out += quasi;
419
+ }
420
+ let j = interStart + 2;
421
+ let depth = 0;
422
+ while (j < tplContent.length) {
423
+ if (tplContent[j] === "{") {
424
+ depth++;
425
+ } else if (tplContent[j] === "}") {
426
+ if (depth === 0) {
427
+ j++;
428
+ break;
429
+ }
430
+ depth--;
431
+ }
432
+ j++;
433
+ }
434
+ const interInner = tplContent.slice(interStart + 2, j - 1);
435
+ const mangledInner = interInner.replace(/"([^"]*)"/g, (qm, inner) => {
436
+ const parts = inner.split(/\s+/).filter(Boolean);
437
+ if (parts.length === 0) {
438
+ return qm;
439
+ }
440
+ const m = parts.map((p) => mangleMap[p] || p).join(" ");
441
+ if (m === inner) {
442
+ return qm;
443
+ }
444
+ changed = true;
445
+ return '"' + m + '"';
446
+ });
447
+ out += "${" + mangledInner + "}";
448
+ i = j;
449
+ }
450
+ return changed ? "className:`" + out + "`" : fullMatch;
451
+ });
452
+ {
453
+ const marker = "className:";
454
+ let searchFrom = 0;
455
+ let out = "";
456
+ while (searchFrom < result.length) {
457
+ const idx = result.indexOf(marker, searchFrom);
458
+ if (idx === -1) {
459
+ out += result.slice(searchFrom);
460
+ break;
461
+ }
462
+ out += result.slice(searchFrom, idx + marker.length);
463
+ const afterColon = idx + marker.length;
464
+ let exprStart = afterColon;
465
+ while (exprStart < result.length && result[exprStart] === " ") {
466
+ exprStart++;
467
+ }
468
+ const firstChar = result[exprStart];
469
+ if (firstChar === '"' || firstChar === "'" || firstChar === "`") {
470
+ searchFrom = afterColon;
471
+ continue;
472
+ }
473
+ let depth = 0;
474
+ let j = afterColon;
475
+ while (j < result.length) {
476
+ const ch = result[j];
477
+ if (ch === "(" || ch === "[") {
478
+ depth++;
479
+ } else if (ch === ")" || ch === "]") {
480
+ if (depth === 0) {
481
+ break;
482
+ }
483
+ depth--;
484
+ } else if (depth === 0 && (ch === "," || ch === ";" || ch === "\n" || ch === "}")) {
485
+ break;
486
+ }
487
+ j++;
488
+ }
489
+ const expr = result.slice(afterColon, j);
490
+ const qIdx = expr.indexOf("?");
491
+ if (qIdx === -1 || !expr.slice(qIdx).includes(":")) {
492
+ out += expr;
493
+ searchFrom = j;
494
+ continue;
495
+ }
496
+ let changed = false;
497
+ const mangled = expr.replace(/"([^"]*)"/g, (qm, inner) => {
498
+ const parts = inner.split(/\s+/).filter(Boolean);
499
+ if (parts.length === 0) {
500
+ return qm;
501
+ }
502
+ const mangledStr = parts.map((p) => mangleMap[p] || p).join(" ");
503
+ if (mangledStr !== inner) {
504
+ changed = true;
505
+ return '"' + mangledStr + '"';
506
+ }
507
+ return qm;
508
+ });
509
+ out += changed ? mangled : expr;
510
+ searchFrom = j;
511
+ }
512
+ result = out;
513
+ }
514
+ result = result.replace(/(?<=(?:[,(]|&&)\s*)"([^"]+)"/g, (match, inner) => {
515
+ const tokens = inner.split(/\s+/).filter(Boolean);
516
+ if (tokens.length === 0) {
517
+ return match;
518
+ }
519
+ let changed = false;
520
+ const mangled = [];
521
+ for (const t of tokens) {
522
+ const m = mangleMap[t];
523
+ if (m === void 0) {
524
+ return match;
525
+ }
526
+ if (m !== t) {
527
+ changed = true;
528
+ }
529
+ mangled.push(m);
530
+ }
531
+ if (!changed) {
532
+ return match;
533
+ }
534
+ return '"' + mangled.join(" ") + '"';
535
+ });
536
+ return result;
537
+ }
127
538
  function createCsszyxPlugins(options = {}) {
128
539
  const manglingEnabled = options.production?.mangle !== false;
129
540
  const state = {
130
541
  classes: /* @__PURE__ */ new Set(),
131
542
  mangleMap: {},
132
543
  checksum: "",
133
- finalized: false
544
+ finalized: false,
545
+ rootDir: process.cwd()
134
546
  };
135
- const SAFELIST_FILENAME = "csszyx-classes.js";
547
+ const SAFELIST_FILENAME = "csszyx-classes.html";
136
548
  const SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js"]);
137
549
  const IGNORE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".next", ".git", "dist", "build", ".turbo"]);
138
- function prescanAndWriteClasses(rootDir) {
550
+ function writeSafelistFile(classes) {
551
+ if (classes.size === 0) {
552
+ return;
553
+ }
554
+ const safelistPath = path.join(state.rootDir, SAFELIST_FILENAME);
555
+ const classList = Array.from(classes).join(" ");
556
+ const content = `<!-- Auto-generated by csszyx \u2014 DO NOT EDIT -->
557
+ <!-- Tailwind CSS scans this file for class name detection -->
558
+ <div class="${classList}"><div class="${classList}">x</div><div class="${classList}">x</div></div>
559
+ `;
560
+ try {
561
+ const existing = fs.existsSync(safelistPath) ? fs.readFileSync(safelistPath, "utf-8") : "";
562
+ if (existing !== content) {
563
+ fs.writeFileSync(safelistPath, content);
564
+ }
565
+ } catch {
566
+ }
567
+ }
568
+ function prescanAndWriteClasses() {
139
569
  const discoveredClasses = /* @__PURE__ */ new Set();
570
+ const rawDiscoveredClasses = /* @__PURE__ */ new Set();
140
571
  function scanDir(dir) {
141
572
  let entries;
142
573
  try {
@@ -163,6 +594,9 @@ function createCsszyxPlugins(options = {}) {
163
594
  for (const cls of result.classes) {
164
595
  discoveredClasses.add(cls);
165
596
  }
597
+ for (const cls of result.rawClassNames) {
598
+ rawDiscoveredClasses.add(cls);
599
+ }
166
600
  if (result.usesRuntime) {
167
601
  const szCallRe = /_sz\(\s*\{/g;
168
602
  let szMatch;
@@ -217,21 +651,12 @@ function createCsszyxPlugins(options = {}) {
217
651
  }
218
652
  }
219
653
  }
220
- scanDir(rootDir);
654
+ scanDir(state.rootDir);
221
655
  for (const cls of discoveredClasses) {
222
656
  state.classes.add(cls);
223
657
  }
224
- if (discoveredClasses.size > 0) {
225
- const safelistPath = path.join(rootDir, SAFELIST_FILENAME);
226
- 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';
227
- try {
228
- const existing = fs.existsSync(safelistPath) ? fs.readFileSync(safelistPath, "utf-8") : "";
229
- if (existing !== content) {
230
- fs.writeFileSync(safelistPath, content);
231
- }
232
- } catch {
233
- }
234
- }
658
+ const safelistClasses = /* @__PURE__ */ new Set([...discoveredClasses, ...rawDiscoveredClasses]);
659
+ writeSafelistFile(safelistClasses);
235
660
  }
236
661
  function extractClasses(code) {
237
662
  const dqPattern = /(?:class(?:Name)?|sz)[:=]\s*"([^"]*)"/g;
@@ -279,47 +704,8 @@ function createCsszyxPlugins(options = {}) {
279
704
  state.checksum = compute_mangle_checksum(state.mangleMap);
280
705
  state.finalized = true;
281
706
  }
282
- function mangleClassString(classString) {
283
- return classString.split(/\s+/).map((cls) => state.mangleMap[cls] || cls).join(" ");
284
- }
285
707
  function mangleCodeClasses(code) {
286
- let result = code.replace(/(?:class(?:Name)?|sz)[:=]\s*"([^"]*)"/g, (match, classes) => {
287
- const mangled = mangleClassString(classes);
288
- if (mangled === classes) {
289
- return match;
290
- }
291
- return match.replace(classes, mangled);
292
- }).replace(/(?:class(?:Name)?|sz)[:=]\s*'([^']*)'/g, (match, classes) => {
293
- const mangled = mangleClassString(classes);
294
- if (mangled === classes) {
295
- return match;
296
- }
297
- return match.replace(classes, mangled);
298
- });
299
- result = result.replace(/className:(?!["'])([^,;}\])\n]+)/g, (fullMatch, expr) => {
300
- const qIdx = expr.indexOf("?");
301
- if (qIdx === -1 || !expr.slice(qIdx).includes(":")) {
302
- return fullMatch;
303
- }
304
- let changed = false;
305
- const mangled = expr.replace(/"([^"]*)"/g, (qm, inner) => {
306
- const parts = inner.split(/\s+/).filter(Boolean);
307
- if (parts.length === 0) {
308
- return qm;
309
- }
310
- const mangledStr = parts.map((p) => state.mangleMap[p] || p).join(" ");
311
- if (mangledStr !== inner) {
312
- changed = true;
313
- return '"' + mangledStr + '"';
314
- }
315
- return qm;
316
- });
317
- if (changed) {
318
- return "className:" + mangled;
319
- }
320
- return fullMatch;
321
- });
322
- return result;
708
+ return mangleCodeClassesSync(code, state.mangleMap);
323
709
  }
324
710
  function replacePlaceholders(code) {
325
711
  let result = code;
@@ -327,7 +713,9 @@ function createCsszyxPlugins(options = {}) {
327
713
  result = result.split(CHECKSUM_PLACEHOLDER).join(state.checksum);
328
714
  }
329
715
  if (result.includes(MANGLE_MAP_PLACEHOLDER)) {
330
- result = result.split(MANGLE_MAP_PLACEHOLDER).join(JSON.stringify(state.mangleMap));
716
+ const jsonMap = JSON.stringify(state.mangleMap);
717
+ const escapedMap = result.includes("eval(") ? jsonMap.replace(/"/g, '\\"') : jsonMap;
718
+ result = result.split(MANGLE_MAP_PLACEHOLDER).join(escapedMap);
331
719
  }
332
720
  return result;
333
721
  }
@@ -389,12 +777,18 @@ function createCsszyxPlugins(options = {}) {
389
777
  if (hasTailwindImport && state.classes.size > 0) {
390
778
  const candidates = Array.from(state.classes).filter((c) => c.length >= 2 && /^[a-z]/.test(c)).join(" ");
391
779
  if (candidates) {
392
- const inlineDirective = `@source inline("${candidates}");
780
+ const safelistPath = path.join(state.rootDir, SAFELIST_FILENAME).replace(/\\/g, "/");
781
+ const cssDir = path.dirname(id).replace(/\\/g, "/");
782
+ let relPath = path.posix.relative(cssDir, safelistPath);
783
+ if (!relPath.startsWith(".")) {
784
+ relPath = "./" + relPath;
785
+ }
786
+ const sourceDirective = `@source "${relPath}";
393
787
  `;
394
788
  const transformed2 = code.replace(
395
789
  /(@import\s+["']tailwindcss[^"']*["'];)/,
396
790
  `$1
397
- ${inlineDirective}`
791
+ ${sourceDirective}`
398
792
  );
399
793
  if (transformed2 !== code) {
400
794
  return { code: transformed2, map: null };
@@ -405,6 +799,7 @@ ${inlineDirective}`
405
799
  }
406
800
  let transformedCode = code;
407
801
  let usesRuntime = false;
802
+ let usesMerge = false;
408
803
  let usesColorVar = false;
409
804
  let transformed = false;
410
805
  let szClasses;
@@ -426,9 +821,16 @@ ${inlineDirective}`
426
821
  const result = transformSourceCode(code);
427
822
  transformedCode = result.code;
428
823
  usesRuntime = result.usesRuntime;
824
+ usesMerge = result.usesMerge;
429
825
  usesColorVar = result.usesColorVar;
430
826
  transformed = result.transformed;
431
827
  szClasses = result.classes;
828
+ if (result.diagnostics.length > 0 && process.env.NODE_ENV !== "production") {
829
+ for (const msg of result.diagnostics) {
830
+ this.warn(`[csszyx] ${id}
831
+ ${msg}`);
832
+ }
833
+ }
432
834
  }
433
835
  }
434
836
  if (transformedCode.includes("<html") && /layout|Root|Document|app\\.tsx?$/i.test(id)) {
@@ -448,18 +850,32 @@ ${inlineDirective}`
448
850
  if (usesRuntime) {
449
851
  imports.push("_sz");
450
852
  }
853
+ if (usesMerge) {
854
+ imports.push("_szMerge");
855
+ }
451
856
  if (usesColorVar) {
452
857
  imports.push("__szColorVar");
453
858
  }
454
- if (imports.length > 0 && !transformedCode.includes("from 'csszyx/lite'")) {
455
- const importStmt = `import { ${imports.join(", ")} } from 'csszyx/lite';
456
- `;
457
- const directiveMatch = transformedCode.match(/^['"]use (client|server)['"];?\s*/);
458
- if (directiveMatch) {
459
- const directive = directiveMatch[0];
460
- transformedCode = transformedCode.replace(directive, `${directive}${importStmt}`);
859
+ const needed = imports.filter(
860
+ (name) => !new RegExp(`\\{[^}]*\\b${name}\\b[^}]*\\}\\s*from\\s*['"]@csszyx/runtime['"]`).test(transformedCode)
861
+ );
862
+ if (needed.length > 0) {
863
+ const existingImport = transformedCode.match(/^(import\s*\{[^}]*)\}\s*from\s*'@csszyx\/runtime'/m);
864
+ if (existingImport) {
865
+ transformedCode = transformedCode.replace(
866
+ existingImport[0],
867
+ `${existingImport[1]}, ${needed.join(", ")} } from '@csszyx/runtime'`
868
+ );
461
869
  } else {
462
- transformedCode = `${importStmt}${transformedCode}`;
870
+ const importStmt = `import { ${needed.join(", ")} } from '@csszyx/runtime';
871
+ `;
872
+ const directiveMatch = transformedCode.match(/^['"]use (client|server)['"];?\s*/);
873
+ if (directiveMatch) {
874
+ const directive = directiveMatch[0];
875
+ transformedCode = transformedCode.replace(directive, `${directive}${importStmt}`);
876
+ } else {
877
+ transformedCode = `${importStmt}${transformedCode}`;
878
+ }
463
879
  }
464
880
  transformed = true;
465
881
  }
@@ -479,6 +895,9 @@ ${inlineDirective}`
479
895
  /** Finalizes the mangle map after all source modules have been processed. */
480
896
  buildEnd() {
481
897
  finalizeMangleMap();
898
+ if (manglingEnabled && Object.keys(state.mangleMap).length > 0) {
899
+ globalThis.__csszyx_ssr_mangle_map = state.mangleMap;
900
+ }
482
901
  },
483
902
  /**
484
903
  * Webpack hook: pre-scans source files before compilation for Tailwind class discovery.
@@ -486,23 +905,94 @@ ${inlineDirective}`
486
905
  */
487
906
  webpack(compiler) {
488
907
  compiler.hooks.beforeCompile.tap("csszyx:prescan", () => {
908
+ const root = compiler.context || process.cwd();
909
+ state.rootDir = root;
489
910
  if (state.classes.size === 0) {
490
- prescanAndWriteClasses(compiler.context || process.cwd());
911
+ prescanAndWriteClasses();
491
912
  }
913
+ runThemeScan(root, options.build?.scanCss);
492
914
  });
915
+ if (options.build?.scanCss) {
916
+ const patterns = Array.isArray(options.build.scanCss) ? options.build.scanCss : [options.build.scanCss];
917
+ compiler.hooks.thisCompilation.tap("csszyx:theme-deps", (compilation) => {
918
+ const root = compiler.context || process.cwd();
919
+ for (const pattern of patterns) {
920
+ const resolved = path.isAbsolute(pattern) ? pattern : path.join(root, pattern);
921
+ if (fs.existsSync(resolved)) {
922
+ compilation.fileDependencies.add(resolved);
923
+ }
924
+ }
925
+ });
926
+ }
493
927
  },
494
928
  vite: {
495
929
  /**
496
930
  * Vite hook: pre-scans source files when config is resolved.
931
+ * Also runs theme scan to generate .csszyx/theme.d.ts if scanCss is configured.
497
932
  * @param config - the resolved Vite configuration object
498
933
  */
499
934
  configResolved(config) {
500
- prescanAndWriteClasses(config.root || process.cwd());
935
+ const root = config.root || process.cwd();
936
+ state.rootDir = root;
937
+ prescanAndWriteClasses();
938
+ runThemeScan(root, options.build?.scanCss);
939
+ },
940
+ /**
941
+ * Vite HMR hook: re-runs theme scan when a watched CSS file changes,
942
+ * and incrementally updates csszyx-classes.html when a source file gains new sz classes.
943
+ * @param ctx - HMR context containing the changed file
944
+ */
945
+ handleHotUpdate(ctx) {
946
+ const scanCss = options.build?.scanCss;
947
+ if (scanCss) {
948
+ const patterns = Array.isArray(scanCss) ? scanCss : [scanCss];
949
+ const root = ctx.server.config.root || process.cwd();
950
+ const isWatched = patterns.some((p) => {
951
+ const resolved = path.isAbsolute(p) ? p : path.join(root, p);
952
+ return ctx.file === resolved;
953
+ });
954
+ if (isWatched) {
955
+ runThemeScan(root, scanCss);
956
+ }
957
+ }
958
+ if (!SOURCE_EXTENSIONS.has(path.extname(ctx.file))) {
959
+ return;
960
+ }
961
+ if (ctx.file.includes("node_modules")) {
962
+ return;
963
+ }
964
+ let fileContent, result;
965
+ try {
966
+ fileContent = fs.readFileSync(ctx.file, "utf-8");
967
+ } catch {
968
+ return;
969
+ }
970
+ if (!fileContent.includes("sz=") && !/\bsz\s*:\s*["'{]/.test(fileContent)) {
971
+ return;
972
+ }
973
+ try {
974
+ result = transformSourceCode(fileContent);
975
+ } catch {
976
+ return;
977
+ }
978
+ if (!result.transformed) {
979
+ return;
980
+ }
981
+ const sizeBefore = state.classes.size;
982
+ for (const cls of result.classes) {
983
+ state.classes.add(cls);
984
+ }
985
+ if (state.classes.size > sizeBefore) {
986
+ writeSafelistFile(state.classes);
987
+ const safelistPath = path.join(state.rootDir, SAFELIST_FILENAME);
988
+ ctx.server.watcher.emit("change", safelistPath);
989
+ }
501
990
  },
502
991
  transformIndexHtml: {
503
992
  order: "pre",
504
993
  /**
505
994
  * Injects hydration data (mangle map + checksum) into the HTML document.
995
+ * Also mangles class attributes in SSR-rendered HTML so they match mangled CSS selectors.
506
996
  * @param html - the raw HTML string to transform
507
997
  * @returns transformed HTML with injected hydration data
508
998
  */
@@ -537,10 +1027,23 @@ ${inlineDirective}`
537
1027
  },
538
1028
  (assets) => {
539
1029
  finalizeMangleMap();
1030
+ const isWebpackDevMode = compiler.options.mode === "development";
1031
+ const manifestData = {
1032
+ version: "0.4.0",
1033
+ buildId: state.checksum,
1034
+ classes: Object.keys(state.mangleMap)
1035
+ };
1036
+ if (manglingEnabled && !isWebpackDevMode && Object.keys(state.mangleMap).length > 0) {
1037
+ manifestData.mangleMap = state.mangleMap;
1038
+ }
1039
+ compilation.emitAsset(
1040
+ "csszyx-manifest.json",
1041
+ new compiler.webpack.sources.RawSource(JSON.stringify(manifestData))
1042
+ );
540
1043
  for (const file in assets) {
541
1044
  const asset = assets[file];
542
1045
  const source = asset.source().toString();
543
- if (manglingEnabled && Object.keys(state.mangleMap).length > 0) {
1046
+ if (manglingEnabled && !isWebpackDevMode && Object.keys(state.mangleMap).length > 0) {
544
1047
  if (file.endsWith(".css")) {
545
1048
  try {
546
1049
  const result = mangleCSSSync(source, state.mangleMap, {
@@ -560,6 +1063,21 @@ ${inlineDirective}`
560
1063
  throw e;
561
1064
  }
562
1065
  }
1066
+ } else if (file.endsWith(".html")) {
1067
+ const mangledHtml = source.replace(/\bclass="([^"]*)"/g, (_m, cls) => {
1068
+ const out = cls.split(/\s+/).filter(Boolean).map((c) => state.mangleMap[c] || c).join(" ");
1069
+ return out !== cls ? `class="${out}"` : _m;
1070
+ }).replace(/\bclass='([^']*)'/g, (_m, cls) => {
1071
+ const out = cls.split(/\s+/).filter(Boolean).map((c) => state.mangleMap[c] || c).join(" ");
1072
+ return out !== cls ? `class='${out}'` : _m;
1073
+ });
1074
+ if (mangledHtml !== source) {
1075
+ compilation.updateAsset(
1076
+ file,
1077
+ new compiler.webpack.sources.RawSource(mangledHtml)
1078
+ );
1079
+ continue;
1080
+ }
563
1081
  } else if (file.endsWith(".js")) {
564
1082
  let mangled = mangleCodeClasses(source);
565
1083
  mangled = replacePlaceholders(mangled);
@@ -594,6 +1112,19 @@ ${inlineDirective}`
594
1112
  */
595
1113
  generateBundle(_options, bundle) {
596
1114
  finalizeMangleMap();
1115
+ const manifestData = {
1116
+ version: "0.4.0",
1117
+ buildId: state.checksum,
1118
+ classes: Object.keys(state.mangleMap)
1119
+ };
1120
+ if (manglingEnabled && Object.keys(state.mangleMap).length > 0) {
1121
+ manifestData.mangleMap = state.mangleMap;
1122
+ }
1123
+ this.emitFile({
1124
+ type: "asset",
1125
+ fileName: "csszyx-manifest.json",
1126
+ source: JSON.stringify(manifestData)
1127
+ });
597
1128
  for (const file in bundle) {
598
1129
  const chunk = bundle[file];
599
1130
  if (manglingEnabled && Object.keys(state.mangleMap).length > 0) {
@@ -667,13 +1198,18 @@ var esbuildPlugin = (options = {}) => {
667
1198
  * @param build - the esbuild plugin build context
668
1199
  */
669
1200
  setup(build) {
670
- prePlugin.esbuild(options).setup(build);
671
- postPlugin.esbuild(options).setup(build);
1201
+ const b = build;
1202
+ prePlugin.esbuild(options).setup(b);
1203
+ postPlugin.esbuild(options).setup(b);
672
1204
  }
673
1205
  };
674
1206
  };
675
1207
 
676
1208
  export {
1209
+ parseThemeBlocks,
1210
+ mergeThemes,
1211
+ hasTokens,
1212
+ mangleCodeClassesSync,
677
1213
  unplugin,
678
1214
  vitePlugin,
679
1215
  webpackPlugin,