@gustcss/vite 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -21,13 +21,13 @@ var __copyProps = (to, from, except, desc) => {
21
21
  }
22
22
  return to;
23
23
  };
24
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
25
25
  value: mod,
26
26
  enumerable: true
27
27
  }) : target, mod));
28
28
  var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
29
29
  //#endregion
30
- //#region src/index.js
30
+ //#region src/mangle.js
31
31
  var import_runner = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
32
32
  /**
33
33
  * gustcss shared runner — helpers for the @gustcss/postcss and @gustcss/vite
@@ -87,6 +87,24 @@ var import_runner = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
87
87
  return null;
88
88
  }
89
89
  /**
90
+ * Report whether a resolved gustcss.config.json turns on class-name mangling.
91
+ *
92
+ * Callers that cannot rewrite source (dev server, PostCSS) use this to decide
93
+ * whether they must pass `--mangle-class-names=false`. Only passing the flag
94
+ * when needed keeps older CLI binaries that predate the flag working.
95
+ *
96
+ * @param {{ configPath: string|null, fs?: { readFileSync: Function } }} options
97
+ * @returns {boolean}
98
+ */
99
+ function configEnablesMangling({ configPath, fs = __require("fs") }) {
100
+ if (!configPath) return false;
101
+ try {
102
+ return JSON.parse(fs.readFileSync(configPath, "utf-8"))?.mangleClassNames === true;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+ /**
90
108
  * Write a temporary config file ({ content }) at <cwd>/<prefix>.<random8hex>.tmp.json,
91
109
  * invoke `fn` with the temp file path, and always clean the file up afterwards.
92
110
  *
@@ -118,25 +136,590 @@ var import_runner = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
118
136
  * @param {string} [options.output] output path (required for 'output' mode)
119
137
  * @param {boolean} [options.cssLayers] append --css-layers when truthy
120
138
  * @param {boolean} [options.watch] append --watch when truthy
139
+ * @param {boolean} [options.mangleClassNames] explicitly enable or disable mangling
140
+ * @param {string} [options.mangleMap] class-name manifest output path
141
+ * @param {string[]} [options.mangleExclude] class names to preserve
121
142
  * @param {string|null} [options.configPath] append --config <path> when present
122
143
  * @returns {string[]}
123
144
  */
124
- function buildArgs({ mode, output, cssLayers, configPath, watch }) {
145
+ function buildArgs({ mode, output, cssLayers, configPath, watch, mangleClassNames, mangleMap, mangleExclude }) {
125
146
  const args = ["build"];
126
147
  if (watch) args.push("--watch");
127
148
  if (mode === "stdout") args.push("--stdout");
128
149
  else args.push("-o", output);
129
150
  if (cssLayers) args.push("--css-layers");
151
+ if (mangleClassNames !== void 0) args.push(`--mangle-class-names=${mangleClassNames}`);
152
+ if (mangleMap) args.push("--mangle-map", mangleMap);
153
+ if (mangleExclude?.length) args.push("--mangle-exclude", mangleExclude.join(","));
130
154
  if (configPath) args.push("--config", configPath);
131
155
  return args;
132
156
  }
133
157
  module.exports = {
134
158
  resolveBinary,
135
159
  resolveConfig,
160
+ configEnablesMangling,
136
161
  withTempConfig,
137
162
  buildArgs
138
163
  };
139
164
  })))(), 1);
165
+ const SUPPORTED_SOURCE = /\.(?:[cm]?[jt]sx?|html)(?:$|\?)/;
166
+ const HTML_SOURCE = /\.html(?:$|\?)/;
167
+ const UNSUPPORTED_FRAMEWORK_SOURCE = /\.(?:vue|astro|svelte)(?:$|\?)/;
168
+ const DEPENDENCY_SOURCE = /(?:^|[\\/])node_modules[\\/]/;
169
+ function hasOwn(classes, token) {
170
+ return Object.hasOwn(classes, token);
171
+ }
172
+ function collectClassListEdits(value, classes, baseOffset, edits) {
173
+ for (const match of value.matchAll(/\S+/g)) {
174
+ const replacement = classes[match[0]];
175
+ if (hasOwn(classes, match[0]) && replacement !== match[0]) edits.push({
176
+ start: baseOffset + match.index,
177
+ end: baseOffset + match.index + match[0].length,
178
+ replacement
179
+ });
180
+ }
181
+ }
182
+ function maskRange(chars, code, start, end) {
183
+ for (let index = start; index < end; index += 1) if (code[index] !== "\n" && code[index] !== "\r") chars[index] = " ";
184
+ }
185
+ function copyRange(chars, code, start, end) {
186
+ for (let index = start; index < end; index += 1) chars[index] = code[index];
187
+ }
188
+ function findHtmlTagEnd(code, start) {
189
+ let quote = null;
190
+ for (let index = start + 1; index < code.length; index += 1) {
191
+ const char = code[index];
192
+ if (quote !== null) {
193
+ if (char === quote) quote = null;
194
+ } else if (char === "\"" || char === "'") quote = char;
195
+ else if (char === ">") return index + 1;
196
+ }
197
+ return -1;
198
+ }
199
+ function findRawTextClosing(code, tagName, start) {
200
+ const lower = code.toLowerCase();
201
+ const needle = `</${tagName}`;
202
+ let index = lower.indexOf(needle, start);
203
+ while (index >= 0) {
204
+ const after = lower[index + needle.length];
205
+ if (after === void 0 || /[\s>]/.test(after)) return index;
206
+ index = lower.indexOf(needle, index + 1);
207
+ }
208
+ return -1;
209
+ }
210
+ function analyzeHtml(code, id) {
211
+ const markup = new Array(code.length).fill(" ");
212
+ const scriptRanges = [];
213
+ const exampleStack = [];
214
+ let index = 0;
215
+ while (index < code.length) {
216
+ const open = code.indexOf("<", index);
217
+ if (open < 0) break;
218
+ if (code.startsWith("<!--", open)) {
219
+ const commentEnd = code.indexOf("-->", open + 4);
220
+ index = commentEnd < 0 ? code.length : commentEnd + 3;
221
+ continue;
222
+ }
223
+ const tagEnd = findHtmlTagEnd(code, open);
224
+ if (tagEnd < 0) break;
225
+ const tag = code.slice(open, tagEnd);
226
+ const closingMatch = tag.match(/^<\s*\/\s*([A-Za-z][\w:-]*)/);
227
+ const openingMatch = closingMatch ? null : tag.match(/^<\s*([A-Za-z][\w:-]*)/);
228
+ const tagName = (closingMatch?.[1] ?? openingMatch?.[1] ?? "").toLowerCase();
229
+ if (!tagName) {
230
+ index = tagEnd;
231
+ continue;
232
+ }
233
+ const isClosing = closingMatch !== null;
234
+ const selfClosing = !isClosing && /\/\s*>$/.test(tag);
235
+ const isExampleTag = tagName === "code" || tagName === "pre";
236
+ if (exampleStack.length === 0 || isExampleTag) copyRange(markup, code, open, tagEnd);
237
+ if (isClosing && isExampleTag) {
238
+ if (exampleStack.at(-1) !== tagName) throw new Error(`[gustcss] mismatched </${tagName}> in ${id}`);
239
+ exampleStack.pop();
240
+ } else if (!isClosing && isExampleTag && !selfClosing) exampleStack.push(tagName);
241
+ if (!isClosing && !selfClosing && exampleStack.length === 0 && (tagName === "script" || tagName === "style")) {
242
+ const rawEnd = findRawTextClosing(code, tagName, tagEnd);
243
+ const contentEnd = rawEnd < 0 ? code.length : rawEnd;
244
+ if (tagName === "script") scriptRanges.push({
245
+ start: tagEnd,
246
+ end: contentEnd
247
+ });
248
+ index = contentEnd;
249
+ continue;
250
+ }
251
+ index = tagEnd;
252
+ }
253
+ if (exampleStack.length > 0) throw new Error(`[gustcss] unclosed <${exampleStack.at(-1)}> in ${id}`);
254
+ return {
255
+ markup: markup.join(""),
256
+ scriptRanges
257
+ };
258
+ }
259
+ function canStartRegex(code, index, previous) {
260
+ if (previous === void 0 || /[({[,:;=!?&|+\-*%^~<>]/.test(previous)) return true;
261
+ return /\b(?:return|throw|case|delete|void|typeof|instanceof|in|of|yield|await)\s*$/.test(code.slice(Math.max(0, index - 24), index));
262
+ }
263
+ function isJsxAttributeAt(structure, index) {
264
+ const before = structure.slice(0, index);
265
+ const open = before.lastIndexOf("<");
266
+ if (open < 0 || open < before.lastIndexOf(">")) return false;
267
+ return /^<\s*[A-Za-z][\w.$:-]*(?:\s|$)/.test(before.slice(open));
268
+ }
269
+ function createJsMasks(code) {
270
+ const structure = code.split("");
271
+ const inspection = code.split("");
272
+ let index = 0;
273
+ let previousSignificant;
274
+ while (index < code.length) {
275
+ const char = code[index];
276
+ const next = code[index + 1];
277
+ if (char === "/" && next === "/") {
278
+ const end = code.indexOf("\n", index + 2);
279
+ const stop = end < 0 ? code.length : end;
280
+ maskRange(structure, code, index, stop);
281
+ maskRange(inspection, code, index, stop);
282
+ index = stop;
283
+ continue;
284
+ }
285
+ if (char === "/" && next === "*") {
286
+ const end = code.indexOf("*/", index + 2);
287
+ const stop = end < 0 ? code.length : end + 2;
288
+ maskRange(structure, code, index, stop);
289
+ maskRange(inspection, code, index, stop);
290
+ index = stop;
291
+ continue;
292
+ }
293
+ if (char === "\"" || char === "'" || char === "`") {
294
+ const quote = char;
295
+ const start = index;
296
+ index += 1;
297
+ let escaped = false;
298
+ while (index < code.length) {
299
+ const current = code[index];
300
+ if (escaped) escaped = false;
301
+ else if (current === "\\") escaped = true;
302
+ else if (current === quote) {
303
+ index += 1;
304
+ break;
305
+ }
306
+ index += 1;
307
+ }
308
+ maskRange(structure, code, start, index);
309
+ previousSignificant = quote;
310
+ continue;
311
+ }
312
+ if (char === "/" && canStartRegex(code, index, previousSignificant)) {
313
+ const start = index;
314
+ index += 1;
315
+ let escaped = false;
316
+ let characterClass = false;
317
+ while (index < code.length) {
318
+ const current = code[index];
319
+ if (escaped) escaped = false;
320
+ else if (current === "\\") escaped = true;
321
+ else if (current === "[") characterClass = true;
322
+ else if (current === "]") characterClass = false;
323
+ else if (current === "/" && !characterClass) {
324
+ index += 1;
325
+ while (/[A-Za-z]/.test(code[index] ?? "")) index += 1;
326
+ break;
327
+ } else if (current === "\n" || current === "\r") break;
328
+ index += 1;
329
+ }
330
+ maskRange(structure, code, start, index);
331
+ maskRange(inspection, code, start, index);
332
+ previousSignificant = "/";
333
+ continue;
334
+ }
335
+ if (!/\s/.test(char)) previousSignificant = char;
336
+ index += 1;
337
+ }
338
+ return {
339
+ structure: structure.join(""),
340
+ inspection: inspection.join("")
341
+ };
342
+ }
343
+ const classNameTrieCache = /* @__PURE__ */ new WeakMap();
344
+ function classNameTrie(classes) {
345
+ let root = classNameTrieCache.get(classes);
346
+ if (root) return root;
347
+ root = /* @__PURE__ */ new Map();
348
+ for (const className of Object.keys(classes)) {
349
+ let node = root;
350
+ for (const char of className) {
351
+ if (!node.has(char)) node.set(char, /* @__PURE__ */ new Map());
352
+ node = node.get(char);
353
+ }
354
+ node.className = className;
355
+ }
356
+ classNameTrieCache.set(classes, root);
357
+ return root;
358
+ }
359
+ function isBeforeBoundary(char) {
360
+ return char === void 0 || !/[A-Za-z0-9_@:/-]/.test(char);
361
+ }
362
+ function isAfterBoundary(char, before, beforeBefore) {
363
+ if (char === void 0 || char === ":") return true;
364
+ if (char === ".") return before === "." && (beforeBefore === void 0 || !/[A-Za-z0-9_/\\.]/.test(beforeBefore));
365
+ return !/[A-Za-z0-9_@/-]/.test(char);
366
+ }
367
+ function findMappedToken(value, classes) {
368
+ const trie = classNameTrie(classes);
369
+ let found = null;
370
+ for (let start = 0; start < value.length; start += 1) {
371
+ const before = start === 0 ? void 0 : value[start - 1];
372
+ if (!isBeforeBoundary(before)) continue;
373
+ const beforeBefore = start < 2 ? void 0 : value[start - 2];
374
+ let node = trie;
375
+ for (let end = start; end < value.length; end += 1) {
376
+ node = node.get(value[end]);
377
+ if (!node) break;
378
+ if (!node.className) continue;
379
+ const afterIndex = end + 1;
380
+ if (!isAfterBoundary(afterIndex === value.length ? void 0 : value[afterIndex], before, beforeBefore)) continue;
381
+ if (found === null || node.className.length > found.length || node.className.length === found.length && node.className.localeCompare(found) < 0) found = node.className;
382
+ }
383
+ }
384
+ return found;
385
+ }
386
+ function scanQuotedSegments(value, visitor) {
387
+ let index = 0;
388
+ while (index < value.length) {
389
+ const quote = value[index];
390
+ if (quote !== "\"" && quote !== "'" && quote !== "`") {
391
+ index += 1;
392
+ continue;
393
+ }
394
+ const start = index;
395
+ index += 1;
396
+ let escaped = false;
397
+ while (index < value.length) {
398
+ const char = value[index];
399
+ if (escaped) escaped = false;
400
+ else if (char === "\\") escaped = true;
401
+ else if (char === quote) {
402
+ index += 1;
403
+ visitor({
404
+ start,
405
+ end: index,
406
+ quote,
407
+ contents: value.slice(start + 1, index - 1)
408
+ });
409
+ break;
410
+ }
411
+ index += 1;
412
+ }
413
+ }
414
+ }
415
+ function findMappedInQuotedSegments(value, classes) {
416
+ let found = null;
417
+ scanQuotedSegments(value, ({ contents }) => {
418
+ if (found === null) found = findMappedToken(contents, classes);
419
+ });
420
+ return found;
421
+ }
422
+ function stripQuotedSegments(value) {
423
+ let output = "";
424
+ let cursor = 0;
425
+ scanQuotedSegments(value, ({ start, end }) => {
426
+ output += value.slice(cursor, start);
427
+ output += " ".repeat(end - start);
428
+ cursor = end;
429
+ });
430
+ return output + value.slice(cursor);
431
+ }
432
+ function collectTemplateBodyEdits(body, classes, baseOffset, edits) {
433
+ let staticStart = 0;
434
+ let index = 0;
435
+ while (index < body.length) {
436
+ if (body[index] !== "$" || body[index + 1] !== "{") {
437
+ index += 1;
438
+ continue;
439
+ }
440
+ collectClassListEdits(body.slice(staticStart, index), classes, baseOffset + staticStart, edits);
441
+ index += 2;
442
+ let depth = 1;
443
+ let quote = null;
444
+ let escaped = false;
445
+ while (index < body.length && depth > 0) {
446
+ const char = body[index];
447
+ if (quote !== null) {
448
+ if (escaped) escaped = false;
449
+ else if (char === "\\") escaped = true;
450
+ else if (char === quote) quote = null;
451
+ } else if (char === "\"" || char === "'" || char === "`") quote = char;
452
+ else if (char === "{") depth += 1;
453
+ else if (char === "}") depth -= 1;
454
+ index += 1;
455
+ }
456
+ staticStart = index;
457
+ }
458
+ collectClassListEdits(body.slice(staticStart), classes, baseOffset + staticStart, edits);
459
+ }
460
+ function findBalancedEnd(code, open, opening, closing) {
461
+ let index = open + 1;
462
+ let depth = 1;
463
+ let quote = null;
464
+ let escaped = false;
465
+ while (index < code.length && depth > 0) {
466
+ const char = code[index];
467
+ if (quote !== null) {
468
+ if (escaped) escaped = false;
469
+ else if (char === "\\") escaped = true;
470
+ else if (char === quote) quote = null;
471
+ } else if (char === "\"" || char === "'" || char === "`") quote = char;
472
+ else if (char === opening) depth += 1;
473
+ else if (char === closing) depth -= 1;
474
+ index += 1;
475
+ }
476
+ return depth === 0 ? index : -1;
477
+ }
478
+ function rejectAmbiguousClassExpressions(code, structure, classes, id) {
479
+ const pattern = /(?<![:\w-])class(?:Name)?\s*=\s*\{/g;
480
+ let match;
481
+ while ((match = pattern.exec(structure)) !== null) {
482
+ const open = code.indexOf("{", match.index);
483
+ const end = findBalancedEnd(code, open, "{", "}");
484
+ if (end < 0) break;
485
+ const ambiguous = findMappedToken(code.slice(open + 1, end - 1), classes);
486
+ if (ambiguous) throw new Error(`[gustcss] mapped class "${ambiguous}" remains in an ambiguous class expression in ${id}. Use a static class attribute/template segment, or add it to mangleExclude.`);
487
+ pattern.lastIndex = end;
488
+ }
489
+ }
490
+ function collectClassListCallEdits(code, structure, classes, id, edits) {
491
+ const pattern = /\.classList\.(?:add|remove|toggle|contains|replace)\s*\(/g;
492
+ let match;
493
+ while ((match = pattern.exec(structure)) !== null) {
494
+ const open = code.indexOf("(", match.index);
495
+ const end = findBalancedEnd(code, open, "(", ")");
496
+ if (end < 0) break;
497
+ const args = code.slice(open + 1, end - 1);
498
+ const withoutStrings = stripQuotedSegments(args);
499
+ const dynamic = findMappedToken(withoutStrings, classes);
500
+ if (dynamic) throw new Error(`[gustcss] mapped class "${dynamic}" remains in a dynamic classList argument in ${id}. Pass it as a direct string argument, or add it to mangleExclude.`);
501
+ if (withoutStrings.includes("(")) {
502
+ const ambiguous = findMappedInQuotedSegments(args, classes);
503
+ if (ambiguous) throw new Error(`[gustcss] mapped class "${ambiguous}" is nested in a classList call in ${id}. Pass it as a direct string argument, or add it to mangleExclude.`);
504
+ }
505
+ scanQuotedSegments(args, ({ start, contents }) => {
506
+ collectClassListEdits(contents, classes, open + 1 + start + 1, edits);
507
+ });
508
+ pattern.lastIndex = end;
509
+ }
510
+ }
511
+ function rejectDynamicSetAttributeCalls(code, structure, classes, id) {
512
+ const pattern = /\.setAttribute\s*\(/g;
513
+ let match;
514
+ while ((match = pattern.exec(structure)) !== null) {
515
+ const open = code.indexOf("(", match.index);
516
+ const end = findBalancedEnd(code, open, "(", ")");
517
+ if (end < 0) break;
518
+ const args = code.slice(open + 1, end - 1);
519
+ if (!/^\s*(["'])class\1\s*,/.test(args)) {
520
+ pattern.lastIndex = end;
521
+ continue;
522
+ }
523
+ const withoutStrings = stripQuotedSegments(args);
524
+ const dynamic = findMappedToken(withoutStrings, classes);
525
+ const nested = withoutStrings.includes("(") ? findMappedInQuotedSegments(args, classes) : null;
526
+ const ambiguous = dynamic ?? nested;
527
+ if (ambiguous) throw new Error(`[gustcss] mapped class "${ambiguous}" remains in a dynamic setAttribute in ${id}. Pass a direct class string, or add it to mangleExclude.`);
528
+ pattern.lastIndex = end;
529
+ }
530
+ }
531
+ function rejectDynamicClassNameAssignments(code, structure, classes, id) {
532
+ const pattern = /\.className\s*=(?!=)\s*/g;
533
+ while (pattern.exec(structure) !== null) {
534
+ const start = pattern.lastIndex;
535
+ const candidates = [code.indexOf(";", start), code.indexOf("\n", start)].filter((index) => index >= 0);
536
+ const end = candidates.length > 0 ? Math.min(...candidates) : code.length;
537
+ const ambiguous = findMappedToken(code.slice(start, end), classes);
538
+ if (ambiguous) throw new Error(`[gustcss] mapped class "${ambiguous}" remains in a dynamic className assignment in ${id}. Assign a direct class string, or add it to mangleExclude.`);
539
+ pattern.lastIndex = end;
540
+ }
541
+ }
542
+ function applyEdits(code, edits) {
543
+ const sorted = [...edits].sort((a, b) => a.start - b.start || a.end - b.end);
544
+ let output = "";
545
+ let cursor = 0;
546
+ const origins = [];
547
+ for (const edit of sorted) {
548
+ if (edit.start < cursor) throw new Error("[gustcss] overlapping class-name rewrites");
549
+ output += code.slice(cursor, edit.start);
550
+ for (let index = cursor; index < edit.start; index += 1) origins.push(index);
551
+ const sourceLength = Math.max(1, edit.end - edit.start);
552
+ if (edit.replacement.length > sourceLength) throw new Error("[gustcss] class-name replacement must not exceed its source length");
553
+ output += edit.replacement;
554
+ for (let index = 0; index < edit.replacement.length; index += 1) origins.push(edit.start + Math.min(index, sourceLength - 1));
555
+ cursor = edit.end;
556
+ }
557
+ output += code.slice(cursor);
558
+ for (let index = cursor; index < code.length; index += 1) origins.push(index);
559
+ return {
560
+ code: output,
561
+ origins
562
+ };
563
+ }
564
+ function sourcesForFile(code, id) {
565
+ if (!HTML_SOURCE.test(id)) {
566
+ const js = createJsMasks(code);
567
+ return {
568
+ attributes: code,
569
+ structure: js.structure,
570
+ inspection: js.inspection
571
+ };
572
+ }
573
+ const html = analyzeHtml(code, id);
574
+ const structure = new Array(code.length).fill(" ");
575
+ const inspection = new Array(code.length).fill(" ");
576
+ for (const range of html.scriptRanges) {
577
+ const script = code.slice(range.start, range.end);
578
+ const js = createJsMasks(script);
579
+ for (let offset = 0; offset < script.length; offset += 1) {
580
+ structure[range.start + offset] = js.structure[offset];
581
+ inspection[range.start + offset] = js.inspection[offset];
582
+ }
583
+ }
584
+ return {
585
+ attributes: html.markup,
586
+ structure: structure.join(""),
587
+ inspection: inspection.join("")
588
+ };
589
+ }
590
+ function collectRecognizedContextEdits(code, classes, id) {
591
+ const edits = [];
592
+ const sources = sourcesForFile(code, id);
593
+ const attributePattern = /((?<![:\w-])class(?:Name)?\s*=\s*)(?:(["'])([\s\S]*?)(\2)|(\{\s*)(["'])([\s\S]*?)(\6)(\s*\}))/g;
594
+ let match;
595
+ while ((match = attributePattern.exec(sources.attributes)) !== null) {
596
+ if (!HTML_SOURCE.test(id) && !isJsxAttributeAt(sources.structure, match.index)) continue;
597
+ if (match[2] !== void 0) collectClassListEdits(match[3], classes, match.index + match[1].length + 1, edits);
598
+ else collectClassListEdits(match[7], classes, match.index + match[1].length + match[5].length + 1, edits);
599
+ }
600
+ const templatePattern = /((?<![:\w-])class(?:Name)?\s*=\s*\{\s*`)([\s\S]*?)(`\s*\})/g;
601
+ while ((match = templatePattern.exec(sources.attributes)) !== null) {
602
+ if (!HTML_SOURCE.test(id) && !isJsxAttributeAt(sources.structure, match.index)) continue;
603
+ collectTemplateBodyEdits(match[2], classes, match.index + match[1].length, edits);
604
+ }
605
+ collectClassListCallEdits(code, sources.structure, classes, id, edits);
606
+ const setAttributePattern = /(\.setAttribute\(\s*["']class["']\s*,\s*)(["'])([\s\S]*?)(\2)(\s*\))/g;
607
+ while ((match = setAttributePattern.exec(code)) !== null) {
608
+ if (sources.structure[match.index] === " ") continue;
609
+ collectClassListEdits(match[3], classes, match.index + match[1].length + 1, edits);
610
+ }
611
+ return applyEdits(code, edits);
612
+ }
613
+ const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
614
+ function encodeVLQ(value) {
615
+ let encoded = "";
616
+ let vlq = Math.abs(value) * 2 + (value < 0 ? 1 : 0);
617
+ do {
618
+ let digit = vlq % 32;
619
+ vlq = Math.floor(vlq / 32);
620
+ if (vlq > 0) digit += 32;
621
+ encoded += BASE64[digit];
622
+ } while (vlq > 0);
623
+ return encoded;
624
+ }
625
+ function createSourceMap(original, generated, origins, id) {
626
+ const originalPositions = new Array(original.length);
627
+ let originalLine = 0;
628
+ let originalColumn = 0;
629
+ for (let index = 0; index < original.length; index += 1) {
630
+ originalPositions[index] = {
631
+ line: originalLine,
632
+ column: originalColumn
633
+ };
634
+ if (original[index] === "\n") {
635
+ originalLine += 1;
636
+ originalColumn = 0;
637
+ } else originalColumn += 1;
638
+ }
639
+ const lines = [""];
640
+ let generatedLine = 0;
641
+ let generatedColumn = 0;
642
+ let previousGeneratedColumn = 0;
643
+ let previousSource = 0;
644
+ let previousOriginalLine = 0;
645
+ let previousOriginalColumn = 0;
646
+ for (let index = 0; index < generated.length; index += 1) {
647
+ if (generated[index] === "\n") {
648
+ generatedLine += 1;
649
+ generatedColumn = 0;
650
+ previousGeneratedColumn = 0;
651
+ lines.push("");
652
+ continue;
653
+ }
654
+ const originalPosition = originalPositions[origins[index]];
655
+ if (!originalPosition) {
656
+ generatedColumn += 1;
657
+ continue;
658
+ }
659
+ const segment = encodeVLQ(generatedColumn - previousGeneratedColumn) + encodeVLQ(0 - previousSource) + encodeVLQ(originalPosition.line - previousOriginalLine) + encodeVLQ(originalPosition.column - previousOriginalColumn);
660
+ lines[generatedLine] += `${lines[generatedLine] ? "," : ""}${segment}`;
661
+ previousGeneratedColumn = generatedColumn;
662
+ previousSource = 0;
663
+ previousOriginalLine = originalPosition.line;
664
+ previousOriginalColumn = originalPosition.column;
665
+ generatedColumn += 1;
666
+ }
667
+ return {
668
+ version: 3,
669
+ sources: [id],
670
+ sourcesContent: [original],
671
+ names: [],
672
+ mappings: lines.join(";")
673
+ };
674
+ }
675
+ function transformSource(code, classes, id) {
676
+ const transformed = collectRecognizedContextEdits(code, classes, id);
677
+ const rewritten = transformed.code;
678
+ const sources = sourcesForFile(rewritten, id);
679
+ rejectDynamicSetAttributeCalls(rewritten, sources.structure, classes, id);
680
+ rejectDynamicClassNameAssignments(rewritten, sources.structure, classes, id);
681
+ rejectAmbiguousClassExpressions(rewritten, sources.structure, classes, id);
682
+ return {
683
+ code: rewritten,
684
+ map: createSourceMap(code, rewritten, transformed.origins, id),
685
+ inspection: sources.inspection
686
+ };
687
+ }
688
+ function findAmbiguousMappedClass(code, classes) {
689
+ return findMappedInQuotedSegments(code, classes);
690
+ }
691
+ /**
692
+ * Create a fail-closed source transformer for a GustCSS class manifest.
693
+ * Only unambiguous class-name contexts are rewritten automatically.
694
+ */
695
+ function createClassNameTransformer(classes) {
696
+ const transformWithSourceMap = (code, id) => {
697
+ if (UNSUPPORTED_FRAMEWORK_SOURCE.test(id)) {
698
+ const referenced = findMappedToken(code, classes);
699
+ if (referenced) throw new Error(`[gustcss] class-name mangling does not support Vue, Astro, or Svelte templates yet (${id}). Add "${referenced}" to mangleExclude or disable mangleClassNames.`);
700
+ return {
701
+ code,
702
+ map: null
703
+ };
704
+ }
705
+ if (DEPENDENCY_SOURCE.test(id) || !SUPPORTED_SOURCE.test(id)) return {
706
+ code,
707
+ map: null
708
+ };
709
+ const result = transformSource(code, classes, id);
710
+ const ambiguous = findAmbiguousMappedClass(result.inspection, classes, id);
711
+ if (ambiguous) throw new Error(`[gustcss] mapped class "${ambiguous}" remains in an ambiguous string in ${id}. Move it to a static class/className or classList context, or add it to mangleExclude.`);
712
+ return {
713
+ code: result.code,
714
+ map: result.map
715
+ };
716
+ };
717
+ const transform = (code, id) => transformWithSourceMap(code, id).code;
718
+ transform.withSourceMap = transformWithSourceMap;
719
+ return transform;
720
+ }
721
+ //#endregion
722
+ //#region src/index.js
140
723
  /**
141
724
  * @gustcss/vite - Vite plugin for CSS Utility Generator
142
725
  *
@@ -155,6 +738,12 @@ function cssUtility(opts = {}) {
155
738
  const content = opts.content || ["./src/**/*.{js,ts,jsx,tsx,astro,vue}"];
156
739
  const configPath = opts.config;
157
740
  const outputToCssLayers = opts.outputToCssLayers;
741
+ const mangleClassNames = opts.mangleClassNames === true;
742
+ const mangleMap = opts.mangleMap || `${output}.classes.json`;
743
+ const mangleExclude = opts.mangleExclude || [];
744
+ let classNameTransformer = null;
745
+ let rollbackContext = null;
746
+ let manifestSnapshot = null;
158
747
  let watchProcess = null;
159
748
  function findBinary(cwd) {
160
749
  return import_runner.default.resolveBinary({
@@ -169,14 +758,21 @@ function cssUtility(opts = {}) {
169
758
  fs
170
759
  });
171
760
  }
172
- function buildCSS(cwd, binaryPath) {
761
+ function buildCSS(cwd, binaryPath, enableMangle) {
173
762
  const resolvedConfigPath = findConfigFile(cwd);
174
763
  const run = (cfgPath) => {
764
+ const disableConfigMangle = !enableMangle && import_runner.default.configEnablesMangling({
765
+ configPath: cfgPath,
766
+ fs
767
+ });
175
768
  const args = import_runner.default.buildArgs({
176
769
  mode: "output",
177
770
  output,
178
771
  cssLayers: outputToCssLayers,
179
- configPath: cfgPath
772
+ configPath: cfgPath,
773
+ mangleClassNames: enableMangle ? true : disableConfigMangle ? false : void 0,
774
+ mangleMap: enableMangle ? mangleMap : void 0,
775
+ mangleExclude: enableMangle ? mangleExclude : void 0
180
776
  });
181
777
  try {
182
778
  const result = spawnSync(binaryPath, args, {
@@ -190,6 +786,24 @@ function cssUtility(opts = {}) {
190
786
  });
191
787
  if (result.status !== 0) throw new Error(`gustcss build failed: ${result.stderr}`);
192
788
  if (result.stderr) process.stderr.write(result.stderr);
789
+ if (enableMangle) {
790
+ const manifestPath = path.resolve(cwd, mangleMap);
791
+ const manifestText = fs.readFileSync(manifestPath, "utf-8");
792
+ const manifest = JSON.parse(manifestText);
793
+ const classes = manifest?.classes;
794
+ const entries = classes && !Array.isArray(classes) ? Object.entries(classes) : [];
795
+ const shortNames = entries.map(([, short]) => short);
796
+ const validEntries = entries.every(([original, short]) => original.length > 0 && typeof short === "string" && /^[A-Za-z]+$/.test(short));
797
+ if (manifest?.version !== 1 || !classes || Array.isArray(classes) || !validEntries || new Set(shortNames).size !== shortNames.length) throw new Error(`invalid class-name manifest: ${manifestPath}`);
798
+ classNameTransformer = createClassNameTransformer(manifest.classes);
799
+ manifestSnapshot = {
800
+ path: manifestPath,
801
+ text: manifestText
802
+ };
803
+ } else {
804
+ classNameTransformer = null;
805
+ manifestSnapshot = null;
806
+ }
193
807
  } catch (error) {
194
808
  throw new Error(`gustcss build failed: ${error.message}`);
195
809
  }
@@ -204,23 +818,38 @@ function cssUtility(opts = {}) {
204
818
  }
205
819
  return {
206
820
  name: "gustcss",
821
+ ...mangleClassNames ? { enforce: "pre" } : {},
207
822
  configResolved(config) {
208
823
  const cwd = config.root || process.cwd();
209
824
  const binaryPath = findBinary(cwd);
210
825
  try {
211
- buildCSS(cwd, binaryPath);
826
+ const enableMangle = mangleClassNames && config.command === "build";
827
+ buildCSS(cwd, binaryPath, enableMangle);
828
+ rollbackContext = enableMangle ? {
829
+ cwd,
830
+ binaryPath
831
+ } : null;
212
832
  } catch (error) {
213
833
  console.error(`[gustcss] Failed to build CSS: ${error.message}`);
214
834
  throw error;
215
835
  }
216
836
  },
837
+ transform(code, id) {
838
+ if (!classNameTransformer) return null;
839
+ const transformed = classNameTransformer.withSourceMap(code, id);
840
+ return transformed.code === code ? null : transformed;
841
+ },
842
+ transformIndexHtml(html, context) {
843
+ if (!classNameTransformer) return html;
844
+ return classNameTransformer(html, context?.filename || "index.html");
845
+ },
217
846
  configureServer(server) {
218
847
  const cwd = server.config.root || process.cwd();
219
848
  const binaryPath = findBinary(cwd);
220
849
  const resolvedConfigPath = findConfigFile(cwd);
221
850
  const outputPath = path.resolve(cwd, output);
222
851
  try {
223
- buildCSS(cwd, binaryPath);
852
+ buildCSS(cwd, binaryPath, false);
224
853
  } catch (error) {
225
854
  console.error(`[gustcss] Failed to build CSS: ${error.message}`);
226
855
  }
@@ -232,13 +861,18 @@ function cssUtility(opts = {}) {
232
861
  fs.writeFileSync(tempConfigPath, JSON.stringify({ content }), { flag: "wx" });
233
862
  watchConfigPath = tempConfigPath;
234
863
  }
235
- watchProcess = spawn(binaryPath, import_runner.default.buildArgs({
864
+ const watchArgs = import_runner.default.buildArgs({
236
865
  mode: "output",
237
866
  output,
238
867
  cssLayers: outputToCssLayers,
239
868
  configPath: watchConfigPath,
240
- watch: true
241
- }), {
869
+ watch: true,
870
+ mangleClassNames: import_runner.default.configEnablesMangling({
871
+ configPath: watchConfigPath,
872
+ fs
873
+ }) ? false : void 0
874
+ });
875
+ watchProcess = spawn(binaryPath, watchArgs, {
242
876
  cwd,
243
877
  stdio: [
244
878
  "pipe",
@@ -272,11 +906,29 @@ function cssUtility(opts = {}) {
272
906
  process.once("SIGINT", cleanup);
273
907
  process.once("SIGTERM", cleanup);
274
908
  },
275
- buildEnd() {
909
+ writeBundle() {
910
+ if (!manifestSnapshot || fs.existsSync(manifestSnapshot.path)) return;
911
+ fs.mkdirSync(path.dirname(manifestSnapshot.path), { recursive: true });
912
+ fs.writeFileSync(manifestSnapshot.path, manifestSnapshot.text);
913
+ },
914
+ buildEnd(error) {
276
915
  if (watchProcess) {
277
916
  watchProcess.kill();
278
917
  watchProcess = null;
279
918
  }
919
+ if (error && rollbackContext) {
920
+ const { cwd, binaryPath } = rollbackContext;
921
+ rollbackContext = null;
922
+ classNameTransformer = null;
923
+ manifestSnapshot = null;
924
+ try {
925
+ buildCSS(cwd, binaryPath, false);
926
+ const manifestPath = path.resolve(cwd, mangleMap);
927
+ if (fs.existsSync(manifestPath)) fs.unlinkSync(manifestPath);
928
+ } catch (rollbackError) {
929
+ console.error(`[gustcss] Failed to restore readable CSS: ${rollbackError.message}`);
930
+ }
931
+ } else rollbackContext = null;
280
932
  }
281
933
  };
282
934
  }