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