@gustcss/vite 0.10.1 → 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 +27 -0
- package/dist/index.cjs +331 -7
- package/dist/index.d.ts +22 -0
- package/dist/index.mjs +331 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -70,12 +70,39 @@ Create `gustcss.config.json` in your project root:
|
|
|
70
70
|
}
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
### Dev-Server Linting
|
|
74
|
+
|
|
75
|
+
Set `lint` to report class names that generate no CSS while the dev server runs.
|
|
76
|
+
The plugin runs `gustcss lint --json` on startup and after source changes, and
|
|
77
|
+
prints the diagnostics through the Vite logger.
|
|
78
|
+
The lint process runs asynchronously: at most one runs at a time, changes that
|
|
79
|
+
arrive while it runs are coalesced into a single follow-up run, and unchanged
|
|
80
|
+
results are not printed twice. Lint problems never stop the dev server, and the
|
|
81
|
+
child process is killed when the server closes.
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
export default defineConfig({
|
|
85
|
+
plugins: [gustcss({ lint: true })],
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Pass `{ strict: true }` to also report custom-looking classes and low-confidence
|
|
90
|
+
extractions (`gustcss lint --strict`).
|
|
91
|
+
|
|
92
|
+
The same check runs on demand with the CLI (`gustcss lint`), which is what CI
|
|
93
|
+
should use. See the CLI documentation for rules, configuration, and baselines.
|
|
94
|
+
|
|
73
95
|
### Production Class-Name Mangling
|
|
74
96
|
|
|
75
97
|
Enable `mangleClassNames` to shorten generated selectors and matching source references during `vite build`.
|
|
76
98
|
|
|
77
99
|
Development mode keeps readable class names.
|
|
78
100
|
|
|
101
|
+
The mangler also rewrites class literals passed to the class-composition helpers `cn`, `cx`, `clsx`, `classNames`, `cva`, `tv`, `twMerge`, and `twJoin` (the same default list the CLI scanner extracts class names from).
|
|
102
|
+
Variant helpers (`cva`, `tv`) keep their object keys untouched, because those keys are variant names, not classes. For `clsx`-style helpers the quoted object keys are classes and are rewritten.
|
|
103
|
+
|
|
104
|
+
A class reference the mangler cannot rewrite stops the build: an aliased helper (`import { cn as merge }`), an unquoted `clsx` object key, or a mapped class inside an unknown string. Add the class to `mangleExclude` or move the reference to a static string when the build reports one of those errors.
|
|
105
|
+
|
|
79
106
|
```javascript
|
|
80
107
|
export default defineConfig({
|
|
81
108
|
plugins: [
|
package/dist/index.cjs
CHANGED
|
@@ -173,6 +173,105 @@ const SUPPORTED_SOURCE = /\.(?:[cm]?[jt]sx?|html)(?:$|\?)/;
|
|
|
173
173
|
const HTML_SOURCE = /\.html(?:$|\?)/;
|
|
174
174
|
const UNSUPPORTED_FRAMEWORK_SOURCE = /\.(?:vue|svelte)(?:$|\?)/;
|
|
175
175
|
const DEPENDENCY_SOURCE = /(?:^|[\\/])node_modules[\\/]/;
|
|
176
|
+
const CLASS_FUNCTION_NAMES = [
|
|
177
|
+
"cn",
|
|
178
|
+
"cx",
|
|
179
|
+
"clsx",
|
|
180
|
+
"classNames",
|
|
181
|
+
"cva",
|
|
182
|
+
"tv",
|
|
183
|
+
"twMerge",
|
|
184
|
+
"twJoin"
|
|
185
|
+
];
|
|
186
|
+
const CLASS_FUNCTION_CALL = new RegExp(`(?<![\\w$])(${CLASS_FUNCTION_NAMES.join("|")})\\s*\\(`, "g");
|
|
187
|
+
const VARIANT_FUNCTION_NAMES = /* @__PURE__ */ new Set(["cva", "tv"]);
|
|
188
|
+
const NESTED_CALL = /(?:^|[^\w$])([A-Za-z_$][\w$]*)\s*\(/g;
|
|
189
|
+
function nestedCallRanges(args) {
|
|
190
|
+
const ranges = [];
|
|
191
|
+
NESTED_CALL.lastIndex = 0;
|
|
192
|
+
let match;
|
|
193
|
+
while ((match = NESTED_CALL.exec(args)) !== null) {
|
|
194
|
+
const open = args.indexOf("(", match.index + match[0].length - 1);
|
|
195
|
+
if (open < 0) continue;
|
|
196
|
+
const end = findBalancedEnd(args, open, "(", ")");
|
|
197
|
+
if (end < 0) continue;
|
|
198
|
+
ranges.push({
|
|
199
|
+
start: match.index,
|
|
200
|
+
end: end + 1,
|
|
201
|
+
isHelper: CLASS_FUNCTION_NAMES.includes(match[1])
|
|
202
|
+
});
|
|
203
|
+
NESTED_CALL.lastIndex = open + 1;
|
|
204
|
+
}
|
|
205
|
+
return ranges;
|
|
206
|
+
}
|
|
207
|
+
function innermostCallAt(ranges, index) {
|
|
208
|
+
let found = null;
|
|
209
|
+
for (const range of ranges) if (index >= range.start && index < range.end) {
|
|
210
|
+
if (found === null || range.start > found.start) found = range;
|
|
211
|
+
}
|
|
212
|
+
return found;
|
|
213
|
+
}
|
|
214
|
+
function parenDepthAt(value, index) {
|
|
215
|
+
let depth = 0;
|
|
216
|
+
for (let i = 0; i < index; i += 1) if (value[i] === "(") depth += 1;
|
|
217
|
+
else if (value[i] === ")") depth -= 1;
|
|
218
|
+
return depth;
|
|
219
|
+
}
|
|
220
|
+
function isComparisonOperand(args, segment) {
|
|
221
|
+
let before = segment.start - 1;
|
|
222
|
+
while (before >= 0 && /\s/.test(args[before])) before -= 1;
|
|
223
|
+
const lead = before >= 1 ? args.slice(before - 1, before + 1) : "";
|
|
224
|
+
if (lead === "==" || lead === "!=" || before >= 1 && args[before] === "=") return true;
|
|
225
|
+
let after = segment.end;
|
|
226
|
+
while (after < args.length && /\s/.test(args[after])) after += 1;
|
|
227
|
+
const tail = args.slice(after, after + 2);
|
|
228
|
+
return tail === "==" || tail === "!=";
|
|
229
|
+
}
|
|
230
|
+
function defaultVariantsRanges(args) {
|
|
231
|
+
const ranges = [];
|
|
232
|
+
const pattern = /defaultVariants\s*:\s*\{/g;
|
|
233
|
+
let match;
|
|
234
|
+
while ((match = pattern.exec(args)) !== null) {
|
|
235
|
+
const open = args.indexOf("{", match.index);
|
|
236
|
+
const end = findBalancedEnd(args, open, "{", "}");
|
|
237
|
+
if (end < 0) break;
|
|
238
|
+
ranges.push([open, end + 1]);
|
|
239
|
+
pattern.lastIndex = end;
|
|
240
|
+
}
|
|
241
|
+
return ranges;
|
|
242
|
+
}
|
|
243
|
+
function inRanges(ranges, index) {
|
|
244
|
+
return ranges.some(([start, end]) => index >= start && index < end);
|
|
245
|
+
}
|
|
246
|
+
function maskIgnoredRanges(text, origins, ranges) {
|
|
247
|
+
if (ranges.length === 0) return text;
|
|
248
|
+
const chars = text.split("");
|
|
249
|
+
let sorted = [...ranges].sort((a, b) => a[0] - b[0]);
|
|
250
|
+
let cursor = 0;
|
|
251
|
+
for (let i = 0; i < chars.length && cursor < sorted.length; i += 1) {
|
|
252
|
+
const origin = origins[i];
|
|
253
|
+
while (cursor < sorted.length && origin >= sorted[cursor][1]) cursor += 1;
|
|
254
|
+
if (cursor >= sorted.length) break;
|
|
255
|
+
if (origin >= sorted[cursor][0] && origin < sorted[cursor][1]) {
|
|
256
|
+
if (chars[i] !== "\n") chars[i] = " ";
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return chars.join("");
|
|
260
|
+
}
|
|
261
|
+
function stripObjectKeys(value) {
|
|
262
|
+
return value.replace(/([{,]\s*)([A-Za-z_$][\w$]*)(\s*:)/g, (_match, lead, name, tail) => {
|
|
263
|
+
return lead + " ".repeat(name.length) + tail;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function isObjectKey(args, segment) {
|
|
267
|
+
let before = segment.start - 1;
|
|
268
|
+
while (before >= 0 && /\s/.test(args[before])) before -= 1;
|
|
269
|
+
const lead = before >= 0 ? args[before] : "";
|
|
270
|
+
if (lead !== "{" && lead !== ",") return false;
|
|
271
|
+
let after = segment.end;
|
|
272
|
+
while (after < args.length && /\s/.test(args[after])) after += 1;
|
|
273
|
+
return args[after] === ":";
|
|
274
|
+
}
|
|
176
275
|
function hasOwn(classes, token) {
|
|
177
276
|
return Object.hasOwn(classes, token);
|
|
178
277
|
}
|
|
@@ -522,6 +621,44 @@ function collectClassListCallEdits(code, structure, classes, id, edits) {
|
|
|
522
621
|
pattern.lastIndex = end;
|
|
523
622
|
}
|
|
524
623
|
}
|
|
624
|
+
function collectClassFunctionCallEdits(code, structure, classes, id, edits, ignores) {
|
|
625
|
+
CLASS_FUNCTION_CALL.lastIndex = 0;
|
|
626
|
+
let match;
|
|
627
|
+
while ((match = CLASS_FUNCTION_CALL.exec(structure)) !== null) {
|
|
628
|
+
const name = match[1];
|
|
629
|
+
const open = code.indexOf("(", match.index + name.length);
|
|
630
|
+
if (open < 0) continue;
|
|
631
|
+
const end = findBalancedEnd(code, open, "(", ")");
|
|
632
|
+
if (end < 0) throw new Error(`[gustcss] incomplete ${name}(...) call in ${id}. Ensure the expression is properly closed.`);
|
|
633
|
+
const args = code.slice(open + 1, end - 1);
|
|
634
|
+
const argsStart = open + 1;
|
|
635
|
+
const skipKeys = VARIANT_FUNCTION_NAMES.has(name);
|
|
636
|
+
const skipRanges = skipKeys ? defaultVariantsRanges(args) : [];
|
|
637
|
+
const nestedCalls = nestedCallRanges(args);
|
|
638
|
+
const depthSource = stripQuotedSegments(args);
|
|
639
|
+
scanQuotedSegments(args, (segment) => {
|
|
640
|
+
const { start, quote, contents } = segment;
|
|
641
|
+
const nested = innermostCallAt(nestedCalls, start);
|
|
642
|
+
if (!(nested !== null && nested.isHelper) && parenDepthAt(depthSource, start) > 0) return;
|
|
643
|
+
if (skipKeys && quote !== "`" && isObjectKey(args, segment) || inRanges(skipRanges, segment.start) || isComparisonOperand(args, segment)) {
|
|
644
|
+
ignores.push([argsStart + segment.start, argsStart + segment.end]);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const offset = argsStart + start + 1;
|
|
648
|
+
if (quote === "`") {
|
|
649
|
+
collectTemplateBodyEdits(contents, classes, offset, edits);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
collectClassListEdits(contents, classes, offset, edits);
|
|
653
|
+
});
|
|
654
|
+
let withoutStrings = stripQuotedSegments(args);
|
|
655
|
+
if (skipKeys) withoutStrings = stripObjectKeys(withoutStrings);
|
|
656
|
+
for (const [rangeStart, rangeEnd] of skipRanges) withoutStrings = withoutStrings.slice(0, rangeStart) + " ".repeat(rangeEnd - rangeStart) + withoutStrings.slice(rangeEnd);
|
|
657
|
+
const dynamic = findMappedToken(withoutStrings, classes);
|
|
658
|
+
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.`);
|
|
659
|
+
CLASS_FUNCTION_CALL.lastIndex = end;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
525
662
|
function rejectDynamicSetAttributeCalls(code, structure, classes, id) {
|
|
526
663
|
const pattern = /\.setAttribute\s*\(/g;
|
|
527
664
|
let match;
|
|
@@ -604,6 +741,7 @@ function sourcesForFile(code, id) {
|
|
|
604
741
|
}
|
|
605
742
|
function collectEdits(code, classes, id) {
|
|
606
743
|
const edits = [];
|
|
744
|
+
const ignores = [];
|
|
607
745
|
const sources = sourcesForFile(code, id);
|
|
608
746
|
const commentRanges = [];
|
|
609
747
|
let commentIndex = 0;
|
|
@@ -640,15 +778,23 @@ function collectEdits(code, classes, id) {
|
|
|
640
778
|
collectTemplateBodyEdits(match[2], classes, match.index + match[1].length, edits);
|
|
641
779
|
}
|
|
642
780
|
collectClassListCallEdits(code, sources.structure, classes, id, edits);
|
|
781
|
+
collectClassFunctionCallEdits(code, sources.structure, classes, id, edits, ignores);
|
|
643
782
|
const setAttributePattern = /(\.setAttribute\(\s*["']class["']\s*,\s*)(["'])([\s\S]*?)(\2)(\s*\))/g;
|
|
644
783
|
while ((match = setAttributePattern.exec(code)) !== null) {
|
|
645
784
|
if (sources.structure[match.index] === " ") continue;
|
|
646
785
|
collectClassListEdits(match[3], classes, match.index + match[1].length + 1, edits);
|
|
647
786
|
}
|
|
648
|
-
return
|
|
787
|
+
return {
|
|
788
|
+
edits,
|
|
789
|
+
ignores
|
|
790
|
+
};
|
|
649
791
|
}
|
|
650
792
|
function collectRecognizedContextEdits(code, classes, id) {
|
|
651
|
-
|
|
793
|
+
const collected = collectEdits(code, classes, id);
|
|
794
|
+
return {
|
|
795
|
+
...applyEdits(code, collected.edits),
|
|
796
|
+
ignores: collected.ignores
|
|
797
|
+
};
|
|
652
798
|
}
|
|
653
799
|
/**
|
|
654
800
|
* Collect the rewrites for a source fragment (TypeScript frontmatter, an inline
|
|
@@ -657,15 +803,17 @@ function collectRecognizedContextEdits(code, classes, id) {
|
|
|
657
803
|
* them inside a larger document.
|
|
658
804
|
*/
|
|
659
805
|
function collectFragmentEdits(code, classes, id) {
|
|
660
|
-
const
|
|
661
|
-
const
|
|
806
|
+
const collected = collectEdits(code, classes, id);
|
|
807
|
+
const applied = applyEdits(code, collected.edits);
|
|
808
|
+
const rewritten = applied.code;
|
|
662
809
|
const sources = sourcesForFile(rewritten, id);
|
|
810
|
+
const inspection = maskIgnoredRanges(sources.inspection, applied.origins, collected.ignores);
|
|
663
811
|
rejectDynamicSetAttributeCalls(rewritten, sources.structure, classes, id);
|
|
664
812
|
rejectDynamicClassNameAssignments(rewritten, sources.structure, classes, id);
|
|
665
813
|
rejectAmbiguousClassExpressions(rewritten, sources.structure, classes, id);
|
|
666
|
-
const ambiguous = findMappedInQuotedSegments(
|
|
814
|
+
const ambiguous = findMappedInQuotedSegments(inspection, classes);
|
|
667
815
|
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;
|
|
816
|
+
return collected.edits;
|
|
669
817
|
}
|
|
670
818
|
const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
671
819
|
function encodeVLQ(value) {
|
|
@@ -733,13 +881,14 @@ function transformSource(code, classes, id) {
|
|
|
733
881
|
const transformed = collectRecognizedContextEdits(code, classes, id);
|
|
734
882
|
const rewritten = transformed.code;
|
|
735
883
|
const sources = sourcesForFile(rewritten, id);
|
|
884
|
+
const inspection = maskIgnoredRanges(sources.inspection, transformed.origins, transformed.ignores);
|
|
736
885
|
rejectDynamicSetAttributeCalls(rewritten, sources.structure, classes, id);
|
|
737
886
|
rejectDynamicClassNameAssignments(rewritten, sources.structure, classes, id);
|
|
738
887
|
rejectAmbiguousClassExpressions(rewritten, sources.structure, classes, id);
|
|
739
888
|
return {
|
|
740
889
|
code: rewritten,
|
|
741
890
|
map: createSourceMap(code, rewritten, transformed.origins, id),
|
|
742
|
-
inspection
|
|
891
|
+
inspection
|
|
743
892
|
};
|
|
744
893
|
}
|
|
745
894
|
function findAmbiguousMappedClass(code, classes) {
|
|
@@ -1174,6 +1323,7 @@ function createAstroTransformer({ parse, classes }) {
|
|
|
1174
1323
|
* designed for build-time use only and should not process untrusted input.
|
|
1175
1324
|
*/
|
|
1176
1325
|
const ASTRO_SOURCE = /\.astro(?:$|\?)/;
|
|
1326
|
+
const lintDebounceMs = 300;
|
|
1177
1327
|
const ASTRO_MODULE = /^[^\0?]*\.astro$/;
|
|
1178
1328
|
/**
|
|
1179
1329
|
* Resolve @astrojs/compiler parse function from the project.
|
|
@@ -1191,12 +1341,156 @@ async function resolveAstroCompiler(root) {
|
|
|
1191
1341
|
} catch {}
|
|
1192
1342
|
return null;
|
|
1193
1343
|
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Vite logger へ出力する。テストなど logger が無い環境では console を使う。
|
|
1346
|
+
*/
|
|
1347
|
+
function lintLogger(server) {
|
|
1348
|
+
const logger = server?.config?.logger;
|
|
1349
|
+
return {
|
|
1350
|
+
warn: (message) => logger?.warn ? logger.warn(message) : console.warn(message),
|
|
1351
|
+
error: (message) => logger?.error ? logger.error(message) : console.error(message)
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Dev-server lint runner.
|
|
1356
|
+
*
|
|
1357
|
+
* Runs `gustcss lint --json` asynchronously and reports the diagnostics through
|
|
1358
|
+
* the logger. At most one lint process runs at a time; changes that arrive while
|
|
1359
|
+
* it runs are coalesced into one follow-up run. This keeps the dev server's
|
|
1360
|
+
* event loop free even on large projects.
|
|
1361
|
+
*/
|
|
1362
|
+
function createLintRunner({ cwd, binaryPath, configPath, logger, strict }) {
|
|
1363
|
+
const maxOutput = 4194304;
|
|
1364
|
+
const timeoutMs = 15e3;
|
|
1365
|
+
let child = null;
|
|
1366
|
+
let running = false;
|
|
1367
|
+
let dirty = false;
|
|
1368
|
+
let closed = false;
|
|
1369
|
+
let lastSignature = null;
|
|
1370
|
+
function report(code, stdout, stderr, aborted) {
|
|
1371
|
+
if (aborted) return;
|
|
1372
|
+
if (code !== 0 && code !== 1) {
|
|
1373
|
+
logger.warn(`[gustcss] lint could not run: ${stderr.trim()}`);
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
let parsed;
|
|
1377
|
+
try {
|
|
1378
|
+
parsed = JSON.parse(stdout);
|
|
1379
|
+
} catch {
|
|
1380
|
+
logger.warn("[gustcss] lint output was not JSON");
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
const diagnostics = Array.isArray(parsed?.diagnostics) ? parsed.diagnostics : [];
|
|
1384
|
+
const signature = JSON.stringify(diagnostics.map((d) => [
|
|
1385
|
+
d.file,
|
|
1386
|
+
d.line,
|
|
1387
|
+
d.column,
|
|
1388
|
+
d.ruleId,
|
|
1389
|
+
d.severity,
|
|
1390
|
+
d.class,
|
|
1391
|
+
d.message
|
|
1392
|
+
]));
|
|
1393
|
+
if (signature === lastSignature) return;
|
|
1394
|
+
lastSignature = signature;
|
|
1395
|
+
for (const d of diagnostics) {
|
|
1396
|
+
const text = `[gustcss] ${d.line ? `${d.file}:${d.line}:${d.column}` : d.file} ${d.ruleId}: ${d.message}`;
|
|
1397
|
+
if (d.severity === "error") logger.error(text);
|
|
1398
|
+
else logger.warn(text);
|
|
1399
|
+
}
|
|
1400
|
+
const suppressed = parsed?.summary?.suppressed ?? 0;
|
|
1401
|
+
if (diagnostics.length > 0 && suppressed > 0) logger.warn(`[gustcss] lint: ${suppressed} problem(s) suppressed by baseline or comments`);
|
|
1402
|
+
}
|
|
1403
|
+
let closing = false;
|
|
1404
|
+
function runOnce() {
|
|
1405
|
+
return new Promise((resolve) => {
|
|
1406
|
+
const args = ["lint", "--json"];
|
|
1407
|
+
if (configPath) args.push("--config", configPath);
|
|
1408
|
+
if (strict) args.push("--strict");
|
|
1409
|
+
let proc;
|
|
1410
|
+
try {
|
|
1411
|
+
proc = (0, child_process.spawn)(binaryPath, args, {
|
|
1412
|
+
cwd,
|
|
1413
|
+
stdio: [
|
|
1414
|
+
"ignore",
|
|
1415
|
+
"pipe",
|
|
1416
|
+
"pipe"
|
|
1417
|
+
]
|
|
1418
|
+
});
|
|
1419
|
+
} catch (error) {
|
|
1420
|
+
logger.warn(`[gustcss] lint failed to run: ${error.message}`);
|
|
1421
|
+
resolve();
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
child = proc;
|
|
1425
|
+
let stdout = "";
|
|
1426
|
+
let stderr = "";
|
|
1427
|
+
let settled = false;
|
|
1428
|
+
let aborted = false;
|
|
1429
|
+
const timer = setTimeout(() => {
|
|
1430
|
+
aborted = true;
|
|
1431
|
+
logger.warn("[gustcss] lint timed out; skipping this run");
|
|
1432
|
+
proc.kill();
|
|
1433
|
+
}, timeoutMs);
|
|
1434
|
+
if (closing) {
|
|
1435
|
+
aborted = true;
|
|
1436
|
+
proc.kill();
|
|
1437
|
+
}
|
|
1438
|
+
const finish = (code) => {
|
|
1439
|
+
if (settled) return;
|
|
1440
|
+
settled = true;
|
|
1441
|
+
clearTimeout(timer);
|
|
1442
|
+
child = null;
|
|
1443
|
+
report(code, stdout, stderr, aborted || closing);
|
|
1444
|
+
resolve();
|
|
1445
|
+
};
|
|
1446
|
+
proc.stdout?.on("data", (chunk) => {
|
|
1447
|
+
if (stdout.length < maxOutput) stdout += chunk;
|
|
1448
|
+
});
|
|
1449
|
+
proc.stderr?.on("data", (chunk) => {
|
|
1450
|
+
if (stderr.length < maxOutput) stderr += chunk;
|
|
1451
|
+
});
|
|
1452
|
+
proc.on("error", (error) => {
|
|
1453
|
+
aborted = true;
|
|
1454
|
+
logger.warn(`[gustcss] lint failed to run: ${error.message}`);
|
|
1455
|
+
finish(-1);
|
|
1456
|
+
});
|
|
1457
|
+
proc.on("close", (code) => finish(code ?? -1));
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
return {
|
|
1461
|
+
async request() {
|
|
1462
|
+
if (closed) return;
|
|
1463
|
+
if (running) {
|
|
1464
|
+
dirty = true;
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
running = true;
|
|
1468
|
+
do {
|
|
1469
|
+
dirty = false;
|
|
1470
|
+
await runOnce();
|
|
1471
|
+
} while (dirty && !closed);
|
|
1472
|
+
running = false;
|
|
1473
|
+
},
|
|
1474
|
+
close() {
|
|
1475
|
+
closed = true;
|
|
1476
|
+
dirty = false;
|
|
1477
|
+
closing = true;
|
|
1478
|
+
if (child) {
|
|
1479
|
+
child.kill();
|
|
1480
|
+
child = null;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1194
1485
|
function cssUtility(opts = {}) {
|
|
1195
1486
|
const output = opts.output || "src/styles/utility.css";
|
|
1196
1487
|
const content = opts.content || ["./src/**/*.{js,ts,jsx,tsx,astro,vue}"];
|
|
1197
1488
|
const configPath = opts.config;
|
|
1198
1489
|
const outputToCssLayers = opts.outputToCssLayers;
|
|
1199
1490
|
const mangleClassNames = opts.mangleClassNames === true;
|
|
1491
|
+
const lintEnabled = opts.lint === true || typeof opts.lint === "object" && opts.lint !== null;
|
|
1492
|
+
const lintOptions = typeof opts.lint === "object" && opts.lint !== null ? opts.lint : {};
|
|
1493
|
+
const lintSource = /\.[cm]?[jt]sx?$|\.(?:astro|html|vue|svelte|mdx?)$/;
|
|
1200
1494
|
const mangleMap = opts.mangleMap || `${output}.classes.json`;
|
|
1201
1495
|
const mangleExclude = opts.mangleExclude || [];
|
|
1202
1496
|
let classNameTransformer = null;
|
|
@@ -1381,7 +1675,37 @@ function cssUtility(opts = {}) {
|
|
|
1381
1675
|
}]
|
|
1382
1676
|
});
|
|
1383
1677
|
});
|
|
1678
|
+
let lintTimer = null;
|
|
1679
|
+
const lintRunner = lintEnabled ? createLintRunner({
|
|
1680
|
+
cwd,
|
|
1681
|
+
binaryPath,
|
|
1682
|
+
configPath: watchConfigPath,
|
|
1683
|
+
logger: lintLogger(server),
|
|
1684
|
+
strict: lintOptions.strict === true
|
|
1685
|
+
}) : null;
|
|
1686
|
+
if (lintRunner) {
|
|
1687
|
+
lintRunner.request();
|
|
1688
|
+
const scheduleLint = (file) => {
|
|
1689
|
+
if (typeof file === "string") {
|
|
1690
|
+
if (file.includes("node_modules")) return;
|
|
1691
|
+
if (!lintSource.test(file)) return;
|
|
1692
|
+
}
|
|
1693
|
+
if (lintTimer) clearTimeout(lintTimer);
|
|
1694
|
+
lintTimer = setTimeout(() => {
|
|
1695
|
+
lintTimer = null;
|
|
1696
|
+
lintRunner.request();
|
|
1697
|
+
}, lintDebounceMs);
|
|
1698
|
+
};
|
|
1699
|
+
server.watcher.on("change", scheduleLint);
|
|
1700
|
+
server.watcher.on("add", scheduleLint);
|
|
1701
|
+
server.watcher.on("unlink", scheduleLint);
|
|
1702
|
+
}
|
|
1384
1703
|
const cleanup = () => {
|
|
1704
|
+
if (lintTimer) {
|
|
1705
|
+
clearTimeout(lintTimer);
|
|
1706
|
+
lintTimer = null;
|
|
1707
|
+
}
|
|
1708
|
+
lintRunner?.close();
|
|
1385
1709
|
if (watchProcess) {
|
|
1386
1710
|
watchProcess.kill();
|
|
1387
1711
|
watchProcess = null;
|
package/dist/index.d.ts
CHANGED
|
@@ -49,6 +49,28 @@ export interface GustcssPluginOptions {
|
|
|
49
49
|
* Class names that must remain readable for external or ambiguous references.
|
|
50
50
|
*/
|
|
51
51
|
mangleExclude?: string[]
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Run `gustcss lint` while the dev server is running and report the
|
|
55
|
+
* diagnostics through the Vite logger. Enabled by `true` or an options
|
|
56
|
+
* object. Diagnostics never fail the dev server.
|
|
57
|
+
*
|
|
58
|
+
* @default false
|
|
59
|
+
*/
|
|
60
|
+
lint?: boolean | GustcssLintOptions
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Options for the dev-server lint integration.
|
|
65
|
+
*/
|
|
66
|
+
export interface GustcssLintOptions {
|
|
67
|
+
/**
|
|
68
|
+
* Also report custom-looking classes and low-confidence extractions
|
|
69
|
+
* (`gustcss lint --strict`).
|
|
70
|
+
*
|
|
71
|
+
* @default false
|
|
72
|
+
*/
|
|
73
|
+
strict?: boolean
|
|
52
74
|
}
|
|
53
75
|
|
|
54
76
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -167,6 +167,105 @@ const SUPPORTED_SOURCE = /\.(?:[cm]?[jt]sx?|html)(?:$|\?)/;
|
|
|
167
167
|
const HTML_SOURCE = /\.html(?:$|\?)/;
|
|
168
168
|
const UNSUPPORTED_FRAMEWORK_SOURCE = /\.(?:vue|svelte)(?:$|\?)/;
|
|
169
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
|
+
}
|
|
170
269
|
function hasOwn(classes, token) {
|
|
171
270
|
return Object.hasOwn(classes, token);
|
|
172
271
|
}
|
|
@@ -516,6 +615,44 @@ function collectClassListCallEdits(code, structure, classes, id, edits) {
|
|
|
516
615
|
pattern.lastIndex = end;
|
|
517
616
|
}
|
|
518
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
|
+
}
|
|
519
656
|
function rejectDynamicSetAttributeCalls(code, structure, classes, id) {
|
|
520
657
|
const pattern = /\.setAttribute\s*\(/g;
|
|
521
658
|
let match;
|
|
@@ -598,6 +735,7 @@ function sourcesForFile(code, id) {
|
|
|
598
735
|
}
|
|
599
736
|
function collectEdits(code, classes, id) {
|
|
600
737
|
const edits = [];
|
|
738
|
+
const ignores = [];
|
|
601
739
|
const sources = sourcesForFile(code, id);
|
|
602
740
|
const commentRanges = [];
|
|
603
741
|
let commentIndex = 0;
|
|
@@ -634,15 +772,23 @@ function collectEdits(code, classes, id) {
|
|
|
634
772
|
collectTemplateBodyEdits(match[2], classes, match.index + match[1].length, edits);
|
|
635
773
|
}
|
|
636
774
|
collectClassListCallEdits(code, sources.structure, classes, id, edits);
|
|
775
|
+
collectClassFunctionCallEdits(code, sources.structure, classes, id, edits, ignores);
|
|
637
776
|
const setAttributePattern = /(\.setAttribute\(\s*["']class["']\s*,\s*)(["'])([\s\S]*?)(\2)(\s*\))/g;
|
|
638
777
|
while ((match = setAttributePattern.exec(code)) !== null) {
|
|
639
778
|
if (sources.structure[match.index] === " ") continue;
|
|
640
779
|
collectClassListEdits(match[3], classes, match.index + match[1].length + 1, edits);
|
|
641
780
|
}
|
|
642
|
-
return
|
|
781
|
+
return {
|
|
782
|
+
edits,
|
|
783
|
+
ignores
|
|
784
|
+
};
|
|
643
785
|
}
|
|
644
786
|
function collectRecognizedContextEdits(code, classes, id) {
|
|
645
|
-
|
|
787
|
+
const collected = collectEdits(code, classes, id);
|
|
788
|
+
return {
|
|
789
|
+
...applyEdits(code, collected.edits),
|
|
790
|
+
ignores: collected.ignores
|
|
791
|
+
};
|
|
646
792
|
}
|
|
647
793
|
/**
|
|
648
794
|
* Collect the rewrites for a source fragment (TypeScript frontmatter, an inline
|
|
@@ -651,15 +797,17 @@ function collectRecognizedContextEdits(code, classes, id) {
|
|
|
651
797
|
* them inside a larger document.
|
|
652
798
|
*/
|
|
653
799
|
function collectFragmentEdits(code, classes, id) {
|
|
654
|
-
const
|
|
655
|
-
const
|
|
800
|
+
const collected = collectEdits(code, classes, id);
|
|
801
|
+
const applied = applyEdits(code, collected.edits);
|
|
802
|
+
const rewritten = applied.code;
|
|
656
803
|
const sources = sourcesForFile(rewritten, id);
|
|
804
|
+
const inspection = maskIgnoredRanges(sources.inspection, applied.origins, collected.ignores);
|
|
657
805
|
rejectDynamicSetAttributeCalls(rewritten, sources.structure, classes, id);
|
|
658
806
|
rejectDynamicClassNameAssignments(rewritten, sources.structure, classes, id);
|
|
659
807
|
rejectAmbiguousClassExpressions(rewritten, sources.structure, classes, id);
|
|
660
|
-
const ambiguous = findMappedInQuotedSegments(
|
|
808
|
+
const ambiguous = findMappedInQuotedSegments(inspection, classes);
|
|
661
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.`);
|
|
662
|
-
return edits;
|
|
810
|
+
return collected.edits;
|
|
663
811
|
}
|
|
664
812
|
const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
665
813
|
function encodeVLQ(value) {
|
|
@@ -727,13 +875,14 @@ function transformSource(code, classes, id) {
|
|
|
727
875
|
const transformed = collectRecognizedContextEdits(code, classes, id);
|
|
728
876
|
const rewritten = transformed.code;
|
|
729
877
|
const sources = sourcesForFile(rewritten, id);
|
|
878
|
+
const inspection = maskIgnoredRanges(sources.inspection, transformed.origins, transformed.ignores);
|
|
730
879
|
rejectDynamicSetAttributeCalls(rewritten, sources.structure, classes, id);
|
|
731
880
|
rejectDynamicClassNameAssignments(rewritten, sources.structure, classes, id);
|
|
732
881
|
rejectAmbiguousClassExpressions(rewritten, sources.structure, classes, id);
|
|
733
882
|
return {
|
|
734
883
|
code: rewritten,
|
|
735
884
|
map: createSourceMap(code, rewritten, transformed.origins, id),
|
|
736
|
-
inspection
|
|
885
|
+
inspection
|
|
737
886
|
};
|
|
738
887
|
}
|
|
739
888
|
function findAmbiguousMappedClass(code, classes) {
|
|
@@ -1168,6 +1317,7 @@ function createAstroTransformer({ parse, classes }) {
|
|
|
1168
1317
|
* designed for build-time use only and should not process untrusted input.
|
|
1169
1318
|
*/
|
|
1170
1319
|
const ASTRO_SOURCE = /\.astro(?:$|\?)/;
|
|
1320
|
+
const lintDebounceMs = 300;
|
|
1171
1321
|
const ASTRO_MODULE = /^[^\0?]*\.astro$/;
|
|
1172
1322
|
/**
|
|
1173
1323
|
* Resolve @astrojs/compiler parse function from the project.
|
|
@@ -1185,12 +1335,156 @@ async function resolveAstroCompiler(root) {
|
|
|
1185
1335
|
} catch {}
|
|
1186
1336
|
return null;
|
|
1187
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
|
+
}
|
|
1188
1479
|
function cssUtility(opts = {}) {
|
|
1189
1480
|
const output = opts.output || "src/styles/utility.css";
|
|
1190
1481
|
const content = opts.content || ["./src/**/*.{js,ts,jsx,tsx,astro,vue}"];
|
|
1191
1482
|
const configPath = opts.config;
|
|
1192
1483
|
const outputToCssLayers = opts.outputToCssLayers;
|
|
1193
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?)$/;
|
|
1194
1488
|
const mangleMap = opts.mangleMap || `${output}.classes.json`;
|
|
1195
1489
|
const mangleExclude = opts.mangleExclude || [];
|
|
1196
1490
|
let classNameTransformer = null;
|
|
@@ -1375,7 +1669,37 @@ function cssUtility(opts = {}) {
|
|
|
1375
1669
|
}]
|
|
1376
1670
|
});
|
|
1377
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
|
+
}
|
|
1378
1697
|
const cleanup = () => {
|
|
1698
|
+
if (lintTimer) {
|
|
1699
|
+
clearTimeout(lintTimer);
|
|
1700
|
+
lintTimer = null;
|
|
1701
|
+
}
|
|
1702
|
+
lintRunner?.close();
|
|
1379
1703
|
if (watchProcess) {
|
|
1380
1704
|
watchProcess.kill();
|
|
1381
1705
|
watchProcess = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gustcss/vite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Vite plugin for GustCSS",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"gustcss": "^0.
|
|
40
|
+
"gustcss": "^0.11.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@astrojs/compiler": "2.13.0",
|