@gustcss/vite 0.10.0 → 0.11.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 +45 -3
- package/dist/index.cjs +822 -11
- package/dist/index.d.ts +22 -0
- package/dist/index.mjs +821 -11
- package/package.json +3 -2
package/dist/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { spawn, spawnSync } from "child_process";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import fs from "fs";
|
|
5
5
|
import { randomBytes } from "crypto";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
6
7
|
//#region \0rolldown/runtime.js
|
|
7
8
|
var __create = Object.create;
|
|
8
9
|
var __defProp = Object.defineProperty;
|
|
@@ -164,8 +165,107 @@ var import_runner = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
|
|
|
164
165
|
})))(), 1);
|
|
165
166
|
const SUPPORTED_SOURCE = /\.(?:[cm]?[jt]sx?|html)(?:$|\?)/;
|
|
166
167
|
const HTML_SOURCE = /\.html(?:$|\?)/;
|
|
167
|
-
const UNSUPPORTED_FRAMEWORK_SOURCE = /\.(?:vue|
|
|
168
|
+
const UNSUPPORTED_FRAMEWORK_SOURCE = /\.(?:vue|svelte)(?:$|\?)/;
|
|
168
169
|
const DEPENDENCY_SOURCE = /(?:^|[\\/])node_modules[\\/]/;
|
|
170
|
+
const CLASS_FUNCTION_NAMES = [
|
|
171
|
+
"cn",
|
|
172
|
+
"cx",
|
|
173
|
+
"clsx",
|
|
174
|
+
"classNames",
|
|
175
|
+
"cva",
|
|
176
|
+
"tv",
|
|
177
|
+
"twMerge",
|
|
178
|
+
"twJoin"
|
|
179
|
+
];
|
|
180
|
+
const CLASS_FUNCTION_CALL = new RegExp(`(?<![\\w$])(${CLASS_FUNCTION_NAMES.join("|")})\\s*\\(`, "g");
|
|
181
|
+
const VARIANT_FUNCTION_NAMES = /* @__PURE__ */ new Set(["cva", "tv"]);
|
|
182
|
+
const NESTED_CALL = /(?:^|[^\w$])([A-Za-z_$][\w$]*)\s*\(/g;
|
|
183
|
+
function nestedCallRanges(args) {
|
|
184
|
+
const ranges = [];
|
|
185
|
+
NESTED_CALL.lastIndex = 0;
|
|
186
|
+
let match;
|
|
187
|
+
while ((match = NESTED_CALL.exec(args)) !== null) {
|
|
188
|
+
const open = args.indexOf("(", match.index + match[0].length - 1);
|
|
189
|
+
if (open < 0) continue;
|
|
190
|
+
const end = findBalancedEnd(args, open, "(", ")");
|
|
191
|
+
if (end < 0) continue;
|
|
192
|
+
ranges.push({
|
|
193
|
+
start: match.index,
|
|
194
|
+
end: end + 1,
|
|
195
|
+
isHelper: CLASS_FUNCTION_NAMES.includes(match[1])
|
|
196
|
+
});
|
|
197
|
+
NESTED_CALL.lastIndex = open + 1;
|
|
198
|
+
}
|
|
199
|
+
return ranges;
|
|
200
|
+
}
|
|
201
|
+
function innermostCallAt(ranges, index) {
|
|
202
|
+
let found = null;
|
|
203
|
+
for (const range of ranges) if (index >= range.start && index < range.end) {
|
|
204
|
+
if (found === null || range.start > found.start) found = range;
|
|
205
|
+
}
|
|
206
|
+
return found;
|
|
207
|
+
}
|
|
208
|
+
function parenDepthAt(value, index) {
|
|
209
|
+
let depth = 0;
|
|
210
|
+
for (let i = 0; i < index; i += 1) if (value[i] === "(") depth += 1;
|
|
211
|
+
else if (value[i] === ")") depth -= 1;
|
|
212
|
+
return depth;
|
|
213
|
+
}
|
|
214
|
+
function isComparisonOperand(args, segment) {
|
|
215
|
+
let before = segment.start - 1;
|
|
216
|
+
while (before >= 0 && /\s/.test(args[before])) before -= 1;
|
|
217
|
+
const lead = before >= 1 ? args.slice(before - 1, before + 1) : "";
|
|
218
|
+
if (lead === "==" || lead === "!=" || before >= 1 && args[before] === "=") return true;
|
|
219
|
+
let after = segment.end;
|
|
220
|
+
while (after < args.length && /\s/.test(args[after])) after += 1;
|
|
221
|
+
const tail = args.slice(after, after + 2);
|
|
222
|
+
return tail === "==" || tail === "!=";
|
|
223
|
+
}
|
|
224
|
+
function defaultVariantsRanges(args) {
|
|
225
|
+
const ranges = [];
|
|
226
|
+
const pattern = /defaultVariants\s*:\s*\{/g;
|
|
227
|
+
let match;
|
|
228
|
+
while ((match = pattern.exec(args)) !== null) {
|
|
229
|
+
const open = args.indexOf("{", match.index);
|
|
230
|
+
const end = findBalancedEnd(args, open, "{", "}");
|
|
231
|
+
if (end < 0) break;
|
|
232
|
+
ranges.push([open, end + 1]);
|
|
233
|
+
pattern.lastIndex = end;
|
|
234
|
+
}
|
|
235
|
+
return ranges;
|
|
236
|
+
}
|
|
237
|
+
function inRanges(ranges, index) {
|
|
238
|
+
return ranges.some(([start, end]) => index >= start && index < end);
|
|
239
|
+
}
|
|
240
|
+
function maskIgnoredRanges(text, origins, ranges) {
|
|
241
|
+
if (ranges.length === 0) return text;
|
|
242
|
+
const chars = text.split("");
|
|
243
|
+
let sorted = [...ranges].sort((a, b) => a[0] - b[0]);
|
|
244
|
+
let cursor = 0;
|
|
245
|
+
for (let i = 0; i < chars.length && cursor < sorted.length; i += 1) {
|
|
246
|
+
const origin = origins[i];
|
|
247
|
+
while (cursor < sorted.length && origin >= sorted[cursor][1]) cursor += 1;
|
|
248
|
+
if (cursor >= sorted.length) break;
|
|
249
|
+
if (origin >= sorted[cursor][0] && origin < sorted[cursor][1]) {
|
|
250
|
+
if (chars[i] !== "\n") chars[i] = " ";
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return chars.join("");
|
|
254
|
+
}
|
|
255
|
+
function stripObjectKeys(value) {
|
|
256
|
+
return value.replace(/([{,]\s*)([A-Za-z_$][\w$]*)(\s*:)/g, (_match, lead, name, tail) => {
|
|
257
|
+
return lead + " ".repeat(name.length) + tail;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
function isObjectKey(args, segment) {
|
|
261
|
+
let before = segment.start - 1;
|
|
262
|
+
while (before >= 0 && /\s/.test(args[before])) before -= 1;
|
|
263
|
+
const lead = before >= 0 ? args[before] : "";
|
|
264
|
+
if (lead !== "{" && lead !== ",") return false;
|
|
265
|
+
let after = segment.end;
|
|
266
|
+
while (after < args.length && /\s/.test(args[after])) after += 1;
|
|
267
|
+
return args[after] === ":";
|
|
268
|
+
}
|
|
169
269
|
function hasOwn(classes, token) {
|
|
170
270
|
return Object.hasOwn(classes, token);
|
|
171
271
|
}
|
|
@@ -210,6 +310,7 @@ function findRawTextClosing(code, tagName, start) {
|
|
|
210
310
|
function analyzeHtml(code, id) {
|
|
211
311
|
const markup = new Array(code.length).fill(" ");
|
|
212
312
|
const scriptRanges = [];
|
|
313
|
+
const commentRanges = [];
|
|
213
314
|
const exampleStack = [];
|
|
214
315
|
let index = 0;
|
|
215
316
|
while (index < code.length) {
|
|
@@ -217,7 +318,12 @@ function analyzeHtml(code, id) {
|
|
|
217
318
|
if (open < 0) break;
|
|
218
319
|
if (code.startsWith("<!--", open)) {
|
|
219
320
|
const commentEnd = code.indexOf("-->", open + 4);
|
|
220
|
-
|
|
321
|
+
const end = commentEnd < 0 ? code.length : commentEnd + 3;
|
|
322
|
+
commentRanges.push({
|
|
323
|
+
start: open,
|
|
324
|
+
end
|
|
325
|
+
});
|
|
326
|
+
index = end;
|
|
221
327
|
continue;
|
|
222
328
|
}
|
|
223
329
|
const tagEnd = findHtmlTagEnd(code, open);
|
|
@@ -253,7 +359,8 @@ function analyzeHtml(code, id) {
|
|
|
253
359
|
if (exampleStack.length > 0) throw new Error(`[gustcss] unclosed <${exampleStack.at(-1)}> in ${id}`);
|
|
254
360
|
return {
|
|
255
361
|
markup: markup.join(""),
|
|
256
|
-
scriptRanges
|
|
362
|
+
scriptRanges,
|
|
363
|
+
commentRanges
|
|
257
364
|
};
|
|
258
365
|
}
|
|
259
366
|
function canStartRegex(code, index, previous) {
|
|
@@ -481,7 +588,7 @@ function rejectAmbiguousClassExpressions(code, structure, classes, id) {
|
|
|
481
588
|
while ((match = pattern.exec(structure)) !== null) {
|
|
482
589
|
const open = code.indexOf("{", match.index);
|
|
483
590
|
const end = findBalancedEnd(code, open, "{", "}");
|
|
484
|
-
if (end < 0)
|
|
591
|
+
if (end < 0) throw new Error(`[gustcss] incomplete class/className expression in ${id}. Ensure the expression is properly closed.`);
|
|
485
592
|
const ambiguous = findMappedToken(code.slice(open + 1, end - 1), classes);
|
|
486
593
|
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
594
|
pattern.lastIndex = end;
|
|
@@ -508,6 +615,44 @@ function collectClassListCallEdits(code, structure, classes, id, edits) {
|
|
|
508
615
|
pattern.lastIndex = end;
|
|
509
616
|
}
|
|
510
617
|
}
|
|
618
|
+
function collectClassFunctionCallEdits(code, structure, classes, id, edits, ignores) {
|
|
619
|
+
CLASS_FUNCTION_CALL.lastIndex = 0;
|
|
620
|
+
let match;
|
|
621
|
+
while ((match = CLASS_FUNCTION_CALL.exec(structure)) !== null) {
|
|
622
|
+
const name = match[1];
|
|
623
|
+
const open = code.indexOf("(", match.index + name.length);
|
|
624
|
+
if (open < 0) continue;
|
|
625
|
+
const end = findBalancedEnd(code, open, "(", ")");
|
|
626
|
+
if (end < 0) throw new Error(`[gustcss] incomplete ${name}(...) call in ${id}. Ensure the expression is properly closed.`);
|
|
627
|
+
const args = code.slice(open + 1, end - 1);
|
|
628
|
+
const argsStart = open + 1;
|
|
629
|
+
const skipKeys = VARIANT_FUNCTION_NAMES.has(name);
|
|
630
|
+
const skipRanges = skipKeys ? defaultVariantsRanges(args) : [];
|
|
631
|
+
const nestedCalls = nestedCallRanges(args);
|
|
632
|
+
const depthSource = stripQuotedSegments(args);
|
|
633
|
+
scanQuotedSegments(args, (segment) => {
|
|
634
|
+
const { start, quote, contents } = segment;
|
|
635
|
+
const nested = innermostCallAt(nestedCalls, start);
|
|
636
|
+
if (!(nested !== null && nested.isHelper) && parenDepthAt(depthSource, start) > 0) return;
|
|
637
|
+
if (skipKeys && quote !== "`" && isObjectKey(args, segment) || inRanges(skipRanges, segment.start) || isComparisonOperand(args, segment)) {
|
|
638
|
+
ignores.push([argsStart + segment.start, argsStart + segment.end]);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const offset = argsStart + start + 1;
|
|
642
|
+
if (quote === "`") {
|
|
643
|
+
collectTemplateBodyEdits(contents, classes, offset, edits);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
collectClassListEdits(contents, classes, offset, edits);
|
|
647
|
+
});
|
|
648
|
+
let withoutStrings = stripQuotedSegments(args);
|
|
649
|
+
if (skipKeys) withoutStrings = stripObjectKeys(withoutStrings);
|
|
650
|
+
for (const [rangeStart, rangeEnd] of skipRanges) withoutStrings = withoutStrings.slice(0, rangeStart) + " ".repeat(rangeEnd - rangeStart) + withoutStrings.slice(rangeEnd);
|
|
651
|
+
const dynamic = findMappedToken(withoutStrings, classes);
|
|
652
|
+
if (dynamic) throw new Error(`[gustcss] mapped class "${dynamic}" remains in a dynamic argument of ${name}(...) in ${id}. Pass it as a direct string literal, or add it to mangleExclude.`);
|
|
653
|
+
CLASS_FUNCTION_CALL.lastIndex = end;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
511
656
|
function rejectDynamicSetAttributeCalls(code, structure, classes, id) {
|
|
512
657
|
const pattern = /\.setAttribute\s*\(/g;
|
|
513
658
|
let match;
|
|
@@ -575,40 +720,94 @@ function sourcesForFile(code, id) {
|
|
|
575
720
|
const inspection = new Array(code.length).fill(" ");
|
|
576
721
|
for (const range of html.scriptRanges) {
|
|
577
722
|
const script = code.slice(range.start, range.end);
|
|
578
|
-
const
|
|
723
|
+
const jsScript = createJsMasks(script);
|
|
579
724
|
for (let offset = 0; offset < script.length; offset += 1) {
|
|
580
|
-
structure[range.start + offset] =
|
|
581
|
-
inspection[range.start + offset] =
|
|
725
|
+
structure[range.start + offset] = jsScript.structure[offset];
|
|
726
|
+
inspection[range.start + offset] = jsScript.inspection[offset];
|
|
582
727
|
}
|
|
583
728
|
}
|
|
729
|
+
for (const range of html.commentRanges) for (let offset = range.start; offset < range.end; offset += 1) inspection[offset] = " ";
|
|
584
730
|
return {
|
|
585
731
|
attributes: html.markup,
|
|
586
732
|
structure: structure.join(""),
|
|
587
733
|
inspection: inspection.join("")
|
|
588
734
|
};
|
|
589
735
|
}
|
|
590
|
-
function
|
|
736
|
+
function collectEdits(code, classes, id) {
|
|
591
737
|
const edits = [];
|
|
738
|
+
const ignores = [];
|
|
592
739
|
const sources = sourcesForFile(code, id);
|
|
740
|
+
const commentRanges = [];
|
|
741
|
+
let commentIndex = 0;
|
|
742
|
+
while ((commentIndex = code.indexOf("<!--", commentIndex)) >= 0) {
|
|
743
|
+
const commentEnd = code.indexOf("-->", commentIndex + 4);
|
|
744
|
+
if (commentEnd < 0) {
|
|
745
|
+
commentRanges.push({
|
|
746
|
+
start: commentIndex,
|
|
747
|
+
end: code.length
|
|
748
|
+
});
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
commentRanges.push({
|
|
752
|
+
start: commentIndex,
|
|
753
|
+
end: commentEnd + 3
|
|
754
|
+
});
|
|
755
|
+
commentIndex = commentEnd + 3;
|
|
756
|
+
}
|
|
757
|
+
function isInComment(pos) {
|
|
758
|
+
return commentRanges.some((range) => pos >= range.start && pos < range.end);
|
|
759
|
+
}
|
|
593
760
|
const attributePattern = /((?<![:\w-])class(?:Name)?\s*=\s*)(?:(["'])([\s\S]*?)(\2)|(\{\s*)(["'])([\s\S]*?)(\6)(\s*\}))/g;
|
|
594
761
|
let match;
|
|
595
762
|
while ((match = attributePattern.exec(sources.attributes)) !== null) {
|
|
763
|
+
if (isInComment(match.index)) continue;
|
|
596
764
|
if (!HTML_SOURCE.test(id) && !isJsxAttributeAt(sources.structure, match.index)) continue;
|
|
597
765
|
if (match[2] !== void 0) collectClassListEdits(match[3], classes, match.index + match[1].length + 1, edits);
|
|
598
766
|
else collectClassListEdits(match[7], classes, match.index + match[1].length + match[5].length + 1, edits);
|
|
599
767
|
}
|
|
600
768
|
const templatePattern = /((?<![:\w-])class(?:Name)?\s*=\s*\{\s*`)([\s\S]*?)(`\s*\})/g;
|
|
601
769
|
while ((match = templatePattern.exec(sources.attributes)) !== null) {
|
|
770
|
+
if (isInComment(match.index)) continue;
|
|
602
771
|
if (!HTML_SOURCE.test(id) && !isJsxAttributeAt(sources.structure, match.index)) continue;
|
|
603
772
|
collectTemplateBodyEdits(match[2], classes, match.index + match[1].length, edits);
|
|
604
773
|
}
|
|
605
774
|
collectClassListCallEdits(code, sources.structure, classes, id, edits);
|
|
775
|
+
collectClassFunctionCallEdits(code, sources.structure, classes, id, edits, ignores);
|
|
606
776
|
const setAttributePattern = /(\.setAttribute\(\s*["']class["']\s*,\s*)(["'])([\s\S]*?)(\2)(\s*\))/g;
|
|
607
777
|
while ((match = setAttributePattern.exec(code)) !== null) {
|
|
608
778
|
if (sources.structure[match.index] === " ") continue;
|
|
609
779
|
collectClassListEdits(match[3], classes, match.index + match[1].length + 1, edits);
|
|
610
780
|
}
|
|
611
|
-
return
|
|
781
|
+
return {
|
|
782
|
+
edits,
|
|
783
|
+
ignores
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
function collectRecognizedContextEdits(code, classes, id) {
|
|
787
|
+
const collected = collectEdits(code, classes, id);
|
|
788
|
+
return {
|
|
789
|
+
...applyEdits(code, collected.edits),
|
|
790
|
+
ignores: collected.ignores
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Collect the rewrites for a source fragment (TypeScript frontmatter, an inline
|
|
795
|
+
* `<script>` body, …) and run the same fail-closed checks as `transformSource`,
|
|
796
|
+
* but return the edits instead of the rewritten code so the caller can place
|
|
797
|
+
* them inside a larger document.
|
|
798
|
+
*/
|
|
799
|
+
function collectFragmentEdits(code, classes, id) {
|
|
800
|
+
const collected = collectEdits(code, classes, id);
|
|
801
|
+
const applied = applyEdits(code, collected.edits);
|
|
802
|
+
const rewritten = applied.code;
|
|
803
|
+
const sources = sourcesForFile(rewritten, id);
|
|
804
|
+
const inspection = maskIgnoredRanges(sources.inspection, applied.origins, collected.ignores);
|
|
805
|
+
rejectDynamicSetAttributeCalls(rewritten, sources.structure, classes, id);
|
|
806
|
+
rejectDynamicClassNameAssignments(rewritten, sources.structure, classes, id);
|
|
807
|
+
rejectAmbiguousClassExpressions(rewritten, sources.structure, classes, id);
|
|
808
|
+
const ambiguous = findMappedInQuotedSegments(inspection, classes);
|
|
809
|
+
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.`);
|
|
810
|
+
return collected.edits;
|
|
612
811
|
}
|
|
613
812
|
const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
614
813
|
function encodeVLQ(value) {
|
|
@@ -676,13 +875,14 @@ function transformSource(code, classes, id) {
|
|
|
676
875
|
const transformed = collectRecognizedContextEdits(code, classes, id);
|
|
677
876
|
const rewritten = transformed.code;
|
|
678
877
|
const sources = sourcesForFile(rewritten, id);
|
|
878
|
+
const inspection = maskIgnoredRanges(sources.inspection, transformed.origins, transformed.ignores);
|
|
679
879
|
rejectDynamicSetAttributeCalls(rewritten, sources.structure, classes, id);
|
|
680
880
|
rejectDynamicClassNameAssignments(rewritten, sources.structure, classes, id);
|
|
681
881
|
rejectAmbiguousClassExpressions(rewritten, sources.structure, classes, id);
|
|
682
882
|
return {
|
|
683
883
|
code: rewritten,
|
|
684
884
|
map: createSourceMap(code, rewritten, transformed.origins, id),
|
|
685
|
-
inspection
|
|
885
|
+
inspection
|
|
686
886
|
};
|
|
687
887
|
}
|
|
688
888
|
function findAmbiguousMappedClass(code, classes) {
|
|
@@ -696,7 +896,7 @@ function createClassNameTransformer(classes) {
|
|
|
696
896
|
const transformWithSourceMap = (code, id) => {
|
|
697
897
|
if (UNSUPPORTED_FRAMEWORK_SOURCE.test(id)) {
|
|
698
898
|
const referenced = findMappedToken(code, classes);
|
|
699
|
-
if (referenced) throw new Error(`[gustcss] class-name mangling does not support Vue
|
|
899
|
+
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.`);
|
|
700
900
|
return {
|
|
701
901
|
code,
|
|
702
902
|
map: null
|
|
@@ -716,9 +916,392 @@ function createClassNameTransformer(classes) {
|
|
|
716
916
|
};
|
|
717
917
|
const transform = (code, id) => transformWithSourceMap(code, id).code;
|
|
718
918
|
transform.withSourceMap = transformWithSourceMap;
|
|
919
|
+
transform.classes = classes;
|
|
719
920
|
return transform;
|
|
720
921
|
}
|
|
721
922
|
//#endregion
|
|
923
|
+
//#region src/astro.js
|
|
924
|
+
/**
|
|
925
|
+
* Class-name mangling for `.astro` sources, driven by the AST that
|
|
926
|
+
* `@astrojs/compiler`'s `parse()` returns.
|
|
927
|
+
*
|
|
928
|
+
* Only unambiguous class contexts are rewritten; any other place where a
|
|
929
|
+
* mapped class name shows up fails the build (fail-closed), because a class
|
|
930
|
+
* that reaches the DOM unrenamed would no longer match the mangled CSS.
|
|
931
|
+
*/
|
|
932
|
+
const CLASS_VALUED_BEFORE = /* @__PURE__ */ new Set([
|
|
933
|
+
"[",
|
|
934
|
+
",",
|
|
935
|
+
"&&",
|
|
936
|
+
"||"
|
|
937
|
+
]);
|
|
938
|
+
const CLASS_VALUED_AFTER = /* @__PURE__ */ new Set([
|
|
939
|
+
",",
|
|
940
|
+
"]",
|
|
941
|
+
"&&",
|
|
942
|
+
"||"
|
|
943
|
+
]);
|
|
944
|
+
const OPERATORS = [
|
|
945
|
+
"===",
|
|
946
|
+
"!==",
|
|
947
|
+
"&&",
|
|
948
|
+
"||",
|
|
949
|
+
"??",
|
|
950
|
+
"=>",
|
|
951
|
+
"==",
|
|
952
|
+
"!=",
|
|
953
|
+
"?.",
|
|
954
|
+
"<=",
|
|
955
|
+
">="
|
|
956
|
+
];
|
|
957
|
+
const PUNCTUATION = "[]{}(),:?.!+-*/%<>=&|~^;";
|
|
958
|
+
/** Map UTF-8 byte offsets (what the compiler reports) to string indexes. */
|
|
959
|
+
function byteToIndexMap(code) {
|
|
960
|
+
const map = [];
|
|
961
|
+
let byte = 0;
|
|
962
|
+
for (let index = 0; index < code.length; index += 1) {
|
|
963
|
+
const point = code.codePointAt(index);
|
|
964
|
+
const width = point < 128 ? 1 : point < 2048 ? 2 : point < 65536 ? 3 : 4;
|
|
965
|
+
for (let i = 0; i < width; i += 1) map[byte + i] = index;
|
|
966
|
+
byte += width;
|
|
967
|
+
if (point > 65535) index += 1;
|
|
968
|
+
}
|
|
969
|
+
map[byte] = code.length;
|
|
970
|
+
return map;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Tokenize a JavaScript expression just far enough to know, for every string
|
|
974
|
+
* literal and identifier, what surrounds it. Template literals are reported
|
|
975
|
+
* as strings with quote "`" and are never rewritten. Returns null when the
|
|
976
|
+
* expression cannot be tokenized (unterminated string, unknown character).
|
|
977
|
+
*/
|
|
978
|
+
function tokenizeExpression(source) {
|
|
979
|
+
const tokens = [];
|
|
980
|
+
let index = 0;
|
|
981
|
+
while (index < source.length) {
|
|
982
|
+
const char = source[index];
|
|
983
|
+
if (/\s/.test(char)) {
|
|
984
|
+
index += 1;
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
if (source.startsWith("//", index)) {
|
|
988
|
+
const newline = source.indexOf("\n", index);
|
|
989
|
+
index = newline < 0 ? source.length : newline + 1;
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (source.startsWith("/*", index)) {
|
|
993
|
+
const close = source.indexOf("*/", index + 2);
|
|
994
|
+
if (close < 0) return null;
|
|
995
|
+
index = close + 2;
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
if (char === "\"" || char === "'" || char === "`") {
|
|
999
|
+
const start = index;
|
|
1000
|
+
index += 1;
|
|
1001
|
+
while (index < source.length && source[index] !== char) {
|
|
1002
|
+
if (source[index] === "\\") index += 1;
|
|
1003
|
+
index += 1;
|
|
1004
|
+
}
|
|
1005
|
+
if (index >= source.length) return null;
|
|
1006
|
+
tokens.push({
|
|
1007
|
+
type: "string",
|
|
1008
|
+
quote: char,
|
|
1009
|
+
start,
|
|
1010
|
+
end: index + 1,
|
|
1011
|
+
value: source.slice(start + 1, index)
|
|
1012
|
+
});
|
|
1013
|
+
index += 1;
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
if (/[A-Za-z_$]/.test(char)) {
|
|
1017
|
+
const start = index;
|
|
1018
|
+
while (index < source.length && /[\w$]/.test(source[index])) index += 1;
|
|
1019
|
+
tokens.push({
|
|
1020
|
+
type: "identifier",
|
|
1021
|
+
start,
|
|
1022
|
+
end: index,
|
|
1023
|
+
value: source.slice(start, index)
|
|
1024
|
+
});
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
if (/[0-9]/.test(char)) {
|
|
1028
|
+
const start = index;
|
|
1029
|
+
while (index < source.length && /[\w.]/.test(source[index])) index += 1;
|
|
1030
|
+
tokens.push({
|
|
1031
|
+
type: "number",
|
|
1032
|
+
start,
|
|
1033
|
+
end: index,
|
|
1034
|
+
value: source.slice(start, index)
|
|
1035
|
+
});
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
const operator = OPERATORS.find((candidate) => source.startsWith(candidate, index));
|
|
1039
|
+
if (operator) {
|
|
1040
|
+
tokens.push({
|
|
1041
|
+
type: "operator",
|
|
1042
|
+
start: index,
|
|
1043
|
+
end: index + operator.length,
|
|
1044
|
+
value: operator
|
|
1045
|
+
});
|
|
1046
|
+
index += operator.length;
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
if (PUNCTUATION.includes(char)) {
|
|
1050
|
+
tokens.push({
|
|
1051
|
+
type: "operator",
|
|
1052
|
+
start: index,
|
|
1053
|
+
end: index + 1,
|
|
1054
|
+
value: char
|
|
1055
|
+
});
|
|
1056
|
+
index += 1;
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
1061
|
+
return tokens;
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Rewrite the class-valued string literals of a `class:list` expression.
|
|
1065
|
+
* Accepted shapes are array literals, object literals and their nesting.
|
|
1066
|
+
* Inside them only array elements (also as the right operand of `&&` / `||`)
|
|
1067
|
+
* and quoted object keys are class-valued; every other occurrence of a mapped
|
|
1068
|
+
* class name fails closed.
|
|
1069
|
+
*/
|
|
1070
|
+
function collectClassListExpressionEdits(source, classes, baseOffset, edits, id) {
|
|
1071
|
+
const fail = (token, reason) => {
|
|
1072
|
+
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.`);
|
|
1073
|
+
};
|
|
1074
|
+
const tokens = tokenizeExpression(source);
|
|
1075
|
+
const first = tokens && tokens[0];
|
|
1076
|
+
const last = tokens && tokens[tokens.length - 1];
|
|
1077
|
+
if (!(tokens && tokens.length >= 2 && (first.value === "[" && last.value === "]" || first.value === "{" && last.value === "}"))) {
|
|
1078
|
+
const mapped = findMappedToken(source, classes);
|
|
1079
|
+
if (mapped) fail(mapped, "remains in an expression that is not an array or object literal");
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
const stack = [];
|
|
1083
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
1084
|
+
const token = tokens[i];
|
|
1085
|
+
const previous = tokens[i - 1];
|
|
1086
|
+
const next = tokens[i + 1];
|
|
1087
|
+
const context = stack[stack.length - 1];
|
|
1088
|
+
if (token.type === "operator") {
|
|
1089
|
+
if (token.value === "[") {
|
|
1090
|
+
const subscript = previous && (previous.type === "identifier" || previous.type === "string" || previous.value === ")" || previous.value === "]");
|
|
1091
|
+
stack.push(subscript ? "subscript" : "array");
|
|
1092
|
+
} else if (token.value === "{") stack.push("object");
|
|
1093
|
+
else if (token.value === "(") stack.push("call");
|
|
1094
|
+
else if (token.value === "]" || token.value === "}" || token.value === ")") {
|
|
1095
|
+
if (stack.length === 0) fail(findMappedToken(source, classes) || "?", "appears in an unbalanced");
|
|
1096
|
+
stack.pop();
|
|
1097
|
+
}
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
if (token.type === "identifier") {
|
|
1101
|
+
if (!hasOwn(classes, token.value)) continue;
|
|
1102
|
+
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}")`);
|
|
1103
|
+
fail(token.value, "is referenced as an identifier");
|
|
1104
|
+
}
|
|
1105
|
+
if (token.type !== "string") continue;
|
|
1106
|
+
const mapped = findMappedToken(token.value, classes);
|
|
1107
|
+
if (token.quote === "`") {
|
|
1108
|
+
if (mapped) fail(mapped, "remains in a template literal");
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
const arrayElement = context === "array" && previous && CLASS_VALUED_BEFORE.has(previous.value) && next && CLASS_VALUED_AFTER.has(next.value);
|
|
1112
|
+
const objectKey = context === "object" && previous && (previous.value === "{" || previous.value === ",") && next && next.value === ":";
|
|
1113
|
+
if (arrayElement || objectKey) collectClassListEdits(token.value, classes, baseOffset + token.start + 1, edits);
|
|
1114
|
+
else if (mapped) fail(mapped, "remains in a position that is not an array element or object key");
|
|
1115
|
+
}
|
|
1116
|
+
if (stack.length !== 0) fail(findMappedToken(source, classes) || "?", "appears in an unbalanced");
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Find a mapped class referenced by a stylesheet selector, including at-rule
|
|
1120
|
+
* preludes (`@scope (.flex)`) and attribute selectors (`[class~="flex"]`).
|
|
1121
|
+
* Declarations and comments are ignored.
|
|
1122
|
+
*/
|
|
1123
|
+
function findMappedClassInStylesheet(css, classes) {
|
|
1124
|
+
const source = css.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
1125
|
+
let prelude = "";
|
|
1126
|
+
let quote = null;
|
|
1127
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
1128
|
+
const char = source[i];
|
|
1129
|
+
if (quote) {
|
|
1130
|
+
prelude += char;
|
|
1131
|
+
if (char === "\\") {
|
|
1132
|
+
prelude += source[i + 1] || "";
|
|
1133
|
+
i += 1;
|
|
1134
|
+
} else if (char === quote) quote = null;
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
if (char === "\"" || char === "'") {
|
|
1138
|
+
quote = char;
|
|
1139
|
+
prelude += char;
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
if (char === "{") {
|
|
1143
|
+
const found = findMappedClassInSelector(prelude.trim(), classes);
|
|
1144
|
+
if (found) return found;
|
|
1145
|
+
prelude = "";
|
|
1146
|
+
} else if (char === "}" || char === ";") prelude = "";
|
|
1147
|
+
else prelude += char;
|
|
1148
|
+
}
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
function findMappedClassInSelector(selector, classes) {
|
|
1152
|
+
const classPattern = /\.((?:\\.|[A-Za-z0-9_-])+)/g;
|
|
1153
|
+
let match;
|
|
1154
|
+
while ((match = classPattern.exec(selector)) !== null) {
|
|
1155
|
+
const name = match[1].replace(/\\(.)/g, "$1");
|
|
1156
|
+
if (hasOwn(classes, name)) return name;
|
|
1157
|
+
}
|
|
1158
|
+
const attributePattern = /\[\s*class\s*[~|^$*]?=\s*(["']?)([^\]"']+)\1\s*[is]?\s*\]/g;
|
|
1159
|
+
while ((match = attributePattern.exec(selector)) !== null) {
|
|
1160
|
+
const mapped = findMappedToken(match[2], classes);
|
|
1161
|
+
if (mapped) return mapped;
|
|
1162
|
+
}
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Locate the value of an attribute from its start offset (the attribute
|
|
1167
|
+
* name). Returns the index range of the value and its delimiter.
|
|
1168
|
+
*/
|
|
1169
|
+
function locateAttributeValue(code, attribute, start) {
|
|
1170
|
+
let index = start + attribute.name.length;
|
|
1171
|
+
while (index < code.length && /\s/.test(code[index])) index += 1;
|
|
1172
|
+
if (code[index] !== "=") return null;
|
|
1173
|
+
index += 1;
|
|
1174
|
+
while (index < code.length && /\s/.test(code[index])) index += 1;
|
|
1175
|
+
const delimiter = code[index];
|
|
1176
|
+
if (delimiter === "\"" || delimiter === "'" || delimiter === "`") {
|
|
1177
|
+
const end = code.indexOf(delimiter, index + 1);
|
|
1178
|
+
return end < 0 ? null : {
|
|
1179
|
+
start: index + 1,
|
|
1180
|
+
end,
|
|
1181
|
+
delimiter
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
if (delimiter === "{") {
|
|
1185
|
+
const end = findBalancedEnd(code, index, "{", "}");
|
|
1186
|
+
return end < 0 ? null : {
|
|
1187
|
+
start: index + 1,
|
|
1188
|
+
end: end - 1,
|
|
1189
|
+
delimiter
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
const STRING_LITERAL = /^\s*(["'])((?:\\.|(?!\1)[^\\])*)\1\s*$/;
|
|
1195
|
+
/**
|
|
1196
|
+
* Create the `.astro` transformer. `parse` is `@astrojs/compiler`'s parse
|
|
1197
|
+
* function, injected so the plugin and the tests can supply their own copy.
|
|
1198
|
+
*/
|
|
1199
|
+
function createAstroTransformer({ parse, classes }) {
|
|
1200
|
+
return async function transformAstro(code, id) {
|
|
1201
|
+
const { ast } = await parse(code, { position: true });
|
|
1202
|
+
const toIndex = byteToIndexMap(code);
|
|
1203
|
+
const edits = [];
|
|
1204
|
+
const offsetOf = (node) => toIndex[node.position.start.offset];
|
|
1205
|
+
const stop = (mapped, where) => {
|
|
1206
|
+
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.`);
|
|
1207
|
+
};
|
|
1208
|
+
const fragmentEdits = (text, start, kind) => {
|
|
1209
|
+
for (const edit of collectFragmentEdits(text, classes, `${id}#${kind}.ts`)) edits.push({
|
|
1210
|
+
start: start + edit.start,
|
|
1211
|
+
end: start + edit.end,
|
|
1212
|
+
replacement: edit.replacement
|
|
1213
|
+
});
|
|
1214
|
+
};
|
|
1215
|
+
const visitAttribute = (attribute, node) => {
|
|
1216
|
+
const start = offsetOf(attribute);
|
|
1217
|
+
const isComponent = node.type === "component";
|
|
1218
|
+
const value = attribute.value || "";
|
|
1219
|
+
if (attribute.name === "class") {
|
|
1220
|
+
const location = locateAttributeValue(code, attribute, start);
|
|
1221
|
+
if (location && attribute.kind === "quoted") {
|
|
1222
|
+
collectClassListEdits(code.slice(location.start, location.end), classes, location.start, edits);
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
if (location && attribute.kind === "expression") {
|
|
1226
|
+
const expression = code.slice(location.start, location.end);
|
|
1227
|
+
const literal = STRING_LITERAL.exec(expression);
|
|
1228
|
+
if (literal) {
|
|
1229
|
+
const inner = expression.indexOf(literal[1]) + 1;
|
|
1230
|
+
collectClassListEdits(literal[2], classes, location.start + inner, edits);
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
const mapped = findMappedToken(value, classes);
|
|
1235
|
+
if (mapped) stop(mapped, `a dynamic class attribute (${attribute.kind})`);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (attribute.name === "class:list") {
|
|
1239
|
+
const location = locateAttributeValue(code, attribute, start);
|
|
1240
|
+
if (!location || attribute.kind !== "expression") {
|
|
1241
|
+
const mapped = findMappedToken(value, classes);
|
|
1242
|
+
if (mapped) stop(mapped, `a class:list attribute of kind ${attribute.kind}`);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
collectClassListExpressionEdits(code.slice(location.start, location.end), classes, location.start, edits, id);
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
if (attribute.kind === "spread") {
|
|
1249
|
+
const mapped = findMappedToken(`${attribute.name} ${value}`, classes);
|
|
1250
|
+
if (mapped) stop(mapped, `a spread attribute on <${node.name}>`);
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
if (attribute.name === "set:html" || attribute.name === "set:text" || isComponent) {
|
|
1254
|
+
const mapped = findMappedToken(value, classes);
|
|
1255
|
+
if (mapped) stop(mapped, isComponent ? `the "${attribute.name}" prop of <${node.name}>` : `a ${attribute.name} value`);
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
const visit = (node) => {
|
|
1259
|
+
switch (node.type) {
|
|
1260
|
+
case "frontmatter": {
|
|
1261
|
+
const start = code.indexOf(node.value, offsetOf(node));
|
|
1262
|
+
if (start >= 0) fragmentEdits(node.value, start, "frontmatter");
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
case "comment":
|
|
1266
|
+
case "text":
|
|
1267
|
+
case "doctype": return;
|
|
1268
|
+
case "expression":
|
|
1269
|
+
for (const child of node.children) if (child.type === "text") {
|
|
1270
|
+
const mapped = findMappedToken(child.value, classes);
|
|
1271
|
+
if (mapped && /["'`]/.test(child.value)) stop(mapped, "a template expression");
|
|
1272
|
+
} else visit(child);
|
|
1273
|
+
return;
|
|
1274
|
+
case "element":
|
|
1275
|
+
case "component":
|
|
1276
|
+
case "custom-element":
|
|
1277
|
+
case "fragment":
|
|
1278
|
+
for (const attribute of node.attributes) visitAttribute(attribute, node);
|
|
1279
|
+
if (node.type === "element" && node.name === "style") {
|
|
1280
|
+
for (const child of node.children) {
|
|
1281
|
+
if (child.type !== "text") continue;
|
|
1282
|
+
const mapped = findMappedClassInStylesheet(child.value, classes);
|
|
1283
|
+
if (mapped) stop(mapped, "a <style> selector");
|
|
1284
|
+
}
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
if (node.type === "element" && node.name === "script") {
|
|
1288
|
+
for (const child of node.children) if (child.type === "text") fragmentEdits(child.value, offsetOf(child), "script");
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
for (const child of node.children) visit(child);
|
|
1292
|
+
return;
|
|
1293
|
+
default: for (const child of node.children || []) visit(child);
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
visit(ast);
|
|
1297
|
+
const applied = applyEdits(code, edits);
|
|
1298
|
+
return {
|
|
1299
|
+
code: applied.code,
|
|
1300
|
+
map: createSourceMap(code, applied.code, applied.origins, id)
|
|
1301
|
+
};
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
//#endregion
|
|
722
1305
|
//#region src/index.js
|
|
723
1306
|
/**
|
|
724
1307
|
* @gustcss/vite - Vite plugin for CSS Utility Generator
|
|
@@ -733,18 +1316,183 @@ function createClassNameTransformer(classes) {
|
|
|
733
1316
|
* are provided by the developer in their build configuration. This plugin is
|
|
734
1317
|
* designed for build-time use only and should not process untrusted input.
|
|
735
1318
|
*/
|
|
1319
|
+
const ASTRO_SOURCE = /\.astro(?:$|\?)/;
|
|
1320
|
+
const lintDebounceMs = 300;
|
|
1321
|
+
const ASTRO_MODULE = /^[^\0?]*\.astro$/;
|
|
1322
|
+
/**
|
|
1323
|
+
* Resolve @astrojs/compiler parse function from the project.
|
|
1324
|
+
*/
|
|
1325
|
+
async function resolveAstroCompiler(root) {
|
|
1326
|
+
const projectRequire = createRequire(path.join(root, "package.json"));
|
|
1327
|
+
const attempts = [
|
|
1328
|
+
() => projectRequire.resolve("@astrojs/compiler"),
|
|
1329
|
+
() => createRequire(projectRequire.resolve("astro/package.json")).resolve("@astrojs/compiler"),
|
|
1330
|
+
() => createRequire(import.meta.url).resolve("@astrojs/compiler")
|
|
1331
|
+
];
|
|
1332
|
+
for (const attempt of attempts) try {
|
|
1333
|
+
const mod = await import(pathToFileURL(attempt()).href);
|
|
1334
|
+
if (typeof mod.parse === "function") return mod.parse;
|
|
1335
|
+
} catch {}
|
|
1336
|
+
return null;
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Vite logger へ出力する。テストなど logger が無い環境では console を使う。
|
|
1340
|
+
*/
|
|
1341
|
+
function lintLogger(server) {
|
|
1342
|
+
const logger = server?.config?.logger;
|
|
1343
|
+
return {
|
|
1344
|
+
warn: (message) => logger?.warn ? logger.warn(message) : console.warn(message),
|
|
1345
|
+
error: (message) => logger?.error ? logger.error(message) : console.error(message)
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Dev-server lint runner.
|
|
1350
|
+
*
|
|
1351
|
+
* Runs `gustcss lint --json` asynchronously and reports the diagnostics through
|
|
1352
|
+
* the logger. At most one lint process runs at a time; changes that arrive while
|
|
1353
|
+
* it runs are coalesced into one follow-up run. This keeps the dev server's
|
|
1354
|
+
* event loop free even on large projects.
|
|
1355
|
+
*/
|
|
1356
|
+
function createLintRunner({ cwd, binaryPath, configPath, logger, strict }) {
|
|
1357
|
+
const maxOutput = 4194304;
|
|
1358
|
+
const timeoutMs = 15e3;
|
|
1359
|
+
let child = null;
|
|
1360
|
+
let running = false;
|
|
1361
|
+
let dirty = false;
|
|
1362
|
+
let closed = false;
|
|
1363
|
+
let lastSignature = null;
|
|
1364
|
+
function report(code, stdout, stderr, aborted) {
|
|
1365
|
+
if (aborted) return;
|
|
1366
|
+
if (code !== 0 && code !== 1) {
|
|
1367
|
+
logger.warn(`[gustcss] lint could not run: ${stderr.trim()}`);
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
let parsed;
|
|
1371
|
+
try {
|
|
1372
|
+
parsed = JSON.parse(stdout);
|
|
1373
|
+
} catch {
|
|
1374
|
+
logger.warn("[gustcss] lint output was not JSON");
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
const diagnostics = Array.isArray(parsed?.diagnostics) ? parsed.diagnostics : [];
|
|
1378
|
+
const signature = JSON.stringify(diagnostics.map((d) => [
|
|
1379
|
+
d.file,
|
|
1380
|
+
d.line,
|
|
1381
|
+
d.column,
|
|
1382
|
+
d.ruleId,
|
|
1383
|
+
d.severity,
|
|
1384
|
+
d.class,
|
|
1385
|
+
d.message
|
|
1386
|
+
]));
|
|
1387
|
+
if (signature === lastSignature) return;
|
|
1388
|
+
lastSignature = signature;
|
|
1389
|
+
for (const d of diagnostics) {
|
|
1390
|
+
const text = `[gustcss] ${d.line ? `${d.file}:${d.line}:${d.column}` : d.file} ${d.ruleId}: ${d.message}`;
|
|
1391
|
+
if (d.severity === "error") logger.error(text);
|
|
1392
|
+
else logger.warn(text);
|
|
1393
|
+
}
|
|
1394
|
+
const suppressed = parsed?.summary?.suppressed ?? 0;
|
|
1395
|
+
if (diagnostics.length > 0 && suppressed > 0) logger.warn(`[gustcss] lint: ${suppressed} problem(s) suppressed by baseline or comments`);
|
|
1396
|
+
}
|
|
1397
|
+
let closing = false;
|
|
1398
|
+
function runOnce() {
|
|
1399
|
+
return new Promise((resolve) => {
|
|
1400
|
+
const args = ["lint", "--json"];
|
|
1401
|
+
if (configPath) args.push("--config", configPath);
|
|
1402
|
+
if (strict) args.push("--strict");
|
|
1403
|
+
let proc;
|
|
1404
|
+
try {
|
|
1405
|
+
proc = spawn(binaryPath, args, {
|
|
1406
|
+
cwd,
|
|
1407
|
+
stdio: [
|
|
1408
|
+
"ignore",
|
|
1409
|
+
"pipe",
|
|
1410
|
+
"pipe"
|
|
1411
|
+
]
|
|
1412
|
+
});
|
|
1413
|
+
} catch (error) {
|
|
1414
|
+
logger.warn(`[gustcss] lint failed to run: ${error.message}`);
|
|
1415
|
+
resolve();
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
child = proc;
|
|
1419
|
+
let stdout = "";
|
|
1420
|
+
let stderr = "";
|
|
1421
|
+
let settled = false;
|
|
1422
|
+
let aborted = false;
|
|
1423
|
+
const timer = setTimeout(() => {
|
|
1424
|
+
aborted = true;
|
|
1425
|
+
logger.warn("[gustcss] lint timed out; skipping this run");
|
|
1426
|
+
proc.kill();
|
|
1427
|
+
}, timeoutMs);
|
|
1428
|
+
if (closing) {
|
|
1429
|
+
aborted = true;
|
|
1430
|
+
proc.kill();
|
|
1431
|
+
}
|
|
1432
|
+
const finish = (code) => {
|
|
1433
|
+
if (settled) return;
|
|
1434
|
+
settled = true;
|
|
1435
|
+
clearTimeout(timer);
|
|
1436
|
+
child = null;
|
|
1437
|
+
report(code, stdout, stderr, aborted || closing);
|
|
1438
|
+
resolve();
|
|
1439
|
+
};
|
|
1440
|
+
proc.stdout?.on("data", (chunk) => {
|
|
1441
|
+
if (stdout.length < maxOutput) stdout += chunk;
|
|
1442
|
+
});
|
|
1443
|
+
proc.stderr?.on("data", (chunk) => {
|
|
1444
|
+
if (stderr.length < maxOutput) stderr += chunk;
|
|
1445
|
+
});
|
|
1446
|
+
proc.on("error", (error) => {
|
|
1447
|
+
aborted = true;
|
|
1448
|
+
logger.warn(`[gustcss] lint failed to run: ${error.message}`);
|
|
1449
|
+
finish(-1);
|
|
1450
|
+
});
|
|
1451
|
+
proc.on("close", (code) => finish(code ?? -1));
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
return {
|
|
1455
|
+
async request() {
|
|
1456
|
+
if (closed) return;
|
|
1457
|
+
if (running) {
|
|
1458
|
+
dirty = true;
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
running = true;
|
|
1462
|
+
do {
|
|
1463
|
+
dirty = false;
|
|
1464
|
+
await runOnce();
|
|
1465
|
+
} while (dirty && !closed);
|
|
1466
|
+
running = false;
|
|
1467
|
+
},
|
|
1468
|
+
close() {
|
|
1469
|
+
closed = true;
|
|
1470
|
+
dirty = false;
|
|
1471
|
+
closing = true;
|
|
1472
|
+
if (child) {
|
|
1473
|
+
child.kill();
|
|
1474
|
+
child = null;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
736
1479
|
function cssUtility(opts = {}) {
|
|
737
1480
|
const output = opts.output || "src/styles/utility.css";
|
|
738
1481
|
const content = opts.content || ["./src/**/*.{js,ts,jsx,tsx,astro,vue}"];
|
|
739
1482
|
const configPath = opts.config;
|
|
740
1483
|
const outputToCssLayers = opts.outputToCssLayers;
|
|
741
1484
|
const mangleClassNames = opts.mangleClassNames === true;
|
|
1485
|
+
const lintEnabled = opts.lint === true || typeof opts.lint === "object" && opts.lint !== null;
|
|
1486
|
+
const lintOptions = typeof opts.lint === "object" && opts.lint !== null ? opts.lint : {};
|
|
1487
|
+
const lintSource = /\.[cm]?[jt]sx?$|\.(?:astro|html|vue|svelte|mdx?)$/;
|
|
742
1488
|
const mangleMap = opts.mangleMap || `${output}.classes.json`;
|
|
743
1489
|
const mangleExclude = opts.mangleExclude || [];
|
|
744
1490
|
let classNameTransformer = null;
|
|
745
1491
|
let rollbackContext = null;
|
|
746
1492
|
let manifestSnapshot = null;
|
|
747
1493
|
let watchProcess = null;
|
|
1494
|
+
const loadedAstroModules = /* @__PURE__ */ new Set();
|
|
1495
|
+
let astroTransformer = null;
|
|
748
1496
|
function findBinary(cwd) {
|
|
749
1497
|
return import_runner.default.resolveBinary({
|
|
750
1498
|
cwd,
|
|
@@ -816,15 +1564,19 @@ function cssUtility(opts = {}) {
|
|
|
816
1564
|
fs
|
|
817
1565
|
}, (tempConfigPath) => run(tempConfigPath));
|
|
818
1566
|
}
|
|
1567
|
+
let viteConfig = null;
|
|
819
1568
|
return {
|
|
820
1569
|
name: "gustcss",
|
|
821
1570
|
...mangleClassNames ? { enforce: "pre" } : {},
|
|
822
1571
|
configResolved(config) {
|
|
1572
|
+
viteConfig = config;
|
|
823
1573
|
const cwd = config.root || process.cwd();
|
|
824
1574
|
const binaryPath = findBinary(cwd);
|
|
825
1575
|
try {
|
|
826
1576
|
const enableMangle = mangleClassNames && config.command === "build";
|
|
827
1577
|
buildCSS(cwd, binaryPath, enableMangle);
|
|
1578
|
+
loadedAstroModules.clear();
|
|
1579
|
+
astroTransformer = null;
|
|
828
1580
|
rollbackContext = enableMangle ? {
|
|
829
1581
|
cwd,
|
|
830
1582
|
binaryPath
|
|
@@ -834,8 +1586,30 @@ function cssUtility(opts = {}) {
|
|
|
834
1586
|
throw error;
|
|
835
1587
|
}
|
|
836
1588
|
},
|
|
1589
|
+
async load(id) {
|
|
1590
|
+
if (!classNameTransformer || !ASTRO_MODULE.test(id)) return null;
|
|
1591
|
+
const filePath = id.startsWith("/@fs/") ? id.slice(4) : id;
|
|
1592
|
+
if (!fs.existsSync(filePath)) throw new Error(`[gustcss] cannot read ${id} to rewrite its class names for mangling. Disable mangleClassNames or exclude its classes with mangleExclude.`);
|
|
1593
|
+
const code = fs.readFileSync(filePath, "utf-8");
|
|
1594
|
+
loadedAstroModules.add(id);
|
|
1595
|
+
if (!astroTransformer) {
|
|
1596
|
+
const parse = await resolveAstroCompiler(viteConfig?.root || process.cwd());
|
|
1597
|
+
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.`);
|
|
1598
|
+
astroTransformer = createAstroTransformer({
|
|
1599
|
+
parse,
|
|
1600
|
+
classes: classNameTransformer.classes
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
const result = await astroTransformer(code, filePath);
|
|
1604
|
+
return result.code === code ? null : result;
|
|
1605
|
+
},
|
|
837
1606
|
transform(code, id) {
|
|
838
1607
|
if (!classNameTransformer) return null;
|
|
1608
|
+
if (ASTRO_MODULE.test(id)) {
|
|
1609
|
+
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.`);
|
|
1610
|
+
return null;
|
|
1611
|
+
}
|
|
1612
|
+
if (ASTRO_SOURCE.test(id)) return null;
|
|
839
1613
|
const transformed = classNameTransformer.withSourceMap(code, id);
|
|
840
1614
|
return transformed.code === code ? null : transformed;
|
|
841
1615
|
},
|
|
@@ -895,16 +1669,52 @@ function cssUtility(opts = {}) {
|
|
|
895
1669
|
}]
|
|
896
1670
|
});
|
|
897
1671
|
});
|
|
1672
|
+
let lintTimer = null;
|
|
1673
|
+
const lintRunner = lintEnabled ? createLintRunner({
|
|
1674
|
+
cwd,
|
|
1675
|
+
binaryPath,
|
|
1676
|
+
configPath: watchConfigPath,
|
|
1677
|
+
logger: lintLogger(server),
|
|
1678
|
+
strict: lintOptions.strict === true
|
|
1679
|
+
}) : null;
|
|
1680
|
+
if (lintRunner) {
|
|
1681
|
+
lintRunner.request();
|
|
1682
|
+
const scheduleLint = (file) => {
|
|
1683
|
+
if (typeof file === "string") {
|
|
1684
|
+
if (file.includes("node_modules")) return;
|
|
1685
|
+
if (!lintSource.test(file)) return;
|
|
1686
|
+
}
|
|
1687
|
+
if (lintTimer) clearTimeout(lintTimer);
|
|
1688
|
+
lintTimer = setTimeout(() => {
|
|
1689
|
+
lintTimer = null;
|
|
1690
|
+
lintRunner.request();
|
|
1691
|
+
}, lintDebounceMs);
|
|
1692
|
+
};
|
|
1693
|
+
server.watcher.on("change", scheduleLint);
|
|
1694
|
+
server.watcher.on("add", scheduleLint);
|
|
1695
|
+
server.watcher.on("unlink", scheduleLint);
|
|
1696
|
+
}
|
|
898
1697
|
const cleanup = () => {
|
|
1698
|
+
if (lintTimer) {
|
|
1699
|
+
clearTimeout(lintTimer);
|
|
1700
|
+
lintTimer = null;
|
|
1701
|
+
}
|
|
1702
|
+
lintRunner?.close();
|
|
899
1703
|
if (watchProcess) {
|
|
900
1704
|
watchProcess.kill();
|
|
901
1705
|
watchProcess = null;
|
|
902
1706
|
}
|
|
903
1707
|
if (tempConfigPath && fs.existsSync(tempConfigPath)) fs.unlinkSync(tempConfigPath);
|
|
904
1708
|
};
|
|
1709
|
+
const close = server.close.bind(server);
|
|
1710
|
+
server.close = async () => {
|
|
1711
|
+
cleanup();
|
|
1712
|
+
return close();
|
|
1713
|
+
};
|
|
905
1714
|
server.httpServer?.on("close", cleanup);
|
|
906
1715
|
process.once("SIGINT", cleanup);
|
|
907
1716
|
process.once("SIGTERM", cleanup);
|
|
1717
|
+
process.once("exit", cleanup);
|
|
908
1718
|
},
|
|
909
1719
|
writeBundle() {
|
|
910
1720
|
if (!manifestSnapshot || fs.existsSync(manifestSnapshot.path)) return;
|