svelte-vitals 0.45.1 → 0.47.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 +1 -1
- package/dist/bin.js +34 -26
- package/dist/{chunk-MUIOPL5F.js → chunk-43FGGQQZ.js} +1 -1
- package/dist/{chunk-VNVRHWO3.js → chunk-5UQ6P2M2.js} +302 -61
- package/dist/{chunk-4O5F7EUJ.js → chunk-77MMJCFK.js} +24 -17
- package/dist/{chunk-MHRO4GU7.js → chunk-A5DVJX5O.js} +2 -2
- package/dist/{chunk-F5P3REI7.js → chunk-CTMKTFST.js} +3 -2
- package/dist/{chunk-ACQ5HB32.js → chunk-ICEJJDDA.js} +2 -2
- package/dist/{chunk-O2CQPUSP.js → chunk-JFLNGYPX.js} +4 -9
- package/dist/{chunk-P4YNVJUO.js → chunk-MZO3HVTN.js} +5 -5
- package/dist/{chunk-MBOONBC4.js → chunk-SLE22PPF.js} +3 -5
- package/dist/{ci-CLKU3TR6.js → ci-J7PGFHK3.js} +2 -2
- package/dist/{complete-HGLO72TI.js → complete-BDKGAUQX.js} +17 -9
- package/dist/{docs-CRZC47P7.js → docs-XNKZQBBZ.js} +1 -1
- package/dist/{explain-52QDQUOY.js → explain-YPWGQE5N.js} +1 -1
- package/dist/gunshi-registry.js +6 -6
- package/dist/index.d.ts +28 -1
- package/dist/index.js +1 -1
- package/dist/{install-3FI7M5BM.js → install-HE76S4ID.js} +3 -3
- package/dist/{ja-BCE5T3H2.js → ja-GAQSBSXF.js} +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
6
|
> **A static SvelteKit code-health scanner — not a runtime Web Vitals reporter.**
|
|
7
|
-
> Diagnose your project's SEO, Performance, Correctness, Security, and
|
|
7
|
+
> Diagnose your project's SEO, Performance, Correctness, Security, Architecture, and Accessibility health by statically analyzing your source code, before it ships. No browser, no build server, no headless Chrome.
|
|
8
8
|
>
|
|
9
9
|
> **ESM-only** (Node 22.13+). Ships ES modules only; `require()` is unsupported by design.
|
|
10
10
|
|
package/dist/bin.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runAnalyzeCliGunshi
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-MZO3HVTN.js";
|
|
5
|
+
import "./chunk-5UQ6P2M2.js";
|
|
6
|
+
import "./chunk-43FGGQQZ.js";
|
|
7
7
|
import "./chunk-M5KM5SV7.js";
|
|
8
8
|
import {
|
|
9
9
|
consoleIO
|
|
@@ -18,29 +18,34 @@ import {
|
|
|
18
18
|
|
|
19
19
|
// src/cli.ts
|
|
20
20
|
async function runCli(argv, io = consoleIO, env = process.env) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
try {
|
|
22
|
+
const locale = resolveLocale(env);
|
|
23
|
+
if (argv[0] === "complete") {
|
|
24
|
+
const { runCompleteCliGunshi } = await import("./complete-BDKGAUQX.js");
|
|
25
|
+
return { code: await runCompleteCliGunshi(argv, io), exit: "natural" };
|
|
26
|
+
}
|
|
27
|
+
if (argv[0] === "docs") {
|
|
28
|
+
const { runDocsCliGunshi } = await import("./docs-XNKZQBBZ.js");
|
|
29
|
+
return { code: await runDocsCliGunshi(argv.slice(1), io, locale), exit: "natural" };
|
|
30
|
+
}
|
|
31
|
+
if (argv[0] === "explain") {
|
|
32
|
+
const { runExplainCliGunshi } = await import("./explain-YPWGQE5N.js");
|
|
33
|
+
return { code: await runExplainCliGunshi(argv.slice(1), io, locale), exit: "natural" };
|
|
34
|
+
}
|
|
35
|
+
if (argv[0] === "install") {
|
|
36
|
+
const { runInstallCliGunshi } = await import("./install-HE76S4ID.js");
|
|
37
|
+
return { code: await runInstallCliGunshi(argv.slice(1), io, locale), exit: "immediate" };
|
|
38
|
+
}
|
|
39
|
+
if (argv[0] === "ci") {
|
|
40
|
+
const { runCiCliGunshi } = await import("./ci-J7PGFHK3.js");
|
|
41
|
+
const code = await runCiCliGunshi(argv.slice(1), { ...realIO(), log: io.log, errorLog: io.errorLog }, locale);
|
|
42
|
+
return { code, exit: "immediate" };
|
|
43
|
+
}
|
|
44
|
+
return await runAnalyzeCliGunshi(argv, io, locale);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
io.errorLog(`svelte-vitals: ${err instanceof Error ? err.message : String(err)}`);
|
|
47
|
+
return { code: 2, exit: "natural" };
|
|
25
48
|
}
|
|
26
|
-
if (argv[0] === "docs") {
|
|
27
|
-
const { runDocsCliGunshi } = await import("./docs-CRZC47P7.js");
|
|
28
|
-
return { code: await runDocsCliGunshi(argv.slice(1), io, locale), exit: "natural" };
|
|
29
|
-
}
|
|
30
|
-
if (argv[0] === "explain") {
|
|
31
|
-
const { runExplainCliGunshi } = await import("./explain-52QDQUOY.js");
|
|
32
|
-
return { code: await runExplainCliGunshi(argv.slice(1), io, locale), exit: "natural" };
|
|
33
|
-
}
|
|
34
|
-
if (argv[0] === "install") {
|
|
35
|
-
const { runInstallCliGunshi } = await import("./install-3FI7M5BM.js");
|
|
36
|
-
return { code: await runInstallCliGunshi(argv.slice(1), io, locale), exit: "immediate" };
|
|
37
|
-
}
|
|
38
|
-
if (argv[0] === "ci") {
|
|
39
|
-
const { runCiCliGunshi } = await import("./ci-CLKU3TR6.js");
|
|
40
|
-
const code = await runCiCliGunshi(argv.slice(1), { ...realIO(), log: io.log, errorLog: io.errorLog }, locale);
|
|
41
|
-
return { code, exit: "immediate" };
|
|
42
|
-
}
|
|
43
|
-
return runAnalyzeCliGunshi(argv, io, locale);
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
// src/bin.ts
|
|
@@ -52,4 +57,7 @@ async function main() {
|
|
|
52
57
|
process.exitCode = code;
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
|
-
|
|
60
|
+
main().catch((err) => {
|
|
61
|
+
console.error(`svelte-vitals: ${err instanceof Error ? err.message : String(err)}`);
|
|
62
|
+
process.exit(2);
|
|
63
|
+
});
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
|
|
9
9
|
// src/resolve-args.ts
|
|
10
10
|
import { parseArgs } from "util";
|
|
11
|
-
var CATEGORIES = ["seo", "performance", "correctness", "security", "architecture"];
|
|
11
|
+
var CATEGORIES = ["seo", "performance", "correctness", "security", "architecture", "a11y"];
|
|
12
12
|
var FAIL_ON_VALUES = ["critical", "warning", "info"];
|
|
13
13
|
function isFailOnValue(value) {
|
|
14
14
|
return typeof value === "string" && FAIL_ON_VALUES.includes(value);
|
|
@@ -32,7 +32,9 @@ import {
|
|
|
32
32
|
applyRuleSeverities,
|
|
33
33
|
applyOverrides,
|
|
34
34
|
settingSeverity,
|
|
35
|
-
withFailedRulesOff
|
|
35
|
+
withFailedRulesOff,
|
|
36
|
+
formatFailedRuleWarning,
|
|
37
|
+
terminalSafe
|
|
36
38
|
} from "@svelte-vitals/core";
|
|
37
39
|
|
|
38
40
|
// src/runtime/node.ts
|
|
@@ -144,10 +146,28 @@ function detectHtmlLang(html) {
|
|
|
144
146
|
const value = match[1] ?? match[2] ?? match[3] ?? "";
|
|
145
147
|
return { presence: "own", value: value.trim().length > 0 ? "static" : "absent" };
|
|
146
148
|
}
|
|
147
|
-
|
|
149
|
+
function detectAppHtmlIds(html) {
|
|
150
|
+
const markup = html.replace(/<!--[\s\S]*?-->/g, "").replace(/<script[\s\S]*?<\/script\s*>/gi, "").replace(/<style[\s\S]*?<\/style\s*>/gi, "");
|
|
151
|
+
const found = markup.matchAll(/(?<![\w-])id\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>{][^\s"'>]*))/gi);
|
|
152
|
+
return [...new Set([...found].map((m) => m[1] ?? m[2] ?? m[3] ?? "").filter(Boolean))];
|
|
153
|
+
}
|
|
154
|
+
async function detectAppHtmlFacts(rt, cwd) {
|
|
148
155
|
const appHtmlPath = rt.join(cwd, "src/app.html");
|
|
149
|
-
if (!await rt.exists(appHtmlPath)) return { presence: "none", value: "absent" };
|
|
150
|
-
|
|
156
|
+
if (!await rt.exists(appHtmlPath)) return { htmlLang: { presence: "none", value: "absent" } };
|
|
157
|
+
let content;
|
|
158
|
+
try {
|
|
159
|
+
content = await rt.readFile(appHtmlPath);
|
|
160
|
+
} catch {
|
|
161
|
+
return { htmlLang: { presence: "none", value: "absent" } };
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
htmlLang: detectHtmlLang(content),
|
|
165
|
+
// Comments are stripped first, then a simple anchored match — a starred group over a lazy
|
|
166
|
+
// [\s\S]*? is ambiguous across iterations and backtracks exponentially on a comment run
|
|
167
|
+
// with no doctype (measured: ~45 leading comments hang the process).
|
|
168
|
+
appHtmlDoctype: /^\s*<!doctype\s+html/i.test(content.replace(/<!--[\s\S]*?-->/g, "")),
|
|
169
|
+
appHtmlIds: detectAppHtmlIds(content)
|
|
170
|
+
};
|
|
151
171
|
}
|
|
152
172
|
async function robotsRefsSitemap(rt, cwd) {
|
|
153
173
|
const p = rt.join(cwd, "static/robots.txt");
|
|
@@ -194,10 +214,10 @@ async function detectKitConfigFacts(rt, cwd) {
|
|
|
194
214
|
};
|
|
195
215
|
}
|
|
196
216
|
async function collectProjectFacts(rt, cwd) {
|
|
197
|
-
const [hasRobotsTxt, hasSitemap,
|
|
217
|
+
const [hasRobotsTxt, hasSitemap, appHtmlFacts, viteMinifyDisabled, kitConfig] = await Promise.all([
|
|
198
218
|
existsAny(rt, cwd, ROBOTS_SOURCE_PATHS),
|
|
199
219
|
existsAny(rt, cwd, SITEMAP_SOURCE_PATHS),
|
|
200
|
-
|
|
220
|
+
detectAppHtmlFacts(rt, cwd),
|
|
201
221
|
detectViteMinifyDisabled(rt, cwd),
|
|
202
222
|
detectKitConfigFacts(rt, cwd)
|
|
203
223
|
]);
|
|
@@ -205,7 +225,7 @@ async function collectProjectFacts(rt, cwd) {
|
|
|
205
225
|
return {
|
|
206
226
|
hasRobotsTxt,
|
|
207
227
|
hasSitemap,
|
|
208
|
-
|
|
228
|
+
...appHtmlFacts,
|
|
209
229
|
...robotsReferencesSitemap !== void 0 ? { robotsReferencesSitemap } : {},
|
|
210
230
|
...viteMinifyDisabled ? { viteMinifyDisabled } : {},
|
|
211
231
|
...kitConfig
|
|
@@ -220,7 +240,7 @@ import {
|
|
|
220
240
|
} from "@svelte-vitals/core";
|
|
221
241
|
|
|
222
242
|
// src/providers/source/routes.ts
|
|
223
|
-
import { defaultConfig } from "@svelte-vitals/core";
|
|
243
|
+
import { defaultConfig, foldOccurrences, isTopFragment } from "@svelte-vitals/core";
|
|
224
244
|
|
|
225
245
|
// src/providers/source/resolve.ts
|
|
226
246
|
import { resolveRepoLocalPath } from "@svelte-vitals/core";
|
|
@@ -367,7 +387,13 @@ import {
|
|
|
367
387
|
valueFromNodes,
|
|
368
388
|
textFromNodes,
|
|
369
389
|
attrText,
|
|
370
|
-
|
|
390
|
+
attrTextOf as attrTextOf3,
|
|
391
|
+
attrValue,
|
|
392
|
+
attrValueOf as attrValueOf3,
|
|
393
|
+
decodeFragmentId,
|
|
394
|
+
splitTokens,
|
|
395
|
+
LANDMARK_ROLES,
|
|
396
|
+
IDREF_ATTRS
|
|
371
397
|
} from "@svelte-vitals/core";
|
|
372
398
|
|
|
373
399
|
// src/providers/source/imports.ts
|
|
@@ -555,6 +581,137 @@ function collectHeadings(node, source, acc) {
|
|
|
555
581
|
if (key in node) collectHeadings(childOf(node, key), source, acc);
|
|
556
582
|
}
|
|
557
583
|
}
|
|
584
|
+
var LANDMARK_TAGS = { main: "main", header: "banner", footer: "contentinfo" };
|
|
585
|
+
var IDREF_ATTR_SET = new Set(IDREF_ATTRS);
|
|
586
|
+
function collectA11y(fragment, source) {
|
|
587
|
+
const nodes = [];
|
|
588
|
+
let groups = 0;
|
|
589
|
+
let slotInLandmark;
|
|
590
|
+
let unknowableContent = false;
|
|
591
|
+
const emit = (ctx, node) => {
|
|
592
|
+
const inLandmark = ctx.landmarks.at(-1);
|
|
593
|
+
nodes.push({ ...node, repeatable: ctx.repeatable, path: ctx.path, ...inLandmark ? { inLandmark } : {} });
|
|
594
|
+
};
|
|
595
|
+
const noteSpread = (node) => {
|
|
596
|
+
const attributes = node.attributes;
|
|
597
|
+
if (Array.isArray(attributes) && attributes.some((a) => a.type === "SpreadAttribute")) {
|
|
598
|
+
unknowableContent = true;
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
const walk = (node, ctx) => {
|
|
602
|
+
if (Array.isArray(node)) {
|
|
603
|
+
for (const child of node) walk(child, ctx);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (!node || typeof node !== "object") return;
|
|
607
|
+
switch (node.type) {
|
|
608
|
+
case "SvelteHead":
|
|
609
|
+
return;
|
|
610
|
+
// head content never renders into the body
|
|
611
|
+
case "HtmlTag":
|
|
612
|
+
unknowableContent = true;
|
|
613
|
+
return;
|
|
614
|
+
case "IfBlock":
|
|
615
|
+
walkIfChain(node, ctx, groups++, 0);
|
|
616
|
+
return;
|
|
617
|
+
case "AwaitBlock": {
|
|
618
|
+
const group = groups++;
|
|
619
|
+
walk(node.pending, { ...ctx, path: [...ctx.path, { group, branch: 0 }] });
|
|
620
|
+
walk(node.then, { ...ctx, path: [...ctx.path, { group, branch: 1 }] });
|
|
621
|
+
walk(node.catch, { ...ctx, path: [...ctx.path, { group, branch: 2 }] });
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
case "EachBlock":
|
|
625
|
+
walk(node.body, { ...ctx, repeatable: true });
|
|
626
|
+
walk(node.fallback, ctx);
|
|
627
|
+
return;
|
|
628
|
+
case "SnippetBlock":
|
|
629
|
+
walk(node.body, { ...ctx, repeatable: true });
|
|
630
|
+
return;
|
|
631
|
+
// <svelte:element> has a dynamic tag (so no tag-derived landmark) but its literal id/idref
|
|
632
|
+
// attributes are real — dropping them would make no-missing-id-ref report phantom misses.
|
|
633
|
+
case "RegularElement":
|
|
634
|
+
case "SvelteElement":
|
|
635
|
+
walkElement(node, ctx);
|
|
636
|
+
return;
|
|
637
|
+
case "Component":
|
|
638
|
+
case "SvelteComponent":
|
|
639
|
+
case "SvelteSelf":
|
|
640
|
+
noteSpread(node);
|
|
641
|
+
emit(ctx, { kind: "component", key: node.name, line: lineOf(source, node.start) });
|
|
642
|
+
walk(node.fragment, { ...ctx, elementDepth: ctx.elementDepth + 1 });
|
|
643
|
+
return;
|
|
644
|
+
case "SlotElement":
|
|
645
|
+
noteSpread(node);
|
|
646
|
+
slotInLandmark ??= ctx.landmarks.at(-1);
|
|
647
|
+
walk(node.fragment, ctx);
|
|
648
|
+
return;
|
|
649
|
+
case "RenderTag":
|
|
650
|
+
if (isChildrenRender(node)) slotInLandmark ??= ctx.landmarks.at(-1);
|
|
651
|
+
return;
|
|
652
|
+
default:
|
|
653
|
+
noteSpread(node);
|
|
654
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
655
|
+
if (key in node) walk(childOf(node, key), ctx);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
const walkIfChain = (node, ctx, group, branch) => {
|
|
660
|
+
walk(node.consequent, { ...ctx, path: [...ctx.path, { group, branch }] });
|
|
661
|
+
if (!node.alternate) return;
|
|
662
|
+
const rest = node.alternate.nodes.filter((n) => n.type !== "Text" || n.data.trim() !== "");
|
|
663
|
+
const chained = rest.length === 1 && rest[0].type === "IfBlock" && rest[0].elseif ? rest[0] : void 0;
|
|
664
|
+
if (chained) walkIfChain(chained, ctx, group, branch + 1);
|
|
665
|
+
else walk(node.alternate, { ...ctx, path: [...ctx.path, { group, branch: branch + 1 }] });
|
|
666
|
+
};
|
|
667
|
+
const walkElement = (node, ctx) => {
|
|
668
|
+
noteSpread(node);
|
|
669
|
+
const line = lineOf(source, node.start);
|
|
670
|
+
const attrs = node.attributes;
|
|
671
|
+
const roleAttr = findAttr3(attrs, "role");
|
|
672
|
+
const role = roleAttr ? splitTokens(attrTextOf3(roleAttr))[0] : void 0;
|
|
673
|
+
const landmark = roleAttr ? role && LANDMARK_ROLES.has(role) ? role : void 0 : LANDMARK_TAGS[node.name];
|
|
674
|
+
if (landmark) {
|
|
675
|
+
const headerFooter = !roleAttr && node.name !== "main";
|
|
676
|
+
emit(ctx, {
|
|
677
|
+
kind: "landmark",
|
|
678
|
+
key: landmark,
|
|
679
|
+
line,
|
|
680
|
+
...headerFooter ? { topLevel: ctx.elementDepth === 0 } : {}
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
for (const attr of node.attributes) {
|
|
684
|
+
if (attr.type !== "Attribute") continue;
|
|
685
|
+
if (attr.name === "id") {
|
|
686
|
+
const v = attrValueOf3(attr);
|
|
687
|
+
if (v === "dynamic") emit(ctx, { kind: "id", key: "", line });
|
|
688
|
+
else if (v === "static") emit(ctx, { kind: "id", key: attrTextOf3(attr), line });
|
|
689
|
+
} else if (attr.name === "href") {
|
|
690
|
+
const href = attrTextOf3(attr);
|
|
691
|
+
if (href?.startsWith("#") && href.length > 1) {
|
|
692
|
+
emit(ctx, { kind: "idref", key: decodeFragmentId(href.slice(1)), line, attr: "href" });
|
|
693
|
+
}
|
|
694
|
+
} else if (IDREF_ATTR_SET.has(attr.name)) {
|
|
695
|
+
for (const token of splitTokens(attrTextOf3(attr))) {
|
|
696
|
+
emit(ctx, { kind: "idref", key: token, line, attr: attr.name });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const literalTag = node.type === "SvelteElement" ? node.tag.type === "Literal" && typeof node.tag.value === "string" ? node.tag.value : void 0 : node.name;
|
|
701
|
+
if (literalTag === "template") return;
|
|
702
|
+
walk(node.fragment, {
|
|
703
|
+
...ctx,
|
|
704
|
+
elementDepth: ctx.elementDepth + 1,
|
|
705
|
+
landmarks: landmark ? [...ctx.landmarks, landmark] : ctx.landmarks
|
|
706
|
+
});
|
|
707
|
+
};
|
|
708
|
+
walk(fragment, { path: [], repeatable: false, landmarks: [], elementDepth: 0 });
|
|
709
|
+
return { nodes, ...slotInLandmark ? { slotInLandmark } : {}, unknowableContent };
|
|
710
|
+
}
|
|
711
|
+
function isChildrenRender(node) {
|
|
712
|
+
const call = node.expression.type === "ChainExpression" ? node.expression.expression : node.expression;
|
|
713
|
+
return call.callee.type === "Identifier" && call.callee.name === "children";
|
|
714
|
+
}
|
|
558
715
|
function parseFile(source, filename) {
|
|
559
716
|
const ast = parse(source, { modern: true, filename });
|
|
560
717
|
const heads = [];
|
|
@@ -570,7 +727,8 @@ function parseFile(source, filename) {
|
|
|
570
727
|
components,
|
|
571
728
|
imports: collectImports(ast),
|
|
572
729
|
images,
|
|
573
|
-
headings
|
|
730
|
+
headings,
|
|
731
|
+
a11y: collectA11y(ast.fragment, source)
|
|
574
732
|
};
|
|
575
733
|
}
|
|
576
734
|
|
|
@@ -728,8 +886,64 @@ function chainFiles(pageRel, layouts) {
|
|
|
728
886
|
}
|
|
729
887
|
return [...chain.map((rel) => ({ rel, isPage: false })), { rel: pageRel, isPage: true }];
|
|
730
888
|
}
|
|
731
|
-
|
|
889
|
+
function groupSpan(nodes) {
|
|
890
|
+
let max = -1;
|
|
891
|
+
for (const node of nodes) {
|
|
892
|
+
for (const step of node.path) if (step.group > max) max = step.group;
|
|
893
|
+
}
|
|
894
|
+
return max + 1;
|
|
895
|
+
}
|
|
896
|
+
function offsetPath(path, base) {
|
|
897
|
+
return base === 0 ? path : path.map((step) => ({ group: step.group + base, branch: step.branch }));
|
|
898
|
+
}
|
|
899
|
+
async function composeA11y(ctx, fileRel, parsed, depth, visited, chain) {
|
|
900
|
+
const { rt, cwd, state } = ctx;
|
|
901
|
+
if (parsed.a11y.unknowableContent) state.fullyResolved = false;
|
|
902
|
+
const base = state.nextGroup;
|
|
903
|
+
state.nextGroup += groupSpan(parsed.a11y.nodes);
|
|
904
|
+
const composed = [];
|
|
905
|
+
for (const node of parsed.a11y.nodes) {
|
|
906
|
+
const path = offsetPath(node.path, base);
|
|
907
|
+
if (node.kind !== "component") {
|
|
908
|
+
composed.push({ ...node, path, file: fileRel, chain });
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
const info = ctx.config.metaComponents.includes(node.key) ? void 0 : parsed.imports.get(node.key);
|
|
912
|
+
const childRel = info ? resolveComponentPath(info.source, fileRel, ctx.aliases) : void 0;
|
|
913
|
+
if (!childRel || depth <= 0 || visited.has(childRel) || !await rt.exists(rt.join(cwd, childRel))) {
|
|
914
|
+
state.fullyResolved = false;
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
const childParsed = await readAndParse(rt, cwd, childRel, ctx.cache);
|
|
918
|
+
const child = await composeA11y(ctx, childRel, childParsed, depth - 1, new Set(visited).add(childRel), false);
|
|
919
|
+
for (const inner of child) {
|
|
920
|
+
composed.push({ ...inner, path: [...path, ...inner.path], repeatable: node.repeatable || inner.repeatable });
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return composed;
|
|
924
|
+
}
|
|
925
|
+
function countsAsLandmark(node) {
|
|
926
|
+
return node.topLevel === void 0 || node.chain && node.topLevel === true;
|
|
927
|
+
}
|
|
928
|
+
function representativeOrder(chainOrder) {
|
|
929
|
+
return (a, b) => {
|
|
930
|
+
const rankA = a.chain ? chainOrder.get(a.file) ?? 0 : chainOrder.size;
|
|
931
|
+
const rankB = b.chain ? chainOrder.get(b.file) ?? 0 : chainOrder.size;
|
|
932
|
+
if (rankA !== rankB) return rankA - rankB;
|
|
933
|
+
if (a.file !== b.file) return a.file < b.file ? -1 : 1;
|
|
934
|
+
return a.line - b.line;
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
function representatives(nodes, chainOrder) {
|
|
938
|
+
const folded = foldOccurrences(nodes);
|
|
939
|
+
const order = representativeOrder(chainOrder);
|
|
940
|
+
return Object.fromEntries(
|
|
941
|
+
[...folded].map(([key, list]) => [key, list.sort(order).map(({ file, line }) => ({ file, line }))])
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
async function resolveRoute(rt, cwd, pageRel, config, layouts, cache, aliases, appHtmlIds) {
|
|
732
945
|
const files = chainFiles(pageRel, layouts);
|
|
946
|
+
const chainOrder = new Map(files.map((f, i) => [f.rel, i]));
|
|
733
947
|
const composed = /* @__PURE__ */ new Map();
|
|
734
948
|
const jsonldTags = [];
|
|
735
949
|
let broadOwn = false;
|
|
@@ -737,8 +951,20 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache, aliases) {
|
|
|
737
951
|
const images = [];
|
|
738
952
|
const headings = [];
|
|
739
953
|
const componentHeadings = [];
|
|
954
|
+
const a11yCtx = { rt, cwd, config, cache, aliases, state: { nextGroup: 0, fullyResolved: true } };
|
|
955
|
+
const a11yNodes = [];
|
|
956
|
+
const nestedLandmarks = [];
|
|
957
|
+
let slotLandmark;
|
|
740
958
|
for (const { rel, isPage } of files) {
|
|
741
959
|
const parsed = await readAndParse(rt, cwd, rel, cache);
|
|
960
|
+
const contributed = await composeA11y(a11yCtx, rel, parsed, MAX_DEPTH, /* @__PURE__ */ new Set([rel]), true);
|
|
961
|
+
for (const node of contributed) {
|
|
962
|
+
if (!node.chain || node.kind !== "landmark" || !countsAsLandmark(node) || node.repeatable) continue;
|
|
963
|
+
const within = node.inLandmark ?? slotLandmark;
|
|
964
|
+
if (within) nestedLandmarks.push({ kind: node.key, within, file: node.file, line: node.line });
|
|
965
|
+
}
|
|
966
|
+
slotLandmark = parsed.a11y.slotInLandmark ?? slotLandmark;
|
|
967
|
+
a11yNodes.push(...contributed);
|
|
742
968
|
for (const img of parsed.images) {
|
|
743
969
|
images.push({ ...img, file: rel });
|
|
744
970
|
}
|
|
@@ -764,20 +990,40 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache, aliases) {
|
|
|
764
990
|
if (!composed.has(key)) composed.set(key, { ...tag, presence });
|
|
765
991
|
}
|
|
766
992
|
}
|
|
993
|
+
const idNodes = a11yNodes.filter((n) => n.kind === "id");
|
|
994
|
+
if (idNodes.some((n) => n.key === "")) a11yCtx.state.fullyResolved = false;
|
|
995
|
+
const literalIds = idNodes.filter((n) => n.key !== "");
|
|
767
996
|
const route = deriveRoute(pageRel);
|
|
768
997
|
return {
|
|
769
998
|
head: { route, source: "static", tags: [...composed.values(), ...jsonldTags], file: pageRel },
|
|
770
999
|
images: { route, images },
|
|
771
|
-
headings: { route, headings, componentHeadings }
|
|
1000
|
+
headings: { route, headings, componentHeadings },
|
|
1001
|
+
a11y: {
|
|
1002
|
+
route,
|
|
1003
|
+
landmarks: representatives(
|
|
1004
|
+
a11yNodes.filter((n) => n.kind === "landmark" && countsAsLandmark(n)),
|
|
1005
|
+
chainOrder
|
|
1006
|
+
),
|
|
1007
|
+
nestedLandmarks,
|
|
1008
|
+
ids: representatives(literalIds, chainOrder),
|
|
1009
|
+
// `href="#top"` scrolls to the document top with no element of that id, so it is
|
|
1010
|
+
// never a missing reference (HTML's "top of the document" fragment).
|
|
1011
|
+
idRefs: a11yNodes.filter((n) => n.kind === "idref" && !(n.attr === "href" && isTopFragment(n.key))).map((n) => ({ id: n.key, attr: n.attr ?? "", file: n.file, line: n.line })),
|
|
1012
|
+
idCandidates: [.../* @__PURE__ */ new Set([...literalIds.map((n) => n.key), ...appHtmlIds ?? []])],
|
|
1013
|
+
fullyResolved: a11yCtx.state.fullyResolved
|
|
1014
|
+
}
|
|
772
1015
|
};
|
|
773
1016
|
}
|
|
774
|
-
async function collectRoutes(rt, cwd, config = defaultConfig, cache = /* @__PURE__ */ new Map(), aliases) {
|
|
1017
|
+
async function collectRoutes(rt, cwd, config = defaultConfig, cache = /* @__PURE__ */ new Map(), aliases, appHtmlIds) {
|
|
775
1018
|
const [pages, layouts] = await Promise.all([enumerateRoutePages(rt, cwd), collectLayouts(rt, cwd)]);
|
|
776
|
-
const facts = await Promise.all(
|
|
1019
|
+
const facts = await Promise.all(
|
|
1020
|
+
pages.map((page) => resolveRoute(rt, cwd, page, config, layouts, cache, aliases, appHtmlIds))
|
|
1021
|
+
);
|
|
777
1022
|
return {
|
|
778
1023
|
heads: facts.map((f) => f.head),
|
|
779
1024
|
images: facts.map((f) => f.images),
|
|
780
|
-
headings: facts.map((f) => f.headings)
|
|
1025
|
+
headings: facts.map((f) => f.headings),
|
|
1026
|
+
a11y: facts.map((f) => f.a11y)
|
|
781
1027
|
};
|
|
782
1028
|
}
|
|
783
1029
|
|
|
@@ -794,7 +1040,7 @@ async function collectAll(rt, cwd, config, opts = {}) {
|
|
|
794
1040
|
const matches = routeMatcher(opts.route);
|
|
795
1041
|
const project = await collectProjectFacts(rt, cwd);
|
|
796
1042
|
const [collected, components, kitModules, sourceFiles] = await Promise.all([
|
|
797
|
-
collectRoutes(rt, cwd, config, opts.parseCache, project.kitAliases),
|
|
1043
|
+
collectRoutes(rt, cwd, config, opts.parseCache, project.kitAliases, project.appHtmlIds),
|
|
798
1044
|
// Component (Correctness) facts are file-scoped with no route attribution yet, so a
|
|
799
1045
|
// route-filtered run skips them rather than reporting unrelated components (#68 review);
|
|
800
1046
|
// kitModules is skipped for the same reason.
|
|
@@ -809,7 +1055,8 @@ async function collectAll(rt, cwd, config, opts = {}) {
|
|
|
809
1055
|
const heads = collected.heads.filter((h) => matches(h.route));
|
|
810
1056
|
const images = collected.images.filter((i) => matches(i.route));
|
|
811
1057
|
const headings = collected.headings.filter((h) => matches(h.route));
|
|
812
|
-
|
|
1058
|
+
const a11y = collected.a11y.filter((a) => matches(a.route));
|
|
1059
|
+
return { heads, images, headings, a11y, project, components, kitModules, sourceFiles };
|
|
813
1060
|
}
|
|
814
1061
|
|
|
815
1062
|
// src/changed-files.ts
|
|
@@ -1002,15 +1249,16 @@ function applySuppressions(results, entries, config, allResults) {
|
|
|
1002
1249
|
}
|
|
1003
1250
|
|
|
1004
1251
|
// src/color.ts
|
|
1252
|
+
import { styleText } from "util";
|
|
1005
1253
|
import { noColorPalette } from "@svelte-vitals/core";
|
|
1006
|
-
var wrap = (
|
|
1254
|
+
var wrap = (format) => (s) => styleText(format, s, { validateStream: false });
|
|
1007
1255
|
var ansiPalette = {
|
|
1008
|
-
bold: wrap(
|
|
1009
|
-
dim: wrap(
|
|
1010
|
-
red: wrap(
|
|
1011
|
-
yellow: wrap(
|
|
1012
|
-
green: wrap(
|
|
1013
|
-
cyan: wrap(
|
|
1256
|
+
bold: wrap("bold"),
|
|
1257
|
+
dim: wrap("dim"),
|
|
1258
|
+
red: wrap("red"),
|
|
1259
|
+
yellow: wrap("yellow"),
|
|
1260
|
+
green: wrap("green"),
|
|
1261
|
+
cyan: wrap("cyan")
|
|
1014
1262
|
};
|
|
1015
1263
|
function colorEnabled(opts) {
|
|
1016
1264
|
if (opts.noColorFlag) return false;
|
|
@@ -1021,28 +1269,6 @@ function colorEnabled(opts) {
|
|
|
1021
1269
|
}
|
|
1022
1270
|
var paletteFor = (enabled) => enabled ? ansiPalette : noColorPalette;
|
|
1023
1271
|
|
|
1024
|
-
// src/spinner.ts
|
|
1025
|
-
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1026
|
-
function startSpinner(text, opts) {
|
|
1027
|
-
const stream = opts.stream ?? process.stderr;
|
|
1028
|
-
if (!opts.enabled) return { stop() {
|
|
1029
|
-
} };
|
|
1030
|
-
let i = 0;
|
|
1031
|
-
const tick = () => {
|
|
1032
|
-
stream.write(`\r${FRAMES[i % FRAMES.length]} ${text}`);
|
|
1033
|
-
i++;
|
|
1034
|
-
};
|
|
1035
|
-
tick();
|
|
1036
|
-
const timer = setInterval(tick, 80);
|
|
1037
|
-
if (typeof timer.unref === "function") timer.unref();
|
|
1038
|
-
return {
|
|
1039
|
-
stop() {
|
|
1040
|
-
clearInterval(timer);
|
|
1041
|
-
stream.write("\r\x1B[K");
|
|
1042
|
-
}
|
|
1043
|
-
};
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
1272
|
// src/mascot.ts
|
|
1047
1273
|
import { createLogUpdate } from "log-update";
|
|
1048
1274
|
function mascotStateFor(score) {
|
|
@@ -1119,24 +1345,28 @@ function renderConfettiFrame(offset, mascotBlock) {
|
|
|
1119
1345
|
return [confettiRow(offset), mascotBlock, confettiRow(offset + 2)].join("\n");
|
|
1120
1346
|
}
|
|
1121
1347
|
var IDLE_TICK_MS = 160;
|
|
1348
|
+
var PLAIN_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1349
|
+
var PLAIN_TICK_MS = 80;
|
|
1122
1350
|
function startMascotSpinner(text, opts) {
|
|
1123
1351
|
if (!opts.enabled) return { stop() {
|
|
1124
1352
|
} };
|
|
1125
1353
|
const stream = opts.stream ?? process.stderr;
|
|
1354
|
+
const mascot = opts.mascot ?? true;
|
|
1126
1355
|
const render = createLogUpdate(stream);
|
|
1127
1356
|
let i = 0;
|
|
1128
1357
|
const tick = () => {
|
|
1129
|
-
render(`${renderMascotIdleFrame(i)}
|
|
1130
|
-
${text}`);
|
|
1358
|
+
render(mascot ? `${renderMascotIdleFrame(i)}
|
|
1359
|
+
${text}` : `${PLAIN_FRAMES[i % PLAIN_FRAMES.length]} ${text}`);
|
|
1131
1360
|
i++;
|
|
1132
1361
|
};
|
|
1133
1362
|
tick();
|
|
1134
|
-
const timer = setInterval(tick, IDLE_TICK_MS);
|
|
1363
|
+
const timer = setInterval(tick, mascot ? IDLE_TICK_MS : PLAIN_TICK_MS);
|
|
1135
1364
|
if (typeof timer.unref === "function") timer.unref();
|
|
1136
1365
|
return {
|
|
1137
1366
|
stop() {
|
|
1138
1367
|
clearInterval(timer);
|
|
1139
1368
|
render.clear();
|
|
1369
|
+
render.done();
|
|
1140
1370
|
}
|
|
1141
1371
|
};
|
|
1142
1372
|
}
|
|
@@ -1307,7 +1537,7 @@ function skippedFileWarnings(facts) {
|
|
|
1307
1537
|
];
|
|
1308
1538
|
}
|
|
1309
1539
|
function failedRuleWarnings(failedRules) {
|
|
1310
|
-
return failedRules.map(
|
|
1540
|
+
return failedRules.map(formatFailedRuleWarning);
|
|
1311
1541
|
}
|
|
1312
1542
|
async function analyzeProject(opts = {}) {
|
|
1313
1543
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -1334,10 +1564,15 @@ async function analyzeProject(opts = {}) {
|
|
|
1334
1564
|
...await checkVersionFloor(rt, cwd),
|
|
1335
1565
|
...overridesOffWarnings(opts.allowRules, config.overrides)
|
|
1336
1566
|
];
|
|
1337
|
-
const { heads, images, headings, project, components, kitModules, sourceFiles } = await collectAll(
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1567
|
+
const { heads, images, headings, a11y, project, components, kitModules, sourceFiles } = await collectAll(
|
|
1568
|
+
rt,
|
|
1569
|
+
cwd,
|
|
1570
|
+
config,
|
|
1571
|
+
{
|
|
1572
|
+
route: opts.route,
|
|
1573
|
+
parseCache: opts.parseCache
|
|
1574
|
+
}
|
|
1575
|
+
);
|
|
1341
1576
|
const selected = selectRules(allRules2, config);
|
|
1342
1577
|
const rules = opts.categories ? selected.filter((r) => opts.categories.includes(r.category)) : selected;
|
|
1343
1578
|
const {
|
|
@@ -1348,6 +1583,7 @@ async function analyzeProject(opts = {}) {
|
|
|
1348
1583
|
heads,
|
|
1349
1584
|
images,
|
|
1350
1585
|
headings,
|
|
1586
|
+
a11y,
|
|
1351
1587
|
components,
|
|
1352
1588
|
project,
|
|
1353
1589
|
config,
|
|
@@ -1355,22 +1591,22 @@ async function analyzeProject(opts = {}) {
|
|
|
1355
1591
|
sourceFiles
|
|
1356
1592
|
});
|
|
1357
1593
|
const results = applyOverrides(applyRuleSeverities(rawResults, config), config);
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1360
|
-
failedRules.map((f) => f.id)
|
|
1361
|
-
);
|
|
1594
|
+
const failedRuleIds = failedRules.map((f) => f.id);
|
|
1595
|
+
const scoringConfig = withFailedRulesOff(config, failedRuleIds);
|
|
1362
1596
|
return {
|
|
1363
1597
|
results,
|
|
1364
1598
|
config: scoringConfig,
|
|
1365
1599
|
version: readPackageVersion(),
|
|
1366
1600
|
ruleIds: rules.map((r) => r.id),
|
|
1367
1601
|
examined,
|
|
1602
|
+
failedRuleIds,
|
|
1368
1603
|
warnings: [...warnings, ...skippedFileWarnings([...components, ...kitModules]), ...failedRuleWarnings(failedRules)],
|
|
1369
1604
|
loadedConfig: loaded
|
|
1370
1605
|
};
|
|
1371
1606
|
}
|
|
1372
1607
|
async function applyScope(results, opts) {
|
|
1373
|
-
const
|
|
1608
|
+
const rawErrorLog = opts.errorLog ?? ((line) => console.error(line));
|
|
1609
|
+
const errorLog = (line) => rawErrorLog(terminalSafe(line));
|
|
1374
1610
|
let scoped = results;
|
|
1375
1611
|
if (opts.staged || opts.diffBase !== void 0) {
|
|
1376
1612
|
const changed = opts.staged ? getChangedFiles(opts.cwd, { staged: true }) : getChangedFiles(opts.cwd, { base: opts.diffBase });
|
|
@@ -1433,7 +1669,8 @@ function runAnalyzeOptions(opts) {
|
|
|
1433
1669
|
}
|
|
1434
1670
|
async function run(opts = {}) {
|
|
1435
1671
|
const log = opts.log ?? ((line) => console.log(line));
|
|
1436
|
-
const
|
|
1672
|
+
const rawErrorLog = opts.errorLog ?? ((line) => console.error(line));
|
|
1673
|
+
const errorLog = (line) => rawErrorLog(terminalSafe(line));
|
|
1437
1674
|
if (opts.minHealth != null && (!Number.isFinite(opts.minHealth) || opts.minHealth < 0 || opts.minHealth > 100)) {
|
|
1438
1675
|
errorLog(`svelte-vitals: invalid minHealth '${opts.minHealth}'; expected a number 0-100.`);
|
|
1439
1676
|
return 2;
|
|
@@ -1452,7 +1689,11 @@ async function run(opts = {}) {
|
|
|
1452
1689
|
if (useMascotSpinner && bubbleFitsWidth(stderrStream.columns)) {
|
|
1453
1690
|
await playMascotGreeting({ enabled: true, stream: stderrStream, holdMs: opts.animationFrameDelayMs });
|
|
1454
1691
|
}
|
|
1455
|
-
const spinner =
|
|
1692
|
+
const spinner = startMascotSpinner("Analyzing\u2026", {
|
|
1693
|
+
enabled: spinnerBaseEnabled,
|
|
1694
|
+
stream: stderrStream,
|
|
1695
|
+
mascot: useMascotSpinner
|
|
1696
|
+
});
|
|
1456
1697
|
let cwd = opts.cwd ?? process.cwd();
|
|
1457
1698
|
let analysis;
|
|
1458
1699
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
toList
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-43FGGQQZ.js";
|
|
4
4
|
import {
|
|
5
5
|
CONFIG_FILENAMES,
|
|
6
6
|
discoverApps,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
WORKFLOW_PATH,
|
|
18
18
|
buildWorkflowYaml,
|
|
19
19
|
planWorkflowWrite
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-CTMKTFST.js";
|
|
21
21
|
import {
|
|
22
22
|
clackPrompts,
|
|
23
23
|
realIO
|
|
@@ -110,13 +110,14 @@ function isKind(id, kind) {
|
|
|
110
110
|
|
|
111
111
|
// src/install/skill-content.ts
|
|
112
112
|
import { allRules, docsUrlFor } from "@svelte-vitals/core";
|
|
113
|
-
var CATEGORY_ORDER = ["seo", "performance", "correctness", "security", "architecture"];
|
|
113
|
+
var CATEGORY_ORDER = ["seo", "performance", "correctness", "security", "architecture", "a11y"];
|
|
114
114
|
var CATEGORY_LABELS = {
|
|
115
115
|
seo: "SEO",
|
|
116
116
|
performance: "Performance",
|
|
117
117
|
correctness: "Correctness",
|
|
118
118
|
security: "Security",
|
|
119
|
-
architecture: "Architecture"
|
|
119
|
+
architecture: "Architecture",
|
|
120
|
+
a11y: "Accessibility"
|
|
120
121
|
};
|
|
121
122
|
function oneLine(text) {
|
|
122
123
|
return text.replace(/\r?\n+/g, " ").trim();
|
|
@@ -151,7 +152,7 @@ function sharedBody(version) {
|
|
|
151
152
|
|
|
152
153
|
## When to use
|
|
153
154
|
|
|
154
|
-
Use this whenever you are writing or reviewing SvelteKit route files (\`+page.svelte\`, \`+layout.svelte\`) or components in this project \u2014 svelte-vitals statically checks SEO, performance, correctness, security, and
|
|
155
|
+
Use this whenever you are writing or reviewing SvelteKit route files (\`+page.svelte\`, \`+layout.svelte\`) or components in this project \u2014 svelte-vitals statically checks SEO, performance, correctness, security, architecture, and accessibility patterns.
|
|
155
156
|
|
|
156
157
|
## Playbook
|
|
157
158
|
|
|
@@ -168,7 +169,7 @@ ${ruleDigest()}
|
|
|
168
169
|
function buildSkillMarkdown(version) {
|
|
169
170
|
const frontmatter = `---
|
|
170
171
|
name: svelte-vitals
|
|
171
|
-
description: Use when writing or reviewing SvelteKit routes/components \u2014 svelte-vitals rule knowledge (SEO, performance, correctness, security, architecture) and how to run the scanner.
|
|
172
|
+
description: Use when writing or reviewing SvelteKit routes/components \u2014 svelte-vitals rule knowledge (SEO, performance, correctness, security, architecture, accessibility) and how to run the scanner.
|
|
172
173
|
---`;
|
|
173
174
|
return `${frontmatter}
|
|
174
175
|
|
|
@@ -176,7 +177,7 @@ ${sharedBody(version)}`;
|
|
|
176
177
|
}
|
|
177
178
|
function buildCursorRules(version) {
|
|
178
179
|
const frontmatter = `---
|
|
179
|
-
description: svelte-vitals code-health rules for SvelteKit (SEO, performance, correctness, security, architecture)
|
|
180
|
+
description: svelte-vitals code-health rules for SvelteKit (SEO, performance, correctness, security, architecture, accessibility)
|
|
180
181
|
globs: ["**/*.svelte", "src/routes/**"]
|
|
181
182
|
alwaysApply: false
|
|
182
183
|
---`;
|
|
@@ -281,7 +282,8 @@ Get the machine map before applying judgment:
|
|
|
281
282
|
\`<title>\`/meta (SEO), image-heavy routes (Performance), forms and
|
|
282
283
|
\`{@html}\` usage (Security), large or unkeyed list-rendering routes
|
|
283
284
|
(Correctness), route/component files that have grown large or deeply
|
|
284
|
-
nested (Architecture)
|
|
285
|
+
nested (Architecture), interactive controls and forms with unclear
|
|
286
|
+
labeling or ARIA usage (Accessibility).
|
|
285
287
|
- **Leverage map** (the judgment the scan lacks): which routes are
|
|
286
288
|
high-traffic/public/indexed (a marketing page, a product listing) versus
|
|
287
289
|
low-traffic or gated (an internal admin tool, a rarely visited settings
|
|
@@ -290,10 +292,10 @@ Get the machine map before applying judgment:
|
|
|
290
292
|
|
|
291
293
|
### Phase 2 \u2014 Audit (parallel)
|
|
292
294
|
|
|
293
|
-
Audit against svelte-vitals'
|
|
294
|
-
Security, Architecture (see the rule catalog below for the
|
|
295
|
-
list per category, generated from svelte-vitals' own rule
|
|
296
|
-
in sync, never invented).
|
|
295
|
+
Audit against svelte-vitals' six categories: SEO, Performance, Correctness,
|
|
296
|
+
Security, Architecture, Accessibility (see the rule catalog below for the
|
|
297
|
+
full "hunt for" list per category, generated from svelte-vitals' own rule
|
|
298
|
+
metadata \u2014 always in sync, never invented).
|
|
297
299
|
|
|
298
300
|
For anything beyond a small project, fan out read-only subagents \u2014 one per
|
|
299
301
|
category. Each subagent prompt must include: the recon facts (stack,
|
|
@@ -311,8 +313,8 @@ Depth follows effort level (default \`standard\`):
|
|
|
311
313
|
| Effort | Coverage | Subagents | Findings |
|
|
312
314
|
| ---------- | -------------------------------------- | --------- | ----------------------------- |
|
|
313
315
|
| \`quick\` | Highest-traffic/public routes only | 0\u20131 | ~5, HIGH severity only |
|
|
314
|
-
| \`standard\` | All routes and components | \
|
|
315
|
-
| \`deep\` | Whole project incl. rarely-hit routes |
|
|
316
|
+
| \`standard\` | All routes and components | \u22646 | Full table |
|
|
317
|
+
| \`deep\` | Whole project incl. rarely-hit routes | 6 | Full table + LOW polish items |
|
|
316
318
|
|
|
317
319
|
### Phase 3 \u2014 Vet, prioritize, confirm
|
|
318
320
|
|
|
@@ -408,6 +410,11 @@ category the rule catalog above can't cover:
|
|
|
408
410
|
complex, well-organized page (leave it \u2014 don't split just to satisfy a
|
|
409
411
|
metric). Look for duplicated \`<svelte:head>\` boilerplate that a shared
|
|
410
412
|
layout or meta component would remove.
|
|
413
|
+
- **Accessibility** \u2014 svelte-vitals checks static markup (ARIA validity,
|
|
414
|
+
landmarks, ids, labels); it can't drive a keyboard or screen reader. Hunt
|
|
415
|
+
for illogical tab order, missing visible focus styles, color contrast
|
|
416
|
+
below WCAG thresholds, and modal/menu components that don't trap or
|
|
417
|
+
restore focus \u2014 all invisible to a static scan.
|
|
411
418
|
|
|
412
419
|
## Plan template
|
|
413
420
|
|
|
@@ -421,7 +428,7 @@ target state.
|
|
|
421
428
|
- **Status**: TODO
|
|
422
429
|
- **Commit**: <output of \`git rev-parse --short HEAD\` when written>
|
|
423
430
|
- **Severity**: HIGH | MEDIUM | LOW
|
|
424
|
-
- **Category**: SEO | Performance | Correctness | Security | Architecture
|
|
431
|
+
- **Category**: SEO | Performance | Correctness | Security | Architecture | Accessibility
|
|
425
432
|
- **Rule**: <RULEID> | Beyond the scan
|
|
426
433
|
- **Estimated scope**: <n files, rough size>
|
|
427
434
|
|
|
@@ -491,7 +498,7 @@ adapted to this file \u2014 never approximated from memory.
|
|
|
491
498
|
| ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
492
499
|
| bare | Full workflow: recon \u2192 audit all categories \u2192 vet \u2192 confirm \u2192 plans |
|
|
493
500
|
| \`quick\` / \`deep\` | Adjust audit effort (see table); composes with a category focus |
|
|
494
|
-
| a category focus (\`seo\`, \`performance\`, \`correctness\`, \`security\`, \`architecture\`)
|
|
501
|
+
| a category focus (\`seo\`, \`performance\`, \`correctness\`, \`security\`, \`architecture\`, \`accessibility\`) | Recon + audit that category only |
|
|
495
502
|
| \`plan <description>\` | Skip the audit; recon just enough to specify, then write a single plan for the described improvement |
|
|
496
503
|
| \`execute <plan>\` | Dispatch an executor subagent to implement the plan in an isolated worktree, then review its diff against svelte-vitals (\`--diff --reporter agent\`) and render a verdict |
|
|
497
504
|
| \`reconcile\` | Re-check \`plans/\` against the current code: mark done plans DONE, refresh stale \`file:line\`/route references, retire fixed findings |
|
|
@@ -1143,7 +1150,7 @@ var INSTALL_ARGS = {
|
|
|
1143
1150
|
help: { type: "boolean", short: "h", description: "Show this help" }
|
|
1144
1151
|
};
|
|
1145
1152
|
async function buildInstallHelpText(installCommand2, locale) {
|
|
1146
|
-
const ja = locale === "ja" ? await import("./ja-
|
|
1153
|
+
const ja = locale === "ja" ? await import("./ja-GAQSBSXF.js") : void 0;
|
|
1147
1154
|
const optionsSection = stripAutoVersionLine(
|
|
1148
1155
|
await localizedOptionsSection(
|
|
1149
1156
|
installCommand2,
|
|
@@ -16,7 +16,7 @@ var JA_ARG_DESCRIPTIONS = {
|
|
|
16
16
|
"min-health": "\u7D44\u307F\u5408\u308F\u305B\u305F Health \u30B9\u30B3\u30A2\u304C\u3053\u306E\u5024\u3092\u4E0B\u56DE\u308C\u3070\u5931\u6557\uFF08\u7D42\u4E86\u30B3\u30FC\u30C9 1\u30010\u301C100\uFF09",
|
|
17
17
|
rules: "\u6709\u52B9\u306B\u3059\u308B\u30EB\u30FC\u30EB ID\uFF08\u30AB\u30F3\u30DE\u533A\u5207\u308A\u3001\u4ED6\u306F\u3059\u3079\u3066\u7121\u52B9\uFF09",
|
|
18
18
|
ignore: "\u7121\u52B9\u306B\u3059\u308B\u30EB\u30FC\u30EB ID\uFF08\u30AB\u30F3\u30DE\u533A\u5207\u308A\uFF09",
|
|
19
|
-
category: "\u89E3\u6790\u5BFE\u8C61\u30AB\u30C6\u30B4\u30EA\uFF08\u30AB\u30F3\u30DE\u533A\u5207\u308A\uFF09: seo | performance | correctness | security | architecture",
|
|
19
|
+
category: "\u89E3\u6790\u5BFE\u8C61\u30AB\u30C6\u30B4\u30EA\uFF08\u30AB\u30F3\u30DE\u533A\u5207\u308A\uFF09: seo | performance | correctness | security | architecture | a11y",
|
|
20
20
|
weights: "\u30AB\u30C6\u30B4\u30EA\u3054\u3068\u306E Health \u91CD\u307F\u4E0A\u66F8\u304D\u3002\u4F8B: seo=2,performance=1\uFF08\u6307\u5B9A\u306E\u306A\u3044\u30AB\u30C6\u30B4\u30EA\u306F\u30C7\u30D5\u30A9\u30EB\u30C8\u5024 1\uFF09",
|
|
21
21
|
score: "\u7D44\u307F\u5408\u308F\u305B\u305F Health \u30B9\u30B3\u30A2\u306E\u307F\u3092\u51FA\u529B\uFF08--min-health \u3068\u4F75\u7528\u3057\u3066\u30B2\u30FC\u30C8\u306B\u5229\u7528\u53EF\u80FD\uFF09",
|
|
22
22
|
noColor: "\u30B3\u30F3\u30BD\u30FC\u30EB\u51FA\u529B\u306E ANSI \u30AB\u30E9\u30FC\u3092\u7121\u52B9\u5316",
|
|
@@ -50,7 +50,7 @@ var JA_ARG_DESCRIPTIONS = {
|
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
52
|
function rootHelpJa(optionsSection) {
|
|
53
|
-
return `svelte-vitals \u2014 \u6C7A\u5B9A\u8AD6\u7684\u306A SvelteKit \u30B3\u30FC\u30C9\u30D8\u30EB\u30B9\u30C1\u30A7\u30C3\u30AB\u30FC\uFF08SEO\u30FB\u30D1\u30D5\u30A9\u30FC\u30DE\u30F3\u30B9\u30FB\u6B63\u78BA\u6027\u30FB\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u30FB\u30A2\u30FC\u30AD\u30C6\u30AF\u30C1\u30E3\uFF09
|
|
53
|
+
return `svelte-vitals \u2014 \u6C7A\u5B9A\u8AD6\u7684\u306A SvelteKit \u30B3\u30FC\u30C9\u30D8\u30EB\u30B9\u30C1\u30A7\u30C3\u30AB\u30FC\uFF08SEO\u30FB\u30D1\u30D5\u30A9\u30FC\u30DE\u30F3\u30B9\u30FB\u6B63\u78BA\u6027\u30FB\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u30FB\u30A2\u30FC\u30AD\u30C6\u30AF\u30C1\u30E3\u30FB\u30A2\u30AF\u30BB\u30B7\u30D3\u30EA\u30C6\u30A3\uFF09
|
|
54
54
|
|
|
55
55
|
\u4F7F\u7528\u65B9\u6CD5:
|
|
56
56
|
svelte-vitals [path] [options]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/ci/workflow.ts
|
|
2
2
|
var WORKFLOW_PATH = ".github/workflows/svelte-vitals.yml";
|
|
3
|
-
var CHECKOUT_SHA = "
|
|
4
|
-
var CHECKOUT_VERSION = "v7.0.
|
|
3
|
+
var CHECKOUT_SHA = "3d3c42e5aac5ba805825da76410c181273ba90b1";
|
|
4
|
+
var CHECKOUT_VERSION = "v7.0.1";
|
|
5
5
|
function planWorkflowWrite(existing, force) {
|
|
6
6
|
if (existing === void 0) return { status: "created" };
|
|
7
7
|
if (!force) return { status: "exists" };
|
|
@@ -28,6 +28,7 @@ function buildWorkflowYaml(opts) {
|
|
|
28
28
|
` - uses: actions/checkout@${CHECKOUT_SHA} # ${CHECKOUT_VERSION}`,
|
|
29
29
|
" with:",
|
|
30
30
|
" fetch-depth: 0",
|
|
31
|
+
" persist-credentials: false",
|
|
31
32
|
` - uses: oekazuma/svelte-vitals-action@${actionSha} # v${actionVersion}`,
|
|
32
33
|
" with:",
|
|
33
34
|
" diff: origin/${{ github.base_ref }}",
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
WORKFLOW_PATH,
|
|
5
5
|
buildWorkflowYaml,
|
|
6
6
|
planWorkflowWrite
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-CTMKTFST.js";
|
|
8
8
|
import {
|
|
9
9
|
realIO
|
|
10
10
|
} from "./chunk-GE7TKVTX.js";
|
|
@@ -86,7 +86,7 @@ var CI_UPGRADE_ARGS = { "dry-run": CI_ARGS["dry-run"], help: CI_ARGS.help };
|
|
|
86
86
|
var KNOWN_LONG_FLAGS = /* @__PURE__ */ new Set(["force", "dry-run", "help"]);
|
|
87
87
|
var KNOWN_SHORT_FLAGS = /* @__PURE__ */ new Set(["h"]);
|
|
88
88
|
async function buildCiHelpText(ciArgsCommand, locale) {
|
|
89
|
-
const ja = locale === "ja" ? await import("./ja-
|
|
89
|
+
const ja = locale === "ja" ? await import("./ja-GAQSBSXF.js") : void 0;
|
|
90
90
|
const optionsSection = stripAutoVersionLine(
|
|
91
91
|
await localizedOptionsSection(ciArgsCommand, "svelte-vitals ci", locale, ja?.JA_ARG_DESCRIPTIONS.ci ?? {})
|
|
92
92
|
);
|
|
@@ -17,10 +17,7 @@ import {
|
|
|
17
17
|
import { cli } from "gunshi/bone";
|
|
18
18
|
import { define } from "gunshi/definition";
|
|
19
19
|
import "gunshi/generator";
|
|
20
|
-
import { explainRule
|
|
21
|
-
|
|
22
|
-
// src/explain.ts
|
|
23
|
-
import { allRules, CATEGORIES } from "@svelte-vitals/core";
|
|
20
|
+
import { explainRule, allRules, CATEGORIES } from "@svelte-vitals/core";
|
|
24
21
|
function describeOptions(id, options) {
|
|
25
22
|
const MERGE = {
|
|
26
23
|
integer: "replaces the default",
|
|
@@ -56,8 +53,6 @@ function renderRuleList() {
|
|
|
56
53
|
"\n\n"
|
|
57
54
|
);
|
|
58
55
|
}
|
|
59
|
-
|
|
60
|
-
// src/gunshi/explain.ts
|
|
61
56
|
var BOOLEAN_FLAGS = ["json", "list", "help"];
|
|
62
57
|
var KNOWN_LONG_FLAGS = new Set(BOOLEAN_FLAGS);
|
|
63
58
|
var KNOWN_SHORT_FLAGS = /* @__PURE__ */ new Set(["h"]);
|
|
@@ -67,7 +62,7 @@ var EXPLAIN_ARGS = {
|
|
|
67
62
|
help: { type: "boolean", short: "h", description: "Show this help" }
|
|
68
63
|
};
|
|
69
64
|
async function buildExplainHelpText(explainCommand, locale) {
|
|
70
|
-
const ja = locale === "ja" ? await import("./ja-
|
|
65
|
+
const ja = locale === "ja" ? await import("./ja-GAQSBSXF.js") : void 0;
|
|
71
66
|
const optionsSection = stripAutoVersionLine(
|
|
72
67
|
await localizedOptionsSection(
|
|
73
68
|
explainCommand,
|
|
@@ -113,7 +108,7 @@ async function runExplainCliGunshi(args, io = consoleIO, locale = "en") {
|
|
|
113
108
|
}
|
|
114
109
|
io.log(
|
|
115
110
|
ctx.values.json ? JSON.stringify(
|
|
116
|
-
|
|
111
|
+
allRules.map((r) => ({ id: r.id, category: r.category, severity: r.severity, title: r.title })),
|
|
117
112
|
null,
|
|
118
113
|
2
|
|
119
114
|
) : renderRuleList()
|
|
@@ -130,7 +125,7 @@ async function runExplainCliGunshi(args, io = consoleIO, locale = "en") {
|
|
|
130
125
|
exitCode = 2;
|
|
131
126
|
return;
|
|
132
127
|
}
|
|
133
|
-
const info =
|
|
128
|
+
const info = explainRule(id);
|
|
134
129
|
if (!info) {
|
|
135
130
|
io.errorLog(`svelte-vitals: unknown rule id '${id}'.`);
|
|
136
131
|
const hint = suggestClosest(id, knownRuleIds());
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
run
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-5UQ6P2M2.js";
|
|
4
4
|
import {
|
|
5
5
|
VALUE_FLAGS,
|
|
6
6
|
parseRunArgs,
|
|
7
7
|
resolveArgs
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-43FGGQQZ.js";
|
|
9
9
|
import {
|
|
10
10
|
readCoreVersion,
|
|
11
11
|
readPackageVersion
|
|
@@ -90,7 +90,7 @@ var ROOT_ARGS = {
|
|
|
90
90
|
ignore: { type: "string", description: "Comma-separated rule ids to disable" },
|
|
91
91
|
category: {
|
|
92
92
|
type: "string",
|
|
93
|
-
description: "Comma-separated categories to analyze: seo | performance | correctness | security | architecture"
|
|
93
|
+
description: "Comma-separated categories to analyze: seo | performance | correctness | security | architecture | a11y"
|
|
94
94
|
},
|
|
95
95
|
weights: {
|
|
96
96
|
type: "string",
|
|
@@ -127,7 +127,7 @@ function neutralizeBareDiffAndBaseline(argv) {
|
|
|
127
127
|
}
|
|
128
128
|
async function buildHelpText(rootCommand, locale) {
|
|
129
129
|
if (locale === "ja") {
|
|
130
|
-
const { JA_ARG_DESCRIPTIONS, rootHelpJa } = await import("./ja-
|
|
130
|
+
const { JA_ARG_DESCRIPTIONS, rootHelpJa } = await import("./ja-GAQSBSXF.js");
|
|
131
131
|
const optionsSection2 = await localizedOptionsSection(
|
|
132
132
|
rootCommand,
|
|
133
133
|
"svelte-vitals",
|
|
@@ -137,7 +137,7 @@ async function buildHelpText(rootCommand, locale) {
|
|
|
137
137
|
return rootHelpJa(optionsSection2);
|
|
138
138
|
}
|
|
139
139
|
const optionsSection = await localizedOptionsSection(rootCommand, "svelte-vitals", locale, {});
|
|
140
|
-
return `svelte-vitals \u2014 a deterministic SvelteKit code-health scanner (SEO \xB7 performance \xB7 correctness \xB7 security \xB7 architecture)
|
|
140
|
+
return `svelte-vitals \u2014 a deterministic SvelteKit code-health scanner (SEO \xB7 performance \xB7 correctness \xB7 security \xB7 architecture \xB7 accessibility)
|
|
141
141
|
|
|
142
142
|
Usage:
|
|
143
143
|
svelte-vitals [path] [options]
|
|
@@ -31,7 +31,7 @@ var EMBEDDED_DOCS = [
|
|
|
31
31
|
name: "config",
|
|
32
32
|
title: "The config file",
|
|
33
33
|
description: "Where svelte-vitals.config lives, every top-level option, how to disable or re-grade a rule, and how to scope rules to routes or files.",
|
|
34
|
-
body: "# The config file\n\n## Where it lives\n\nIn the **analyzed directory only** \u2014 no upward search. First match wins:\n\n1. `svelte-vitals.config.mjs`\n2. `svelte-vitals.config.js`\n3. `svelte-vitals.config.ts`\n\nNo file means built-in defaults. `svelte-vitals install --client config-file` scaffolds one with\nevery option commented out.\n\n```js\n// svelte-vitals.config.mjs\nexport default {\n treatDynamicAs: 'warn',\n metaComponents: ['Seo'],\n rules: { 'seo/json-ld': 'off' },\n failOn: 'warning',\n weights: { seo: 2 }\n};\n```\n\nA `.ts` config can `import { defineConfig } from 'svelte-vitals'` for type-checking, but that is a\n**runtime** import: it needs svelte-vitals as a declared dependency and Node 22.18+ (or 23.6+).\nA plain `export default {}` in `.mjs` behaves identically and always works.\n\n## Options\n\n| Option | Type | Default |\n| ---------------- | -------------------------------------------------------------- | ------------------ |\n| `treatDynamicAs` | `'pass' \\| 'warn' \\| 'fail'` | `'pass'` |\n| `metaComponents` | `string[]` | `[]` |\n| `rules` | `Record<ruleId, 'off' \\| Severity \\| { severity?, options? }>` | `{}` |\n| `failOn` | `'critical' \\| 'warning' \\| 'info'` | `'critical'` |\n| `weights` | `Partial<Record<Category, number>>` | every category `1` |\n| `overrides` | `RuleOverride[]` | (none) |\n\n`Severity` is `'critical' | 'warning' | 'info'`. `Category` is `'seo' | 'performance' |\n'correctness' | 'security' | 'architecture'`. A weight of `0` drops a category from the Health\naverage; setting every category to `0` is an error (exit `2`).\n\n## Turning a rule off or down\n\n```js\nexport default {\n rules: {\n 'seo/json-ld': 'off', // remove its findings entirely\n 'architecture/prop-count': 'info' // keep it, stop it failing the build\n }\n};\n```\n\nMany rules take options, so check whether the finding is a **threshold disagreement** rather than\na defect first. `svelte-vitals explain <rule-id>` prints each option's name, default, bounds, and\nmerge semantics (`integer` replaces, `string-list` appends, `string-map` is spread over).\n\n```js\nexport default {\n rules: {\n 'architecture/prop-count': { options: { max: 12 } }\n }\n};\n```\n\n## Scoping to routes or files (`overrides`)\n\n`rules` applies everywhere; `overrides` applies only where it matches \u2014 typically routes that\nare deliberately not public.\n\n```js\nexport default {\n overrides: [\n { files: 'src/routes/(app)/**', rules: { seo: 'off' } },\n { route: '/admin/**', rules: { 'seo/title-presence': 'info' } }\n ]\n};\n```\n\nEach entry needs `rules` (keys are rule ids **or** category names) plus at least one of:\n\n- **`route`** \u2014 glob(s) against the route id as reported (`/blog/[slug]`). SvelteKit `(group)`\n segments are **not** in the route id, so use `files` to target a group.\n- **`files`** \u2014 glob(s) against the source path.\n\nGlobs are deliberately small: `*` within a segment, `**` across segments, a trailing `/**` also\nmatches the bare prefix. Everything else \u2014 including `(`, `)`, `[`, `]` \u2014 is literal. Later entries win.\n\n## Precedence\n\nPer field: **CLI flag > config file > built-in default**. One exception \u2014 `--rules` and `--ignore`\nare selection, not configuration: `--rules` narrows the run to the ids it names and overrides a\nconfig-file `off` for them, but keeps their declared severity and options; `--ignore` adds `off`\nentries for the ids it names, layered on top of whatever `rules` resolved to, and beats `--rules`\nwhen both name the same rule.\n\n`overrides` has no CLI flag; route policy belongs in a committed file.\n\n## Validation\n\nAn unknown rule id or category, a negative weight, a malformed `overrides` entry, or an invalid\nrule setting is a **hard error (exit `2`)** \u2014 a typo must not silently un-gate CI. An unrecognized\n`treatDynamicAs`/`failOn` value, or an unknown top-level key, only warns.\n\n## Related\n\n- `svelte-vitals explain --list` \u2014 every rule id\n- `svelte-vitals docs show scoping` \u2014 accepting an existing backlog instead of disabling rules"
|
|
34
|
+
body: "# The config file\n\n## Where it lives\n\nIn the **analyzed directory only** \u2014 no upward search. First match wins:\n\n1. `svelte-vitals.config.mjs`\n2. `svelte-vitals.config.js`\n3. `svelte-vitals.config.ts`\n\nNo file means built-in defaults. `svelte-vitals install --client config-file` scaffolds one with\nevery option commented out.\n\n```js\n// svelte-vitals.config.mjs\nexport default {\n treatDynamicAs: 'warn',\n metaComponents: ['Seo'],\n rules: { 'seo/json-ld': 'off' },\n failOn: 'warning',\n weights: { seo: 2 }\n};\n```\n\nA `.ts` config can `import { defineConfig } from 'svelte-vitals'` for type-checking, but that is a\n**runtime** import: it needs svelte-vitals as a declared dependency and Node 22.18+ (or 23.6+).\nA plain `export default {}` in `.mjs` behaves identically and always works.\n\n## Options\n\n| Option | Type | Default |\n| ---------------- | -------------------------------------------------------------- | ------------------ |\n| `treatDynamicAs` | `'pass' \\| 'warn' \\| 'fail'` | `'pass'` |\n| `metaComponents` | `string[]` | `[]` |\n| `rules` | `Record<ruleId, 'off' \\| Severity \\| { severity?, options? }>` | `{}` |\n| `failOn` | `'critical' \\| 'warning' \\| 'info'` | `'critical'` |\n| `weights` | `Partial<Record<Category, number>>` | every category `1` |\n| `overrides` | `RuleOverride[]` | (none) |\n\n`Severity` is `'critical' | 'warning' | 'info'`. `Category` is `'seo' | 'performance' |\n'correctness' | 'security' | 'architecture' | 'a11y'`. A weight of `0` drops a category from the Health\naverage; setting every category to `0` is an error (exit `2`).\n\n## Turning a rule off or down\n\n```js\nexport default {\n rules: {\n 'seo/json-ld': 'off', // remove its findings entirely\n 'architecture/prop-count': 'info' // keep it, stop it failing the build\n }\n};\n```\n\nMany rules take options, so check whether the finding is a **threshold disagreement** rather than\na defect first. `svelte-vitals explain <rule-id>` prints each option's name, default, bounds, and\nmerge semantics (`integer` replaces, `string-list` appends, `string-map` is spread over).\n\n```js\nexport default {\n rules: {\n 'architecture/prop-count': { options: { max: 12 } }\n }\n};\n```\n\n## Scoping to routes or files (`overrides`)\n\n`rules` applies everywhere; `overrides` applies only where it matches \u2014 typically routes that\nare deliberately not public.\n\n```js\nexport default {\n overrides: [\n { files: 'src/routes/(app)/**', rules: { seo: 'off' } },\n { route: '/admin/**', rules: { 'seo/title-presence': 'info' } }\n ]\n};\n```\n\nEach entry needs `rules` (keys are rule ids **or** category names) plus at least one of:\n\n- **`route`** \u2014 glob(s) against the route id as reported (`/blog/[slug]`). SvelteKit `(group)`\n segments are **not** in the route id, so use `files` to target a group.\n- **`files`** \u2014 glob(s) against the source path.\n\nGlobs are deliberately small: `*` within a segment, `**` across segments, a trailing `/**` also\nmatches the bare prefix. Everything else \u2014 including `(`, `)`, `[`, `]` \u2014 is literal. Later entries win.\n\n## Precedence\n\nPer field: **CLI flag > config file > built-in default**. One exception \u2014 `--rules` and `--ignore`\nare selection, not configuration: `--rules` narrows the run to the ids it names and overrides a\nconfig-file `off` for them, but keeps their declared severity and options; `--ignore` adds `off`\nentries for the ids it names, layered on top of whatever `rules` resolved to, and beats `--rules`\nwhen both name the same rule.\n\n`overrides` has no CLI flag; route policy belongs in a committed file.\n\n## Validation\n\nAn unknown rule id or category, a negative weight, a malformed `overrides` entry, or an invalid\nrule setting is a **hard error (exit `2`)** \u2014 a typo must not silently un-gate CI. An unrecognized\n`treatDynamicAs`/`failOn` value, or an unknown top-level key, only warns.\n\n## Related\n\n- `svelte-vitals explain --list` \u2014 every rule id\n- `svelte-vitals docs show scoping` \u2014 accepting an existing backlog instead of disabling rules"
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
name: "monorepo",
|
|
@@ -53,7 +53,7 @@ var EMBEDDED_DOCS = [
|
|
|
53
53
|
}
|
|
54
54
|
];
|
|
55
55
|
|
|
56
|
-
// src/docs
|
|
56
|
+
// src/gunshi/docs.ts
|
|
57
57
|
var DOCS_HELP = `svelte-vitals docs \u2014 read the bundled guides without leaving the terminal
|
|
58
58
|
|
|
59
59
|
Usage:
|
|
@@ -83,8 +83,6 @@ function renderList() {
|
|
|
83
83
|
"Rule-level detail is a separate command: `svelte-vitals explain --list`."
|
|
84
84
|
].join("\n");
|
|
85
85
|
}
|
|
86
|
-
|
|
87
|
-
// src/gunshi/docs.ts
|
|
88
86
|
var BOOLEAN_FLAGS = ["json", "help"];
|
|
89
87
|
var HELP_ARG = { help: { type: "boolean", short: "h", description: "Show this help" } };
|
|
90
88
|
var JSON_ARG = { json: { type: "boolean", description: "Machine-readable output (list only)" } };
|
|
@@ -98,7 +96,7 @@ var DOCS_SHOW_ARGS = {
|
|
|
98
96
|
...HELP_ARG
|
|
99
97
|
};
|
|
100
98
|
async function buildDocsHelpText(rootCommand, locale) {
|
|
101
|
-
const ja = locale === "ja" ? await import("./ja-
|
|
99
|
+
const ja = locale === "ja" ? await import("./ja-GAQSBSXF.js") : void 0;
|
|
102
100
|
const optionsSection = stripAutoVersionLine(
|
|
103
101
|
await localizedOptionsSection(rootCommand, "svelte-vitals docs", locale, ja?.JA_ARG_DESCRIPTIONS.docs ?? {})
|
|
104
102
|
);
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ROOT_ARGS
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-MZO3HVTN.js";
|
|
4
|
+
import "./chunk-5UQ6P2M2.js";
|
|
5
5
|
import {
|
|
6
6
|
DOCS_LIST_ARGS,
|
|
7
7
|
DOCS_ROOT_ARGS,
|
|
8
8
|
DOCS_SHOW_ARGS
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-SLE22PPF.js";
|
|
10
10
|
import {
|
|
11
11
|
EXPLAIN_ARGS
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-JFLNGYPX.js";
|
|
13
13
|
import {
|
|
14
14
|
INSTALL_ARGS
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-77MMJCFK.js";
|
|
16
16
|
import {
|
|
17
17
|
CATEGORIES,
|
|
18
18
|
FAIL_ON_VALUES,
|
|
19
19
|
TREAT_DYNAMIC_AS_VALUES
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-43FGGQQZ.js";
|
|
21
21
|
import {
|
|
22
22
|
REPORTER_NAMES
|
|
23
23
|
} from "./chunk-M5KM5SV7.js";
|
|
@@ -28,8 +28,8 @@ import "./chunk-TFBLQUAC.js";
|
|
|
28
28
|
import {
|
|
29
29
|
CI_ARGS,
|
|
30
30
|
CI_UPGRADE_ARGS
|
|
31
|
-
} from "./chunk-
|
|
32
|
-
import "./chunk-
|
|
31
|
+
} from "./chunk-ICEJJDDA.js";
|
|
32
|
+
import "./chunk-CTMKTFST.js";
|
|
33
33
|
import "./chunk-GE7TKVTX.js";
|
|
34
34
|
import "./chunk-NMVBVKLX.js";
|
|
35
35
|
|
|
@@ -43,7 +43,15 @@ function forCompletion(args) {
|
|
|
43
43
|
const out = {};
|
|
44
44
|
for (const [key, schema] of Object.entries(args)) {
|
|
45
45
|
if (schema.hidden) continue;
|
|
46
|
-
|
|
46
|
+
const outKey = schema.type === "positional" || !schema.toKebab ? key : kebabnize(key);
|
|
47
|
+
const description = schema.description?.replace(/\s*\n\s*/g, " ");
|
|
48
|
+
out[outKey] = description === schema.description ? schema : { ...schema, description };
|
|
49
|
+
}
|
|
50
|
+
for (const [key, schema] of Object.entries(out)) {
|
|
51
|
+
const stripped = key.startsWith("no-") ? key.slice(3) : void 0;
|
|
52
|
+
if (stripped && !(stripped in out)) {
|
|
53
|
+
out[stripped] = { type: "positional", required: false, description: schema.description };
|
|
54
|
+
}
|
|
47
55
|
}
|
|
48
56
|
return out;
|
|
49
57
|
}
|
package/dist/gunshi-registry.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ROOT_ARGS
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-MZO3HVTN.js";
|
|
4
|
+
import "./chunk-5UQ6P2M2.js";
|
|
5
5
|
import {
|
|
6
6
|
JA_ARG_DESCRIPTIONS
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-A5DVJX5O.js";
|
|
8
8
|
import {
|
|
9
9
|
INSTALL_ARGS
|
|
10
|
-
} from "./chunk-
|
|
11
|
-
import "./chunk-
|
|
10
|
+
} from "./chunk-77MMJCFK.js";
|
|
11
|
+
import "./chunk-43FGGQQZ.js";
|
|
12
12
|
import "./chunk-M5KM5SV7.js";
|
|
13
13
|
import "./chunk-SLUMRYUD.js";
|
|
14
14
|
import "./chunk-TFBLQUAC.js";
|
|
15
|
-
import "./chunk-
|
|
15
|
+
import "./chunk-CTMKTFST.js";
|
|
16
16
|
import "./chunk-GE7TKVTX.js";
|
|
17
17
|
import "./chunk-NMVBVKLX.js";
|
|
18
18
|
export {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HeadTag, Config, RuleOptionsSpec, Severity, RuleSetting, Category, Result } from '@svelte-vitals/core';
|
|
1
|
+
import { HeadTag, BranchStep, Config, RuleOptionsSpec, Severity, RuleSetting, Category, Result } from '@svelte-vitals/core';
|
|
2
2
|
export { defineConfig } from '@svelte-vitals/core';
|
|
3
3
|
import { AST } from 'svelte/compiler';
|
|
4
4
|
|
|
@@ -34,12 +34,37 @@ interface ParsedHeading {
|
|
|
34
34
|
/** 1-based source line, or 0 if unknown. */
|
|
35
35
|
line: number;
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
interface A11yNode {
|
|
39
|
+
kind: 'landmark' | 'id' | 'idref' | 'component';
|
|
40
|
+
/** landmark → 'main'|'banner'|'contentinfo'|'complementary'; id/idref → the literal id; component → component name */
|
|
41
|
+
key: string;
|
|
42
|
+
line: number;
|
|
43
|
+
/** inside {#each} body or {#snippet} definition at any depth (excluded from duplication counting) */
|
|
44
|
+
repeatable: boolean;
|
|
45
|
+
/** branch address from template root (empty = unconditional) */
|
|
46
|
+
path: BranchStep[];
|
|
47
|
+
/** for kind 'idref': the referencing attribute ('for', 'aria-labelledby', …, 'href') */
|
|
48
|
+
attr?: string;
|
|
49
|
+
/** the landmark ancestor element within this file, if any */
|
|
50
|
+
inLandmark?: string;
|
|
51
|
+
/** for kind 'landmark' from <header>/<footer>: at template top level in this file (which also implies "not inside sectioning content" — depth 0 has no ancestors at all) */
|
|
52
|
+
topLevel?: boolean;
|
|
53
|
+
}
|
|
54
|
+
interface ParsedA11y {
|
|
55
|
+
nodes: A11yNode[];
|
|
56
|
+
/** landmark ancestor of this file's <slot>/{@render children()} position, if any */
|
|
57
|
+
slotInLandmark?: string;
|
|
58
|
+
/** file contains {@html} or a spread attribute — poisons the closed world for no-missing-id-ref */
|
|
59
|
+
unknowableContent: boolean;
|
|
60
|
+
}
|
|
37
61
|
interface ParsedFile {
|
|
38
62
|
headTags: ParsedTag[];
|
|
39
63
|
components: ComponentUse[];
|
|
40
64
|
imports: ImportMap;
|
|
41
65
|
images: ParsedImage[];
|
|
42
66
|
headings: ParsedHeading[];
|
|
67
|
+
a11y: ParsedA11y;
|
|
43
68
|
}
|
|
44
69
|
|
|
45
70
|
/**
|
|
@@ -231,6 +256,8 @@ interface AnalyzeResult {
|
|
|
231
256
|
ruleIds: string[];
|
|
232
257
|
/** Per-rule, per-declaration counts of places examined, unfiltered by `--diff`/`--baseline`/suppressions. */
|
|
233
258
|
examined: Record<string, Record<string, number>>;
|
|
259
|
+
/** Ids of rules `runRules` caught throwing — already folded into `config` via `withFailedRulesOff`; exposed separately so a caller with its own base config (the vite dev dashboard) can apply the same correction without adopting this call's `config`. */
|
|
260
|
+
failedRuleIds: string[];
|
|
234
261
|
/** Non-fatal issues surfaced during analysis: config-file problems (unknown top-level keys, invalid enum values), version-floor notices, `--rules`/overrides conflicts, and skipped-file notices. Empty when none apply. */
|
|
235
262
|
warnings: string[];
|
|
236
263
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
INSTALL_ARGS,
|
|
3
3
|
runInstallCliGunshi
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-77MMJCFK.js";
|
|
5
|
+
import "./chunk-43FGGQQZ.js";
|
|
6
6
|
import "./chunk-M5KM5SV7.js";
|
|
7
7
|
import "./chunk-SLUMRYUD.js";
|
|
8
8
|
import "./chunk-TFBLQUAC.js";
|
|
9
|
-
import "./chunk-
|
|
9
|
+
import "./chunk-CTMKTFST.js";
|
|
10
10
|
import "./chunk-GE7TKVTX.js";
|
|
11
11
|
import "./chunk-NMVBVKLX.js";
|
|
12
12
|
export {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-vitals",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A deterministic SvelteKit code-health scanner (SEO, performance, correctness, security, architecture) — not a runtime Web Vitals reporter.",
|
|
3
|
+
"version": "0.47.0",
|
|
4
|
+
"description": "A deterministic SvelteKit code-health scanner (SEO, performance, correctness, security, architecture, accessibility) — not a runtime Web Vitals reporter.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Kazuma Oe (https://github.com/oekazuma)",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"magicast": "^0.5.4",
|
|
53
53
|
"svelte": "^5.56.8",
|
|
54
54
|
"tinyglobby": "^0.2.17",
|
|
55
|
-
"@svelte-vitals/core": "0.
|
|
55
|
+
"@svelte-vitals/core": "0.43.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@gunshi/docs": "0.37.1",
|