svelte-vitals 0.46.0 → 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 +8 -8
- package/dist/{chunk-MUIOPL5F.js → chunk-43FGGQQZ.js} +1 -1
- package/dist/{chunk-FVN7R2YK.js → chunk-5UQ6P2M2.js} +271 -20
- package/dist/{chunk-2RQRPUCR.js → chunk-77MMJCFK.js} +23 -16
- package/dist/{chunk-MHRO4GU7.js → chunk-A5DVJX5O.js} +2 -2
- package/dist/{chunk-6BUOZU6F.js → chunk-ICEJJDDA.js} +1 -1
- package/dist/{chunk-4ZE7NBVE.js → chunk-JFLNGYPX.js} +1 -1
- package/dist/{chunk-Z2DBOC5X.js → chunk-MZO3HVTN.js} +5 -5
- package/dist/{chunk-X7BU4DLG.js → chunk-SLE22PPF.js} +2 -2
- package/dist/{ci-TFTS5QAU.js → ci-J7PGFHK3.js} +1 -1
- package/dist/{complete-7TW5PTE6.js → complete-BDKGAUQX.js} +7 -7
- package/dist/{docs-WRZT4ZJN.js → docs-XNKZQBBZ.js} +1 -1
- package/dist/{explain-W3BU3CYJ.js → explain-YPWGQE5N.js} +1 -1
- package/dist/gunshi-registry.js +5 -5
- package/dist/index.d.ts +26 -1
- package/dist/index.js +1 -1
- package/dist/{install-EP364SRT.js → install-HE76S4ID.js} +2 -2
- 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
|
|
@@ -21,23 +21,23 @@ async function runCli(argv, io = consoleIO, env = process.env) {
|
|
|
21
21
|
try {
|
|
22
22
|
const locale = resolveLocale(env);
|
|
23
23
|
if (argv[0] === "complete") {
|
|
24
|
-
const { runCompleteCliGunshi } = await import("./complete-
|
|
24
|
+
const { runCompleteCliGunshi } = await import("./complete-BDKGAUQX.js");
|
|
25
25
|
return { code: await runCompleteCliGunshi(argv, io), exit: "natural" };
|
|
26
26
|
}
|
|
27
27
|
if (argv[0] === "docs") {
|
|
28
|
-
const { runDocsCliGunshi } = await import("./docs-
|
|
28
|
+
const { runDocsCliGunshi } = await import("./docs-XNKZQBBZ.js");
|
|
29
29
|
return { code: await runDocsCliGunshi(argv.slice(1), io, locale), exit: "natural" };
|
|
30
30
|
}
|
|
31
31
|
if (argv[0] === "explain") {
|
|
32
|
-
const { runExplainCliGunshi } = await import("./explain-
|
|
32
|
+
const { runExplainCliGunshi } = await import("./explain-YPWGQE5N.js");
|
|
33
33
|
return { code: await runExplainCliGunshi(argv.slice(1), io, locale), exit: "natural" };
|
|
34
34
|
}
|
|
35
35
|
if (argv[0] === "install") {
|
|
36
|
-
const { runInstallCliGunshi } = await import("./install-
|
|
36
|
+
const { runInstallCliGunshi } = await import("./install-HE76S4ID.js");
|
|
37
37
|
return { code: await runInstallCliGunshi(argv.slice(1), io, locale), exit: "immediate" };
|
|
38
38
|
}
|
|
39
39
|
if (argv[0] === "ci") {
|
|
40
|
-
const { runCiCliGunshi } = await import("./ci-
|
|
40
|
+
const { runCiCliGunshi } = await import("./ci-J7PGFHK3.js");
|
|
41
41
|
const code = await runCiCliGunshi(argv.slice(1), { ...realIO(), log: io.log, errorLog: io.errorLog }, locale);
|
|
42
42
|
return { code, exit: "immediate" };
|
|
43
43
|
}
|
|
@@ -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);
|
|
@@ -146,10 +146,28 @@ function detectHtmlLang(html) {
|
|
|
146
146
|
const value = match[1] ?? match[2] ?? match[3] ?? "";
|
|
147
147
|
return { presence: "own", value: value.trim().length > 0 ? "static" : "absent" };
|
|
148
148
|
}
|
|
149
|
-
|
|
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) {
|
|
150
155
|
const appHtmlPath = rt.join(cwd, "src/app.html");
|
|
151
|
-
if (!await rt.exists(appHtmlPath)) return { presence: "none", value: "absent" };
|
|
152
|
-
|
|
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
|
+
};
|
|
153
171
|
}
|
|
154
172
|
async function robotsRefsSitemap(rt, cwd) {
|
|
155
173
|
const p = rt.join(cwd, "static/robots.txt");
|
|
@@ -196,10 +214,10 @@ async function detectKitConfigFacts(rt, cwd) {
|
|
|
196
214
|
};
|
|
197
215
|
}
|
|
198
216
|
async function collectProjectFacts(rt, cwd) {
|
|
199
|
-
const [hasRobotsTxt, hasSitemap,
|
|
217
|
+
const [hasRobotsTxt, hasSitemap, appHtmlFacts, viteMinifyDisabled, kitConfig] = await Promise.all([
|
|
200
218
|
existsAny(rt, cwd, ROBOTS_SOURCE_PATHS),
|
|
201
219
|
existsAny(rt, cwd, SITEMAP_SOURCE_PATHS),
|
|
202
|
-
|
|
220
|
+
detectAppHtmlFacts(rt, cwd),
|
|
203
221
|
detectViteMinifyDisabled(rt, cwd),
|
|
204
222
|
detectKitConfigFacts(rt, cwd)
|
|
205
223
|
]);
|
|
@@ -207,7 +225,7 @@ async function collectProjectFacts(rt, cwd) {
|
|
|
207
225
|
return {
|
|
208
226
|
hasRobotsTxt,
|
|
209
227
|
hasSitemap,
|
|
210
|
-
|
|
228
|
+
...appHtmlFacts,
|
|
211
229
|
...robotsReferencesSitemap !== void 0 ? { robotsReferencesSitemap } : {},
|
|
212
230
|
...viteMinifyDisabled ? { viteMinifyDisabled } : {},
|
|
213
231
|
...kitConfig
|
|
@@ -222,7 +240,7 @@ import {
|
|
|
222
240
|
} from "@svelte-vitals/core";
|
|
223
241
|
|
|
224
242
|
// src/providers/source/routes.ts
|
|
225
|
-
import { defaultConfig } from "@svelte-vitals/core";
|
|
243
|
+
import { defaultConfig, foldOccurrences, isTopFragment } from "@svelte-vitals/core";
|
|
226
244
|
|
|
227
245
|
// src/providers/source/resolve.ts
|
|
228
246
|
import { resolveRepoLocalPath } from "@svelte-vitals/core";
|
|
@@ -369,7 +387,13 @@ import {
|
|
|
369
387
|
valueFromNodes,
|
|
370
388
|
textFromNodes,
|
|
371
389
|
attrText,
|
|
372
|
-
|
|
390
|
+
attrTextOf as attrTextOf3,
|
|
391
|
+
attrValue,
|
|
392
|
+
attrValueOf as attrValueOf3,
|
|
393
|
+
decodeFragmentId,
|
|
394
|
+
splitTokens,
|
|
395
|
+
LANDMARK_ROLES,
|
|
396
|
+
IDREF_ATTRS
|
|
373
397
|
} from "@svelte-vitals/core";
|
|
374
398
|
|
|
375
399
|
// src/providers/source/imports.ts
|
|
@@ -557,6 +581,137 @@ function collectHeadings(node, source, acc) {
|
|
|
557
581
|
if (key in node) collectHeadings(childOf(node, key), source, acc);
|
|
558
582
|
}
|
|
559
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
|
+
}
|
|
560
715
|
function parseFile(source, filename) {
|
|
561
716
|
const ast = parse(source, { modern: true, filename });
|
|
562
717
|
const heads = [];
|
|
@@ -572,7 +727,8 @@ function parseFile(source, filename) {
|
|
|
572
727
|
components,
|
|
573
728
|
imports: collectImports(ast),
|
|
574
729
|
images,
|
|
575
|
-
headings
|
|
730
|
+
headings,
|
|
731
|
+
a11y: collectA11y(ast.fragment, source)
|
|
576
732
|
};
|
|
577
733
|
}
|
|
578
734
|
|
|
@@ -730,8 +886,64 @@ function chainFiles(pageRel, layouts) {
|
|
|
730
886
|
}
|
|
731
887
|
return [...chain.map((rel) => ({ rel, isPage: false })), { rel: pageRel, isPage: true }];
|
|
732
888
|
}
|
|
733
|
-
|
|
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) {
|
|
734
945
|
const files = chainFiles(pageRel, layouts);
|
|
946
|
+
const chainOrder = new Map(files.map((f, i) => [f.rel, i]));
|
|
735
947
|
const composed = /* @__PURE__ */ new Map();
|
|
736
948
|
const jsonldTags = [];
|
|
737
949
|
let broadOwn = false;
|
|
@@ -739,8 +951,20 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache, aliases) {
|
|
|
739
951
|
const images = [];
|
|
740
952
|
const headings = [];
|
|
741
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;
|
|
742
958
|
for (const { rel, isPage } of files) {
|
|
743
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);
|
|
744
968
|
for (const img of parsed.images) {
|
|
745
969
|
images.push({ ...img, file: rel });
|
|
746
970
|
}
|
|
@@ -766,20 +990,40 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache, aliases) {
|
|
|
766
990
|
if (!composed.has(key)) composed.set(key, { ...tag, presence });
|
|
767
991
|
}
|
|
768
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 !== "");
|
|
769
996
|
const route = deriveRoute(pageRel);
|
|
770
997
|
return {
|
|
771
998
|
head: { route, source: "static", tags: [...composed.values(), ...jsonldTags], file: pageRel },
|
|
772
999
|
images: { route, images },
|
|
773
|
-
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
|
+
}
|
|
774
1015
|
};
|
|
775
1016
|
}
|
|
776
|
-
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) {
|
|
777
1018
|
const [pages, layouts] = await Promise.all([enumerateRoutePages(rt, cwd), collectLayouts(rt, cwd)]);
|
|
778
|
-
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
|
+
);
|
|
779
1022
|
return {
|
|
780
1023
|
heads: facts.map((f) => f.head),
|
|
781
1024
|
images: facts.map((f) => f.images),
|
|
782
|
-
headings: facts.map((f) => f.headings)
|
|
1025
|
+
headings: facts.map((f) => f.headings),
|
|
1026
|
+
a11y: facts.map((f) => f.a11y)
|
|
783
1027
|
};
|
|
784
1028
|
}
|
|
785
1029
|
|
|
@@ -796,7 +1040,7 @@ async function collectAll(rt, cwd, config, opts = {}) {
|
|
|
796
1040
|
const matches = routeMatcher(opts.route);
|
|
797
1041
|
const project = await collectProjectFacts(rt, cwd);
|
|
798
1042
|
const [collected, components, kitModules, sourceFiles] = await Promise.all([
|
|
799
|
-
collectRoutes(rt, cwd, config, opts.parseCache, project.kitAliases),
|
|
1043
|
+
collectRoutes(rt, cwd, config, opts.parseCache, project.kitAliases, project.appHtmlIds),
|
|
800
1044
|
// Component (Correctness) facts are file-scoped with no route attribution yet, so a
|
|
801
1045
|
// route-filtered run skips them rather than reporting unrelated components (#68 review);
|
|
802
1046
|
// kitModules is skipped for the same reason.
|
|
@@ -811,7 +1055,8 @@ async function collectAll(rt, cwd, config, opts = {}) {
|
|
|
811
1055
|
const heads = collected.heads.filter((h) => matches(h.route));
|
|
812
1056
|
const images = collected.images.filter((i) => matches(i.route));
|
|
813
1057
|
const headings = collected.headings.filter((h) => matches(h.route));
|
|
814
|
-
|
|
1058
|
+
const a11y = collected.a11y.filter((a) => matches(a.route));
|
|
1059
|
+
return { heads, images, headings, a11y, project, components, kitModules, sourceFiles };
|
|
815
1060
|
}
|
|
816
1061
|
|
|
817
1062
|
// src/changed-files.ts
|
|
@@ -1319,10 +1564,15 @@ async function analyzeProject(opts = {}) {
|
|
|
1319
1564
|
...await checkVersionFloor(rt, cwd),
|
|
1320
1565
|
...overridesOffWarnings(opts.allowRules, config.overrides)
|
|
1321
1566
|
];
|
|
1322
|
-
const { heads, images, headings, project, components, kitModules, sourceFiles } = await collectAll(
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
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
|
+
);
|
|
1326
1576
|
const selected = selectRules(allRules2, config);
|
|
1327
1577
|
const rules = opts.categories ? selected.filter((r) => opts.categories.includes(r.category)) : selected;
|
|
1328
1578
|
const {
|
|
@@ -1333,6 +1583,7 @@ async function analyzeProject(opts = {}) {
|
|
|
1333
1583
|
heads,
|
|
1334
1584
|
images,
|
|
1335
1585
|
headings,
|
|
1586
|
+
a11y,
|
|
1336
1587
|
components,
|
|
1337
1588
|
project,
|
|
1338
1589
|
config,
|
|
@@ -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,
|
|
@@ -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]
|
|
@@ -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
|
);
|
|
@@ -62,7 +62,7 @@ var EXPLAIN_ARGS = {
|
|
|
62
62
|
help: { type: "boolean", short: "h", description: "Show this help" }
|
|
63
63
|
};
|
|
64
64
|
async function buildExplainHelpText(explainCommand, locale) {
|
|
65
|
-
const ja = locale === "ja" ? await import("./ja-
|
|
65
|
+
const ja = locale === "ja" ? await import("./ja-GAQSBSXF.js") : void 0;
|
|
66
66
|
const optionsSection = stripAutoVersionLine(
|
|
67
67
|
await localizedOptionsSection(
|
|
68
68
|
explainCommand,
|
|
@@ -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",
|
|
@@ -96,7 +96,7 @@ var DOCS_SHOW_ARGS = {
|
|
|
96
96
|
...HELP_ARG
|
|
97
97
|
};
|
|
98
98
|
async function buildDocsHelpText(rootCommand, locale) {
|
|
99
|
-
const ja = locale === "ja" ? await import("./ja-
|
|
99
|
+
const ja = locale === "ja" ? await import("./ja-GAQSBSXF.js") : void 0;
|
|
100
100
|
const optionsSection = stripAutoVersionLine(
|
|
101
101
|
await localizedOptionsSection(rootCommand, "svelte-vitals docs", locale, ja?.JA_ARG_DESCRIPTIONS.docs ?? {})
|
|
102
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,7 +28,7 @@ import "./chunk-TFBLQUAC.js";
|
|
|
28
28
|
import {
|
|
29
29
|
CI_ARGS,
|
|
30
30
|
CI_UPGRADE_ARGS
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-ICEJJDDA.js";
|
|
32
32
|
import "./chunk-CTMKTFST.js";
|
|
33
33
|
import "./chunk-GE7TKVTX.js";
|
|
34
34
|
import "./chunk-NMVBVKLX.js";
|
package/dist/gunshi-registry.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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";
|
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
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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";
|
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",
|