@gustcss/vite 0.9.2 → 0.10.1
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 +56 -0
- package/dist/index.cjs +1153 -14
- package/dist/index.d.ts +21 -0
- package/dist/index.mjs +1149 -11
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -20,7 +20,7 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
20
20
|
}
|
|
21
21
|
return to;
|
|
22
22
|
};
|
|
23
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
24
24
|
value: mod,
|
|
25
25
|
enumerable: true
|
|
26
26
|
}) : target, mod));
|
|
@@ -31,8 +31,10 @@ path = __toESM(path, 1);
|
|
|
31
31
|
let fs = require("fs");
|
|
32
32
|
fs = __toESM(fs, 1);
|
|
33
33
|
let crypto = require("crypto");
|
|
34
|
+
let node_module = require("node:module");
|
|
35
|
+
let node_url = require("node:url");
|
|
34
36
|
//#endregion
|
|
35
|
-
//#region src/
|
|
37
|
+
//#region src/mangle.js
|
|
36
38
|
var import_runner = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
37
39
|
/**
|
|
38
40
|
* gustcss shared runner — helpers for the @gustcss/postcss and @gustcss/vite
|
|
@@ -92,6 +94,24 @@ var import_runner = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
|
|
|
92
94
|
return null;
|
|
93
95
|
}
|
|
94
96
|
/**
|
|
97
|
+
* Report whether a resolved gustcss.config.json turns on class-name mangling.
|
|
98
|
+
*
|
|
99
|
+
* Callers that cannot rewrite source (dev server, PostCSS) use this to decide
|
|
100
|
+
* whether they must pass `--mangle-class-names=false`. Only passing the flag
|
|
101
|
+
* when needed keeps older CLI binaries that predate the flag working.
|
|
102
|
+
*
|
|
103
|
+
* @param {{ configPath: string|null, fs?: { readFileSync: Function } }} options
|
|
104
|
+
* @returns {boolean}
|
|
105
|
+
*/
|
|
106
|
+
function configEnablesMangling({ configPath, fs: fs$4 = require("fs") }) {
|
|
107
|
+
if (!configPath) return false;
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(fs$4.readFileSync(configPath, "utf-8"))?.mangleClassNames === true;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
95
115
|
* Write a temporary config file ({ content }) at <cwd>/<prefix>.<random8hex>.tmp.json,
|
|
96
116
|
* invoke `fn` with the temp file path, and always clean the file up afterwards.
|
|
97
117
|
*
|
|
@@ -104,14 +124,14 @@ var import_runner = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
|
|
|
104
124
|
* @param {(tempConfigPath: string) => any} fn
|
|
105
125
|
* @returns {any} whatever `fn` returns
|
|
106
126
|
*/
|
|
107
|
-
function withTempConfig({ cwd, content, prefix, fs: fs$
|
|
127
|
+
function withTempConfig({ cwd, content, prefix, fs: fs$5 = require("fs") }, fn) {
|
|
108
128
|
const tmpName = `${prefix}.${crypto$1.randomBytes(8).toString("hex")}.tmp.json`;
|
|
109
129
|
const tempConfigPath = path$2.join(cwd, tmpName);
|
|
110
|
-
fs$
|
|
130
|
+
fs$5.writeFileSync(tempConfigPath, JSON.stringify({ content }), { flag: "wx" });
|
|
111
131
|
try {
|
|
112
132
|
return fn(tempConfigPath);
|
|
113
133
|
} finally {
|
|
114
|
-
if (fs$
|
|
134
|
+
if (fs$5.existsSync(tempConfigPath)) fs$5.unlinkSync(tempConfigPath);
|
|
115
135
|
}
|
|
116
136
|
}
|
|
117
137
|
/**
|
|
@@ -123,25 +143,1023 @@ var import_runner = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
|
|
|
123
143
|
* @param {string} [options.output] output path (required for 'output' mode)
|
|
124
144
|
* @param {boolean} [options.cssLayers] append --css-layers when truthy
|
|
125
145
|
* @param {boolean} [options.watch] append --watch when truthy
|
|
146
|
+
* @param {boolean} [options.mangleClassNames] explicitly enable or disable mangling
|
|
147
|
+
* @param {string} [options.mangleMap] class-name manifest output path
|
|
148
|
+
* @param {string[]} [options.mangleExclude] class names to preserve
|
|
126
149
|
* @param {string|null} [options.configPath] append --config <path> when present
|
|
127
150
|
* @returns {string[]}
|
|
128
151
|
*/
|
|
129
|
-
function buildArgs({ mode, output, cssLayers, configPath, watch }) {
|
|
152
|
+
function buildArgs({ mode, output, cssLayers, configPath, watch, mangleClassNames, mangleMap, mangleExclude }) {
|
|
130
153
|
const args = ["build"];
|
|
131
154
|
if (watch) args.push("--watch");
|
|
132
155
|
if (mode === "stdout") args.push("--stdout");
|
|
133
156
|
else args.push("-o", output);
|
|
134
157
|
if (cssLayers) args.push("--css-layers");
|
|
158
|
+
if (mangleClassNames !== void 0) args.push(`--mangle-class-names=${mangleClassNames}`);
|
|
159
|
+
if (mangleMap) args.push("--mangle-map", mangleMap);
|
|
160
|
+
if (mangleExclude?.length) args.push("--mangle-exclude", mangleExclude.join(","));
|
|
135
161
|
if (configPath) args.push("--config", configPath);
|
|
136
162
|
return args;
|
|
137
163
|
}
|
|
138
164
|
module.exports = {
|
|
139
165
|
resolveBinary,
|
|
140
166
|
resolveConfig,
|
|
167
|
+
configEnablesMangling,
|
|
141
168
|
withTempConfig,
|
|
142
169
|
buildArgs
|
|
143
170
|
};
|
|
144
171
|
})))(), 1);
|
|
172
|
+
const SUPPORTED_SOURCE = /\.(?:[cm]?[jt]sx?|html)(?:$|\?)/;
|
|
173
|
+
const HTML_SOURCE = /\.html(?:$|\?)/;
|
|
174
|
+
const UNSUPPORTED_FRAMEWORK_SOURCE = /\.(?:vue|svelte)(?:$|\?)/;
|
|
175
|
+
const DEPENDENCY_SOURCE = /(?:^|[\\/])node_modules[\\/]/;
|
|
176
|
+
function hasOwn(classes, token) {
|
|
177
|
+
return Object.hasOwn(classes, token);
|
|
178
|
+
}
|
|
179
|
+
function collectClassListEdits(value, classes, baseOffset, edits) {
|
|
180
|
+
for (const match of value.matchAll(/\S+/g)) {
|
|
181
|
+
const replacement = classes[match[0]];
|
|
182
|
+
if (hasOwn(classes, match[0]) && replacement !== match[0]) edits.push({
|
|
183
|
+
start: baseOffset + match.index,
|
|
184
|
+
end: baseOffset + match.index + match[0].length,
|
|
185
|
+
replacement
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function maskRange(chars, code, start, end) {
|
|
190
|
+
for (let index = start; index < end; index += 1) if (code[index] !== "\n" && code[index] !== "\r") chars[index] = " ";
|
|
191
|
+
}
|
|
192
|
+
function copyRange(chars, code, start, end) {
|
|
193
|
+
for (let index = start; index < end; index += 1) chars[index] = code[index];
|
|
194
|
+
}
|
|
195
|
+
function findHtmlTagEnd(code, start) {
|
|
196
|
+
let quote = null;
|
|
197
|
+
for (let index = start + 1; index < code.length; index += 1) {
|
|
198
|
+
const char = code[index];
|
|
199
|
+
if (quote !== null) {
|
|
200
|
+
if (char === quote) quote = null;
|
|
201
|
+
} else if (char === "\"" || char === "'") quote = char;
|
|
202
|
+
else if (char === ">") return index + 1;
|
|
203
|
+
}
|
|
204
|
+
return -1;
|
|
205
|
+
}
|
|
206
|
+
function findRawTextClosing(code, tagName, start) {
|
|
207
|
+
const lower = code.toLowerCase();
|
|
208
|
+
const needle = `</${tagName}`;
|
|
209
|
+
let index = lower.indexOf(needle, start);
|
|
210
|
+
while (index >= 0) {
|
|
211
|
+
const after = lower[index + needle.length];
|
|
212
|
+
if (after === void 0 || /[\s>]/.test(after)) return index;
|
|
213
|
+
index = lower.indexOf(needle, index + 1);
|
|
214
|
+
}
|
|
215
|
+
return -1;
|
|
216
|
+
}
|
|
217
|
+
function analyzeHtml(code, id) {
|
|
218
|
+
const markup = new Array(code.length).fill(" ");
|
|
219
|
+
const scriptRanges = [];
|
|
220
|
+
const commentRanges = [];
|
|
221
|
+
const exampleStack = [];
|
|
222
|
+
let index = 0;
|
|
223
|
+
while (index < code.length) {
|
|
224
|
+
const open = code.indexOf("<", index);
|
|
225
|
+
if (open < 0) break;
|
|
226
|
+
if (code.startsWith("<!--", open)) {
|
|
227
|
+
const commentEnd = code.indexOf("-->", open + 4);
|
|
228
|
+
const end = commentEnd < 0 ? code.length : commentEnd + 3;
|
|
229
|
+
commentRanges.push({
|
|
230
|
+
start: open,
|
|
231
|
+
end
|
|
232
|
+
});
|
|
233
|
+
index = end;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const tagEnd = findHtmlTagEnd(code, open);
|
|
237
|
+
if (tagEnd < 0) break;
|
|
238
|
+
const tag = code.slice(open, tagEnd);
|
|
239
|
+
const closingMatch = tag.match(/^<\s*\/\s*([A-Za-z][\w:-]*)/);
|
|
240
|
+
const openingMatch = closingMatch ? null : tag.match(/^<\s*([A-Za-z][\w:-]*)/);
|
|
241
|
+
const tagName = (closingMatch?.[1] ?? openingMatch?.[1] ?? "").toLowerCase();
|
|
242
|
+
if (!tagName) {
|
|
243
|
+
index = tagEnd;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const isClosing = closingMatch !== null;
|
|
247
|
+
const selfClosing = !isClosing && /\/\s*>$/.test(tag);
|
|
248
|
+
const isExampleTag = tagName === "code" || tagName === "pre";
|
|
249
|
+
if (exampleStack.length === 0 || isExampleTag) copyRange(markup, code, open, tagEnd);
|
|
250
|
+
if (isClosing && isExampleTag) {
|
|
251
|
+
if (exampleStack.at(-1) !== tagName) throw new Error(`[gustcss] mismatched </${tagName}> in ${id}`);
|
|
252
|
+
exampleStack.pop();
|
|
253
|
+
} else if (!isClosing && isExampleTag && !selfClosing) exampleStack.push(tagName);
|
|
254
|
+
if (!isClosing && !selfClosing && exampleStack.length === 0 && (tagName === "script" || tagName === "style")) {
|
|
255
|
+
const rawEnd = findRawTextClosing(code, tagName, tagEnd);
|
|
256
|
+
const contentEnd = rawEnd < 0 ? code.length : rawEnd;
|
|
257
|
+
if (tagName === "script") scriptRanges.push({
|
|
258
|
+
start: tagEnd,
|
|
259
|
+
end: contentEnd
|
|
260
|
+
});
|
|
261
|
+
index = contentEnd;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
index = tagEnd;
|
|
265
|
+
}
|
|
266
|
+
if (exampleStack.length > 0) throw new Error(`[gustcss] unclosed <${exampleStack.at(-1)}> in ${id}`);
|
|
267
|
+
return {
|
|
268
|
+
markup: markup.join(""),
|
|
269
|
+
scriptRanges,
|
|
270
|
+
commentRanges
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function canStartRegex(code, index, previous) {
|
|
274
|
+
if (previous === void 0 || /[({[,:;=!?&|+\-*%^~<>]/.test(previous)) return true;
|
|
275
|
+
return /\b(?:return|throw|case|delete|void|typeof|instanceof|in|of|yield|await)\s*$/.test(code.slice(Math.max(0, index - 24), index));
|
|
276
|
+
}
|
|
277
|
+
function isJsxAttributeAt(structure, index) {
|
|
278
|
+
const before = structure.slice(0, index);
|
|
279
|
+
const open = before.lastIndexOf("<");
|
|
280
|
+
if (open < 0 || open < before.lastIndexOf(">")) return false;
|
|
281
|
+
return /^<\s*[A-Za-z][\w.$:-]*(?:\s|$)/.test(before.slice(open));
|
|
282
|
+
}
|
|
283
|
+
function createJsMasks(code) {
|
|
284
|
+
const structure = code.split("");
|
|
285
|
+
const inspection = code.split("");
|
|
286
|
+
let index = 0;
|
|
287
|
+
let previousSignificant;
|
|
288
|
+
while (index < code.length) {
|
|
289
|
+
const char = code[index];
|
|
290
|
+
const next = code[index + 1];
|
|
291
|
+
if (char === "/" && next === "/") {
|
|
292
|
+
const end = code.indexOf("\n", index + 2);
|
|
293
|
+
const stop = end < 0 ? code.length : end;
|
|
294
|
+
maskRange(structure, code, index, stop);
|
|
295
|
+
maskRange(inspection, code, index, stop);
|
|
296
|
+
index = stop;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (char === "/" && next === "*") {
|
|
300
|
+
const end = code.indexOf("*/", index + 2);
|
|
301
|
+
const stop = end < 0 ? code.length : end + 2;
|
|
302
|
+
maskRange(structure, code, index, stop);
|
|
303
|
+
maskRange(inspection, code, index, stop);
|
|
304
|
+
index = stop;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (char === "\"" || char === "'" || char === "`") {
|
|
308
|
+
const quote = char;
|
|
309
|
+
const start = index;
|
|
310
|
+
index += 1;
|
|
311
|
+
let escaped = false;
|
|
312
|
+
while (index < code.length) {
|
|
313
|
+
const current = code[index];
|
|
314
|
+
if (escaped) escaped = false;
|
|
315
|
+
else if (current === "\\") escaped = true;
|
|
316
|
+
else if (current === quote) {
|
|
317
|
+
index += 1;
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
index += 1;
|
|
321
|
+
}
|
|
322
|
+
maskRange(structure, code, start, index);
|
|
323
|
+
previousSignificant = quote;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (char === "/" && canStartRegex(code, index, previousSignificant)) {
|
|
327
|
+
const start = index;
|
|
328
|
+
index += 1;
|
|
329
|
+
let escaped = false;
|
|
330
|
+
let characterClass = false;
|
|
331
|
+
while (index < code.length) {
|
|
332
|
+
const current = code[index];
|
|
333
|
+
if (escaped) escaped = false;
|
|
334
|
+
else if (current === "\\") escaped = true;
|
|
335
|
+
else if (current === "[") characterClass = true;
|
|
336
|
+
else if (current === "]") characterClass = false;
|
|
337
|
+
else if (current === "/" && !characterClass) {
|
|
338
|
+
index += 1;
|
|
339
|
+
while (/[A-Za-z]/.test(code[index] ?? "")) index += 1;
|
|
340
|
+
break;
|
|
341
|
+
} else if (current === "\n" || current === "\r") break;
|
|
342
|
+
index += 1;
|
|
343
|
+
}
|
|
344
|
+
maskRange(structure, code, start, index);
|
|
345
|
+
maskRange(inspection, code, start, index);
|
|
346
|
+
previousSignificant = "/";
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (!/\s/.test(char)) previousSignificant = char;
|
|
350
|
+
index += 1;
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
structure: structure.join(""),
|
|
354
|
+
inspection: inspection.join("")
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const classNameTrieCache = /* @__PURE__ */ new WeakMap();
|
|
358
|
+
function classNameTrie(classes) {
|
|
359
|
+
let root = classNameTrieCache.get(classes);
|
|
360
|
+
if (root) return root;
|
|
361
|
+
root = /* @__PURE__ */ new Map();
|
|
362
|
+
for (const className of Object.keys(classes)) {
|
|
363
|
+
let node = root;
|
|
364
|
+
for (const char of className) {
|
|
365
|
+
if (!node.has(char)) node.set(char, /* @__PURE__ */ new Map());
|
|
366
|
+
node = node.get(char);
|
|
367
|
+
}
|
|
368
|
+
node.className = className;
|
|
369
|
+
}
|
|
370
|
+
classNameTrieCache.set(classes, root);
|
|
371
|
+
return root;
|
|
372
|
+
}
|
|
373
|
+
function isBeforeBoundary(char) {
|
|
374
|
+
return char === void 0 || !/[A-Za-z0-9_@:/-]/.test(char);
|
|
375
|
+
}
|
|
376
|
+
function isAfterBoundary(char, before, beforeBefore) {
|
|
377
|
+
if (char === void 0 || char === ":") return true;
|
|
378
|
+
if (char === ".") return before === "." && (beforeBefore === void 0 || !/[A-Za-z0-9_/\\.]/.test(beforeBefore));
|
|
379
|
+
return !/[A-Za-z0-9_@/-]/.test(char);
|
|
380
|
+
}
|
|
381
|
+
function findMappedToken(value, classes) {
|
|
382
|
+
const trie = classNameTrie(classes);
|
|
383
|
+
let found = null;
|
|
384
|
+
for (let start = 0; start < value.length; start += 1) {
|
|
385
|
+
const before = start === 0 ? void 0 : value[start - 1];
|
|
386
|
+
if (!isBeforeBoundary(before)) continue;
|
|
387
|
+
const beforeBefore = start < 2 ? void 0 : value[start - 2];
|
|
388
|
+
let node = trie;
|
|
389
|
+
for (let end = start; end < value.length; end += 1) {
|
|
390
|
+
node = node.get(value[end]);
|
|
391
|
+
if (!node) break;
|
|
392
|
+
if (!node.className) continue;
|
|
393
|
+
const afterIndex = end + 1;
|
|
394
|
+
if (!isAfterBoundary(afterIndex === value.length ? void 0 : value[afterIndex], before, beforeBefore)) continue;
|
|
395
|
+
if (found === null || node.className.length > found.length || node.className.length === found.length && node.className.localeCompare(found) < 0) found = node.className;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return found;
|
|
399
|
+
}
|
|
400
|
+
function scanQuotedSegments(value, visitor) {
|
|
401
|
+
let index = 0;
|
|
402
|
+
while (index < value.length) {
|
|
403
|
+
const quote = value[index];
|
|
404
|
+
if (quote !== "\"" && quote !== "'" && quote !== "`") {
|
|
405
|
+
index += 1;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
const start = index;
|
|
409
|
+
index += 1;
|
|
410
|
+
let escaped = false;
|
|
411
|
+
while (index < value.length) {
|
|
412
|
+
const char = value[index];
|
|
413
|
+
if (escaped) escaped = false;
|
|
414
|
+
else if (char === "\\") escaped = true;
|
|
415
|
+
else if (char === quote) {
|
|
416
|
+
index += 1;
|
|
417
|
+
visitor({
|
|
418
|
+
start,
|
|
419
|
+
end: index,
|
|
420
|
+
quote,
|
|
421
|
+
contents: value.slice(start + 1, index - 1)
|
|
422
|
+
});
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
index += 1;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function findMappedInQuotedSegments(value, classes) {
|
|
430
|
+
let found = null;
|
|
431
|
+
scanQuotedSegments(value, ({ contents }) => {
|
|
432
|
+
if (found === null) found = findMappedToken(contents, classes);
|
|
433
|
+
});
|
|
434
|
+
return found;
|
|
435
|
+
}
|
|
436
|
+
function stripQuotedSegments(value) {
|
|
437
|
+
let output = "";
|
|
438
|
+
let cursor = 0;
|
|
439
|
+
scanQuotedSegments(value, ({ start, end }) => {
|
|
440
|
+
output += value.slice(cursor, start);
|
|
441
|
+
output += " ".repeat(end - start);
|
|
442
|
+
cursor = end;
|
|
443
|
+
});
|
|
444
|
+
return output + value.slice(cursor);
|
|
445
|
+
}
|
|
446
|
+
function collectTemplateBodyEdits(body, classes, baseOffset, edits) {
|
|
447
|
+
let staticStart = 0;
|
|
448
|
+
let index = 0;
|
|
449
|
+
while (index < body.length) {
|
|
450
|
+
if (body[index] !== "$" || body[index + 1] !== "{") {
|
|
451
|
+
index += 1;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
collectClassListEdits(body.slice(staticStart, index), classes, baseOffset + staticStart, edits);
|
|
455
|
+
index += 2;
|
|
456
|
+
let depth = 1;
|
|
457
|
+
let quote = null;
|
|
458
|
+
let escaped = false;
|
|
459
|
+
while (index < body.length && depth > 0) {
|
|
460
|
+
const char = body[index];
|
|
461
|
+
if (quote !== null) {
|
|
462
|
+
if (escaped) escaped = false;
|
|
463
|
+
else if (char === "\\") escaped = true;
|
|
464
|
+
else if (char === quote) quote = null;
|
|
465
|
+
} else if (char === "\"" || char === "'" || char === "`") quote = char;
|
|
466
|
+
else if (char === "{") depth += 1;
|
|
467
|
+
else if (char === "}") depth -= 1;
|
|
468
|
+
index += 1;
|
|
469
|
+
}
|
|
470
|
+
staticStart = index;
|
|
471
|
+
}
|
|
472
|
+
collectClassListEdits(body.slice(staticStart), classes, baseOffset + staticStart, edits);
|
|
473
|
+
}
|
|
474
|
+
function findBalancedEnd(code, open, opening, closing) {
|
|
475
|
+
let index = open + 1;
|
|
476
|
+
let depth = 1;
|
|
477
|
+
let quote = null;
|
|
478
|
+
let escaped = false;
|
|
479
|
+
while (index < code.length && depth > 0) {
|
|
480
|
+
const char = code[index];
|
|
481
|
+
if (quote !== null) {
|
|
482
|
+
if (escaped) escaped = false;
|
|
483
|
+
else if (char === "\\") escaped = true;
|
|
484
|
+
else if (char === quote) quote = null;
|
|
485
|
+
} else if (char === "\"" || char === "'" || char === "`") quote = char;
|
|
486
|
+
else if (char === opening) depth += 1;
|
|
487
|
+
else if (char === closing) depth -= 1;
|
|
488
|
+
index += 1;
|
|
489
|
+
}
|
|
490
|
+
return depth === 0 ? index : -1;
|
|
491
|
+
}
|
|
492
|
+
function rejectAmbiguousClassExpressions(code, structure, classes, id) {
|
|
493
|
+
const pattern = /(?<![:\w-])class(?:Name)?\s*=\s*\{/g;
|
|
494
|
+
let match;
|
|
495
|
+
while ((match = pattern.exec(structure)) !== null) {
|
|
496
|
+
const open = code.indexOf("{", match.index);
|
|
497
|
+
const end = findBalancedEnd(code, open, "{", "}");
|
|
498
|
+
if (end < 0) throw new Error(`[gustcss] incomplete class/className expression in ${id}. Ensure the expression is properly closed.`);
|
|
499
|
+
const ambiguous = findMappedToken(code.slice(open + 1, end - 1), classes);
|
|
500
|
+
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.`);
|
|
501
|
+
pattern.lastIndex = end;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function collectClassListCallEdits(code, structure, classes, id, edits) {
|
|
505
|
+
const pattern = /\.classList\.(?:add|remove|toggle|contains|replace)\s*\(/g;
|
|
506
|
+
let match;
|
|
507
|
+
while ((match = pattern.exec(structure)) !== null) {
|
|
508
|
+
const open = code.indexOf("(", match.index);
|
|
509
|
+
const end = findBalancedEnd(code, open, "(", ")");
|
|
510
|
+
if (end < 0) break;
|
|
511
|
+
const args = code.slice(open + 1, end - 1);
|
|
512
|
+
const withoutStrings = stripQuotedSegments(args);
|
|
513
|
+
const dynamic = findMappedToken(withoutStrings, classes);
|
|
514
|
+
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.`);
|
|
515
|
+
if (withoutStrings.includes("(")) {
|
|
516
|
+
const ambiguous = findMappedInQuotedSegments(args, classes);
|
|
517
|
+
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.`);
|
|
518
|
+
}
|
|
519
|
+
scanQuotedSegments(args, ({ start, contents }) => {
|
|
520
|
+
collectClassListEdits(contents, classes, open + 1 + start + 1, edits);
|
|
521
|
+
});
|
|
522
|
+
pattern.lastIndex = end;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function rejectDynamicSetAttributeCalls(code, structure, classes, id) {
|
|
526
|
+
const pattern = /\.setAttribute\s*\(/g;
|
|
527
|
+
let match;
|
|
528
|
+
while ((match = pattern.exec(structure)) !== null) {
|
|
529
|
+
const open = code.indexOf("(", match.index);
|
|
530
|
+
const end = findBalancedEnd(code, open, "(", ")");
|
|
531
|
+
if (end < 0) break;
|
|
532
|
+
const args = code.slice(open + 1, end - 1);
|
|
533
|
+
if (!/^\s*(["'])class\1\s*,/.test(args)) {
|
|
534
|
+
pattern.lastIndex = end;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const withoutStrings = stripQuotedSegments(args);
|
|
538
|
+
const dynamic = findMappedToken(withoutStrings, classes);
|
|
539
|
+
const nested = withoutStrings.includes("(") ? findMappedInQuotedSegments(args, classes) : null;
|
|
540
|
+
const ambiguous = dynamic ?? nested;
|
|
541
|
+
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.`);
|
|
542
|
+
pattern.lastIndex = end;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function rejectDynamicClassNameAssignments(code, structure, classes, id) {
|
|
546
|
+
const pattern = /\.className\s*=(?!=)\s*/g;
|
|
547
|
+
while (pattern.exec(structure) !== null) {
|
|
548
|
+
const start = pattern.lastIndex;
|
|
549
|
+
const candidates = [code.indexOf(";", start), code.indexOf("\n", start)].filter((index) => index >= 0);
|
|
550
|
+
const end = candidates.length > 0 ? Math.min(...candidates) : code.length;
|
|
551
|
+
const ambiguous = findMappedToken(code.slice(start, end), classes);
|
|
552
|
+
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.`);
|
|
553
|
+
pattern.lastIndex = end;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function applyEdits(code, edits) {
|
|
557
|
+
const sorted = [...edits].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
558
|
+
let output = "";
|
|
559
|
+
let cursor = 0;
|
|
560
|
+
const origins = [];
|
|
561
|
+
for (const edit of sorted) {
|
|
562
|
+
if (edit.start < cursor) throw new Error("[gustcss] overlapping class-name rewrites");
|
|
563
|
+
output += code.slice(cursor, edit.start);
|
|
564
|
+
for (let index = cursor; index < edit.start; index += 1) origins.push(index);
|
|
565
|
+
const sourceLength = Math.max(1, edit.end - edit.start);
|
|
566
|
+
if (edit.replacement.length > sourceLength) throw new Error("[gustcss] class-name replacement must not exceed its source length");
|
|
567
|
+
output += edit.replacement;
|
|
568
|
+
for (let index = 0; index < edit.replacement.length; index += 1) origins.push(edit.start + Math.min(index, sourceLength - 1));
|
|
569
|
+
cursor = edit.end;
|
|
570
|
+
}
|
|
571
|
+
output += code.slice(cursor);
|
|
572
|
+
for (let index = cursor; index < code.length; index += 1) origins.push(index);
|
|
573
|
+
return {
|
|
574
|
+
code: output,
|
|
575
|
+
origins
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function sourcesForFile(code, id) {
|
|
579
|
+
if (!HTML_SOURCE.test(id)) {
|
|
580
|
+
const js = createJsMasks(code);
|
|
581
|
+
return {
|
|
582
|
+
attributes: code,
|
|
583
|
+
structure: js.structure,
|
|
584
|
+
inspection: js.inspection
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
const html = analyzeHtml(code, id);
|
|
588
|
+
const structure = new Array(code.length).fill(" ");
|
|
589
|
+
const inspection = new Array(code.length).fill(" ");
|
|
590
|
+
for (const range of html.scriptRanges) {
|
|
591
|
+
const script = code.slice(range.start, range.end);
|
|
592
|
+
const jsScript = createJsMasks(script);
|
|
593
|
+
for (let offset = 0; offset < script.length; offset += 1) {
|
|
594
|
+
structure[range.start + offset] = jsScript.structure[offset];
|
|
595
|
+
inspection[range.start + offset] = jsScript.inspection[offset];
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
for (const range of html.commentRanges) for (let offset = range.start; offset < range.end; offset += 1) inspection[offset] = " ";
|
|
599
|
+
return {
|
|
600
|
+
attributes: html.markup,
|
|
601
|
+
structure: structure.join(""),
|
|
602
|
+
inspection: inspection.join("")
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function collectEdits(code, classes, id) {
|
|
606
|
+
const edits = [];
|
|
607
|
+
const sources = sourcesForFile(code, id);
|
|
608
|
+
const commentRanges = [];
|
|
609
|
+
let commentIndex = 0;
|
|
610
|
+
while ((commentIndex = code.indexOf("<!--", commentIndex)) >= 0) {
|
|
611
|
+
const commentEnd = code.indexOf("-->", commentIndex + 4);
|
|
612
|
+
if (commentEnd < 0) {
|
|
613
|
+
commentRanges.push({
|
|
614
|
+
start: commentIndex,
|
|
615
|
+
end: code.length
|
|
616
|
+
});
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
commentRanges.push({
|
|
620
|
+
start: commentIndex,
|
|
621
|
+
end: commentEnd + 3
|
|
622
|
+
});
|
|
623
|
+
commentIndex = commentEnd + 3;
|
|
624
|
+
}
|
|
625
|
+
function isInComment(pos) {
|
|
626
|
+
return commentRanges.some((range) => pos >= range.start && pos < range.end);
|
|
627
|
+
}
|
|
628
|
+
const attributePattern = /((?<![:\w-])class(?:Name)?\s*=\s*)(?:(["'])([\s\S]*?)(\2)|(\{\s*)(["'])([\s\S]*?)(\6)(\s*\}))/g;
|
|
629
|
+
let match;
|
|
630
|
+
while ((match = attributePattern.exec(sources.attributes)) !== null) {
|
|
631
|
+
if (isInComment(match.index)) continue;
|
|
632
|
+
if (!HTML_SOURCE.test(id) && !isJsxAttributeAt(sources.structure, match.index)) continue;
|
|
633
|
+
if (match[2] !== void 0) collectClassListEdits(match[3], classes, match.index + match[1].length + 1, edits);
|
|
634
|
+
else collectClassListEdits(match[7], classes, match.index + match[1].length + match[5].length + 1, edits);
|
|
635
|
+
}
|
|
636
|
+
const templatePattern = /((?<![:\w-])class(?:Name)?\s*=\s*\{\s*`)([\s\S]*?)(`\s*\})/g;
|
|
637
|
+
while ((match = templatePattern.exec(sources.attributes)) !== null) {
|
|
638
|
+
if (isInComment(match.index)) continue;
|
|
639
|
+
if (!HTML_SOURCE.test(id) && !isJsxAttributeAt(sources.structure, match.index)) continue;
|
|
640
|
+
collectTemplateBodyEdits(match[2], classes, match.index + match[1].length, edits);
|
|
641
|
+
}
|
|
642
|
+
collectClassListCallEdits(code, sources.structure, classes, id, edits);
|
|
643
|
+
const setAttributePattern = /(\.setAttribute\(\s*["']class["']\s*,\s*)(["'])([\s\S]*?)(\2)(\s*\))/g;
|
|
644
|
+
while ((match = setAttributePattern.exec(code)) !== null) {
|
|
645
|
+
if (sources.structure[match.index] === " ") continue;
|
|
646
|
+
collectClassListEdits(match[3], classes, match.index + match[1].length + 1, edits);
|
|
647
|
+
}
|
|
648
|
+
return edits;
|
|
649
|
+
}
|
|
650
|
+
function collectRecognizedContextEdits(code, classes, id) {
|
|
651
|
+
return applyEdits(code, collectEdits(code, classes, id));
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Collect the rewrites for a source fragment (TypeScript frontmatter, an inline
|
|
655
|
+
* `<script>` body, …) and run the same fail-closed checks as `transformSource`,
|
|
656
|
+
* but return the edits instead of the rewritten code so the caller can place
|
|
657
|
+
* them inside a larger document.
|
|
658
|
+
*/
|
|
659
|
+
function collectFragmentEdits(code, classes, id) {
|
|
660
|
+
const edits = collectEdits(code, classes, id);
|
|
661
|
+
const rewritten = applyEdits(code, edits).code;
|
|
662
|
+
const sources = sourcesForFile(rewritten, id);
|
|
663
|
+
rejectDynamicSetAttributeCalls(rewritten, sources.structure, classes, id);
|
|
664
|
+
rejectDynamicClassNameAssignments(rewritten, sources.structure, classes, id);
|
|
665
|
+
rejectAmbiguousClassExpressions(rewritten, sources.structure, classes, id);
|
|
666
|
+
const ambiguous = findMappedInQuotedSegments(sources.inspection, classes);
|
|
667
|
+
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.`);
|
|
668
|
+
return edits;
|
|
669
|
+
}
|
|
670
|
+
const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
671
|
+
function encodeVLQ(value) {
|
|
672
|
+
let encoded = "";
|
|
673
|
+
let vlq = Math.abs(value) * 2 + (value < 0 ? 1 : 0);
|
|
674
|
+
do {
|
|
675
|
+
let digit = vlq % 32;
|
|
676
|
+
vlq = Math.floor(vlq / 32);
|
|
677
|
+
if (vlq > 0) digit += 32;
|
|
678
|
+
encoded += BASE64[digit];
|
|
679
|
+
} while (vlq > 0);
|
|
680
|
+
return encoded;
|
|
681
|
+
}
|
|
682
|
+
function createSourceMap(original, generated, origins, id) {
|
|
683
|
+
const originalPositions = new Array(original.length);
|
|
684
|
+
let originalLine = 0;
|
|
685
|
+
let originalColumn = 0;
|
|
686
|
+
for (let index = 0; index < original.length; index += 1) {
|
|
687
|
+
originalPositions[index] = {
|
|
688
|
+
line: originalLine,
|
|
689
|
+
column: originalColumn
|
|
690
|
+
};
|
|
691
|
+
if (original[index] === "\n") {
|
|
692
|
+
originalLine += 1;
|
|
693
|
+
originalColumn = 0;
|
|
694
|
+
} else originalColumn += 1;
|
|
695
|
+
}
|
|
696
|
+
const lines = [""];
|
|
697
|
+
let generatedLine = 0;
|
|
698
|
+
let generatedColumn = 0;
|
|
699
|
+
let previousGeneratedColumn = 0;
|
|
700
|
+
let previousSource = 0;
|
|
701
|
+
let previousOriginalLine = 0;
|
|
702
|
+
let previousOriginalColumn = 0;
|
|
703
|
+
for (let index = 0; index < generated.length; index += 1) {
|
|
704
|
+
if (generated[index] === "\n") {
|
|
705
|
+
generatedLine += 1;
|
|
706
|
+
generatedColumn = 0;
|
|
707
|
+
previousGeneratedColumn = 0;
|
|
708
|
+
lines.push("");
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
const originalPosition = originalPositions[origins[index]];
|
|
712
|
+
if (!originalPosition) {
|
|
713
|
+
generatedColumn += 1;
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
const segment = encodeVLQ(generatedColumn - previousGeneratedColumn) + encodeVLQ(0 - previousSource) + encodeVLQ(originalPosition.line - previousOriginalLine) + encodeVLQ(originalPosition.column - previousOriginalColumn);
|
|
717
|
+
lines[generatedLine] += `${lines[generatedLine] ? "," : ""}${segment}`;
|
|
718
|
+
previousGeneratedColumn = generatedColumn;
|
|
719
|
+
previousSource = 0;
|
|
720
|
+
previousOriginalLine = originalPosition.line;
|
|
721
|
+
previousOriginalColumn = originalPosition.column;
|
|
722
|
+
generatedColumn += 1;
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
version: 3,
|
|
726
|
+
sources: [id],
|
|
727
|
+
sourcesContent: [original],
|
|
728
|
+
names: [],
|
|
729
|
+
mappings: lines.join(";")
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
function transformSource(code, classes, id) {
|
|
733
|
+
const transformed = collectRecognizedContextEdits(code, classes, id);
|
|
734
|
+
const rewritten = transformed.code;
|
|
735
|
+
const sources = sourcesForFile(rewritten, id);
|
|
736
|
+
rejectDynamicSetAttributeCalls(rewritten, sources.structure, classes, id);
|
|
737
|
+
rejectDynamicClassNameAssignments(rewritten, sources.structure, classes, id);
|
|
738
|
+
rejectAmbiguousClassExpressions(rewritten, sources.structure, classes, id);
|
|
739
|
+
return {
|
|
740
|
+
code: rewritten,
|
|
741
|
+
map: createSourceMap(code, rewritten, transformed.origins, id),
|
|
742
|
+
inspection: sources.inspection
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
function findAmbiguousMappedClass(code, classes) {
|
|
746
|
+
return findMappedInQuotedSegments(code, classes);
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Create a fail-closed source transformer for a GustCSS class manifest.
|
|
750
|
+
* Only unambiguous class-name contexts are rewritten automatically.
|
|
751
|
+
*/
|
|
752
|
+
function createClassNameTransformer(classes) {
|
|
753
|
+
const transformWithSourceMap = (code, id) => {
|
|
754
|
+
if (UNSUPPORTED_FRAMEWORK_SOURCE.test(id)) {
|
|
755
|
+
const referenced = findMappedToken(code, classes);
|
|
756
|
+
if (referenced) throw new Error(`[gustcss] class-name mangling does not support Vue or Svelte templates yet (${id}). Add "${referenced}" to mangleExclude or disable mangleClassNames.`);
|
|
757
|
+
return {
|
|
758
|
+
code,
|
|
759
|
+
map: null
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
if (DEPENDENCY_SOURCE.test(id) || !SUPPORTED_SOURCE.test(id)) return {
|
|
763
|
+
code,
|
|
764
|
+
map: null
|
|
765
|
+
};
|
|
766
|
+
const result = transformSource(code, classes, id);
|
|
767
|
+
const ambiguous = findAmbiguousMappedClass(result.inspection, classes, id);
|
|
768
|
+
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.`);
|
|
769
|
+
return {
|
|
770
|
+
code: result.code,
|
|
771
|
+
map: result.map
|
|
772
|
+
};
|
|
773
|
+
};
|
|
774
|
+
const transform = (code, id) => transformWithSourceMap(code, id).code;
|
|
775
|
+
transform.withSourceMap = transformWithSourceMap;
|
|
776
|
+
transform.classes = classes;
|
|
777
|
+
return transform;
|
|
778
|
+
}
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/astro.js
|
|
781
|
+
/**
|
|
782
|
+
* Class-name mangling for `.astro` sources, driven by the AST that
|
|
783
|
+
* `@astrojs/compiler`'s `parse()` returns.
|
|
784
|
+
*
|
|
785
|
+
* Only unambiguous class contexts are rewritten; any other place where a
|
|
786
|
+
* mapped class name shows up fails the build (fail-closed), because a class
|
|
787
|
+
* that reaches the DOM unrenamed would no longer match the mangled CSS.
|
|
788
|
+
*/
|
|
789
|
+
const CLASS_VALUED_BEFORE = /* @__PURE__ */ new Set([
|
|
790
|
+
"[",
|
|
791
|
+
",",
|
|
792
|
+
"&&",
|
|
793
|
+
"||"
|
|
794
|
+
]);
|
|
795
|
+
const CLASS_VALUED_AFTER = /* @__PURE__ */ new Set([
|
|
796
|
+
",",
|
|
797
|
+
"]",
|
|
798
|
+
"&&",
|
|
799
|
+
"||"
|
|
800
|
+
]);
|
|
801
|
+
const OPERATORS = [
|
|
802
|
+
"===",
|
|
803
|
+
"!==",
|
|
804
|
+
"&&",
|
|
805
|
+
"||",
|
|
806
|
+
"??",
|
|
807
|
+
"=>",
|
|
808
|
+
"==",
|
|
809
|
+
"!=",
|
|
810
|
+
"?.",
|
|
811
|
+
"<=",
|
|
812
|
+
">="
|
|
813
|
+
];
|
|
814
|
+
const PUNCTUATION = "[]{}(),:?.!+-*/%<>=&|~^;";
|
|
815
|
+
/** Map UTF-8 byte offsets (what the compiler reports) to string indexes. */
|
|
816
|
+
function byteToIndexMap(code) {
|
|
817
|
+
const map = [];
|
|
818
|
+
let byte = 0;
|
|
819
|
+
for (let index = 0; index < code.length; index += 1) {
|
|
820
|
+
const point = code.codePointAt(index);
|
|
821
|
+
const width = point < 128 ? 1 : point < 2048 ? 2 : point < 65536 ? 3 : 4;
|
|
822
|
+
for (let i = 0; i < width; i += 1) map[byte + i] = index;
|
|
823
|
+
byte += width;
|
|
824
|
+
if (point > 65535) index += 1;
|
|
825
|
+
}
|
|
826
|
+
map[byte] = code.length;
|
|
827
|
+
return map;
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Tokenize a JavaScript expression just far enough to know, for every string
|
|
831
|
+
* literal and identifier, what surrounds it. Template literals are reported
|
|
832
|
+
* as strings with quote "`" and are never rewritten. Returns null when the
|
|
833
|
+
* expression cannot be tokenized (unterminated string, unknown character).
|
|
834
|
+
*/
|
|
835
|
+
function tokenizeExpression(source) {
|
|
836
|
+
const tokens = [];
|
|
837
|
+
let index = 0;
|
|
838
|
+
while (index < source.length) {
|
|
839
|
+
const char = source[index];
|
|
840
|
+
if (/\s/.test(char)) {
|
|
841
|
+
index += 1;
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
if (source.startsWith("//", index)) {
|
|
845
|
+
const newline = source.indexOf("\n", index);
|
|
846
|
+
index = newline < 0 ? source.length : newline + 1;
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (source.startsWith("/*", index)) {
|
|
850
|
+
const close = source.indexOf("*/", index + 2);
|
|
851
|
+
if (close < 0) return null;
|
|
852
|
+
index = close + 2;
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
if (char === "\"" || char === "'" || char === "`") {
|
|
856
|
+
const start = index;
|
|
857
|
+
index += 1;
|
|
858
|
+
while (index < source.length && source[index] !== char) {
|
|
859
|
+
if (source[index] === "\\") index += 1;
|
|
860
|
+
index += 1;
|
|
861
|
+
}
|
|
862
|
+
if (index >= source.length) return null;
|
|
863
|
+
tokens.push({
|
|
864
|
+
type: "string",
|
|
865
|
+
quote: char,
|
|
866
|
+
start,
|
|
867
|
+
end: index + 1,
|
|
868
|
+
value: source.slice(start + 1, index)
|
|
869
|
+
});
|
|
870
|
+
index += 1;
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
if (/[A-Za-z_$]/.test(char)) {
|
|
874
|
+
const start = index;
|
|
875
|
+
while (index < source.length && /[\w$]/.test(source[index])) index += 1;
|
|
876
|
+
tokens.push({
|
|
877
|
+
type: "identifier",
|
|
878
|
+
start,
|
|
879
|
+
end: index,
|
|
880
|
+
value: source.slice(start, index)
|
|
881
|
+
});
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
if (/[0-9]/.test(char)) {
|
|
885
|
+
const start = index;
|
|
886
|
+
while (index < source.length && /[\w.]/.test(source[index])) index += 1;
|
|
887
|
+
tokens.push({
|
|
888
|
+
type: "number",
|
|
889
|
+
start,
|
|
890
|
+
end: index,
|
|
891
|
+
value: source.slice(start, index)
|
|
892
|
+
});
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
const operator = OPERATORS.find((candidate) => source.startsWith(candidate, index));
|
|
896
|
+
if (operator) {
|
|
897
|
+
tokens.push({
|
|
898
|
+
type: "operator",
|
|
899
|
+
start: index,
|
|
900
|
+
end: index + operator.length,
|
|
901
|
+
value: operator
|
|
902
|
+
});
|
|
903
|
+
index += operator.length;
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
if (PUNCTUATION.includes(char)) {
|
|
907
|
+
tokens.push({
|
|
908
|
+
type: "operator",
|
|
909
|
+
start: index,
|
|
910
|
+
end: index + 1,
|
|
911
|
+
value: char
|
|
912
|
+
});
|
|
913
|
+
index += 1;
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
return tokens;
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Rewrite the class-valued string literals of a `class:list` expression.
|
|
922
|
+
* Accepted shapes are array literals, object literals and their nesting.
|
|
923
|
+
* Inside them only array elements (also as the right operand of `&&` / `||`)
|
|
924
|
+
* and quoted object keys are class-valued; every other occurrence of a mapped
|
|
925
|
+
* class name fails closed.
|
|
926
|
+
*/
|
|
927
|
+
function collectClassListExpressionEdits(source, classes, baseOffset, edits, id) {
|
|
928
|
+
const fail = (token, reason) => {
|
|
929
|
+
throw new Error(`[gustcss] mapped class "${token}" ${reason} in class:list of ${id}. Use an array/object literal with quoted entries, or add it to mangleExclude.`);
|
|
930
|
+
};
|
|
931
|
+
const tokens = tokenizeExpression(source);
|
|
932
|
+
const first = tokens && tokens[0];
|
|
933
|
+
const last = tokens && tokens[tokens.length - 1];
|
|
934
|
+
if (!(tokens && tokens.length >= 2 && (first.value === "[" && last.value === "]" || first.value === "{" && last.value === "}"))) {
|
|
935
|
+
const mapped = findMappedToken(source, classes);
|
|
936
|
+
if (mapped) fail(mapped, "remains in an expression that is not an array or object literal");
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const stack = [];
|
|
940
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
941
|
+
const token = tokens[i];
|
|
942
|
+
const previous = tokens[i - 1];
|
|
943
|
+
const next = tokens[i + 1];
|
|
944
|
+
const context = stack[stack.length - 1];
|
|
945
|
+
if (token.type === "operator") {
|
|
946
|
+
if (token.value === "[") {
|
|
947
|
+
const subscript = previous && (previous.type === "identifier" || previous.type === "string" || previous.value === ")" || previous.value === "]");
|
|
948
|
+
stack.push(subscript ? "subscript" : "array");
|
|
949
|
+
} else if (token.value === "{") stack.push("object");
|
|
950
|
+
else if (token.value === "(") stack.push("call");
|
|
951
|
+
else if (token.value === "]" || token.value === "}" || token.value === ")") {
|
|
952
|
+
if (stack.length === 0) fail(findMappedToken(source, classes) || "?", "appears in an unbalanced");
|
|
953
|
+
stack.pop();
|
|
954
|
+
}
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
if (token.type === "identifier") {
|
|
958
|
+
if (!hasOwn(classes, token.value)) continue;
|
|
959
|
+
if (context === "object" && previous && (previous.value === "{" || previous.value === ",") && next && (next.value === ":" || next.value === "," || next.value === "}")) fail(token.value, `is an unquoted object key (write it as "${token.value}")`);
|
|
960
|
+
fail(token.value, "is referenced as an identifier");
|
|
961
|
+
}
|
|
962
|
+
if (token.type !== "string") continue;
|
|
963
|
+
const mapped = findMappedToken(token.value, classes);
|
|
964
|
+
if (token.quote === "`") {
|
|
965
|
+
if (mapped) fail(mapped, "remains in a template literal");
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const arrayElement = context === "array" && previous && CLASS_VALUED_BEFORE.has(previous.value) && next && CLASS_VALUED_AFTER.has(next.value);
|
|
969
|
+
const objectKey = context === "object" && previous && (previous.value === "{" || previous.value === ",") && next && next.value === ":";
|
|
970
|
+
if (arrayElement || objectKey) collectClassListEdits(token.value, classes, baseOffset + token.start + 1, edits);
|
|
971
|
+
else if (mapped) fail(mapped, "remains in a position that is not an array element or object key");
|
|
972
|
+
}
|
|
973
|
+
if (stack.length !== 0) fail(findMappedToken(source, classes) || "?", "appears in an unbalanced");
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Find a mapped class referenced by a stylesheet selector, including at-rule
|
|
977
|
+
* preludes (`@scope (.flex)`) and attribute selectors (`[class~="flex"]`).
|
|
978
|
+
* Declarations and comments are ignored.
|
|
979
|
+
*/
|
|
980
|
+
function findMappedClassInStylesheet(css, classes) {
|
|
981
|
+
const source = css.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
982
|
+
let prelude = "";
|
|
983
|
+
let quote = null;
|
|
984
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
985
|
+
const char = source[i];
|
|
986
|
+
if (quote) {
|
|
987
|
+
prelude += char;
|
|
988
|
+
if (char === "\\") {
|
|
989
|
+
prelude += source[i + 1] || "";
|
|
990
|
+
i += 1;
|
|
991
|
+
} else if (char === quote) quote = null;
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
if (char === "\"" || char === "'") {
|
|
995
|
+
quote = char;
|
|
996
|
+
prelude += char;
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
if (char === "{") {
|
|
1000
|
+
const found = findMappedClassInSelector(prelude.trim(), classes);
|
|
1001
|
+
if (found) return found;
|
|
1002
|
+
prelude = "";
|
|
1003
|
+
} else if (char === "}" || char === ";") prelude = "";
|
|
1004
|
+
else prelude += char;
|
|
1005
|
+
}
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
function findMappedClassInSelector(selector, classes) {
|
|
1009
|
+
const classPattern = /\.((?:\\.|[A-Za-z0-9_-])+)/g;
|
|
1010
|
+
let match;
|
|
1011
|
+
while ((match = classPattern.exec(selector)) !== null) {
|
|
1012
|
+
const name = match[1].replace(/\\(.)/g, "$1");
|
|
1013
|
+
if (hasOwn(classes, name)) return name;
|
|
1014
|
+
}
|
|
1015
|
+
const attributePattern = /\[\s*class\s*[~|^$*]?=\s*(["']?)([^\]"']+)\1\s*[is]?\s*\]/g;
|
|
1016
|
+
while ((match = attributePattern.exec(selector)) !== null) {
|
|
1017
|
+
const mapped = findMappedToken(match[2], classes);
|
|
1018
|
+
if (mapped) return mapped;
|
|
1019
|
+
}
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Locate the value of an attribute from its start offset (the attribute
|
|
1024
|
+
* name). Returns the index range of the value and its delimiter.
|
|
1025
|
+
*/
|
|
1026
|
+
function locateAttributeValue(code, attribute, start) {
|
|
1027
|
+
let index = start + attribute.name.length;
|
|
1028
|
+
while (index < code.length && /\s/.test(code[index])) index += 1;
|
|
1029
|
+
if (code[index] !== "=") return null;
|
|
1030
|
+
index += 1;
|
|
1031
|
+
while (index < code.length && /\s/.test(code[index])) index += 1;
|
|
1032
|
+
const delimiter = code[index];
|
|
1033
|
+
if (delimiter === "\"" || delimiter === "'" || delimiter === "`") {
|
|
1034
|
+
const end = code.indexOf(delimiter, index + 1);
|
|
1035
|
+
return end < 0 ? null : {
|
|
1036
|
+
start: index + 1,
|
|
1037
|
+
end,
|
|
1038
|
+
delimiter
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
if (delimiter === "{") {
|
|
1042
|
+
const end = findBalancedEnd(code, index, "{", "}");
|
|
1043
|
+
return end < 0 ? null : {
|
|
1044
|
+
start: index + 1,
|
|
1045
|
+
end: end - 1,
|
|
1046
|
+
delimiter
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
const STRING_LITERAL = /^\s*(["'])((?:\\.|(?!\1)[^\\])*)\1\s*$/;
|
|
1052
|
+
/**
|
|
1053
|
+
* Create the `.astro` transformer. `parse` is `@astrojs/compiler`'s parse
|
|
1054
|
+
* function, injected so the plugin and the tests can supply their own copy.
|
|
1055
|
+
*/
|
|
1056
|
+
function createAstroTransformer({ parse, classes }) {
|
|
1057
|
+
return async function transformAstro(code, id) {
|
|
1058
|
+
const { ast } = await parse(code, { position: true });
|
|
1059
|
+
const toIndex = byteToIndexMap(code);
|
|
1060
|
+
const edits = [];
|
|
1061
|
+
const offsetOf = (node) => toIndex[node.position.start.offset];
|
|
1062
|
+
const stop = (mapped, where) => {
|
|
1063
|
+
throw new Error(`[gustcss] mapped class "${mapped}" remains in ${where} in ${id}. Move it to a static class attribute or class:list literal, or add it to mangleExclude.`);
|
|
1064
|
+
};
|
|
1065
|
+
const fragmentEdits = (text, start, kind) => {
|
|
1066
|
+
for (const edit of collectFragmentEdits(text, classes, `${id}#${kind}.ts`)) edits.push({
|
|
1067
|
+
start: start + edit.start,
|
|
1068
|
+
end: start + edit.end,
|
|
1069
|
+
replacement: edit.replacement
|
|
1070
|
+
});
|
|
1071
|
+
};
|
|
1072
|
+
const visitAttribute = (attribute, node) => {
|
|
1073
|
+
const start = offsetOf(attribute);
|
|
1074
|
+
const isComponent = node.type === "component";
|
|
1075
|
+
const value = attribute.value || "";
|
|
1076
|
+
if (attribute.name === "class") {
|
|
1077
|
+
const location = locateAttributeValue(code, attribute, start);
|
|
1078
|
+
if (location && attribute.kind === "quoted") {
|
|
1079
|
+
collectClassListEdits(code.slice(location.start, location.end), classes, location.start, edits);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (location && attribute.kind === "expression") {
|
|
1083
|
+
const expression = code.slice(location.start, location.end);
|
|
1084
|
+
const literal = STRING_LITERAL.exec(expression);
|
|
1085
|
+
if (literal) {
|
|
1086
|
+
const inner = expression.indexOf(literal[1]) + 1;
|
|
1087
|
+
collectClassListEdits(literal[2], classes, location.start + inner, edits);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
const mapped = findMappedToken(value, classes);
|
|
1092
|
+
if (mapped) stop(mapped, `a dynamic class attribute (${attribute.kind})`);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (attribute.name === "class:list") {
|
|
1096
|
+
const location = locateAttributeValue(code, attribute, start);
|
|
1097
|
+
if (!location || attribute.kind !== "expression") {
|
|
1098
|
+
const mapped = findMappedToken(value, classes);
|
|
1099
|
+
if (mapped) stop(mapped, `a class:list attribute of kind ${attribute.kind}`);
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
collectClassListExpressionEdits(code.slice(location.start, location.end), classes, location.start, edits, id);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (attribute.kind === "spread") {
|
|
1106
|
+
const mapped = findMappedToken(`${attribute.name} ${value}`, classes);
|
|
1107
|
+
if (mapped) stop(mapped, `a spread attribute on <${node.name}>`);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
if (attribute.name === "set:html" || attribute.name === "set:text" || isComponent) {
|
|
1111
|
+
const mapped = findMappedToken(value, classes);
|
|
1112
|
+
if (mapped) stop(mapped, isComponent ? `the "${attribute.name}" prop of <${node.name}>` : `a ${attribute.name} value`);
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
const visit = (node) => {
|
|
1116
|
+
switch (node.type) {
|
|
1117
|
+
case "frontmatter": {
|
|
1118
|
+
const start = code.indexOf(node.value, offsetOf(node));
|
|
1119
|
+
if (start >= 0) fragmentEdits(node.value, start, "frontmatter");
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
case "comment":
|
|
1123
|
+
case "text":
|
|
1124
|
+
case "doctype": return;
|
|
1125
|
+
case "expression":
|
|
1126
|
+
for (const child of node.children) if (child.type === "text") {
|
|
1127
|
+
const mapped = findMappedToken(child.value, classes);
|
|
1128
|
+
if (mapped && /["'`]/.test(child.value)) stop(mapped, "a template expression");
|
|
1129
|
+
} else visit(child);
|
|
1130
|
+
return;
|
|
1131
|
+
case "element":
|
|
1132
|
+
case "component":
|
|
1133
|
+
case "custom-element":
|
|
1134
|
+
case "fragment":
|
|
1135
|
+
for (const attribute of node.attributes) visitAttribute(attribute, node);
|
|
1136
|
+
if (node.type === "element" && node.name === "style") {
|
|
1137
|
+
for (const child of node.children) {
|
|
1138
|
+
if (child.type !== "text") continue;
|
|
1139
|
+
const mapped = findMappedClassInStylesheet(child.value, classes);
|
|
1140
|
+
if (mapped) stop(mapped, "a <style> selector");
|
|
1141
|
+
}
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
if (node.type === "element" && node.name === "script") {
|
|
1145
|
+
for (const child of node.children) if (child.type === "text") fragmentEdits(child.value, offsetOf(child), "script");
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
for (const child of node.children) visit(child);
|
|
1149
|
+
return;
|
|
1150
|
+
default: for (const child of node.children || []) visit(child);
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
visit(ast);
|
|
1154
|
+
const applied = applyEdits(code, edits);
|
|
1155
|
+
return {
|
|
1156
|
+
code: applied.code,
|
|
1157
|
+
map: createSourceMap(code, applied.code, applied.origins, id)
|
|
1158
|
+
};
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
//#endregion
|
|
1162
|
+
//#region src/index.js
|
|
145
1163
|
/**
|
|
146
1164
|
* @gustcss/vite - Vite plugin for CSS Utility Generator
|
|
147
1165
|
*
|
|
@@ -155,12 +1173,38 @@ var import_runner = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
|
|
|
155
1173
|
* are provided by the developer in their build configuration. This plugin is
|
|
156
1174
|
* designed for build-time use only and should not process untrusted input.
|
|
157
1175
|
*/
|
|
1176
|
+
const ASTRO_SOURCE = /\.astro(?:$|\?)/;
|
|
1177
|
+
const ASTRO_MODULE = /^[^\0?]*\.astro$/;
|
|
1178
|
+
/**
|
|
1179
|
+
* Resolve @astrojs/compiler parse function from the project.
|
|
1180
|
+
*/
|
|
1181
|
+
async function resolveAstroCompiler(root) {
|
|
1182
|
+
const projectRequire = (0, node_module.createRequire)(path.default.join(root, "package.json"));
|
|
1183
|
+
const attempts = [
|
|
1184
|
+
() => projectRequire.resolve("@astrojs/compiler"),
|
|
1185
|
+
() => (0, node_module.createRequire)(projectRequire.resolve("astro/package.json")).resolve("@astrojs/compiler"),
|
|
1186
|
+
() => (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href).resolve("@astrojs/compiler")
|
|
1187
|
+
];
|
|
1188
|
+
for (const attempt of attempts) try {
|
|
1189
|
+
const mod = await import((0, node_url.pathToFileURL)(attempt()).href);
|
|
1190
|
+
if (typeof mod.parse === "function") return mod.parse;
|
|
1191
|
+
} catch {}
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
158
1194
|
function cssUtility(opts = {}) {
|
|
159
1195
|
const output = opts.output || "src/styles/utility.css";
|
|
160
1196
|
const content = opts.content || ["./src/**/*.{js,ts,jsx,tsx,astro,vue}"];
|
|
161
1197
|
const configPath = opts.config;
|
|
162
1198
|
const outputToCssLayers = opts.outputToCssLayers;
|
|
1199
|
+
const mangleClassNames = opts.mangleClassNames === true;
|
|
1200
|
+
const mangleMap = opts.mangleMap || `${output}.classes.json`;
|
|
1201
|
+
const mangleExclude = opts.mangleExclude || [];
|
|
1202
|
+
let classNameTransformer = null;
|
|
1203
|
+
let rollbackContext = null;
|
|
1204
|
+
let manifestSnapshot = null;
|
|
163
1205
|
let watchProcess = null;
|
|
1206
|
+
const loadedAstroModules = /* @__PURE__ */ new Set();
|
|
1207
|
+
let astroTransformer = null;
|
|
164
1208
|
function findBinary(cwd) {
|
|
165
1209
|
return import_runner.default.resolveBinary({
|
|
166
1210
|
cwd,
|
|
@@ -174,14 +1218,21 @@ function cssUtility(opts = {}) {
|
|
|
174
1218
|
fs: fs.default
|
|
175
1219
|
});
|
|
176
1220
|
}
|
|
177
|
-
function buildCSS(cwd, binaryPath) {
|
|
1221
|
+
function buildCSS(cwd, binaryPath, enableMangle) {
|
|
178
1222
|
const resolvedConfigPath = findConfigFile(cwd);
|
|
179
1223
|
const run = (cfgPath) => {
|
|
1224
|
+
const disableConfigMangle = !enableMangle && import_runner.default.configEnablesMangling({
|
|
1225
|
+
configPath: cfgPath,
|
|
1226
|
+
fs: fs.default
|
|
1227
|
+
});
|
|
180
1228
|
const args = import_runner.default.buildArgs({
|
|
181
1229
|
mode: "output",
|
|
182
1230
|
output,
|
|
183
1231
|
cssLayers: outputToCssLayers,
|
|
184
|
-
configPath: cfgPath
|
|
1232
|
+
configPath: cfgPath,
|
|
1233
|
+
mangleClassNames: enableMangle ? true : disableConfigMangle ? false : void 0,
|
|
1234
|
+
mangleMap: enableMangle ? mangleMap : void 0,
|
|
1235
|
+
mangleExclude: enableMangle ? mangleExclude : void 0
|
|
185
1236
|
});
|
|
186
1237
|
try {
|
|
187
1238
|
const result = (0, child_process.spawnSync)(binaryPath, args, {
|
|
@@ -195,6 +1246,24 @@ function cssUtility(opts = {}) {
|
|
|
195
1246
|
});
|
|
196
1247
|
if (result.status !== 0) throw new Error(`gustcss build failed: ${result.stderr}`);
|
|
197
1248
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
1249
|
+
if (enableMangle) {
|
|
1250
|
+
const manifestPath = path.default.resolve(cwd, mangleMap);
|
|
1251
|
+
const manifestText = fs.default.readFileSync(manifestPath, "utf-8");
|
|
1252
|
+
const manifest = JSON.parse(manifestText);
|
|
1253
|
+
const classes = manifest?.classes;
|
|
1254
|
+
const entries = classes && !Array.isArray(classes) ? Object.entries(classes) : [];
|
|
1255
|
+
const shortNames = entries.map(([, short]) => short);
|
|
1256
|
+
const validEntries = entries.every(([original, short]) => original.length > 0 && typeof short === "string" && /^[A-Za-z]+$/.test(short));
|
|
1257
|
+
if (manifest?.version !== 1 || !classes || Array.isArray(classes) || !validEntries || new Set(shortNames).size !== shortNames.length) throw new Error(`invalid class-name manifest: ${manifestPath}`);
|
|
1258
|
+
classNameTransformer = createClassNameTransformer(manifest.classes);
|
|
1259
|
+
manifestSnapshot = {
|
|
1260
|
+
path: manifestPath,
|
|
1261
|
+
text: manifestText
|
|
1262
|
+
};
|
|
1263
|
+
} else {
|
|
1264
|
+
classNameTransformer = null;
|
|
1265
|
+
manifestSnapshot = null;
|
|
1266
|
+
}
|
|
198
1267
|
} catch (error) {
|
|
199
1268
|
throw new Error(`gustcss build failed: ${error.message}`);
|
|
200
1269
|
}
|
|
@@ -207,25 +1276,66 @@ function cssUtility(opts = {}) {
|
|
|
207
1276
|
fs: fs.default
|
|
208
1277
|
}, (tempConfigPath) => run(tempConfigPath));
|
|
209
1278
|
}
|
|
1279
|
+
let viteConfig = null;
|
|
210
1280
|
return {
|
|
211
1281
|
name: "gustcss",
|
|
1282
|
+
...mangleClassNames ? { enforce: "pre" } : {},
|
|
212
1283
|
configResolved(config) {
|
|
1284
|
+
viteConfig = config;
|
|
213
1285
|
const cwd = config.root || process.cwd();
|
|
214
1286
|
const binaryPath = findBinary(cwd);
|
|
215
1287
|
try {
|
|
216
|
-
|
|
1288
|
+
const enableMangle = mangleClassNames && config.command === "build";
|
|
1289
|
+
buildCSS(cwd, binaryPath, enableMangle);
|
|
1290
|
+
loadedAstroModules.clear();
|
|
1291
|
+
astroTransformer = null;
|
|
1292
|
+
rollbackContext = enableMangle ? {
|
|
1293
|
+
cwd,
|
|
1294
|
+
binaryPath
|
|
1295
|
+
} : null;
|
|
217
1296
|
} catch (error) {
|
|
218
1297
|
console.error(`[gustcss] Failed to build CSS: ${error.message}`);
|
|
219
1298
|
throw error;
|
|
220
1299
|
}
|
|
221
1300
|
},
|
|
1301
|
+
async load(id) {
|
|
1302
|
+
if (!classNameTransformer || !ASTRO_MODULE.test(id)) return null;
|
|
1303
|
+
const filePath = id.startsWith("/@fs/") ? id.slice(4) : id;
|
|
1304
|
+
if (!fs.default.existsSync(filePath)) throw new Error(`[gustcss] cannot read ${id} to rewrite its class names for mangling. Disable mangleClassNames or exclude its classes with mangleExclude.`);
|
|
1305
|
+
const code = fs.default.readFileSync(filePath, "utf-8");
|
|
1306
|
+
loadedAstroModules.add(id);
|
|
1307
|
+
if (!astroTransformer) {
|
|
1308
|
+
const parse = await resolveAstroCompiler(viteConfig?.root || process.cwd());
|
|
1309
|
+
if (!parse) throw new Error(`[gustcss] @astrojs/compiler could not be resolved from the project, so ${id} cannot be mangled. Install astro, or disable mangleClassNames.`);
|
|
1310
|
+
astroTransformer = createAstroTransformer({
|
|
1311
|
+
parse,
|
|
1312
|
+
classes: classNameTransformer.classes
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
const result = await astroTransformer(code, filePath);
|
|
1316
|
+
return result.code === code ? null : result;
|
|
1317
|
+
},
|
|
1318
|
+
transform(code, id) {
|
|
1319
|
+
if (!classNameTransformer) return null;
|
|
1320
|
+
if (ASTRO_MODULE.test(id)) {
|
|
1321
|
+
if (!loadedAstroModules.has(id)) throw new Error(`[gustcss] ${id} was compiled without passing through the gustcss load hook, so its class names cannot be mangled safely. Disable mangleClassNames or move the plugin so it loads .astro sources first.`);
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1324
|
+
if (ASTRO_SOURCE.test(id)) return null;
|
|
1325
|
+
const transformed = classNameTransformer.withSourceMap(code, id);
|
|
1326
|
+
return transformed.code === code ? null : transformed;
|
|
1327
|
+
},
|
|
1328
|
+
transformIndexHtml(html, context) {
|
|
1329
|
+
if (!classNameTransformer) return html;
|
|
1330
|
+
return classNameTransformer(html, context?.filename || "index.html");
|
|
1331
|
+
},
|
|
222
1332
|
configureServer(server) {
|
|
223
1333
|
const cwd = server.config.root || process.cwd();
|
|
224
1334
|
const binaryPath = findBinary(cwd);
|
|
225
1335
|
const resolvedConfigPath = findConfigFile(cwd);
|
|
226
1336
|
const outputPath = path.default.resolve(cwd, output);
|
|
227
1337
|
try {
|
|
228
|
-
buildCSS(cwd, binaryPath);
|
|
1338
|
+
buildCSS(cwd, binaryPath, false);
|
|
229
1339
|
} catch (error) {
|
|
230
1340
|
console.error(`[gustcss] Failed to build CSS: ${error.message}`);
|
|
231
1341
|
}
|
|
@@ -237,13 +1347,18 @@ function cssUtility(opts = {}) {
|
|
|
237
1347
|
fs.default.writeFileSync(tempConfigPath, JSON.stringify({ content }), { flag: "wx" });
|
|
238
1348
|
watchConfigPath = tempConfigPath;
|
|
239
1349
|
}
|
|
240
|
-
|
|
1350
|
+
const watchArgs = import_runner.default.buildArgs({
|
|
241
1351
|
mode: "output",
|
|
242
1352
|
output,
|
|
243
1353
|
cssLayers: outputToCssLayers,
|
|
244
1354
|
configPath: watchConfigPath,
|
|
245
|
-
watch: true
|
|
246
|
-
|
|
1355
|
+
watch: true,
|
|
1356
|
+
mangleClassNames: import_runner.default.configEnablesMangling({
|
|
1357
|
+
configPath: watchConfigPath,
|
|
1358
|
+
fs: fs.default
|
|
1359
|
+
}) ? false : void 0
|
|
1360
|
+
});
|
|
1361
|
+
watchProcess = (0, child_process.spawn)(binaryPath, watchArgs, {
|
|
247
1362
|
cwd,
|
|
248
1363
|
stdio: [
|
|
249
1364
|
"pipe",
|
|
@@ -273,15 +1388,39 @@ function cssUtility(opts = {}) {
|
|
|
273
1388
|
}
|
|
274
1389
|
if (tempConfigPath && fs.default.existsSync(tempConfigPath)) fs.default.unlinkSync(tempConfigPath);
|
|
275
1390
|
};
|
|
1391
|
+
const close = server.close.bind(server);
|
|
1392
|
+
server.close = async () => {
|
|
1393
|
+
cleanup();
|
|
1394
|
+
return close();
|
|
1395
|
+
};
|
|
276
1396
|
server.httpServer?.on("close", cleanup);
|
|
277
1397
|
process.once("SIGINT", cleanup);
|
|
278
1398
|
process.once("SIGTERM", cleanup);
|
|
1399
|
+
process.once("exit", cleanup);
|
|
1400
|
+
},
|
|
1401
|
+
writeBundle() {
|
|
1402
|
+
if (!manifestSnapshot || fs.default.existsSync(manifestSnapshot.path)) return;
|
|
1403
|
+
fs.default.mkdirSync(path.default.dirname(manifestSnapshot.path), { recursive: true });
|
|
1404
|
+
fs.default.writeFileSync(manifestSnapshot.path, manifestSnapshot.text);
|
|
279
1405
|
},
|
|
280
|
-
buildEnd() {
|
|
1406
|
+
buildEnd(error) {
|
|
281
1407
|
if (watchProcess) {
|
|
282
1408
|
watchProcess.kill();
|
|
283
1409
|
watchProcess = null;
|
|
284
1410
|
}
|
|
1411
|
+
if (error && rollbackContext) {
|
|
1412
|
+
const { cwd, binaryPath } = rollbackContext;
|
|
1413
|
+
rollbackContext = null;
|
|
1414
|
+
classNameTransformer = null;
|
|
1415
|
+
manifestSnapshot = null;
|
|
1416
|
+
try {
|
|
1417
|
+
buildCSS(cwd, binaryPath, false);
|
|
1418
|
+
const manifestPath = path.default.resolve(cwd, mangleMap);
|
|
1419
|
+
if (fs.default.existsSync(manifestPath)) fs.default.unlinkSync(manifestPath);
|
|
1420
|
+
} catch (rollbackError) {
|
|
1421
|
+
console.error(`[gustcss] Failed to restore readable CSS: ${rollbackError.message}`);
|
|
1422
|
+
}
|
|
1423
|
+
} else rollbackContext = null;
|
|
285
1424
|
}
|
|
286
1425
|
};
|
|
287
1426
|
}
|