@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.
- package/dist/{chunk-3J4XUYF5.js → chunk-CW4XYEQR.js} +607 -71
- package/dist/index.cjs +611 -71
- package/dist/index.d.cts +51 -1
- package/dist/index.d.ts +51 -1
- package/dist/index.js +9 -1
- package/dist/{unplugin-B9noIooS.d.cts → unplugin-DUbr5w-N.d.cts} +17 -1
- package/dist/{unplugin-B9noIooS.d.ts → unplugin-DUbr5w-N.d.ts} +17 -1
- package/dist/vite.cjs +597 -69
- package/dist/vite.d.cts +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +1 -1
- package/dist/webpack.cjs +597 -69
- package/dist/webpack.d.cts +1 -1
- package/dist/webpack.d.ts +1 -1
- package/dist/webpack.js +1 -1
- package/package.json +6 -6
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
${
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
transformedCode = transformedCode.replace(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
671
|
-
|
|
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,
|