svelte-vitals 0.48.1 → 0.50.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/dist/bin.js +4 -4
- package/dist/{chunk-ZAHTKEJ5.js → chunk-K363W33X.js} +5 -1
- package/dist/{chunk-LJPABOWU.js → chunk-PNHWRKRD.js} +205 -40
- package/dist/{chunk-4LSRQXJ6.js → chunk-QKULNWQ5.js} +1 -1
- package/dist/{complete-IOAMJRBY.js → complete-5VANQTDN.js} +3 -3
- package/dist/{explain-WOKUZJVY.js → explain-PRCEATL4.js} +1 -1
- package/dist/gunshi-registry.js +2 -2
- package/dist/index.d.ts +18 -5
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runAnalyzeCliGunshi
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-QKULNWQ5.js";
|
|
5
5
|
import "./chunk-DZVXCUHG.js";
|
|
6
6
|
import {
|
|
7
7
|
realIO
|
|
8
8
|
} from "./chunk-RSSMIHIM.js";
|
|
9
|
-
import "./chunk-
|
|
9
|
+
import "./chunk-PNHWRKRD.js";
|
|
10
10
|
import "./chunk-WGFXFKBB.js";
|
|
11
11
|
import {
|
|
12
12
|
consoleIO
|
|
@@ -21,7 +21,7 @@ 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-5VANQTDN.js");
|
|
25
25
|
return { code: await runCompleteCliGunshi(argv, io), exit: "natural" };
|
|
26
26
|
}
|
|
27
27
|
if (argv[0] === "docs") {
|
|
@@ -29,7 +29,7 @@ async function runCli(argv, io = consoleIO, env = process.env) {
|
|
|
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-PRCEATL4.js");
|
|
33
33
|
return { code: await runExplainCliGunshi(argv.slice(1), io, locale), exit: "natural" };
|
|
34
34
|
}
|
|
35
35
|
if (argv[0] === "install") {
|
|
@@ -26,7 +26,11 @@ function describeOptions(id, options) {
|
|
|
26
26
|
"string-map": "merged over the default entries \u2014 a new key is added, a built-in key has its value overridden"
|
|
27
27
|
};
|
|
28
28
|
const lines = options.map((o) => {
|
|
29
|
-
const bounds = [
|
|
29
|
+
const bounds = [
|
|
30
|
+
o.min !== void 0 ? `>= ${o.min}` : "",
|
|
31
|
+
o.max !== void 0 ? `<= ${o.max}` : "",
|
|
32
|
+
o.pattern ? `each entry ${o.pattern}` : ""
|
|
33
|
+
].filter(Boolean).join(", ");
|
|
30
34
|
return `- ${o.name} (${o.kind}, default ${JSON.stringify(o.default)}${bounds ? `, ${bounds}` : ""}) \u2014 ${MERGE[o.kind]}`;
|
|
31
35
|
});
|
|
32
36
|
return `set in svelte-vitals.config.* as \`rules: { '${id}': { options: { \u2026 } } }\`, or per path in \`overrides\`:
|
|
@@ -34,12 +34,34 @@ import {
|
|
|
34
34
|
selectRules,
|
|
35
35
|
applyRuleSeverities,
|
|
36
36
|
applyOverrides,
|
|
37
|
+
applyInlineDirectives,
|
|
38
|
+
unknownDirectiveIds,
|
|
37
39
|
settingSeverity,
|
|
38
40
|
withFailedRulesOff,
|
|
39
41
|
formatFailedRuleWarning,
|
|
40
42
|
terminalSafe
|
|
41
43
|
} from "@svelte-vitals/core/internal";
|
|
42
44
|
|
|
45
|
+
// src/a11y-skips.ts
|
|
46
|
+
var ID_REF_RULE = "a11y/no-missing-id-ref";
|
|
47
|
+
function buildIdRefSkips(a11y) {
|
|
48
|
+
return a11y.filter((r) => !r.fullyResolved).map((r) => ({ route: r.route, refs: r.idRefs.length, causes: r.unresolvedCauses ?? [] })).sort((a, b) => a.route.localeCompare(b.route));
|
|
49
|
+
}
|
|
50
|
+
var KIND_LABELS = [
|
|
51
|
+
["component", "unresolved component"],
|
|
52
|
+
["spread", "spread"],
|
|
53
|
+
["html", "{@html}"],
|
|
54
|
+
["dynamic-id", "dynamic id"]
|
|
55
|
+
];
|
|
56
|
+
function idRefSkipWarning(entries, analyzedRoutes) {
|
|
57
|
+
const parts = [];
|
|
58
|
+
for (const [kind, label] of KIND_LABELS) {
|
|
59
|
+
const n = entries.filter((e) => e.causes.some((c) => c.kind === kind)).length;
|
|
60
|
+
if (n > 0) parts.push(`${label} ${n}`);
|
|
61
|
+
}
|
|
62
|
+
return `${ID_REF_RULE} skipped ${entries.length} of ${analyzedRoutes} analyzed route(s) (${parts.join(", ")} \u2014 per-route detail in the JSON report's "skipped").`;
|
|
63
|
+
}
|
|
64
|
+
|
|
43
65
|
// src/runtime/node.ts
|
|
44
66
|
import { readFile, access } from "fs/promises";
|
|
45
67
|
import { join } from "path";
|
|
@@ -75,7 +97,9 @@ import {
|
|
|
75
97
|
SITEMAP_SOURCE_PATHS,
|
|
76
98
|
SVELTE_CONFIG_FILES,
|
|
77
99
|
VITE_CONFIG_FILES,
|
|
100
|
+
collectSuppressions,
|
|
78
101
|
findMinifyDisabled,
|
|
102
|
+
lineOf,
|
|
79
103
|
resolveKitAliases,
|
|
80
104
|
resolveKitPathsBase
|
|
81
105
|
} from "@svelte-vitals/core/internal";
|
|
@@ -152,10 +176,28 @@ function detectHtmlLang(html) {
|
|
|
152
176
|
const value = match[1] ?? match[2] ?? match[3] ?? "";
|
|
153
177
|
return { presence: "own", value: value.trim().length > 0 ? "static" : "absent" };
|
|
154
178
|
}
|
|
179
|
+
function detectAppHtmlBodyTags(html) {
|
|
180
|
+
let markup = html.replace(/<!--[\s\S]*?-->/g, "").replace(/<script[\s\S]*?<\/script\s*>/gi, "").replace(/<style[\s\S]*?<\/style\s*>/gi, "");
|
|
181
|
+
const innermost = /<template\b[^>]*>(?:(?!<template\b)[\s\S])*?<\/template\s*>/gi;
|
|
182
|
+
for (let prev = ""; prev !== markup; ) {
|
|
183
|
+
prev = markup;
|
|
184
|
+
markup = markup.replace(innermost, "\0");
|
|
185
|
+
}
|
|
186
|
+
markup = markup.replaceAll("\0", "<template></template>");
|
|
187
|
+
const body = /<body\b[^>]*>([\s\S]*?)(?:<\/body\s*>|$)/i.exec(markup)?.[1];
|
|
188
|
+
if (body === void 0) return [];
|
|
189
|
+
return [...new Set([...body.matchAll(/<([a-zA-Z][a-zA-Z0-9-]*)\b/g)].map((m) => m[1].toLowerCase()))];
|
|
190
|
+
}
|
|
155
191
|
function detectAppHtmlIds(html) {
|
|
156
|
-
const
|
|
192
|
+
const keepNewlines = (m) => m.replace(/[^\n]/g, "");
|
|
193
|
+
const markup = html.replace(/<!--[\s\S]*?-->/g, keepNewlines).replace(/<script[\s\S]*?<\/script\s*>/gi, keepNewlines).replace(/<style[\s\S]*?<\/style\s*>/gi, keepNewlines);
|
|
157
194
|
const found = markup.matchAll(/(?<![\w-])id\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>{][^\s"'>]*))/gi);
|
|
158
|
-
|
|
195
|
+
const out = /* @__PURE__ */ new Map();
|
|
196
|
+
for (const m of found) {
|
|
197
|
+
const id = m[1] ?? m[2] ?? m[3] ?? "";
|
|
198
|
+
if (id && !out.has(id)) out.set(id, lineOf(markup, m.index));
|
|
199
|
+
}
|
|
200
|
+
return [...out].map(([id, line]) => ({ id, line }));
|
|
159
201
|
}
|
|
160
202
|
async function detectAppHtmlFacts(rt, cwd) {
|
|
161
203
|
const appHtmlPath = rt.join(cwd, "src/app.html");
|
|
@@ -172,7 +214,8 @@ async function detectAppHtmlFacts(rt, cwd) {
|
|
|
172
214
|
// [\s\S]*? is ambiguous across iterations and backtracks exponentially on a comment run
|
|
173
215
|
// with no doctype (measured: ~45 leading comments hang the process).
|
|
174
216
|
appHtmlDoctype: /^\s*<!doctype\s+html/i.test(content.replace(/<!--[\s\S]*?-->/g, "")),
|
|
175
|
-
appHtmlIds: detectAppHtmlIds(content)
|
|
217
|
+
appHtmlIds: detectAppHtmlIds(content),
|
|
218
|
+
appHtmlBodyTags: detectAppHtmlBodyTags(content)
|
|
176
219
|
};
|
|
177
220
|
}
|
|
178
221
|
async function robotsRefsSitemap(rt, cwd) {
|
|
@@ -189,8 +232,9 @@ async function detectViteMinifyDisabled(rt, cwd) {
|
|
|
189
232
|
const file = VITE_CONFIG_FILES[exists.indexOf(true)];
|
|
190
233
|
if (!file) return void 0;
|
|
191
234
|
try {
|
|
192
|
-
const
|
|
193
|
-
|
|
235
|
+
const source = await rt.readFile(rt.join(cwd, file));
|
|
236
|
+
const hit = findMinifyDisabled(source);
|
|
237
|
+
return hit ? { file, line: hit.line, suppressions: collectSuppressions(source) } : void 0;
|
|
194
238
|
} catch {
|
|
195
239
|
return void 0;
|
|
196
240
|
}
|
|
@@ -243,7 +287,12 @@ import "@svelte-vitals/core";
|
|
|
243
287
|
import {
|
|
244
288
|
collectComponentFacts,
|
|
245
289
|
collectKitModuleFacts,
|
|
246
|
-
collectSourceFiles
|
|
290
|
+
collectSourceFiles,
|
|
291
|
+
compileOverrides,
|
|
292
|
+
ROBOTS_SOURCE_PATHS as ROBOTS_SOURCE_PATHS2,
|
|
293
|
+
SITEMAP_SOURCE_PATHS as SITEMAP_SOURCE_PATHS2,
|
|
294
|
+
SVELTE_CONFIG_FILES as SVELTE_CONFIG_FILES2,
|
|
295
|
+
VITE_CONFIG_FILES as VITE_CONFIG_FILES2
|
|
247
296
|
} from "@svelte-vitals/core/internal";
|
|
248
297
|
|
|
249
298
|
// src/providers/source/routes.ts
|
|
@@ -387,10 +436,11 @@ function findAdapter(info) {
|
|
|
387
436
|
|
|
388
437
|
// src/providers/source/parse.ts
|
|
389
438
|
import {
|
|
439
|
+
collectSuppressions as collectSuppressions2,
|
|
390
440
|
stripTextDirective,
|
|
391
441
|
parseSvelte,
|
|
392
442
|
CHILD_NODE_KEYS,
|
|
393
|
-
lineOf,
|
|
443
|
+
lineOf as lineOf2,
|
|
394
444
|
findAttr as findAttr3,
|
|
395
445
|
valueFromNodes,
|
|
396
446
|
textFromNodes,
|
|
@@ -568,7 +618,7 @@ function collectImages(node, source, acc) {
|
|
|
568
618
|
// A literal loading="lazy" only — a spread or dynamic loading={…} must not be flagged.
|
|
569
619
|
lazy: attrText(attrs, "loading") === "lazy",
|
|
570
620
|
hasSrcset: hasSpread || Boolean(findAttr3(attrs, "srcset")),
|
|
571
|
-
line:
|
|
621
|
+
line: lineOf2(source, node.start)
|
|
572
622
|
});
|
|
573
623
|
}
|
|
574
624
|
for (const key of CHILD_NODE_KEYS) {
|
|
@@ -583,7 +633,7 @@ function collectHeadings(node, source, acc) {
|
|
|
583
633
|
if (!node || typeof node !== "object") return;
|
|
584
634
|
if (node.type === "SvelteHead") return;
|
|
585
635
|
if (node.type === "RegularElement" && /^h[1-6]$/.test(node.name)) {
|
|
586
|
-
acc.push({ level: Number(node.name[1]), line:
|
|
636
|
+
acc.push({ level: Number(node.name[1]), line: lineOf2(source, node.start) });
|
|
587
637
|
}
|
|
588
638
|
for (const key of CHILD_NODE_KEYS) {
|
|
589
639
|
if (key in node) collectHeadings(childOf(node, key), source, acc);
|
|
@@ -604,16 +654,18 @@ function collectA11y(fragment, source) {
|
|
|
604
654
|
const nodes = [];
|
|
605
655
|
let groups = 0;
|
|
606
656
|
let slotInLandmark;
|
|
607
|
-
|
|
657
|
+
const unknowable = [];
|
|
658
|
+
const elementTags = /* @__PURE__ */ new Set();
|
|
659
|
+
let elementsUnknowable = false;
|
|
608
660
|
const emit = (ctx, node) => {
|
|
609
661
|
const inLandmark = ctx.landmarks.at(-1);
|
|
610
662
|
nodes.push({ ...node, repeatable: ctx.repeatable, path: ctx.path, ...inLandmark ? { inLandmark } : {} });
|
|
611
663
|
};
|
|
612
664
|
const noteSpread = (node) => {
|
|
613
665
|
const attributes = node.attributes;
|
|
614
|
-
if (Array.isArray(attributes)
|
|
615
|
-
|
|
616
|
-
}
|
|
666
|
+
if (!Array.isArray(attributes)) return;
|
|
667
|
+
const spread = attributes.find((a) => a.type === "SpreadAttribute");
|
|
668
|
+
if (spread) unknowable.push({ kind: "spread", line: lineOf2(source, spread.start) });
|
|
617
669
|
};
|
|
618
670
|
const walk = (node, ctx) => {
|
|
619
671
|
if (Array.isArray(node)) {
|
|
@@ -626,7 +678,8 @@ function collectA11y(fragment, source) {
|
|
|
626
678
|
return;
|
|
627
679
|
// head content never renders into the body
|
|
628
680
|
case "HtmlTag":
|
|
629
|
-
|
|
681
|
+
unknowable.push({ kind: "html", line: lineOf2(source, node.start) });
|
|
682
|
+
elementsUnknowable = true;
|
|
630
683
|
return;
|
|
631
684
|
case "IfBlock":
|
|
632
685
|
walkIfChain(node, ctx, groups++, 0);
|
|
@@ -649,13 +702,15 @@ function collectA11y(fragment, source) {
|
|
|
649
702
|
// attributes are real — dropping them would make no-missing-id-ref report phantom misses.
|
|
650
703
|
case "RegularElement":
|
|
651
704
|
case "SvelteElement":
|
|
705
|
+
if (node.type === "SvelteElement") elementsUnknowable = true;
|
|
706
|
+
else elementTags.add(node.name.toLowerCase());
|
|
652
707
|
walkElement(node, ctx);
|
|
653
708
|
return;
|
|
654
709
|
case "Component":
|
|
655
710
|
case "SvelteComponent":
|
|
656
711
|
case "SvelteSelf":
|
|
657
712
|
noteSpread(node);
|
|
658
|
-
emit(ctx, { kind: "component", key: node.name, line:
|
|
713
|
+
emit(ctx, { kind: "component", key: node.name, line: lineOf2(source, node.start) });
|
|
659
714
|
walk(node.fragment, { ...ctx, elementDepth: ctx.elementDepth + 1 });
|
|
660
715
|
return;
|
|
661
716
|
case "SlotElement":
|
|
@@ -683,7 +738,7 @@ function collectA11y(fragment, source) {
|
|
|
683
738
|
};
|
|
684
739
|
const walkElement = (node, ctx) => {
|
|
685
740
|
noteSpread(node);
|
|
686
|
-
const line =
|
|
741
|
+
const line = lineOf2(source, node.start);
|
|
687
742
|
const attrs = node.attributes;
|
|
688
743
|
const roleAttr = findAttr3(attrs, "role");
|
|
689
744
|
const role = roleAttr ? splitTokens(attrTextOf3(roleAttr))[0] : void 0;
|
|
@@ -728,7 +783,13 @@ function collectA11y(fragment, source) {
|
|
|
728
783
|
});
|
|
729
784
|
};
|
|
730
785
|
walk(fragment, { path: [], repeatable: false, landmarks: [], elementDepth: 0, asideDemoting: 0 });
|
|
731
|
-
return {
|
|
786
|
+
return {
|
|
787
|
+
nodes,
|
|
788
|
+
...slotInLandmark ? { slotInLandmark } : {},
|
|
789
|
+
unknowable,
|
|
790
|
+
elementTags: [...elementTags],
|
|
791
|
+
elementsUnknowable
|
|
792
|
+
};
|
|
732
793
|
}
|
|
733
794
|
function isChildrenRender(node) {
|
|
734
795
|
const call = node.expression.type === "ChainExpression" ? node.expression.expression : node.expression;
|
|
@@ -750,7 +811,8 @@ function parseFile(source, filename) {
|
|
|
750
811
|
imports: collectImports(ast),
|
|
751
812
|
images,
|
|
752
813
|
headings,
|
|
753
|
-
a11y: collectA11y(ast.fragment, source)
|
|
814
|
+
a11y: collectA11y(ast.fragment, source),
|
|
815
|
+
suppressions: collectSuppressions2(source)
|
|
754
816
|
};
|
|
755
817
|
}
|
|
756
818
|
|
|
@@ -915,12 +977,25 @@ function groupSpan(nodes) {
|
|
|
915
977
|
}
|
|
916
978
|
return max + 1;
|
|
917
979
|
}
|
|
980
|
+
function dedupeCauses(causes) {
|
|
981
|
+
const seen = /* @__PURE__ */ new Map();
|
|
982
|
+
for (const c of causes) {
|
|
983
|
+
const key = `${c.kind}::${c.file}::${c.detail ?? ""}`;
|
|
984
|
+
if (!seen.has(key)) seen.set(key, c);
|
|
985
|
+
}
|
|
986
|
+
return [...seen.values()];
|
|
987
|
+
}
|
|
918
988
|
function offsetPath(path, base) {
|
|
919
989
|
return base === 0 ? path : path.map((step) => ({ group: step.group + base, branch: step.branch }));
|
|
920
990
|
}
|
|
921
991
|
async function composeA11y(ctx, fileRel, parsed, depth, visited, chain) {
|
|
922
992
|
const { rt, cwd, state } = ctx;
|
|
923
|
-
if (parsed.a11y.
|
|
993
|
+
if (parsed.a11y.unknowable.length > 0) {
|
|
994
|
+
state.fullyResolved = false;
|
|
995
|
+
for (const u of parsed.a11y.unknowable) state.causes.push({ ...u, file: fileRel });
|
|
996
|
+
}
|
|
997
|
+
for (const t of parsed.a11y.elementTags) state.elementTags.add(t);
|
|
998
|
+
if (parsed.a11y.elementsUnknowable) state.elementsClosed = false;
|
|
924
999
|
const base = state.nextGroup;
|
|
925
1000
|
state.nextGroup += groupSpan(parsed.a11y.nodes);
|
|
926
1001
|
const composed = [];
|
|
@@ -934,6 +1009,8 @@ async function composeA11y(ctx, fileRel, parsed, depth, visited, chain) {
|
|
|
934
1009
|
const childRel = info ? resolveComponentPath(info.source, fileRel, ctx.aliases) : void 0;
|
|
935
1010
|
if (!childRel || depth <= 0 || visited.has(childRel) || !await rt.exists(rt.join(cwd, childRel))) {
|
|
936
1011
|
state.fullyResolved = false;
|
|
1012
|
+
state.causes.push({ kind: "component", detail: node.key, file: fileRel, line: node.line });
|
|
1013
|
+
state.elementsClosed = false;
|
|
937
1014
|
continue;
|
|
938
1015
|
}
|
|
939
1016
|
const childParsed = await readAndParse(rt, cwd, childRel, ctx.cache);
|
|
@@ -963,7 +1040,7 @@ function representatives(nodes, chainOrder) {
|
|
|
963
1040
|
[...folded].map(([key, list]) => [key, list.sort(order).map(({ file, line }) => ({ file, line }))])
|
|
964
1041
|
);
|
|
965
1042
|
}
|
|
966
|
-
async function resolveRoute(rt, cwd, pageRel, config, layouts, cache, aliases, appHtmlIds) {
|
|
1043
|
+
async function resolveRoute(rt, cwd, pageRel, config, layouts, cache, aliases, appHtmlIds, appHtmlBodyTags) {
|
|
967
1044
|
const files = chainFiles(pageRel, layouts);
|
|
968
1045
|
const chainOrder = new Map(files.map((f, i) => [f.rel, i]));
|
|
969
1046
|
const composed = /* @__PURE__ */ new Map();
|
|
@@ -973,7 +1050,20 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache, aliases, a
|
|
|
973
1050
|
const images = [];
|
|
974
1051
|
const headings = [];
|
|
975
1052
|
const componentHeadings = [];
|
|
976
|
-
const a11yCtx = {
|
|
1053
|
+
const a11yCtx = {
|
|
1054
|
+
rt,
|
|
1055
|
+
cwd,
|
|
1056
|
+
config,
|
|
1057
|
+
cache,
|
|
1058
|
+
aliases,
|
|
1059
|
+
state: {
|
|
1060
|
+
nextGroup: 0,
|
|
1061
|
+
fullyResolved: true,
|
|
1062
|
+
causes: [],
|
|
1063
|
+
elementTags: new Set(appHtmlBodyTags ?? []),
|
|
1064
|
+
elementsClosed: true
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
977
1067
|
const a11yNodes = [];
|
|
978
1068
|
const nestedLandmarks = [];
|
|
979
1069
|
let slotLandmark;
|
|
@@ -1014,8 +1104,20 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache, aliases, a
|
|
|
1014
1104
|
}
|
|
1015
1105
|
}
|
|
1016
1106
|
const idNodes = a11yNodes.filter((n) => n.kind === "id");
|
|
1017
|
-
|
|
1107
|
+
for (const n of idNodes) {
|
|
1108
|
+
if (n.key !== "") continue;
|
|
1109
|
+
a11yCtx.state.fullyResolved = false;
|
|
1110
|
+
a11yCtx.state.causes.push({ kind: "dynamic-id", file: n.file, line: n.line });
|
|
1111
|
+
}
|
|
1018
1112
|
const literalIds = idNodes.filter((n) => n.key !== "");
|
|
1113
|
+
const ids = representatives(literalIds, chainOrder);
|
|
1114
|
+
if (appHtmlIds) {
|
|
1115
|
+
const shell = new Map(appHtmlIds.map((s) => [s.id, s.line]));
|
|
1116
|
+
for (const key of Object.keys(ids)) {
|
|
1117
|
+
const line = shell.get(key);
|
|
1118
|
+
if (line !== void 0) ids[key] = [{ file: "src/app.html", line }, ...ids[key]];
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1019
1121
|
const route = deriveRoute(pageRel);
|
|
1020
1122
|
return {
|
|
1021
1123
|
head: { route, source: "static", tags: [...composed.values(), ...additiveTags], file: pageRel },
|
|
@@ -1028,19 +1130,23 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache, aliases, a
|
|
|
1028
1130
|
chainOrder
|
|
1029
1131
|
),
|
|
1030
1132
|
nestedLandmarks,
|
|
1031
|
-
ids
|
|
1133
|
+
ids,
|
|
1032
1134
|
// `href="#top"` scrolls to the document top with no element of that id, so it is
|
|
1033
1135
|
// never a missing reference (HTML's "top of the document" fragment).
|
|
1034
1136
|
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 })),
|
|
1035
|
-
idCandidates: [.../* @__PURE__ */ new Set([...literalIds.map((n) => n.key), ...appHtmlIds ?? []])],
|
|
1036
|
-
fullyResolved: a11yCtx.state.fullyResolved
|
|
1137
|
+
idCandidates: [.../* @__PURE__ */ new Set([...literalIds.map((n) => n.key), ...(appHtmlIds ?? []).map((s) => s.id)])],
|
|
1138
|
+
fullyResolved: a11yCtx.state.fullyResolved,
|
|
1139
|
+
...a11yCtx.state.causes.length > 0 ? { unresolvedCauses: dedupeCauses(a11yCtx.state.causes) } : {},
|
|
1140
|
+
elementTags: [...a11yCtx.state.elementTags],
|
|
1141
|
+
elementsClosed: a11yCtx.state.elementsClosed,
|
|
1142
|
+
file: pageRel
|
|
1037
1143
|
}
|
|
1038
1144
|
};
|
|
1039
1145
|
}
|
|
1040
|
-
async function collectRoutes(rt, cwd, config = defaultConfig, cache = /* @__PURE__ */ new Map(), aliases, appHtmlIds) {
|
|
1146
|
+
async function collectRoutes(rt, cwd, config = defaultConfig, cache = /* @__PURE__ */ new Map(), aliases, appHtmlIds, appHtmlBodyTags) {
|
|
1041
1147
|
const [pages, layouts] = await Promise.all([enumerateRoutePages(rt, cwd), collectLayouts(rt, cwd)]);
|
|
1042
1148
|
const facts = await Promise.all(
|
|
1043
|
-
pages.map((page) => resolveRoute(rt, cwd, page, config, layouts, cache, aliases, appHtmlIds))
|
|
1149
|
+
pages.map((page) => resolveRoute(rt, cwd, page, config, layouts, cache, aliases, appHtmlIds, appHtmlBodyTags))
|
|
1044
1150
|
);
|
|
1045
1151
|
return {
|
|
1046
1152
|
heads: facts.map((f) => f.head),
|
|
@@ -1059,11 +1165,15 @@ function routeMatcher(glob) {
|
|
|
1059
1165
|
}
|
|
1060
1166
|
|
|
1061
1167
|
// src/collect-all.ts
|
|
1168
|
+
function globList(globs) {
|
|
1169
|
+
return globs === void 0 ? [] : Array.isArray(globs) ? globs : [globs];
|
|
1170
|
+
}
|
|
1062
1171
|
async function collectAll(rt, cwd, config, opts = {}) {
|
|
1063
1172
|
const matches = routeMatcher(opts.route);
|
|
1173
|
+
const parseCache = opts.parseCache ?? /* @__PURE__ */ new Map();
|
|
1064
1174
|
const project = await collectProjectFacts(rt, cwd);
|
|
1065
1175
|
const [collected, components, kitModules, sourceFiles] = await Promise.all([
|
|
1066
|
-
collectRoutes(rt, cwd, config,
|
|
1176
|
+
collectRoutes(rt, cwd, config, parseCache, project.kitAliases, project.appHtmlIds, project.appHtmlBodyTags),
|
|
1067
1177
|
// Component (Correctness) facts are file-scoped with no route attribution yet, so a
|
|
1068
1178
|
// route-filtered run skips them rather than reporting unrelated components (#68 review);
|
|
1069
1179
|
// kitModules is skipped for the same reason.
|
|
@@ -1079,7 +1189,45 @@ async function collectAll(rt, cwd, config, opts = {}) {
|
|
|
1079
1189
|
const images = collected.images.filter((i) => matches(i.route));
|
|
1080
1190
|
const headings = collected.headings.filter((h) => matches(h.route));
|
|
1081
1191
|
const a11y = collected.a11y.filter((a) => matches(a.route));
|
|
1082
|
-
|
|
1192
|
+
const directives = /* @__PURE__ */ new Map();
|
|
1193
|
+
for (const [file, parsed] of parseCache) {
|
|
1194
|
+
const suppressions = await parsed.then(
|
|
1195
|
+
(parsedFile) => parsedFile.suppressions,
|
|
1196
|
+
() => void 0
|
|
1197
|
+
);
|
|
1198
|
+
if (suppressions) directives.set(file, suppressions);
|
|
1199
|
+
}
|
|
1200
|
+
for (const c of components) directives.set(c.file, c.suppressions ?? []);
|
|
1201
|
+
for (const m of kitModules) directives.set(m.file, m.suppressions ?? []);
|
|
1202
|
+
const viteConfig = project.viteMinifyDisabled;
|
|
1203
|
+
if (viteConfig?.file) directives.set(viteConfig.file, viteConfig.suppressions ?? []);
|
|
1204
|
+
const routes = collected.heads.map((h) => h.route);
|
|
1205
|
+
const emptySelections = [];
|
|
1206
|
+
if (opts.route !== void 0 && routes.length > 0 && !routes.some(matches))
|
|
1207
|
+
emptySelections.push(`--route '${opts.route}' matched none of the ${routes.length} route(s) found.`);
|
|
1208
|
+
if (opts.route === void 0 && routes.length > 0) {
|
|
1209
|
+
const attributable = [
|
|
1210
|
+
...directives.keys(),
|
|
1211
|
+
...ROBOTS_SOURCE_PATHS2,
|
|
1212
|
+
...SITEMAP_SOURCE_PATHS2,
|
|
1213
|
+
...VITE_CONFIG_FILES2,
|
|
1214
|
+
...SVELTE_CONFIG_FILES2
|
|
1215
|
+
];
|
|
1216
|
+
const entries = config.overrides ?? [];
|
|
1217
|
+
const compiled = compileOverrides(config);
|
|
1218
|
+
entries.forEach((entry, i) => {
|
|
1219
|
+
globList(entry.route).forEach((glob) => {
|
|
1220
|
+
if (!routes.some(routeMatcher(glob)))
|
|
1221
|
+
emptySelections.push(`overrides entry for route '${glob}' matched no route.`);
|
|
1222
|
+
});
|
|
1223
|
+
globList(entry.files).forEach((glob, j) => {
|
|
1224
|
+
const pattern = compiled[i]?.files[j];
|
|
1225
|
+
if (pattern && !attributable.some((f) => pattern.test(f)))
|
|
1226
|
+
emptySelections.push(`overrides entry for files '${glob}' matched no file.`);
|
|
1227
|
+
});
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
return { heads, images, headings, a11y, project, components, kitModules, sourceFiles, directives, emptySelections };
|
|
1083
1231
|
}
|
|
1084
1232
|
|
|
1085
1233
|
// src/changed-files.ts
|
|
@@ -1520,6 +1668,11 @@ function resolveRuleSelection(input) {
|
|
|
1520
1668
|
else out[id] = rest;
|
|
1521
1669
|
}
|
|
1522
1670
|
}
|
|
1671
|
+
for (const id of allowed) {
|
|
1672
|
+
if (out[id] !== void 0) continue;
|
|
1673
|
+
const rule = allRules.find((r) => r.id === id);
|
|
1674
|
+
if (rule?.defaultOff) out[id] = rule.severity;
|
|
1675
|
+
}
|
|
1523
1676
|
}
|
|
1524
1677
|
for (const id of input.ignoreRules ?? []) out[id] = "off";
|
|
1525
1678
|
return out;
|
|
@@ -1580,17 +1733,23 @@ async function analyzeProject(opts = {}) {
|
|
|
1580
1733
|
...await checkVersionFloor(rt, cwd),
|
|
1581
1734
|
...overridesOffWarnings(opts.allowRules, config.overrides)
|
|
1582
1735
|
];
|
|
1583
|
-
const { heads, images, headings, a11y, project, components, kitModules, sourceFiles } = await collectAll(
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
parseCache: opts.parseCache
|
|
1590
|
-
}
|
|
1591
|
-
);
|
|
1736
|
+
const { heads, images, headings, a11y, project, components, kitModules, sourceFiles, directives, emptySelections } = await collectAll(rt, cwd, config, {
|
|
1737
|
+
route: opts.route,
|
|
1738
|
+
parseCache: opts.parseCache
|
|
1739
|
+
});
|
|
1740
|
+
warnings.push(...emptySelections);
|
|
1741
|
+
if (opts.route === void 0) warnings.push(...unknownDirectiveIds(directives, allRules2));
|
|
1592
1742
|
const selected = selectRules(allRules2, config);
|
|
1593
1743
|
const rules = opts.categories ? selected.filter((r) => opts.categories.includes(r.category)) : selected;
|
|
1744
|
+
const idRefSkips = rules.some((r) => r.id === ID_REF_RULE) ? buildIdRefSkips(a11y) : [];
|
|
1745
|
+
if (idRefSkips.length > 0) warnings.push(idRefSkipWarning(idRefSkips, a11y.length));
|
|
1746
|
+
if (opts.route !== void 0 && opts.allowRules?.length) {
|
|
1747
|
+
const starved = rules.filter((r) => opts.allowRules.includes(r.id) && r.scope !== "route").map((r) => r.id);
|
|
1748
|
+
if (starved.length > 0)
|
|
1749
|
+
warnings.push(
|
|
1750
|
+
`--rules ${starved.map((id) => `'${id}'`).join(", ")} examined nothing: --route collects route facts only.`
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1594
1753
|
const {
|
|
1595
1754
|
results: rawResults,
|
|
1596
1755
|
examined,
|
|
@@ -1606,7 +1765,12 @@ async function analyzeProject(opts = {}) {
|
|
|
1606
1765
|
kitModules,
|
|
1607
1766
|
sourceFiles
|
|
1608
1767
|
});
|
|
1609
|
-
const results =
|
|
1768
|
+
const results = applyInlineDirectives(
|
|
1769
|
+
applyOverrides(applyRuleSeverities(rawResults, config), config),
|
|
1770
|
+
directives,
|
|
1771
|
+
rules,
|
|
1772
|
+
config
|
|
1773
|
+
);
|
|
1610
1774
|
const failedRuleIds = failedRules.map((f) => f.id);
|
|
1611
1775
|
const scoringConfig = withFailedRulesOff(config, failedRuleIds);
|
|
1612
1776
|
return {
|
|
@@ -1615,6 +1779,7 @@ async function analyzeProject(opts = {}) {
|
|
|
1615
1779
|
version: readPackageVersion(),
|
|
1616
1780
|
ruleIds: rules.map((r) => r.id),
|
|
1617
1781
|
examined,
|
|
1782
|
+
...idRefSkips.length > 0 ? { skipped: { [ID_REF_RULE]: idRefSkips } } : {},
|
|
1618
1783
|
failedRuleIds,
|
|
1619
1784
|
warnings: [...warnings, ...skippedFileWarnings([...components, ...kitModules]), ...failedRuleWarnings(failedRules)],
|
|
1620
1785
|
loadedConfig: loaded
|
|
@@ -1805,7 +1970,7 @@ async function run(opts = {}) {
|
|
|
1805
1970
|
);
|
|
1806
1971
|
}
|
|
1807
1972
|
if (reporter === "json") {
|
|
1808
|
-
log(formatJsonReport(results, config, { version }, analysis.ruleIds, analysis.examined));
|
|
1973
|
+
log(formatJsonReport(results, config, { version }, analysis.ruleIds, analysis.examined, analysis.skipped));
|
|
1809
1974
|
} else if (reporter === "agent") {
|
|
1810
1975
|
log(formatAgentReport(results, config));
|
|
1811
1976
|
} else if (reporter === "sarif") {
|
|
@@ -8,14 +8,14 @@ import {
|
|
|
8
8
|
import "./chunk-CTMKTFST.js";
|
|
9
9
|
import {
|
|
10
10
|
ROOT_ARGS
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-QKULNWQ5.js";
|
|
12
12
|
import {
|
|
13
13
|
CATEGORIES,
|
|
14
14
|
FAIL_ON_VALUES,
|
|
15
15
|
TREAT_DYNAMIC_AS_VALUES
|
|
16
16
|
} from "./chunk-DZVXCUHG.js";
|
|
17
17
|
import "./chunk-RSSMIHIM.js";
|
|
18
|
-
import "./chunk-
|
|
18
|
+
import "./chunk-PNHWRKRD.js";
|
|
19
19
|
import {
|
|
20
20
|
REPORTER_NAMES
|
|
21
21
|
} from "./chunk-WGFXFKBB.js";
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from "./chunk-72ZLERW3.js";
|
|
28
28
|
import {
|
|
29
29
|
EXPLAIN_ARGS
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-K363W33X.js";
|
|
31
31
|
import {
|
|
32
32
|
consoleIO
|
|
33
33
|
} from "./chunk-SLUMRYUD.js";
|
package/dist/gunshi-registry.js
CHANGED
|
@@ -4,10 +4,10 @@ import {
|
|
|
4
4
|
import "./chunk-CTMKTFST.js";
|
|
5
5
|
import {
|
|
6
6
|
ROOT_ARGS
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-QKULNWQ5.js";
|
|
8
8
|
import "./chunk-DZVXCUHG.js";
|
|
9
9
|
import "./chunk-RSSMIHIM.js";
|
|
10
|
-
import "./chunk-
|
|
10
|
+
import "./chunk-PNHWRKRD.js";
|
|
11
11
|
import "./chunk-WGFXFKBB.js";
|
|
12
12
|
import "./chunk-MLIODPHZ.js";
|
|
13
13
|
import {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Config, Severity, RuleSetting, Category, Result } from '@svelte-vitals/core';
|
|
1
|
+
import { Config, Severity, RuleSetting, Category, Result, JsonReport } from '@svelte-vitals/core';
|
|
2
2
|
export { defineConfig } from '@svelte-vitals/core';
|
|
3
3
|
import { AST } from 'svelte/compiler';
|
|
4
|
-
import { HeadTag, BranchStep, RuleOptionsSpec } from '@svelte-vitals/core/internal';
|
|
4
|
+
import { HeadTag, BranchStep, SuppressionDirective, RuleOptionsSpec } from '@svelte-vitals/core/internal';
|
|
5
5
|
|
|
6
6
|
/** A resolved import binding: which module, and which export ('default' for default imports). */
|
|
7
7
|
interface ImportInfo {
|
|
@@ -56,8 +56,15 @@ interface ParsedA11y {
|
|
|
56
56
|
nodes: A11yNode[];
|
|
57
57
|
/** landmark ancestor of this file's <slot>/{@render children()} position, if any */
|
|
58
58
|
slotInLandmark?: string;
|
|
59
|
-
/**
|
|
60
|
-
|
|
59
|
+
/** {@html} tags and spread attributes, located — each poisons the closed world for no-missing-id-ref */
|
|
60
|
+
unknowable: {
|
|
61
|
+
kind: 'spread' | 'html';
|
|
62
|
+
line: number;
|
|
63
|
+
}[];
|
|
64
|
+
/** Distinct lowercased tag names of the body's `RegularElement`s (a11y/required-element's presence set). */
|
|
65
|
+
elementTags: string[];
|
|
66
|
+
/** file contains `{@html}` or a `<svelte:element>` — either can render an element the walk cannot see */
|
|
67
|
+
elementsUnknowable: boolean;
|
|
61
68
|
}
|
|
62
69
|
interface ParsedFile {
|
|
63
70
|
headTags: ParsedTag[];
|
|
@@ -66,6 +73,10 @@ interface ParsedFile {
|
|
|
66
73
|
images: ParsedImage[];
|
|
67
74
|
headings: ParsedHeading[];
|
|
68
75
|
a11y: ParsedA11y;
|
|
76
|
+
/** Inline `svelte-vitals-disable-next-line` directives in this file, for the central
|
|
77
|
+
* suppression pass. Collected here because a route-scoped finding can be located in any file
|
|
78
|
+
* the composition reads, including ones no component-fact collection visited (`--route`). */
|
|
79
|
+
suppressions: SuppressionDirective[];
|
|
69
80
|
}
|
|
70
81
|
|
|
71
82
|
/**
|
|
@@ -268,9 +279,11 @@ interface AnalyzeResult {
|
|
|
268
279
|
ruleIds: string[];
|
|
269
280
|
/** Per-rule, per-declaration counts of places examined, unfiltered by `--diff`/`--baseline`/suppressions. */
|
|
270
281
|
examined: Record<string, Record<string, number>>;
|
|
282
|
+
/** Routes a closed-world rule skipped, keyed by rule id — the analysis-side companion to `examined`; unfiltered by `--diff`/`--baseline`/suppressions. Absent when no analyzed route was skipped or the rule was not selected. */
|
|
283
|
+
skipped?: JsonReport['skipped'];
|
|
271
284
|
/** 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`. */
|
|
272
285
|
failedRuleIds: string[];
|
|
273
|
-
/** 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. */
|
|
286
|
+
/** Non-fatal issues surfaced during analysis: config-file problems (unknown top-level keys, invalid enum values), version-floor notices, `--rules`/overrides conflicts, closed-world skip notices (`a11y/no-missing-id-ref`), and skipped-file notices. Empty when none apply. */
|
|
274
287
|
warnings: string[];
|
|
275
288
|
/**
|
|
276
289
|
* This analysis's config-file load result (`undefined` when no config file exists at its
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-vitals",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"description": "A deterministic SvelteKit code-health scanner (SEO, performance, correctness, security, architecture, accessibility).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"magicast": "^0.5.4",
|
|
53
53
|
"svelte": "^5.56.9",
|
|
54
54
|
"tinyglobby": "^0.2.17",
|
|
55
|
-
"@svelte-vitals/core": "0.
|
|
55
|
+
"@svelte-vitals/core": "0.47.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@gunshi/docs": "0.37.1",
|