@csszyx/unplugin 0.8.0 → 0.9.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.
@@ -1,10 +1,14 @@
1
1
  import * as fs from 'node:fs';
2
2
  import { mkdirSync, writeFileSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
3
4
  import * as path from 'node:path';
4
5
  import { dirname } from 'node:path';
5
- import { transformSourceCode, transformOxc, transform } from '@csszyx/compiler';
6
+ import { performance } from 'node:perf_hooks';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { ensureRustTransformAvailable, transformSourceCode, transformRust, transformOxc, transform } from '@csszyx/compiler';
6
9
  import { encode, compute_mangle_checksum } from '@csszyx/core';
7
10
  import { preprocess as preprocess$1 } from '@csszyx/svelte-adapter';
11
+ import { DEFAULT_BUILD_CONFIG } from '@csszyx/types';
8
12
  import { preprocess } from '@csszyx/vue-adapter';
9
13
  import { createUnplugin } from 'unplugin';
10
14
  import { mangleCSSSync } from '../css-mangler.mjs';
@@ -12,12 +16,16 @@ import { createHash } from 'node:crypto';
12
16
 
13
17
  const SERVER_DIRECTIVE_RE = /^['"]use server['"];?$/;
14
18
  const CLIENT_DIRECTIVE_RE = /^['"]use client['"];?$/;
15
- const RUNTIME_MODULES = /* @__PURE__ */ new Set([
19
+ const RUNTIME_HELPER_MODULES = /* @__PURE__ */ new Set([
16
20
  "@csszyx/runtime",
17
21
  "@csszyx/runtime/lite",
18
22
  "csszyx",
19
23
  "csszyx/lite"
20
24
  ]);
25
+ const CLIENT_RUNTIME_MODULES = /* @__PURE__ */ new Set(["csszyx/browser"]);
26
+ const CLIENT_RUNTIME_MODULE_ROOTS = ["@csszyx/dynamic", "csszyx/dynamic"];
27
+ const normalizedModuleIdCache = /* @__PURE__ */ new Map();
28
+ const resolvedLocalModuleCache = /* @__PURE__ */ new Map();
21
29
  const FORBIDDEN_SYMBOLS = /* @__PURE__ */ new Set([
22
30
  "_sz",
23
31
  "_sz2",
@@ -87,6 +95,20 @@ function createRSCModuleRecord(code, id) {
87
95
  )
88
96
  };
89
97
  }
98
+ function deleteRSCModuleRecord(records, id) {
99
+ const normalized = normalizeModuleId(id);
100
+ const clean = id.split("?")[0]?.replace(/\\/g, "/") ?? id;
101
+ const resolved = path.resolve(clean).replace(/\\/g, "/");
102
+ pruneRSCModulePathCaches(/* @__PURE__ */ new Set([normalized, resolved, clean]));
103
+ let deleted = records.delete(normalized);
104
+ if (resolved !== normalized) {
105
+ deleted = records.delete(resolved) || deleted;
106
+ }
107
+ if (clean !== normalized && clean !== resolved) {
108
+ deleted = records.delete(clean) || deleted;
109
+ }
110
+ return deleted;
111
+ }
90
112
  function findRSCGraphViolation(records) {
91
113
  for (const root of records.values()) {
92
114
  if (!root.isServer) {
@@ -110,11 +132,26 @@ function assertNoRSCGraphViolation(records) {
110
132
  }
111
133
  throw new Error(formatRSCViolation(violation));
112
134
  }
135
+ const APP_ROUTER_ENTRIES = /* @__PURE__ */ new Set([
136
+ "page",
137
+ "layout",
138
+ "template",
139
+ "loading",
140
+ "error",
141
+ "not-found",
142
+ "global-error",
143
+ "default",
144
+ "route"
145
+ ]);
113
146
  function isNextAppRouterEntry(id) {
114
147
  const clean = id.split("?")[0]?.replace(/\\/g, "/") ?? id;
115
- return /(^|\/)app\/.*\/?(?:page|layout|template|loading|error|not-found|global-error|default|route)\.[cm]?[tj]sx?$/.test(
116
- clean
117
- );
148
+ if (!clean.includes("/app/") && !clean.startsWith("app/")) return false;
149
+ const basename = clean.split("/").pop() ?? "";
150
+ const dotIdx = basename.indexOf(".");
151
+ if (dotIdx === -1) return false;
152
+ const stem = basename.slice(0, dotIdx);
153
+ const ext = basename.slice(dotIdx + 1);
154
+ return APP_ROUTER_ENTRIES.has(stem) && /^[cm]?[tj]sx?$/.test(ext);
118
155
  }
119
156
  function assertNoRSCBoundaryViolation(code, id) {
120
157
  const violation = findRSCBoundaryViolation(code, id);
@@ -215,35 +252,51 @@ function skipWhitespaceAndComments(code, start) {
215
252
  }
216
253
  function findRuntimeImports(code) {
217
254
  const imports = [];
218
- const staticImportRe = /import\s+(?!type\b)([\s\S]*?)\s+from\s+['"]([^'"]+)['"]/g;
255
+ const scanCode = stripCommentsForImportScan(code);
256
+ const staticImportRe = /import\s+(?!type\b)(\S(?:.*\S)?)\s+from\s+['"]([^'"]+)['"]/g;
219
257
  const sideEffectImportRe = /import\s+['"]([^'"]+)['"]/g;
220
258
  const dynamicImportRe = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
221
- for (const match of code.matchAll(staticImportRe)) {
259
+ for (const match of scanCode.matchAll(staticImportRe)) {
222
260
  const clause = match[1];
223
261
  const source = match[2];
224
- if (!RUNTIME_MODULES.has(source)) {
262
+ if (!isRuntimeImportSource(source)) {
225
263
  continue;
226
264
  }
227
- imports.push({ source, symbols: readImportedSymbols(clause) });
265
+ imports.push({ source, symbols: readRuntimeImportSymbols(source, clause) });
228
266
  }
229
- for (const match of code.matchAll(sideEffectImportRe)) {
267
+ for (const match of scanCode.matchAll(sideEffectImportRe)) {
230
268
  const source = match[1];
231
- if (RUNTIME_MODULES.has(source)) {
232
- imports.push({ source, symbols: [] });
269
+ if (isRuntimeImportSource(source)) {
270
+ imports.push({
271
+ source,
272
+ symbols: isWholeRuntimeModuleForbidden(source) ? Array.from(FORBIDDEN_SYMBOLS) : []
273
+ });
233
274
  }
234
275
  }
235
- for (const match of code.matchAll(dynamicImportRe)) {
276
+ for (const match of scanCode.matchAll(dynamicImportRe)) {
236
277
  const source = match[1];
237
- if (RUNTIME_MODULES.has(source)) {
278
+ if (isRuntimeImportSource(source)) {
238
279
  imports.push({ source, symbols: Array.from(FORBIDDEN_SYMBOLS) });
239
280
  }
240
281
  }
241
282
  return imports;
242
283
  }
284
+ function isRuntimeImportSource(source) {
285
+ return RUNTIME_HELPER_MODULES.has(source) || source.startsWith("@csszyx/runtime/") || CLIENT_RUNTIME_MODULES.has(source) || CLIENT_RUNTIME_MODULE_ROOTS.some((root) => source === root || source.startsWith(`${root}/`));
286
+ }
287
+ function isWholeRuntimeModuleForbidden(source) {
288
+ return source.startsWith("@csszyx/runtime/") || CLIENT_RUNTIME_MODULES.has(source) || CLIENT_RUNTIME_MODULE_ROOTS.some((root) => source === root || source.startsWith(`${root}/`));
289
+ }
290
+ function readRuntimeImportSymbols(source, clause) {
291
+ if (isWholeRuntimeModuleForbidden(source)) {
292
+ return Array.from(FORBIDDEN_SYMBOLS);
293
+ }
294
+ return readImportedSymbols(clause);
295
+ }
243
296
  function findLocalImportSources(code) {
244
297
  const out = [];
245
- const staticImportRe = /import\s+(?!type\b)(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g;
246
- const exportFromRe = /export\s+(?!type\b)(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"]/g;
298
+ const staticImportRe = /import\s+(?!type\b)(?:\S(?:.*\S)?\s+from\s+)?['"]([^'"]+)['"]/g;
299
+ const exportFromRe = /export\s+(?!type\b)\S(?:.*\S)?\s+from\s+['"]([^'"]+)['"]/g;
247
300
  const dynamicImportRe = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
248
301
  for (const re of [staticImportRe, exportFromRe, dynamicImportRe]) {
249
302
  for (const match of code.matchAll(re)) {
@@ -257,13 +310,25 @@ function findLocalImportSources(code) {
257
310
  }
258
311
  function normalizeModuleId(id) {
259
312
  const clean = id.split("?")[0] ?? id;
313
+ const cached = normalizedModuleIdCache.get(clean);
314
+ if (cached) {
315
+ return cached;
316
+ }
317
+ let normalized;
260
318
  try {
261
- return fs.realpathSync.native(clean).replace(/\\/g, "/");
319
+ normalized = fs.realpathSync.native(clean).replace(/\\/g, "/");
262
320
  } catch {
263
- return path.resolve(clean).replace(/\\/g, "/");
321
+ normalized = path.resolve(clean).replace(/\\/g, "/");
264
322
  }
323
+ normalizedModuleIdCache.set(clean, normalized);
324
+ return normalized;
265
325
  }
266
326
  function resolveLocalModule(importer, source) {
327
+ const cacheKey = `${importer}\0${source}`;
328
+ const cached = resolvedLocalModuleCache.get(cacheKey);
329
+ if (cached) {
330
+ return cached;
331
+ }
267
332
  const base = source.startsWith("/") ? source : path.resolve(path.dirname(importer), source);
268
333
  const candidates = [
269
334
  base,
@@ -280,11 +345,26 @@ function resolveLocalModule(importer, source) {
280
345
  ];
281
346
  for (const candidate of candidates) {
282
347
  if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
283
- return normalizeModuleId(candidate);
348
+ const resolved = normalizeModuleId(candidate);
349
+ resolvedLocalModuleCache.set(cacheKey, resolved);
350
+ return resolved;
284
351
  }
285
352
  }
286
353
  return null;
287
354
  }
355
+ function pruneRSCModulePathCaches(moduleIds) {
356
+ for (const [key, value] of normalizedModuleIdCache) {
357
+ const normalizedKey = key.replace(/\\/g, "/");
358
+ if (moduleIds.has(value) || moduleIds.has(normalizedKey)) {
359
+ normalizedModuleIdCache.delete(key);
360
+ }
361
+ }
362
+ for (const [key, value] of resolvedLocalModuleCache) {
363
+ if (moduleIds.has(value)) {
364
+ resolvedLocalModuleCache.delete(key);
365
+ }
366
+ }
367
+ }
288
368
  function readImportedSymbols(clause) {
289
369
  const symbols = [];
290
370
  const named = clause.match(/\{([\s\S]*?)\}/);
@@ -303,8 +383,72 @@ function readImportedSymbols(clause) {
303
383
  if (/\*\s+as\s+\w+/.test(clause)) {
304
384
  symbols.push(...FORBIDDEN_SYMBOLS);
305
385
  }
386
+ const braceStart = clause.indexOf("{");
387
+ const braceEnd = clause.indexOf("}", braceStart);
388
+ const stripped = braceStart !== -1 && braceEnd !== -1 ? clause.slice(0, braceStart) + clause.slice(braceEnd + 1) : clause;
389
+ const defaultImport = stripped.match(/^\s*([A-Z_$][\w$]*)\s*(?:,|$)/i);
390
+ const defaultSymbol = defaultImport?.[1];
391
+ if (defaultSymbol && FORBIDDEN_SYMBOLS.has(defaultSymbol)) {
392
+ symbols.push(defaultSymbol);
393
+ }
306
394
  return symbols;
307
395
  }
396
+ function stripCommentsForImportScan(code) {
397
+ let out = "";
398
+ let i = 0;
399
+ let quote = null;
400
+ let escaped = false;
401
+ while (i < code.length) {
402
+ const ch = code[i];
403
+ const next = code[i + 1];
404
+ if (quote) {
405
+ out += ch;
406
+ if (escaped) {
407
+ escaped = false;
408
+ } else if (ch === "\\") {
409
+ escaped = true;
410
+ } else if (ch === quote) {
411
+ quote = null;
412
+ }
413
+ i++;
414
+ continue;
415
+ }
416
+ if (ch === '"' || ch === "'" || ch === "`") {
417
+ quote = ch;
418
+ out += ch;
419
+ i++;
420
+ continue;
421
+ }
422
+ if (ch === "/" && next === "/") {
423
+ out += " ";
424
+ i += 2;
425
+ while (i < code.length && code[i] !== "\n") {
426
+ out += " ";
427
+ i++;
428
+ }
429
+ continue;
430
+ }
431
+ if (ch === "/" && next === "*") {
432
+ out += " ";
433
+ i += 2;
434
+ while (i < code.length) {
435
+ const blockCh = code[i];
436
+ const blockNext = code[i + 1];
437
+ if (blockCh === "*" && blockNext === "/") {
438
+ out += " ";
439
+ i += 2;
440
+ break;
441
+ }
442
+ out += blockCh === "\n" ? "\n" : " ";
443
+ i++;
444
+ }
445
+ continue;
446
+ }
447
+ out += ch;
448
+ i++;
449
+ }
450
+ return out;
451
+ }
308
452
 
309
453
  const EMPTY_THEME = { colors: [], spacings: [], fonts: [], radii: [], shadows: [] };
310
454
  function stripLayerWrappers(css) {
@@ -703,6 +847,149 @@ function writeThemeDts(opts) {
703
847
  writeFileSync(opts.outputPath, content, "utf-8");
704
848
  }
705
849
 
850
+ const CACHE_SCHEMA_VERSION = 2;
851
+ function resolveTransformCacheDir(rootDir, cacheDir) {
852
+ return path.resolve(rootDir, cacheDir ?? ".csszyx/cache", "transform");
853
+ }
854
+ function createTransformCacheKey(input) {
855
+ const inputSha256 = createHash("sha256").update(input.source).digest("hex");
856
+ const keyMaterial = [
857
+ `schema=${CACHE_SCHEMA_VERSION}`,
858
+ `plugin=${input.pluginVersion}`,
859
+ `compiler=${input.compilerVersion}`,
860
+ `parser=${input.parserMode}`,
861
+ `producer=${input.producer}`,
862
+ `astBudget=${input.astBudget ?? "default"}`,
863
+ `filename=${input.filename}`,
864
+ `source=${inputSha256}`
865
+ ].join("\n");
866
+ return {
867
+ key: createHash("sha256").update(keyMaterial).digest("hex").slice(0, 16),
868
+ inputSha256
869
+ };
870
+ }
871
+ function readTransformCache(cacheRoot, input, precomputedKey) {
872
+ const { key, inputSha256 } = precomputedKey ?? createTransformCacheKey(input);
873
+ const file = cacheEntryPath(cacheRoot, key);
874
+ let entry;
875
+ try {
876
+ entry = JSON.parse(fs.readFileSync(file, "utf8"));
877
+ } catch {
878
+ return null;
879
+ }
880
+ if (entry.version !== CACHE_SCHEMA_VERSION || entry.pluginVersion !== input.pluginVersion || entry.compilerVersion !== input.compilerVersion || entry.parserMode !== input.parserMode || entry.producer !== input.producer || entry.astBudget !== (input.astBudget ?? null) || entry.filename !== input.filename || entry.inputSha256 !== inputSha256) {
881
+ return null;
882
+ }
883
+ return deserializeResult(entry.result);
884
+ }
885
+ function writeTransformCache(cacheRoot, input, result, precomputedKey) {
886
+ const { key, inputSha256 } = precomputedKey ?? createTransformCacheKey(input);
887
+ const file = cacheEntryPath(cacheRoot, key);
888
+ const dir = path.dirname(file);
889
+ const tmp = path.join(
890
+ dir,
891
+ `.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`
892
+ );
893
+ const entry = {
894
+ version: CACHE_SCHEMA_VERSION,
895
+ pluginVersion: input.pluginVersion,
896
+ compilerVersion: input.compilerVersion,
897
+ parserMode: input.parserMode,
898
+ producer: input.producer,
899
+ astBudget: input.astBudget ?? null,
900
+ filename: input.filename,
901
+ inputSha256,
902
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
903
+ result: serializeResult(result)
904
+ };
905
+ try {
906
+ fs.mkdirSync(dir, { recursive: true });
907
+ fs.writeFileSync(tmp, JSON.stringify(entry), "utf8");
908
+ fs.renameSync(tmp, file);
909
+ } catch {
910
+ try {
911
+ fs.rmSync(tmp, { force: true });
912
+ } catch {
913
+ }
914
+ }
915
+ }
916
+ function evictOldTransformCacheEntries(cacheRoot, options) {
917
+ let deleted = 0;
918
+ const now = options.now ?? Date.now();
919
+ const survivors = [];
920
+ for (const file of listJsonFiles(cacheRoot)) {
921
+ try {
922
+ const entry = JSON.parse(fs.readFileSync(file, "utf8"));
923
+ const timestamp = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : 0;
924
+ if (!Number.isFinite(timestamp) || now - timestamp > options.maxAgeMs) {
925
+ fs.rmSync(file, { force: true });
926
+ deleted++;
927
+ } else {
928
+ survivors.push({ file, timestamp });
929
+ }
930
+ } catch {
931
+ fs.rmSync(file, { force: true });
932
+ deleted++;
933
+ }
934
+ }
935
+ const overflow = survivors.length - options.maxEntries ;
936
+ if (overflow > 0) {
937
+ survivors.sort((a, b) => a.timestamp - b.timestamp);
938
+ for (const survivor of survivors.slice(0, overflow)) {
939
+ fs.rmSync(survivor.file, { force: true });
940
+ deleted++;
941
+ }
942
+ }
943
+ return deleted;
944
+ }
945
+ function cacheEntryPath(cacheRoot, key) {
946
+ return path.join(cacheRoot, key.slice(0, 2), `${key.slice(2)}.json`);
947
+ }
948
+ function serializeResult(result) {
949
+ return {
950
+ code: result.code,
951
+ transformed: result.transformed,
952
+ usesRuntime: result.usesRuntime,
953
+ usesMerge: result.usesMerge,
954
+ usesColorVar: result.usesColorVar,
955
+ classes: [...result.classes],
956
+ rawClassNames: [...result.rawClassNames],
957
+ diagnostics: [...result.diagnostics],
958
+ recoveryTokens: [...result.recoveryTokens]
959
+ };
960
+ }
961
+ function deserializeResult(result) {
962
+ return {
963
+ code: result.code,
964
+ transformed: result.transformed,
965
+ usesRuntime: result.usesRuntime,
966
+ usesMerge: result.usesMerge,
967
+ usesColorVar: result.usesColorVar,
968
+ classes: new Set(result.classes),
969
+ rawClassNames: new Set(result.rawClassNames),
970
+ diagnostics: [...result.diagnostics],
971
+ recoveryTokens: new Map(result.recoveryTokens)
972
+ };
973
+ }
974
+ function listJsonFiles(dir) {
975
+ let entries;
976
+ try {
977
+ entries = fs.readdirSync(dir, { withFileTypes: true });
978
+ } catch {
979
+ return [];
980
+ }
981
+ const files = [];
982
+ for (const entry of entries) {
983
+ const fullPath = path.join(dir, entry.name);
984
+ if (entry.isDirectory()) {
985
+ files.push(...listJsonFiles(fullPath));
986
+ } else if (entry.isFile() && entry.name.endsWith(".json")) {
987
+ files.push(fullPath);
988
+ }
989
+ }
990
+ return files;
991
+ }
992
+
706
993
  const VIRTUAL_MODULE_ID = "virtual:csszyx/mangle-map";
707
994
  const RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
708
995
  const VIRTUAL_CHECKSUM_ID = "virtual:csszyx/checksum";
@@ -753,7 +1040,37 @@ function resolveVirtualModule(id) {
753
1040
 
754
1041
  const CHECKSUM_PLACEHOLDER = "___CSSZYX_CHECKSUM___";
755
1042
  const MANGLE_MAP_PLACEHOLDER = "___CSSZYX_MANGLE_MAP___";
1043
+ const UNKNOWN_PACKAGE_VERSION = "0.0.0";
1044
+ const TRANSFORM_CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
1045
+ const TRANSFORM_CACHE_MAX_ENTRIES = 1e4;
1046
+ const TRANSFORM_MEMORY_CACHE_MAX_ENTRIES = 1e3;
1047
+ const DIRECTIVE_PROLOGUE_PREFIX_RE = /^((?:\s|\/\/[^\n]*\n|\/\*(?:[^*]|\*(?!\/))*\*\/)*)(['"]use (?:client|server)['"];?\s*)/;
1048
+ const RUNTIME_HELPER_IMPORT_RE = {
1049
+ _sz: /\{[^}]*\b_sz\b[^}]*\}\s*from\s*['"]@csszyx\/runtime['"]/,
1050
+ _szMerge: /\{[^}]*\b_szMerge\b[^}]*\}\s*from\s*['"]@csszyx\/runtime['"]/,
1051
+ __szColorVar: /\{[^}]*\b__szColorVar\b[^}]*\}\s*from\s*['"]@csszyx\/runtime['"]/
1052
+ };
756
1053
  let _hasWarnedTsConfig = false;
1054
+ let _hasWarnedTransformCacheVersion = false;
1055
+ const requireFromHere = createRequire(import.meta.url);
1056
+ const PLUGIN_VERSION = findPackageVersionFromFile(
1057
+ fileURLToPath(import.meta.url),
1058
+ UNKNOWN_PACKAGE_VERSION
1059
+ );
1060
+ const COMPILER_VERSION = findPackageVersionFromModule("@csszyx/compiler", UNKNOWN_PACKAGE_VERSION);
1061
+ const BENCH_TRACE_ENABLED = process.env.CSSZYX_BENCH_TRACE === "1";
1062
+ const BENCH_TRACE_FILE = process.env.CSSZYX_BENCH_TRACE_FILE;
1063
+ function traceBenchTiming(label, filename, elapsedMs) {
1064
+ if (!BENCH_TRACE_ENABLED) {
1065
+ return;
1066
+ }
1067
+ if (BENCH_TRACE_FILE && !filename.includes(BENCH_TRACE_FILE)) {
1068
+ return;
1069
+ }
1070
+ console.log(
1071
+ `[csszyx:bench] ${label} ${elapsedMs.toFixed(3)}ms ${normalizeSourceFilename(filename)}`
1072
+ );
1073
+ }
757
1074
  function runThemeScan(rootDir, scanCss) {
758
1075
  if (!scanCss) {
759
1076
  return;
@@ -796,6 +1113,42 @@ function runThemeScan(rootDir, scanCss) {
796
1113
  }
797
1114
  }
798
1115
  }
1116
+ function findPackageVersionFromModule(specifier, fallback) {
1117
+ try {
1118
+ return findPackageVersionFromFile(requireFromHere.resolve(specifier), fallback);
1119
+ } catch {
1120
+ return fallback;
1121
+ }
1122
+ }
1123
+ function findPackageVersionFromFile(file, fallback) {
1124
+ let dir = path.dirname(file);
1125
+ while (true) {
1126
+ const packageJson = path.join(dir, "package.json");
1127
+ try {
1128
+ const parsed = JSON.parse(fs.readFileSync(packageJson, "utf8"));
1129
+ if (typeof parsed.version === "string") {
1130
+ return parsed.version;
1131
+ }
1132
+ return fallback;
1133
+ } catch {
1134
+ }
1135
+ const parent = path.dirname(dir);
1136
+ if (parent === dir) {
1137
+ return fallback;
1138
+ }
1139
+ dir = parent;
1140
+ }
1141
+ }
1142
+ function normalizeSourceFilename(filename) {
1143
+ return filename.replace(/\\/g, "/");
1144
+ }
1145
+ function insertRuntimeImport(code, importStmt) {
1146
+ const directiveMatch = code.match(DIRECTIVE_PROLOGUE_PREFIX_RE);
1147
+ if (!directiveMatch) {
1148
+ return `${importStmt}${code}`;
1149
+ }
1150
+ return code.replace(directiveMatch[0], `${directiveMatch[1]}${directiveMatch[2]}${importStmt}`);
1151
+ }
799
1152
  function mangleCodeClassesSync(code, mangleMap) {
800
1153
  function mangleClassString(classString) {
801
1154
  return classString.split(/\s+/).filter(Boolean).map((cls) => {
@@ -971,8 +1324,20 @@ function mangleCodeClassesSync(code, mangleMap) {
971
1324
  function createCsszyxPlugins(options = {}) {
972
1325
  const manglingEnabled = options.production?.mangle !== false;
973
1326
  const astBudgetOverride = options.build?.astBudgetLimit;
1327
+ const cacheRequested = (options.build?.cache ?? DEFAULT_BUILD_CONFIG.cache) !== false;
1328
+ const cacheVersionsKnown = PLUGIN_VERSION !== UNKNOWN_PACKAGE_VERSION && COMPILER_VERSION !== UNKNOWN_PACKAGE_VERSION;
1329
+ const cacheEnabled = cacheRequested && cacheVersionsKnown;
1330
+ if (cacheRequested && !cacheVersionsKnown && !_hasWarnedTransformCacheVersion) {
1331
+ _hasWarnedTransformCacheVersion = true;
1332
+ console.warn(
1333
+ "[csszyx] Transform cache disabled because package versions could not be resolved."
1334
+ );
1335
+ }
974
1336
  const parserOverride = process.env.CSSZYX_PARSER;
975
- const parserMode = parserOverride === "babel" || parserOverride === "oxc" ? parserOverride : options.build?.parser ?? "oxc";
1337
+ const defaultParser = DEFAULT_BUILD_CONFIG.parser ?? "rust";
1338
+ const parserMode = parserOverride === "babel" || parserOverride === "oxc" || parserOverride === "rust" ? parserOverride : options.build?.parser ?? defaultParser;
1339
+ let evictedCacheRoot = null;
1340
+ const transformMemoryCache = /* @__PURE__ */ new Map();
976
1341
  const state = {
977
1342
  classes: /* @__PURE__ */ new Set(),
978
1343
  mangleMap: {},
@@ -1002,19 +1367,84 @@ function createCsszyxPlugins(options = {}) {
1002
1367
  }
1003
1368
  function transformConfiguredSource(source, filename) {
1004
1369
  const compilerOptions = { astBudget: astBudgetOverride };
1005
- if (parserMode !== "oxc") {
1006
- return transformSourceCode(source, filename, compilerOptions);
1370
+ const effectiveFilename = normalizeSourceFilename(filename);
1371
+ const cacheRoot = resolveTransformCacheDir(state.rootDir, options.build?.cacheDir);
1372
+ if (cacheEnabled) {
1373
+ evictTransformCacheOnce();
1007
1374
  }
1008
- try {
1009
- return transformOxc(source, filename, compilerOptions);
1010
- } catch (err) {
1011
- const result = transformSourceCode(source, filename, compilerOptions);
1012
- const reason = err instanceof Error ? err.message : String(err);
1013
- result.diagnostics.push(
1014
- `[csszyx] oxc parser fell back to Babel for ${filename}: ${reason}`
1015
- );
1016
- return result;
1375
+ const cacheInput = {
1376
+ pluginVersion: PLUGIN_VERSION,
1377
+ compilerVersion: COMPILER_VERSION,
1378
+ parserMode,
1379
+ producer: parserMode,
1380
+ astBudget: astBudgetOverride,
1381
+ filename: effectiveFilename,
1382
+ source
1383
+ };
1384
+ if (parserMode === "rust") {
1385
+ ensureRustTransformAvailable();
1386
+ }
1387
+ const cacheKey = cacheEnabled ? createTransformCacheKey(cacheInput) : null;
1388
+ if (cacheEnabled && cacheKey) {
1389
+ const memoryCached = transformMemoryCache.get(cacheKey.key);
1390
+ if (memoryCached) {
1391
+ transformMemoryCache.delete(cacheKey.key);
1392
+ transformMemoryCache.set(cacheKey.key, memoryCached);
1393
+ return memoryCached;
1394
+ }
1395
+ const cached = readTransformCache(cacheRoot, cacheInput, cacheKey);
1396
+ if (cached) {
1397
+ rememberTransformCacheEntry(cacheKey.key, cached);
1398
+ return cached;
1399
+ }
1017
1400
  }
1401
+ let result;
1402
+ if (parserMode === "babel") {
1403
+ result = transformSourceCode(source, effectiveFilename, compilerOptions);
1404
+ } else if (parserMode === "rust") {
1405
+ result = transformRust(source, effectiveFilename, compilerOptions);
1406
+ } else {
1407
+ try {
1408
+ result = transformOxc(source, effectiveFilename, compilerOptions);
1409
+ } catch (err) {
1410
+ result = transformSourceCode(source, effectiveFilename, compilerOptions);
1411
+ const reason = err instanceof Error ? err.message : String(err);
1412
+ result.diagnostics.push(
1413
+ `[csszyx] oxc parser fell back to Babel for ${effectiveFilename}: ${reason}`
1414
+ );
1415
+ return result;
1416
+ }
1417
+ }
1418
+ if (cacheEnabled && cacheKey) {
1419
+ writeTransformCache(cacheRoot, cacheInput, result, cacheKey);
1420
+ rememberTransformCacheEntry(cacheKey.key, result);
1421
+ }
1422
+ return result;
1423
+ }
1424
+ function rememberTransformCacheEntry(key, result) {
1425
+ transformMemoryCache.delete(key);
1426
+ transformMemoryCache.set(key, result);
1427
+ if (transformMemoryCache.size <= TRANSFORM_MEMORY_CACHE_MAX_ENTRIES) {
1428
+ return;
1429
+ }
1430
+ const oldest = transformMemoryCache.keys().next().value;
1431
+ if (oldest) {
1432
+ transformMemoryCache.delete(oldest);
1433
+ }
1434
+ }
1435
+ function evictTransformCacheOnce() {
1436
+ if (!cacheEnabled) {
1437
+ return;
1438
+ }
1439
+ const cacheRoot = resolveTransformCacheDir(state.rootDir, options.build?.cacheDir);
1440
+ if (evictedCacheRoot === cacheRoot) {
1441
+ return;
1442
+ }
1443
+ evictedCacheRoot = cacheRoot;
1444
+ evictOldTransformCacheEntries(cacheRoot, {
1445
+ maxAgeMs: TRANSFORM_CACHE_MAX_AGE_MS,
1446
+ maxEntries: TRANSFORM_CACHE_MAX_ENTRIES
1447
+ });
1018
1448
  }
1019
1449
  function writeSafelistFile(classes) {
1020
1450
  if (classes.size === 0) {
@@ -1296,7 +1726,13 @@ ${sourceDirective}`
1296
1726
  transformed = true;
1297
1727
  }
1298
1728
  } else {
1729
+ const transformStarted = performance.now();
1299
1730
  const result = transformConfiguredSource(code, id);
1731
+ traceBenchTiming(
1732
+ "transform-hook",
1733
+ id,
1734
+ performance.now() - transformStarted
1735
+ );
1300
1736
  transformedCode = result.code;
1301
1737
  usesRuntime = result.usesRuntime;
1302
1738
  usesMerge = result.usesMerge;
@@ -1340,11 +1776,10 @@ ${sourceDirective}`
1340
1776
  if (usesColorVar) {
1341
1777
  imports.push("__szColorVar");
1342
1778
  }
1343
- const needed = imports.filter(
1344
- (name) => !new RegExp(
1345
- `\\{[^}]*\\b${name}\\b[^}]*\\}\\s*from\\s*['"]@csszyx/runtime['"]`
1346
- ).test(transformedCode)
1347
- );
1779
+ const hasRuntimeImport = imports.length > 0 && transformedCode.includes("@csszyx/runtime");
1780
+ const needed = hasRuntimeImport ? imports.filter(
1781
+ (name) => !RUNTIME_HELPER_IMPORT_RE[name]?.test(transformedCode)
1782
+ ) : imports;
1348
1783
  if (needed.length > 0) {
1349
1784
  const existingImport = transformedCode.match(
1350
1785
  /^(import\s*\{[^}]*)\}\s*from\s*'@csszyx\/runtime'/m
@@ -1357,18 +1792,7 @@ ${sourceDirective}`
1357
1792
  } else {
1358
1793
  const importStmt = `import { ${needed.join(", ")} } from '@csszyx/runtime';
1359
1794
  `;
1360
- const directiveMatch = transformedCode.match(
1361
- /^['"]use (client|server)['"];?\s*/
1362
- );
1363
- if (directiveMatch) {
1364
- const directive = directiveMatch[0];
1365
- transformedCode = transformedCode.replace(
1366
- directive,
1367
- `${directive}${importStmt}`
1368
- );
1369
- } else {
1370
- transformedCode = `${importStmt}${transformedCode}`;
1371
- }
1795
+ transformedCode = insertRuntimeImport(transformedCode, importStmt);
1372
1796
  }
1373
1797
  transformed = true;
1374
1798
  }
@@ -1398,6 +1822,11 @@ ${sourceDirective}`
1398
1822
  globalThis.__csszyx_ssr_mangle_map = state.mangleMap;
1399
1823
  }
1400
1824
  },
1825
+ watchChange(id, change) {
1826
+ if (change.event === "delete") {
1827
+ deleteRSCModuleRecord(state.rscModules, id);
1828
+ }
1829
+ },
1401
1830
  /**
1402
1831
  * Webpack hook: pre-scans source files before compilation for Tailwind class discovery.
1403
1832
  * @param compiler - the Webpack compiler instance
@@ -1406,6 +1835,7 @@ ${sourceDirective}`
1406
1835
  compiler.hooks.beforeCompile.tap("csszyx:prescan", () => {
1407
1836
  const root = compiler.context || process.cwd();
1408
1837
  state.rootDir = root;
1838
+ evictTransformCacheOnce();
1409
1839
  if (state.classes.size === 0) {
1410
1840
  prescanAndWriteClasses();
1411
1841
  }
@@ -1429,6 +1859,7 @@ ${sourceDirective}`
1429
1859
  configResolved(config) {
1430
1860
  const root = config.root || process.cwd();
1431
1861
  state.rootDir = root;
1862
+ evictTransformCacheOnce();
1432
1863
  prescanAndWriteClasses();
1433
1864
  runThemeScan(root, options.build?.scanCss);
1434
1865
  },
@@ -1458,7 +1889,13 @@ ${sourceDirective}`
1458
1889
  return;
1459
1890
  }
1460
1891
  try {
1892
+ const hmrTransformStarted = performance.now();
1461
1893
  result = transformConfiguredSource(fileContent, ctx.file);
1894
+ traceBenchTiming(
1895
+ "handle-hot-update",
1896
+ ctx.file,
1897
+ performance.now() - hmrTransformStarted
1898
+ );
1462
1899
  } catch {
1463
1900
  return;
1464
1901
  }
@@ -1719,4 +2156,4 @@ const esbuildPlugin = (options = {}) => {
1719
2156
  };
1720
2157
  };
1721
2158
 
1722
- export { assertNoRSCBoundaryViolation as a, assertNoRSCGraphViolation as b, createRSCModuleRecord as c, findRSCGraphViolation as d, esbuildPlugin as e, findRSCBoundaryViolation as f, hasUseClientDirective as g, hasTokens as h, hasUseServerDirective as i, isRSCServerModule as j, mergeThemes as k, mangleCodeClassesSync as m, parseThemeBlocks as p, rollupPlugin as r, unplugin as u, vitePlugin as v, webpackPlugin as w };
2159
+ export { assertNoRSCBoundaryViolation as a, assertNoRSCGraphViolation as b, createRSCModuleRecord as c, deleteRSCModuleRecord as d, esbuildPlugin as e, findRSCBoundaryViolation as f, findRSCGraphViolation as g, hasTokens as h, hasUseClientDirective as i, hasUseServerDirective as j, isRSCServerModule as k, mergeThemes as l, mangleCodeClassesSync as m, parseThemeBlocks as p, rollupPlugin as r, unplugin as u, vitePlugin as v, webpackPlugin as w };