@gustcss/vite 0.9.1 → 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/README.md +41 -0
- package/dist/index.cjs +666 -14
- package/dist/index.d.ts +21 -0
- package/dist/index.mjs +663 -11
- package/package.json +2 -2
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|