@svelte-vitals/core 0.29.0 → 0.31.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/index.d.ts +491 -25
- package/dist/index.js +1477 -138
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ var defaultProject = {
|
|
|
4
4
|
hasSitemap: false,
|
|
5
5
|
htmlLang: { presence: "none", value: "absent" }
|
|
6
6
|
};
|
|
7
|
+
var CATEGORIES = ["seo", "performance", "correctness", "security", "architecture"];
|
|
7
8
|
var defaultConfig = {
|
|
8
9
|
treatDynamicAs: "pass",
|
|
9
10
|
metaComponents: [],
|
|
@@ -17,6 +18,11 @@ function defineConfig(config = {}) {
|
|
|
17
18
|
// src/component-parse.ts
|
|
18
19
|
import { parse } from "svelte/compiler";
|
|
19
20
|
|
|
21
|
+
// src/base-path.ts
|
|
22
|
+
function isRootRelativePath(value) {
|
|
23
|
+
return value.startsWith("/") && !value.startsWith("//");
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
// src/svelte-ast.ts
|
|
21
27
|
var CHILD_NODE_KEYS = [
|
|
22
28
|
"fragment",
|
|
@@ -264,6 +270,12 @@ function scopeIntroducedNames(node) {
|
|
|
264
270
|
} else if (node.type === "AwaitBlock") {
|
|
265
271
|
if (node.value) addBoundNames(node.value, introduced);
|
|
266
272
|
if (node.error) addBoundNames(node.error, introduced);
|
|
273
|
+
} else if (node.type === "Fragment") {
|
|
274
|
+
for (const child of node.nodes ?? []) {
|
|
275
|
+
if (child?.type === "ConstTag" || child?.type === "DeclarationTag") {
|
|
276
|
+
for (const d of child.declaration?.declarations ?? []) addBoundNames(d.id, introduced);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
267
279
|
}
|
|
268
280
|
return introduced;
|
|
269
281
|
}
|
|
@@ -671,6 +683,7 @@ function bodyIsEmpty(fn) {
|
|
|
671
683
|
return false;
|
|
672
684
|
}
|
|
673
685
|
var URL_ATTRS = ["href", "src", "action", "formaction"];
|
|
686
|
+
var CHECKABLE_INPUT_TYPES = /* @__PURE__ */ new Set(["checkbox", "radio"]);
|
|
674
687
|
function collectSecurityFacts(node, source, htmlTags, jsUrls) {
|
|
675
688
|
if (Array.isArray(node)) {
|
|
676
689
|
for (const child of node) collectSecurityFacts(child, source, htmlTags, jsUrls);
|
|
@@ -692,6 +705,59 @@ function collectSecurityFacts(node, source, htmlTags, jsUrls) {
|
|
|
692
705
|
if (key in node) collectSecurityFacts(node[key], source, htmlTags, jsUrls);
|
|
693
706
|
}
|
|
694
707
|
}
|
|
708
|
+
function collectCheckableBindValues(node, source, acc) {
|
|
709
|
+
if (Array.isArray(node)) {
|
|
710
|
+
for (const child of node) collectCheckableBindValues(child, source, acc);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (!node || typeof node !== "object") return;
|
|
714
|
+
if (node.type === "RegularElement" && node.name === "input" && Array.isArray(node.attributes)) {
|
|
715
|
+
const typeAttr = findAttr(node.attributes, "type");
|
|
716
|
+
const typeValue = typeAttr ? attrTextOf(typeAttr) : void 0;
|
|
717
|
+
if (typeValue && CHECKABLE_INPUT_TYPES.has(typeValue)) {
|
|
718
|
+
const bindValue = node.attributes.find((a) => a?.type === "BindDirective" && a.name === "value");
|
|
719
|
+
if (bindValue) {
|
|
720
|
+
acc.push({
|
|
721
|
+
kind: typeValue,
|
|
722
|
+
line: lineOf(source, bindValue.start ?? node.start)
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
728
|
+
if (key in node) collectCheckableBindValues(node[key], source, acc);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function collectHrefLinks(node, source, acc) {
|
|
732
|
+
if (Array.isArray(node)) {
|
|
733
|
+
for (const child of node) collectHrefLinks(child, source, acc);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (!node || typeof node !== "object") return;
|
|
737
|
+
if (node.type === "RegularElement" && node.name === "a" && Array.isArray(node.attributes)) {
|
|
738
|
+
const attr = findAttr(node.attributes, "href");
|
|
739
|
+
const value = attr ? attrTextOf(attr) : void 0;
|
|
740
|
+
if (value !== void 0 && isRootRelativePath(value)) {
|
|
741
|
+
acc.push({ kind: "href", path: value, line: lineOf(source, attr?.start ?? node.start) });
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
745
|
+
if (key in node) collectHrefLinks(node[key], source, acc);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
var GOTO_NAMES = /* @__PURE__ */ new Set(["goto"]);
|
|
749
|
+
function collectGotoLinks(locals, roots, source, acc) {
|
|
750
|
+
if (locals.size === 0) return;
|
|
751
|
+
for (const root of roots) {
|
|
752
|
+
if (!root) continue;
|
|
753
|
+
walkEstree(root, (n) => {
|
|
754
|
+
if (n.type !== "CallExpression" || n.callee?.type !== "Identifier" || !locals.has(n.callee.name)) return;
|
|
755
|
+
const arg = n.arguments?.[0];
|
|
756
|
+
if (arg?.type !== "Literal" || typeof arg.value !== "string" || !isRootRelativePath(arg.value)) return;
|
|
757
|
+
acc.push({ kind: "goto", path: arg.value, line: lineOf(source, n.start) });
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
695
761
|
function isPropsCall(node) {
|
|
696
762
|
return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "$props";
|
|
697
763
|
}
|
|
@@ -791,10 +857,19 @@ function countLines(source) {
|
|
|
791
857
|
if (source.length === 0) return 0;
|
|
792
858
|
return source.split("\n").length - (source.endsWith("\n") ? 1 : 0);
|
|
793
859
|
}
|
|
860
|
+
function isTypeOnlyImport(n) {
|
|
861
|
+
if (n.importKind === "type") return true;
|
|
862
|
+
const specs = n.specifiers;
|
|
863
|
+
return Array.isArray(specs) && specs.length > 0 && specs.every((s) => s?.importKind === "type");
|
|
864
|
+
}
|
|
794
865
|
function collectImportSources(program, source, acc) {
|
|
795
866
|
walkEstree(program, (n) => {
|
|
796
867
|
if (n.type === "ImportDeclaration" && typeof n.source?.value === "string") {
|
|
797
|
-
acc.push({
|
|
868
|
+
acc.push({
|
|
869
|
+
source: n.source.value,
|
|
870
|
+
line: lineOf(source, n.start),
|
|
871
|
+
...isTypeOnlyImport(n) ? { type: true } : {}
|
|
872
|
+
});
|
|
798
873
|
}
|
|
799
874
|
});
|
|
800
875
|
}
|
|
@@ -977,20 +1052,25 @@ var BROWSER_GLOBALS = /* @__PURE__ */ new Set([
|
|
|
977
1052
|
"confirm",
|
|
978
1053
|
"prompt"
|
|
979
1054
|
]);
|
|
980
|
-
function
|
|
1055
|
+
function collectNamedImportAliases(program, moduleSource, names) {
|
|
981
1056
|
const out = /* @__PURE__ */ new Set();
|
|
982
1057
|
for (const stmt of program.body ?? []) {
|
|
983
|
-
if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type" || stmt.source?.value !==
|
|
1058
|
+
if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type" || stmt.source?.value !== moduleSource) {
|
|
984
1059
|
continue;
|
|
1060
|
+
}
|
|
985
1061
|
for (const s of stmt.specifiers ?? []) {
|
|
986
1062
|
if (s?.importKind === "type" || s?.local?.type !== "Identifier") continue;
|
|
987
|
-
if (s.type === "ImportSpecifier" && s.imported?.type === "Identifier" && s.imported.name
|
|
1063
|
+
if (s.type === "ImportSpecifier" && s.imported?.type === "Identifier" && names.has(s.imported.name)) {
|
|
988
1064
|
out.add(s.local.name);
|
|
989
1065
|
}
|
|
990
1066
|
}
|
|
991
1067
|
}
|
|
992
1068
|
return out;
|
|
993
1069
|
}
|
|
1070
|
+
var BROWSER_GUARD_NAMES = /* @__PURE__ */ new Set(["browser"]);
|
|
1071
|
+
function collectBrowserGuardImports(program) {
|
|
1072
|
+
return collectNamedImportAliases(program, "$app/environment", BROWSER_GUARD_NAMES);
|
|
1073
|
+
}
|
|
994
1074
|
function collectProgramBindings(program) {
|
|
995
1075
|
const bound = /* @__PURE__ */ new Set();
|
|
996
1076
|
for (const stmt of program.body ?? []) {
|
|
@@ -1168,6 +1248,14 @@ function parseModuleFacts(source, filename) {
|
|
|
1168
1248
|
const orphanLifecycleCalls = program ? collectOrphanLifecycleCalls(program, wrapped).map((f) => ({ ...f, line: shift(f.line) })) : [];
|
|
1169
1249
|
const browserGlobalRefs = program ? collectBrowserGlobalRefs(program, wrapped).map((r) => ({ ...r, line: shift(r.line), context: "module" })) : [];
|
|
1170
1250
|
const moduleStateDecls = program ? collectModuleStateDecls(program, wrapped).map((d) => ({ ...d, line: shift(d.line) })) : [];
|
|
1251
|
+
const basePathLinks = [];
|
|
1252
|
+
if (program) {
|
|
1253
|
+
const locals = collectNamedImportAliases(program, "$app/navigation", GOTO_NAMES);
|
|
1254
|
+
const raw = [];
|
|
1255
|
+
collectGotoLinks(locals, [program], wrapped, raw);
|
|
1256
|
+
for (const l of raw) basePathLinks.push({ ...l, line: shift(l.line) });
|
|
1257
|
+
basePathLinks.sort((a, b) => a.line - b.line);
|
|
1258
|
+
}
|
|
1171
1259
|
return {
|
|
1172
1260
|
eachBlocks: [],
|
|
1173
1261
|
effects: [],
|
|
@@ -1183,6 +1271,8 @@ function parseModuleFacts(source, filename) {
|
|
|
1183
1271
|
stalePropDerivations: [],
|
|
1184
1272
|
rawableStates: [],
|
|
1185
1273
|
nonreactiveBuiltinStates: [],
|
|
1274
|
+
checkableBindValues: [],
|
|
1275
|
+
basePathLinks,
|
|
1186
1276
|
suppressions: collectSuppressions(source),
|
|
1187
1277
|
orphanEffects,
|
|
1188
1278
|
orphanLifecycleCalls,
|
|
@@ -1198,6 +1288,18 @@ function parseComponentFacts(source, filename) {
|
|
|
1198
1288
|
const htmlTags = [];
|
|
1199
1289
|
const javascriptUrls = [];
|
|
1200
1290
|
collectSecurityFacts(ast.fragment ?? ast, source, htmlTags, javascriptUrls);
|
|
1291
|
+
const checkableBindValues = [];
|
|
1292
|
+
collectCheckableBindValues(ast.fragment ?? ast, source, checkableBindValues);
|
|
1293
|
+
const basePathLinks = [];
|
|
1294
|
+
collectHrefLinks(ast.fragment ?? ast, source, basePathLinks);
|
|
1295
|
+
const gotoPrograms = [ast.module?.content, ast.instance?.content].filter(Boolean);
|
|
1296
|
+
const gotoLocals = /* @__PURE__ */ new Set();
|
|
1297
|
+
for (const p of gotoPrograms)
|
|
1298
|
+
for (const n of collectNamedImportAliases(p, "$app/navigation", GOTO_NAMES)) {
|
|
1299
|
+
gotoLocals.add(n);
|
|
1300
|
+
}
|
|
1301
|
+
collectGotoLinks(gotoLocals, [...gotoPrograms, ast.fragment], source, basePathLinks);
|
|
1302
|
+
basePathLinks.sort((a, b) => a.line - b.line);
|
|
1201
1303
|
const loc = countLines(source);
|
|
1202
1304
|
const suppressions = collectSuppressions(source);
|
|
1203
1305
|
const moduleProgram = ast.module?.content;
|
|
@@ -1372,6 +1474,8 @@ function parseComponentFacts(source, filename) {
|
|
|
1372
1474
|
stalePropDerivations,
|
|
1373
1475
|
rawableStates,
|
|
1374
1476
|
nonreactiveBuiltinStates,
|
|
1477
|
+
checkableBindValues,
|
|
1478
|
+
basePathLinks,
|
|
1375
1479
|
orphanEffects,
|
|
1376
1480
|
orphanLifecycleCalls,
|
|
1377
1481
|
browserGlobalRefs,
|
|
@@ -1398,6 +1502,8 @@ function emptyComponentFacts(file) {
|
|
|
1398
1502
|
stalePropDerivations: [],
|
|
1399
1503
|
rawableStates: [],
|
|
1400
1504
|
nonreactiveBuiltinStates: [],
|
|
1505
|
+
checkableBindValues: [],
|
|
1506
|
+
basePathLinks: [],
|
|
1401
1507
|
orphanEffects: [],
|
|
1402
1508
|
orphanLifecycleCalls: [],
|
|
1403
1509
|
browserGlobalRefs: [],
|
|
@@ -1419,6 +1525,12 @@ async function collectComponentFacts(rt, cwd) {
|
|
|
1419
1525
|
);
|
|
1420
1526
|
}
|
|
1421
1527
|
|
|
1528
|
+
// src/source-files.ts
|
|
1529
|
+
async function collectSourceFiles(rt, cwd) {
|
|
1530
|
+
const files = await rt.glob("src/**/*", cwd);
|
|
1531
|
+
return files.slice().sort();
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1422
1534
|
// src/kit-module-parse.ts
|
|
1423
1535
|
var HANDLER_NAMES = /* @__PURE__ */ new Set([
|
|
1424
1536
|
"load",
|
|
@@ -1534,6 +1646,22 @@ function collectAwaits(node, out = []) {
|
|
|
1534
1646
|
}
|
|
1535
1647
|
return out;
|
|
1536
1648
|
}
|
|
1649
|
+
var REDIRECT_NAMES = /* @__PURE__ */ new Set(["redirect"]);
|
|
1650
|
+
function collectRedirectCalls(node, locals, out = []) {
|
|
1651
|
+
if (Array.isArray(node)) {
|
|
1652
|
+
for (const child of node) collectRedirectCalls(child, locals, out);
|
|
1653
|
+
return out;
|
|
1654
|
+
}
|
|
1655
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return out;
|
|
1656
|
+
if (node.type === "CallExpression" && node.callee?.type === "Identifier" && locals.has(node.callee.name)) {
|
|
1657
|
+
out.push(node);
|
|
1658
|
+
}
|
|
1659
|
+
for (const key of Object.keys(node)) {
|
|
1660
|
+
if (WALK_IGNORED_KEYS.has(key)) continue;
|
|
1661
|
+
collectRedirectCalls(node[key], locals, out);
|
|
1662
|
+
}
|
|
1663
|
+
return out;
|
|
1664
|
+
}
|
|
1537
1665
|
function isParentCall(arg) {
|
|
1538
1666
|
const e = unwrapTs(arg);
|
|
1539
1667
|
if (e?.type !== "CallExpression") return false;
|
|
@@ -1683,28 +1811,45 @@ function normalizePosix(path) {
|
|
|
1683
1811
|
}
|
|
1684
1812
|
return out.join("/");
|
|
1685
1813
|
}
|
|
1686
|
-
|
|
1814
|
+
var DEFAULT_KIT_ALIASES = [{ find: "$lib", replacement: "src/lib", match: "prefix" }];
|
|
1815
|
+
function aliasMatches(entry, spec) {
|
|
1816
|
+
if (entry.match === "exact") return spec === entry.find;
|
|
1817
|
+
if (spec.startsWith(`${entry.find}/`)) return true;
|
|
1818
|
+
return entry.match === "prefix" && spec === entry.find;
|
|
1819
|
+
}
|
|
1820
|
+
function resolveRepoLocalPath(spec, importerFile, aliases = DEFAULT_KIT_ALIASES) {
|
|
1687
1821
|
let path;
|
|
1688
|
-
if (spec.startsWith("
|
|
1689
|
-
else if (spec.startsWith("./") || spec.startsWith("../")) {
|
|
1822
|
+
if (spec.startsWith("./") || spec.startsWith("../")) {
|
|
1690
1823
|
const dir = importerFile.split("/").slice(0, -1).join("/");
|
|
1691
1824
|
path = `${dir}/${spec}`;
|
|
1692
|
-
} else
|
|
1825
|
+
} else {
|
|
1826
|
+
const entry = aliases.find((a) => aliasMatches(a, spec));
|
|
1827
|
+
if (entry?.replacement == null) return void 0;
|
|
1828
|
+
if (entry.replacement.startsWith("/") || /^[A-Za-z]:\//.test(entry.replacement)) return void 0;
|
|
1829
|
+
path = entry.replacement + spec.slice(entry.find.length);
|
|
1830
|
+
}
|
|
1693
1831
|
return normalizePosix(path);
|
|
1694
1832
|
}
|
|
1695
|
-
function resolveRunesModuleSpecifier(spec, importerFile) {
|
|
1696
|
-
const path = resolveRepoLocalPath(spec, importerFile);
|
|
1833
|
+
function resolveRunesModuleSpecifier(spec, importerFile, aliases) {
|
|
1834
|
+
const path = resolveRepoLocalPath(spec, importerFile, aliases);
|
|
1697
1835
|
if (path === void 0) return void 0;
|
|
1698
1836
|
if (/\.svelte\.(ts|js)$/.test(path)) return path;
|
|
1699
1837
|
if (path.endsWith(".svelte")) return `${path}.ts`;
|
|
1700
1838
|
return void 0;
|
|
1701
1839
|
}
|
|
1702
|
-
function
|
|
1703
|
-
const
|
|
1840
|
+
function libServerRoot(aliases) {
|
|
1841
|
+
const lib = aliases?.find((a) => a.find === "$lib");
|
|
1842
|
+
if (lib && lib.replacement === null) return void 0;
|
|
1843
|
+
return `${lib?.replacement ?? "src/lib"}/server`;
|
|
1844
|
+
}
|
|
1845
|
+
function isLocalStateSpecifier(spec, importerFile, aliases) {
|
|
1846
|
+
const serverRoot = libServerRoot(aliases);
|
|
1847
|
+
if (serverRoot === void 0) return false;
|
|
1848
|
+
const path = resolveRepoLocalPath(spec, importerFile, aliases);
|
|
1704
1849
|
if (path === void 0) return false;
|
|
1705
|
-
return path !==
|
|
1850
|
+
return path !== serverRoot && !path.startsWith(`${serverRoot}/`);
|
|
1706
1851
|
}
|
|
1707
|
-
function parseKitModuleFacts(source, filename) {
|
|
1852
|
+
function parseKitModuleFacts(source, filename, aliases) {
|
|
1708
1853
|
const suppressions = collectSuppressions(source);
|
|
1709
1854
|
const { program, wrapped } = parseModuleProgram(source, filename);
|
|
1710
1855
|
const moduleStateReassignments = [];
|
|
@@ -1721,6 +1866,7 @@ function parseKitModuleFacts(source, filename) {
|
|
|
1721
1866
|
runesModuleImports,
|
|
1722
1867
|
lifecycleCalls,
|
|
1723
1868
|
browserGlobalRefs,
|
|
1869
|
+
basePathLinks: [],
|
|
1724
1870
|
suppressions
|
|
1725
1871
|
};
|
|
1726
1872
|
}
|
|
@@ -1736,7 +1882,7 @@ function parseKitModuleFacts(source, filename) {
|
|
|
1736
1882
|
importedSpecifiers.set(s.local.name, spec);
|
|
1737
1883
|
}
|
|
1738
1884
|
if (names.length === 0) continue;
|
|
1739
|
-
const resolved = resolveRunesModuleSpecifier(spec, filename);
|
|
1885
|
+
const resolved = resolveRunesModuleSpecifier(spec, filename, aliases);
|
|
1740
1886
|
if (resolved) runesModuleImports.push({ source: spec, resolved, names, line: line(stmt.start) });
|
|
1741
1887
|
}
|
|
1742
1888
|
const moduleLets = /* @__PURE__ */ new Set();
|
|
@@ -1810,7 +1956,8 @@ function parseKitModuleFacts(source, filename) {
|
|
|
1810
1956
|
const method = n.callee.property?.type === "Identifier" ? n.callee.property.name : void 0;
|
|
1811
1957
|
if (method === "set" || method === "update") {
|
|
1812
1958
|
const r = importedRoot(n.callee.object);
|
|
1813
|
-
if (r && isLocalStateSpecifier(importedSpecifiers.get(r), filename))
|
|
1959
|
+
if (r && isLocalStateSpecifier(importedSpecifiers.get(r), filename, aliases))
|
|
1960
|
+
write = { name: r, via: "set-call" };
|
|
1814
1961
|
}
|
|
1815
1962
|
} else if (n.type === "AssignmentExpression" && (n.left?.type === "ObjectPattern" || n.left?.type === "ArrayPattern")) {
|
|
1816
1963
|
const scanPatternTargets = (pat) => {
|
|
@@ -1845,6 +1992,15 @@ function parseKitModuleFacts(source, filename) {
|
|
|
1845
1992
|
}
|
|
1846
1993
|
});
|
|
1847
1994
|
const byLine = (arr) => arr.sort((a, b) => a.line - b.line);
|
|
1995
|
+
const basePathLinks = [];
|
|
1996
|
+
const redirectLocals = collectNamedImportAliases(program, "@sveltejs/kit", REDIRECT_NAMES);
|
|
1997
|
+
if (redirectLocals.size > 0) {
|
|
1998
|
+
for (const call of collectRedirectCalls(program, redirectLocals)) {
|
|
1999
|
+
const arg = call.arguments?.[1];
|
|
2000
|
+
if (arg?.type !== "Literal" || typeof arg.value !== "string" || !isRootRelativePath(arg.value)) continue;
|
|
2001
|
+
basePathLinks.push({ kind: "redirect", path: arg.value, line: line(call.start) });
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
1848
2004
|
return {
|
|
1849
2005
|
moduleStateReassignments: byLine(moduleStateReassignments),
|
|
1850
2006
|
importedStateWrites: byLine(importedStateWrites),
|
|
@@ -1852,6 +2008,7 @@ function parseKitModuleFacts(source, filename) {
|
|
|
1852
2008
|
runesModuleImports: byLine(runesModuleImports),
|
|
1853
2009
|
lifecycleCalls: byLine(lifecycleCalls),
|
|
1854
2010
|
browserGlobalRefs: byLine(browserGlobalRefs),
|
|
2011
|
+
basePathLinks: byLine(basePathLinks),
|
|
1855
2012
|
...ssrOptOut ? { ssrDisabled: { line: Math.max(0, ssrOptOut.line - 1) } } : {},
|
|
1856
2013
|
...csrOptOut ? { csrDisabled: { line: Math.max(0, csrOptOut.line - 1) } } : {},
|
|
1857
2014
|
...waterfalls.dependentLines.length > 0 || waterfalls.independentLines.length > 0 ? { loadWaterfalls: waterfalls } : {},
|
|
@@ -1870,6 +2027,7 @@ function emptyKitModuleFacts(file, kind) {
|
|
|
1870
2027
|
runesModuleImports: [],
|
|
1871
2028
|
lifecycleCalls: [],
|
|
1872
2029
|
browserGlobalRefs: [],
|
|
2030
|
+
basePathLinks: [],
|
|
1873
2031
|
suppressions: []
|
|
1874
2032
|
};
|
|
1875
2033
|
}
|
|
@@ -1877,7 +2035,7 @@ function kindOf(file) {
|
|
|
1877
2035
|
const base = file.split("/").pop() ?? file;
|
|
1878
2036
|
return base.includes(".server.") || base.startsWith("+server.") ? "server" : "universal";
|
|
1879
2037
|
}
|
|
1880
|
-
async function collectKitModuleFacts(rt, cwd) {
|
|
2038
|
+
async function collectKitModuleFacts(rt, cwd, aliases) {
|
|
1881
2039
|
const patterns = [
|
|
1882
2040
|
"src/routes/**/+{page,layout}.server.{ts,js}",
|
|
1883
2041
|
"src/routes/**/+{page,layout}.{ts,js}",
|
|
@@ -1891,7 +2049,7 @@ async function collectKitModuleFacts(rt, cwd) {
|
|
|
1891
2049
|
const kind = kindOf(rel);
|
|
1892
2050
|
try {
|
|
1893
2051
|
const source = await rt.readFile(rt.join(cwd, rel));
|
|
1894
|
-
return { file: rel, kind, ...parseKitModuleFacts(source, rel) };
|
|
2052
|
+
return { file: rel, kind, ...parseKitModuleFacts(source, rel, aliases) };
|
|
1895
2053
|
} catch {
|
|
1896
2054
|
return emptyKitModuleFacts(rel, kind);
|
|
1897
2055
|
}
|
|
@@ -1899,7 +2057,7 @@ async function collectKitModuleFacts(rt, cwd) {
|
|
|
1899
2057
|
);
|
|
1900
2058
|
}
|
|
1901
2059
|
|
|
1902
|
-
// src/
|
|
2060
|
+
// src/config-object.ts
|
|
1903
2061
|
function propOf(obj, name) {
|
|
1904
2062
|
let found;
|
|
1905
2063
|
for (const p of obj.properties) {
|
|
@@ -1954,6 +2112,8 @@ function resolveConfigObject(program) {
|
|
|
1954
2112
|
if (!exported) return void 0;
|
|
1955
2113
|
return unwrapToObjectExpression(exported, collectTopLevelBindings(program));
|
|
1956
2114
|
}
|
|
2115
|
+
|
|
2116
|
+
// src/vite-config-parse.ts
|
|
1957
2117
|
function findMinifyDisabled(source) {
|
|
1958
2118
|
let program;
|
|
1959
2119
|
let wrapped;
|
|
@@ -1974,6 +2134,152 @@ function findMinifyDisabled(source) {
|
|
|
1974
2134
|
return { line: Math.max(0, lineOf(wrapped, minify.start) - 1) };
|
|
1975
2135
|
}
|
|
1976
2136
|
|
|
2137
|
+
// src/svelte-config-parse.ts
|
|
2138
|
+
function basePathOf(kitConfig, bindings) {
|
|
2139
|
+
const paths = propOf(kitConfig, "paths");
|
|
2140
|
+
const pathsObj = paths ? unwrapToObjectExpression(paths.value, bindings) : void 0;
|
|
2141
|
+
if (!pathsObj) return void 0;
|
|
2142
|
+
const base = propOf(pathsObj, "base");
|
|
2143
|
+
if (!base) return void 0;
|
|
2144
|
+
const value = unwrapTs(base.value);
|
|
2145
|
+
if (value.type === "Literal") {
|
|
2146
|
+
return typeof value.value === "string" && value.value !== "" ? { value: value.value } : void 0;
|
|
2147
|
+
}
|
|
2148
|
+
return {};
|
|
2149
|
+
}
|
|
2150
|
+
function keyNameOf(p) {
|
|
2151
|
+
if (p.computed) return void 0;
|
|
2152
|
+
if (p.key.type === "Identifier") return p.key.name;
|
|
2153
|
+
if (p.key.type === "Literal" && typeof p.key.value === "string") return p.key.value;
|
|
2154
|
+
return void 0;
|
|
2155
|
+
}
|
|
2156
|
+
function stringValueOf(p) {
|
|
2157
|
+
const v = unwrapTs(p.value);
|
|
2158
|
+
return v.type === "Literal" && typeof v.value === "string" ? v.value : void 0;
|
|
2159
|
+
}
|
|
2160
|
+
function aliasEntriesOf(kitConfig, bindings) {
|
|
2161
|
+
const alias = propOf(kitConfig, "alias");
|
|
2162
|
+
if (!alias) return [];
|
|
2163
|
+
const obj = unwrapToObjectExpression(alias.value, bindings);
|
|
2164
|
+
if (!obj) return void 0;
|
|
2165
|
+
const out = [];
|
|
2166
|
+
const at = /* @__PURE__ */ new Map();
|
|
2167
|
+
for (const p of obj.properties) {
|
|
2168
|
+
if (p.type !== "Property") return void 0;
|
|
2169
|
+
const key = keyNameOf(p);
|
|
2170
|
+
if (key === void 0) return void 0;
|
|
2171
|
+
const entry = { key, value: stringValueOf(p) ?? null };
|
|
2172
|
+
const seen = at.get(key);
|
|
2173
|
+
if (seen === void 0) {
|
|
2174
|
+
at.set(key, out.length);
|
|
2175
|
+
out.push(entry);
|
|
2176
|
+
} else out[seen] = entry;
|
|
2177
|
+
}
|
|
2178
|
+
return out;
|
|
2179
|
+
}
|
|
2180
|
+
function filesLibOf(kitConfig, bindings) {
|
|
2181
|
+
const files = propOf(kitConfig, "files");
|
|
2182
|
+
const obj = files ? unwrapToObjectExpression(files.value, bindings) : void 0;
|
|
2183
|
+
const lib = obj ? propOf(obj, "lib") : void 0;
|
|
2184
|
+
if (!lib) return void 0;
|
|
2185
|
+
return stringValueOf(lib) ?? null;
|
|
2186
|
+
}
|
|
2187
|
+
function findKitAliasesInSvelteConfig(source) {
|
|
2188
|
+
const program = programOf(source, "svelte.config.js");
|
|
2189
|
+
const config = program ? resolveConfigObject(program) : void 0;
|
|
2190
|
+
if (!program || !config) return { entries: [] };
|
|
2191
|
+
const bindings = collectTopLevelBindings(program);
|
|
2192
|
+
const kit = propOf(config, "kit");
|
|
2193
|
+
const kitObj = kit ? unwrapToObjectExpression(kit.value, bindings) : void 0;
|
|
2194
|
+
if (!kitObj) return { entries: [] };
|
|
2195
|
+
const filesLib = filesLibOf(kitObj, bindings);
|
|
2196
|
+
return { entries: aliasEntriesOf(kitObj, bindings), ...filesLib !== void 0 ? { filesLib } : {} };
|
|
2197
|
+
}
|
|
2198
|
+
function normalizeAliasValue(value) {
|
|
2199
|
+
const posix = value.replace(/\\/g, "/");
|
|
2200
|
+
const noStar = posix.endsWith("/*") ? posix.slice(0, -2) : posix;
|
|
2201
|
+
return noStar.replace(/\/+$/, "");
|
|
2202
|
+
}
|
|
2203
|
+
function compileKitAliases(raw) {
|
|
2204
|
+
const filesLib = raw.filesLib === null ? null : normalizeAliasValue(raw.filesLib ?? "src/lib");
|
|
2205
|
+
const out = [{ find: "$lib", replacement: filesLib, match: "prefix" }];
|
|
2206
|
+
const entries = raw.entries ?? [];
|
|
2207
|
+
const declared = new Set(entries.map((e) => e.key));
|
|
2208
|
+
for (const { key, value } of entries) {
|
|
2209
|
+
const star = key.endsWith("/*");
|
|
2210
|
+
out.push({
|
|
2211
|
+
find: star ? key.slice(0, -2) : key,
|
|
2212
|
+
replacement: value === null ? null : normalizeAliasValue(value),
|
|
2213
|
+
match: star ? "contents" : declared.has(`${key}/*`) ? "exact" : "prefix"
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
return out;
|
|
2217
|
+
}
|
|
2218
|
+
function resolveKitAliases(viteConfig, svelteConfig) {
|
|
2219
|
+
if (viteConfig && findKitPathsBaseInViteConfig(viteConfig.source).kind !== "no-plugin-config") return void 0;
|
|
2220
|
+
if (!svelteConfig) return void 0;
|
|
2221
|
+
return compileKitAliases(findKitAliasesInSvelteConfig(svelteConfig.source));
|
|
2222
|
+
}
|
|
2223
|
+
function programOf(source, filename) {
|
|
2224
|
+
try {
|
|
2225
|
+
return parseModuleProgram(source, filename).program ?? void 0;
|
|
2226
|
+
} catch {
|
|
2227
|
+
return void 0;
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
function findKitPathsBaseInSvelteConfig(source) {
|
|
2231
|
+
const program = programOf(source, "svelte.config.js");
|
|
2232
|
+
if (!program) return void 0;
|
|
2233
|
+
const config = resolveConfigObject(program);
|
|
2234
|
+
if (!config) return void 0;
|
|
2235
|
+
const bindings = collectTopLevelBindings(program);
|
|
2236
|
+
const kit = propOf(config, "kit");
|
|
2237
|
+
const kitObj = kit ? unwrapToObjectExpression(kit.value, bindings) : void 0;
|
|
2238
|
+
return kitObj ? basePathOf(kitObj, bindings) : void 0;
|
|
2239
|
+
}
|
|
2240
|
+
function sveltekitLocalNames(program) {
|
|
2241
|
+
const out = collectNamedImportAliases(program, "@sveltejs/kit/vite", /* @__PURE__ */ new Set(["sveltekit"]));
|
|
2242
|
+
if (out.size === 0) out.add("sveltekit");
|
|
2243
|
+
return out;
|
|
2244
|
+
}
|
|
2245
|
+
function findKitPathsBaseInViteConfig(source) {
|
|
2246
|
+
const none = { kind: "no-plugin-config" };
|
|
2247
|
+
const program = programOf(source, "vite.config.ts");
|
|
2248
|
+
if (!program) return none;
|
|
2249
|
+
const config = resolveConfigObject(program);
|
|
2250
|
+
if (!config) return none;
|
|
2251
|
+
const bindings = collectTopLevelBindings(program);
|
|
2252
|
+
const plugins = propOf(config, "plugins");
|
|
2253
|
+
const pluginsValue = plugins ? unwrapTs(plugins.value) : void 0;
|
|
2254
|
+
if (pluginsValue?.type !== "ArrayExpression") return none;
|
|
2255
|
+
const locals = sveltekitLocalNames(program);
|
|
2256
|
+
for (const el of pluginsValue.elements) {
|
|
2257
|
+
if (!el || el.type === "SpreadElement") continue;
|
|
2258
|
+
const call = unwrapTs(el);
|
|
2259
|
+
if (call.type !== "CallExpression") continue;
|
|
2260
|
+
if (call.callee.type !== "Identifier" || !locals.has(call.callee.name)) continue;
|
|
2261
|
+
const arg = call.arguments[0];
|
|
2262
|
+
if (arg === void 0) return none;
|
|
2263
|
+
const kitConfig = unwrapToObjectExpression(arg, bindings);
|
|
2264
|
+
if (!kitConfig) return { kind: "unresolvable" };
|
|
2265
|
+
const base = basePathOf(kitConfig, bindings);
|
|
2266
|
+
return base ? { kind: "resolved", base } : { kind: "resolved" };
|
|
2267
|
+
}
|
|
2268
|
+
return none;
|
|
2269
|
+
}
|
|
2270
|
+
function resolveKitPathsBase(viteConfig, svelteConfig) {
|
|
2271
|
+
if (viteConfig) {
|
|
2272
|
+
const result = findKitPathsBaseInViteConfig(viteConfig.source);
|
|
2273
|
+
if (result.kind === "unresolvable") return void 0;
|
|
2274
|
+
if (result.kind === "resolved") {
|
|
2275
|
+
return result.base ? { ...result.base, file: viteConfig.file } : void 0;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
if (!svelteConfig) return void 0;
|
|
2279
|
+
const base = findKitPathsBaseInSvelteConfig(svelteConfig.source);
|
|
2280
|
+
return base ? { ...base, file: svelteConfig.file } : void 0;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
1977
2283
|
// src/project-paths.ts
|
|
1978
2284
|
var ROBOTS_SOURCE_PATHS = [
|
|
1979
2285
|
"static/robots.txt",
|
|
@@ -1985,6 +2291,15 @@ var SITEMAP_SOURCE_PATHS = [
|
|
|
1985
2291
|
"src/routes/sitemap.xml/+server.ts",
|
|
1986
2292
|
"src/routes/sitemap.xml/+server.js"
|
|
1987
2293
|
];
|
|
2294
|
+
var VITE_CONFIG_FILES = [
|
|
2295
|
+
"vite.config.js",
|
|
2296
|
+
"vite.config.mjs",
|
|
2297
|
+
"vite.config.ts",
|
|
2298
|
+
"vite.config.cjs",
|
|
2299
|
+
"vite.config.mts",
|
|
2300
|
+
"vite.config.cts"
|
|
2301
|
+
];
|
|
2302
|
+
var SVELTE_CONFIG_FILES = ["svelte.config.js", "svelte.config.ts"];
|
|
1988
2303
|
|
|
1989
2304
|
// src/rule.ts
|
|
1990
2305
|
function docsUrlFor(id) {
|
|
@@ -2054,7 +2369,7 @@ function detect(head, match) {
|
|
|
2054
2369
|
return tag ? { presence: tag.presence, value: tag.value } : { presence: "none", value: "absent" };
|
|
2055
2370
|
}
|
|
2056
2371
|
function headTagRule(opts) {
|
|
2057
|
-
const
|
|
2372
|
+
const docsUrl11 = docsUrlFor(opts.id);
|
|
2058
2373
|
return {
|
|
2059
2374
|
id: opts.id,
|
|
2060
2375
|
title: opts.title,
|
|
@@ -2077,7 +2392,7 @@ function headTagRule(opts) {
|
|
|
2077
2392
|
location: head.file,
|
|
2078
2393
|
message,
|
|
2079
2394
|
recommendation: opts.recommendation,
|
|
2080
|
-
docsUrl:
|
|
2395
|
+
docsUrl: docsUrl11,
|
|
2081
2396
|
// Copy per finding: opts.fix is a rule-level template shared across all
|
|
2082
2397
|
// results this rule emits; a fresh object keeps findings independent.
|
|
2083
2398
|
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
@@ -2270,7 +2585,7 @@ var seoHtmlLang = {
|
|
|
2270
2585
|
|
|
2271
2586
|
// src/rules/perf/image-rule.ts
|
|
2272
2587
|
function imageRule(opts) {
|
|
2273
|
-
const
|
|
2588
|
+
const docsUrl11 = docsUrlFor(opts.id);
|
|
2274
2589
|
const category = opts.category ?? "performance";
|
|
2275
2590
|
return {
|
|
2276
2591
|
id: opts.id,
|
|
@@ -2294,7 +2609,7 @@ function imageRule(opts) {
|
|
|
2294
2609
|
route: route.route,
|
|
2295
2610
|
message: opts.label,
|
|
2296
2611
|
recommendation: opts.recommendation,
|
|
2297
|
-
docsUrl:
|
|
2612
|
+
docsUrl: docsUrl11
|
|
2298
2613
|
});
|
|
2299
2614
|
continue;
|
|
2300
2615
|
}
|
|
@@ -2309,7 +2624,7 @@ function imageRule(opts) {
|
|
|
2309
2624
|
...img.line > 0 ? { line: img.line } : {},
|
|
2310
2625
|
message: `Missing ${opts.label}`,
|
|
2311
2626
|
recommendation: opts.recommendation,
|
|
2312
|
-
docsUrl:
|
|
2627
|
+
docsUrl: docsUrl11,
|
|
2313
2628
|
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
2314
2629
|
});
|
|
2315
2630
|
}
|
|
@@ -2369,7 +2684,7 @@ var performanceResponsiveImage = imageRule({
|
|
|
2369
2684
|
|
|
2370
2685
|
// src/rules/perf/link-rule.ts
|
|
2371
2686
|
function linkRule(opts) {
|
|
2372
|
-
const
|
|
2687
|
+
const docsUrl11 = docsUrlFor(opts.id);
|
|
2373
2688
|
return {
|
|
2374
2689
|
id: opts.id,
|
|
2375
2690
|
title: opts.title,
|
|
@@ -2393,7 +2708,7 @@ function linkRule(opts) {
|
|
|
2393
2708
|
route: head.route,
|
|
2394
2709
|
message: opts.label,
|
|
2395
2710
|
recommendation: opts.recommendation,
|
|
2396
|
-
docsUrl:
|
|
2711
|
+
docsUrl: docsUrl11
|
|
2397
2712
|
});
|
|
2398
2713
|
continue;
|
|
2399
2714
|
}
|
|
@@ -2410,7 +2725,7 @@ function linkRule(opts) {
|
|
|
2410
2725
|
location: tag.file ?? head.file,
|
|
2411
2726
|
message: `Missing ${opts.label}`,
|
|
2412
2727
|
recommendation: opts.recommendation,
|
|
2413
|
-
docsUrl:
|
|
2728
|
+
docsUrl: docsUrl11,
|
|
2414
2729
|
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
2415
2730
|
});
|
|
2416
2731
|
}
|
|
@@ -2557,10 +2872,201 @@ var performanceRenderBlockingScript = {
|
|
|
2557
2872
|
}
|
|
2558
2873
|
};
|
|
2559
2874
|
|
|
2875
|
+
// src/config-apply.ts
|
|
2876
|
+
function settingSeverity(setting) {
|
|
2877
|
+
if (setting === void 0) return void 0;
|
|
2878
|
+
if (typeof setting === "string") return setting;
|
|
2879
|
+
return setting.severity;
|
|
2880
|
+
}
|
|
2881
|
+
function settingOptions(setting) {
|
|
2882
|
+
return setting !== void 0 && typeof setting !== "string" ? setting.options : void 0;
|
|
2883
|
+
}
|
|
2884
|
+
function selectRules(rules, config) {
|
|
2885
|
+
return rules.filter((rule) => settingSeverity(config.rules[rule.id]) !== "off");
|
|
2886
|
+
}
|
|
2887
|
+
function applyRuleSeverities(results, config) {
|
|
2888
|
+
return results.map((result) => {
|
|
2889
|
+
const severity = settingSeverity(config.rules[result.id]);
|
|
2890
|
+
return severity !== void 0 && severity !== "off" ? { ...result, severity } : result;
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
function routeGlobToRegExp(pattern) {
|
|
2894
|
+
const body = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").split("\0").join(".*");
|
|
2895
|
+
const source = body.endsWith("/.*") ? `${body.slice(0, -3)}(/.*)?` : body;
|
|
2896
|
+
return new RegExp(`^${source}$`);
|
|
2897
|
+
}
|
|
2898
|
+
function toPatterns(globs) {
|
|
2899
|
+
if (globs === void 0) return [];
|
|
2900
|
+
return (Array.isArray(globs) ? globs : [globs]).map(routeGlobToRegExp);
|
|
2901
|
+
}
|
|
2902
|
+
function compileOverrides(config) {
|
|
2903
|
+
return (config.overrides ?? []).map((o) => ({
|
|
2904
|
+
routes: toPatterns(o.route),
|
|
2905
|
+
files: toPatterns(o.files),
|
|
2906
|
+
rules: o.rules
|
|
2907
|
+
}));
|
|
2908
|
+
}
|
|
2909
|
+
function overrideMatches(o, target) {
|
|
2910
|
+
const { route, file } = target;
|
|
2911
|
+
return route !== void 0 && o.routes.some((p) => p.test(route)) || file !== void 0 && o.files.some((p) => p.test(file));
|
|
2912
|
+
}
|
|
2913
|
+
function applyOverrides(results, config) {
|
|
2914
|
+
const compiled = compileOverrides(config);
|
|
2915
|
+
if (compiled.length === 0) return results;
|
|
2916
|
+
const out = [];
|
|
2917
|
+
for (const result of results) {
|
|
2918
|
+
let severity;
|
|
2919
|
+
for (const o of compiled) {
|
|
2920
|
+
if (!overrideMatches(o, { route: result.route, file: result.location })) continue;
|
|
2921
|
+
const sev = settingSeverity(o.rules[result.id]) ?? settingSeverity(o.rules[result.category ?? "seo"]);
|
|
2922
|
+
if (sev !== void 0) severity = sev;
|
|
2923
|
+
}
|
|
2924
|
+
if (severity === void 0) out.push(result);
|
|
2925
|
+
else if (severity !== "off") out.push({ ...result, severity });
|
|
2926
|
+
}
|
|
2927
|
+
return out;
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
// src/rule-options.ts
|
|
2931
|
+
var RULE_SETTING_VALUES = ["off", "critical", "warning", "info"];
|
|
2932
|
+
function defaultsOf(spec) {
|
|
2933
|
+
const out = {};
|
|
2934
|
+
for (const [key, s] of Object.entries(spec)) {
|
|
2935
|
+
out[key] = s.kind === "integer" ? s.default : s.kind === "string-list" ? [...s.default] : { ...s.default };
|
|
2936
|
+
}
|
|
2937
|
+
return out;
|
|
2938
|
+
}
|
|
2939
|
+
function intOption(options, key, fallback = 0) {
|
|
2940
|
+
const v = options[key];
|
|
2941
|
+
return typeof v === "number" ? v : fallback;
|
|
2942
|
+
}
|
|
2943
|
+
function listOption(options, key) {
|
|
2944
|
+
const v = options[key];
|
|
2945
|
+
return Array.isArray(v) ? v : [];
|
|
2946
|
+
}
|
|
2947
|
+
function mapOption(options, key) {
|
|
2948
|
+
const v = options[key];
|
|
2949
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
|
|
2950
|
+
}
|
|
2951
|
+
function isMentionedAnywhere(config, ruleId) {
|
|
2952
|
+
if (Object.hasOwn(config.rules, ruleId)) return true;
|
|
2953
|
+
return (config.overrides ?? []).some((entry) => entry.rules !== void 0 && Object.hasOwn(entry.rules, ruleId));
|
|
2954
|
+
}
|
|
2955
|
+
function resolveRuleOptions(ruleId, spec, config, target, compiled) {
|
|
2956
|
+
if (!spec) return {};
|
|
2957
|
+
const out = defaultsOf(spec);
|
|
2958
|
+
const layers = [settingOptions(config.rules[ruleId])];
|
|
2959
|
+
if (target) {
|
|
2960
|
+
for (const o of compiled ?? compileOverrides(config)) {
|
|
2961
|
+
if (overrideMatches(o, target)) layers.push(settingOptions(o.rules[ruleId]));
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
for (const layer of layers) {
|
|
2965
|
+
if (!layer) continue;
|
|
2966
|
+
for (const [key, value] of Object.entries(layer)) {
|
|
2967
|
+
const s = spec[key];
|
|
2968
|
+
if (!s) continue;
|
|
2969
|
+
if (s.kind === "integer") out[key] = value;
|
|
2970
|
+
else if (s.kind === "string-list") out[key] = [...out[key], ...value];
|
|
2971
|
+
else out[key] = { ...out[key], ...value };
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
return out;
|
|
2975
|
+
}
|
|
2976
|
+
function validateRuleOptions(ruleId, spec, options, baseline, skipRangeCheck) {
|
|
2977
|
+
if (!spec) return Object.keys(options).length === 0 ? [] : [`${ruleId} takes no options.`];
|
|
2978
|
+
const errors = [];
|
|
2979
|
+
const badKeys = /* @__PURE__ */ new Set();
|
|
2980
|
+
const isNonEmptyString = (v) => typeof v === "string" && v.length > 0;
|
|
2981
|
+
for (const [key, value] of Object.entries(options)) {
|
|
2982
|
+
const s = spec[key];
|
|
2983
|
+
if (!s) {
|
|
2984
|
+
errors.push(`${ruleId}: unknown option '${key}'. Known options: ${Object.keys(spec).join(", ")}.`);
|
|
2985
|
+
continue;
|
|
2986
|
+
}
|
|
2987
|
+
if (s.kind === "integer") {
|
|
2988
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
2989
|
+
errors.push(`${ruleId}.${key} must be an integer.`);
|
|
2990
|
+
badKeys.add(key);
|
|
2991
|
+
} else if (s.min !== void 0 && value < s.min) {
|
|
2992
|
+
errors.push(`${ruleId}.${key} must be >= ${s.min}.`);
|
|
2993
|
+
badKeys.add(key);
|
|
2994
|
+
} else if (s.max !== void 0 && value > s.max) {
|
|
2995
|
+
errors.push(`${ruleId}.${key} must be <= ${s.max}.`);
|
|
2996
|
+
badKeys.add(key);
|
|
2997
|
+
}
|
|
2998
|
+
} else if (s.kind === "string-list") {
|
|
2999
|
+
if (!Array.isArray(value) || !value.every(isNonEmptyString)) {
|
|
3000
|
+
errors.push(`${ruleId}.${key} must be an array of non-empty strings.`);
|
|
3001
|
+
}
|
|
3002
|
+
} else if (typeof value !== "object" || value === null || Array.isArray(value) || !Object.values(value).every(isNonEmptyString)) {
|
|
3003
|
+
errors.push(`${ruleId}.${key} must be an object of string \u2192 non-empty string.`);
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
const minSpec = spec.min;
|
|
3007
|
+
const maxSpec = spec.max;
|
|
3008
|
+
if (minSpec?.kind === "integer" && maxSpec?.kind === "integer" && !badKeys.has("min") && !badKeys.has("max") && !skipRangeCheck) {
|
|
3009
|
+
const base = baseline ?? defaultsOf(spec);
|
|
3010
|
+
const minVal = "min" in options ? options.min : base.min;
|
|
3011
|
+
const maxVal = "max" in options ? options.max : base.max;
|
|
3012
|
+
if (typeof minVal === "number" && typeof maxVal === "number" && minVal > maxVal) {
|
|
3013
|
+
errors.push(`${ruleId}: min (${minVal}) must be <= max (${maxVal}).`);
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
return errors;
|
|
3017
|
+
}
|
|
3018
|
+
function isPlainObject(value) {
|
|
3019
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3020
|
+
}
|
|
3021
|
+
function otherOverrideNarrowsOppositeSide(overrides, selfIndex, key, side) {
|
|
3022
|
+
return overrides.some((entry, i) => {
|
|
3023
|
+
if (i === selfIndex || !isPlainObject(entry) || !isPlainObject(entry.rules)) return false;
|
|
3024
|
+
const setting = entry.rules[key];
|
|
3025
|
+
return isPlainObject(setting) && isPlainObject(setting.options) && side in setting.options;
|
|
3026
|
+
});
|
|
3027
|
+
}
|
|
3028
|
+
function shouldSkipRangeCheck(overrides, selfIndex, key, setting) {
|
|
3029
|
+
if (!isPlainObject(setting) || !isPlainObject(setting.options)) return false;
|
|
3030
|
+
const setsMin = "min" in setting.options;
|
|
3031
|
+
const setsMax = "max" in setting.options;
|
|
3032
|
+
if (setsMin === setsMax) return false;
|
|
3033
|
+
return otherOverrideNarrowsOppositeSide(overrides, selfIndex, key, setsMin ? "max" : "min");
|
|
3034
|
+
}
|
|
3035
|
+
function validateRuleSetting(label, ruleId, setting, spec, opts) {
|
|
3036
|
+
const expected = RULE_SETTING_VALUES.join("|");
|
|
3037
|
+
if (typeof setting === "string") {
|
|
3038
|
+
return RULE_SETTING_VALUES.includes(setting) ? [] : [`${label}: invalid setting '${setting}'; expected ${expected}.`];
|
|
3039
|
+
}
|
|
3040
|
+
if (!isPlainObject(setting)) {
|
|
3041
|
+
return [`${label}: must be ${expected} or an object with 'severity' and/or 'options'.`];
|
|
3042
|
+
}
|
|
3043
|
+
const errors = [];
|
|
3044
|
+
const unknownKeys = Object.keys(setting).filter((k) => k !== "severity" && k !== "options");
|
|
3045
|
+
if (unknownKeys.length > 0) {
|
|
3046
|
+
errors.push(`${label}: unknown key(s) ${unknownKeys.join(", ")}; expected severity, options.`);
|
|
3047
|
+
}
|
|
3048
|
+
if (setting.severity !== void 0 && !RULE_SETTING_VALUES.includes(setting.severity)) {
|
|
3049
|
+
errors.push(`${label}.severity: invalid setting '${String(setting.severity)}'; expected ${expected}.`);
|
|
3050
|
+
}
|
|
3051
|
+
if (setting.options === void 0) return errors;
|
|
3052
|
+
if (!opts.allowOptions) {
|
|
3053
|
+
errors.push(`${label}: options are not allowed on a category key.`);
|
|
3054
|
+
return errors;
|
|
3055
|
+
}
|
|
3056
|
+
if (!isPlainObject(setting.options)) {
|
|
3057
|
+
errors.push(`${label}.options: must be an object.`);
|
|
3058
|
+
return errors;
|
|
3059
|
+
}
|
|
3060
|
+
const optionErrors = validateRuleOptions(ruleId, spec, setting.options, opts.baseline, opts.skipRangeCheck);
|
|
3061
|
+
if (optionErrors.length > 0) errors.push(`${label}: ${optionErrors.join(" ")}`);
|
|
3062
|
+
return errors;
|
|
3063
|
+
}
|
|
3064
|
+
|
|
2560
3065
|
// src/rules/perf/preconnect.ts
|
|
2561
3066
|
var docsUrl3 = docsUrlFor("performance/preconnect");
|
|
2562
3067
|
var recommendation3 = 'Add <link rel="preconnect"> (or dns-prefetch) for the third-party origin so the connection is set up early.';
|
|
2563
3068
|
var THIRD_PARTY_ORIGINS = /* @__PURE__ */ new Set(["fonts.googleapis.com", "fonts.gstatic.com"]);
|
|
3069
|
+
var OPTIONS = { origins: { kind: "string-list", default: [...THIRD_PARTY_ORIGINS] } };
|
|
2564
3070
|
function hostOf(href) {
|
|
2565
3071
|
const m = /^(?:https?:)?\/\/([^/?#]+)/i.exec(href);
|
|
2566
3072
|
return m ? m[1].toLowerCase() : void 0;
|
|
@@ -2577,17 +3083,30 @@ var performancePreconnect = {
|
|
|
2577
3083
|
snippet: '<link rel="preconnect" href="https://fonts.googleapis.com" />',
|
|
2578
3084
|
lang: "html"
|
|
2579
3085
|
},
|
|
3086
|
+
options: OPTIONS,
|
|
2580
3087
|
async check(ctx) {
|
|
2581
3088
|
const out = [];
|
|
3089
|
+
const compiled = compileOverrides(ctx.config);
|
|
2582
3090
|
for (const head of ctx.heads) {
|
|
2583
3091
|
const referenced = /* @__PURE__ */ new Map();
|
|
2584
3092
|
const covered = /* @__PURE__ */ new Set();
|
|
2585
3093
|
for (const tag of head.tags) {
|
|
2586
3094
|
if (tag.kind !== "link" && tag.kind !== "script" || typeof tag.href !== "string") continue;
|
|
2587
3095
|
const host = hostOf(tag.href);
|
|
2588
|
-
if (!host
|
|
2589
|
-
if (tag.kind === "link" && (tag.rel === "preconnect" || tag.rel === "dns-prefetch"))
|
|
2590
|
-
|
|
3096
|
+
if (!host) continue;
|
|
3097
|
+
if (tag.kind === "link" && (tag.rel === "preconnect" || tag.rel === "dns-prefetch")) {
|
|
3098
|
+
covered.add(host);
|
|
3099
|
+
continue;
|
|
3100
|
+
}
|
|
3101
|
+
const o = resolveRuleOptions(
|
|
3102
|
+
"performance/preconnect",
|
|
3103
|
+
OPTIONS,
|
|
3104
|
+
ctx.config,
|
|
3105
|
+
{ route: head.route, file: tag.file ?? head.file },
|
|
3106
|
+
compiled
|
|
3107
|
+
);
|
|
3108
|
+
if (!listOption(o, "origins").includes(host)) continue;
|
|
3109
|
+
if (!referenced.has(host)) referenced.set(host, tag.file);
|
|
2591
3110
|
}
|
|
2592
3111
|
if (referenced.size === 0) continue;
|
|
2593
3112
|
const missing = [...referenced].filter(([host]) => !covered.has(host));
|
|
@@ -2638,7 +3157,7 @@ var seoIndexability = {
|
|
|
2638
3157
|
rationale: "A noindex directive removes the page from search results; an accidental noindex on a public route silently deindexes it.",
|
|
2639
3158
|
fix: FIX5,
|
|
2640
3159
|
async check(ctx) {
|
|
2641
|
-
const
|
|
3160
|
+
const docsUrl11 = docsUrlFor("seo/indexability");
|
|
2642
3161
|
const out = [];
|
|
2643
3162
|
for (const head of ctx.heads) {
|
|
2644
3163
|
const noindexed = head.tags.some((t) => t.kind === "meta" && t.name === "robots" && t.noindex === true);
|
|
@@ -2653,7 +3172,7 @@ var seoIndexability = {
|
|
|
2653
3172
|
location: head.file,
|
|
2654
3173
|
message: "Route is noindex \u2014 verify this is intentional",
|
|
2655
3174
|
recommendation: 'If this route should be indexed, remove noindex from its <meta name="robots">.',
|
|
2656
|
-
docsUrl:
|
|
3175
|
+
docsUrl: docsUrl11,
|
|
2657
3176
|
fix: { ...FIX5 }
|
|
2658
3177
|
});
|
|
2659
3178
|
}
|
|
@@ -2904,7 +3423,7 @@ function jsonldTags(head) {
|
|
|
2904
3423
|
return head.tags.filter((t) => t.kind === "jsonld" && typeof t.jsonld === "string");
|
|
2905
3424
|
}
|
|
2906
3425
|
function jsonldRule(opts) {
|
|
2907
|
-
const
|
|
3426
|
+
const docsUrl11 = docsUrlFor(opts.id);
|
|
2908
3427
|
return {
|
|
2909
3428
|
id: opts.id,
|
|
2910
3429
|
title: opts.title,
|
|
@@ -2932,7 +3451,7 @@ function jsonldRule(opts) {
|
|
|
2932
3451
|
location: head.file,
|
|
2933
3452
|
message: problem,
|
|
2934
3453
|
recommendation: opts.recommendation,
|
|
2935
|
-
docsUrl:
|
|
3454
|
+
docsUrl: docsUrl11,
|
|
2936
3455
|
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
2937
3456
|
} : {
|
|
2938
3457
|
id: opts.id,
|
|
@@ -2942,7 +3461,7 @@ function jsonldRule(opts) {
|
|
|
2942
3461
|
route: head.route,
|
|
2943
3462
|
message: opts.label,
|
|
2944
3463
|
recommendation: opts.recommendation,
|
|
2945
|
-
docsUrl:
|
|
3464
|
+
docsUrl: docsUrl11
|
|
2946
3465
|
}
|
|
2947
3466
|
);
|
|
2948
3467
|
}
|
|
@@ -2966,7 +3485,7 @@ var seoJsonLdValidity = {
|
|
|
2966
3485
|
lang: "svelte"
|
|
2967
3486
|
},
|
|
2968
3487
|
async check(ctx) {
|
|
2969
|
-
const
|
|
3488
|
+
const docsUrl11 = docsUrlFor("seo/json-ld-validity");
|
|
2970
3489
|
const out = [];
|
|
2971
3490
|
for (const head of ctx.heads) {
|
|
2972
3491
|
for (const tag of jsonldTags(head)) {
|
|
@@ -2985,7 +3504,7 @@ var seoJsonLdValidity = {
|
|
|
2985
3504
|
location: head.file,
|
|
2986
3505
|
message: problem,
|
|
2987
3506
|
recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
|
|
2988
|
-
docsUrl:
|
|
3507
|
+
docsUrl: docsUrl11,
|
|
2989
3508
|
fix: { ...seoJsonLdValidity.fix }
|
|
2990
3509
|
} : {
|
|
2991
3510
|
id: "seo/json-ld-validity",
|
|
@@ -2995,7 +3514,7 @@ var seoJsonLdValidity = {
|
|
|
2995
3514
|
route: head.route,
|
|
2996
3515
|
message: "JSON-LD validity",
|
|
2997
3516
|
recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
|
|
2998
|
-
docsUrl:
|
|
3517
|
+
docsUrl: docsUrl11
|
|
2999
3518
|
}
|
|
3000
3519
|
);
|
|
3001
3520
|
}
|
|
@@ -3106,7 +3625,11 @@ function visibleLength(s) {
|
|
|
3106
3625
|
|
|
3107
3626
|
// src/rules/seo/length-rule.ts
|
|
3108
3627
|
function lengthRule(opts) {
|
|
3109
|
-
const
|
|
3628
|
+
const docsUrl11 = docsUrlFor(opts.id);
|
|
3629
|
+
const spec = {
|
|
3630
|
+
min: { kind: "integer", default: opts.min, min: 0 },
|
|
3631
|
+
max: { kind: "integer", default: opts.max, min: 1 }
|
|
3632
|
+
};
|
|
3110
3633
|
return {
|
|
3111
3634
|
id: opts.id,
|
|
3112
3635
|
title: opts.title,
|
|
@@ -3114,15 +3637,22 @@ function lengthRule(opts) {
|
|
|
3114
3637
|
severity: "info",
|
|
3115
3638
|
scope: "route",
|
|
3116
3639
|
rationale: opts.rationale,
|
|
3640
|
+
options: spec,
|
|
3117
3641
|
async check(ctx) {
|
|
3118
3642
|
const out = [];
|
|
3643
|
+
const compiled = compileOverrides(ctx.config);
|
|
3119
3644
|
for (const head of ctx.heads) {
|
|
3120
3645
|
const tag = head.tags.find(opts.match);
|
|
3121
3646
|
if (!tag || typeof tag.text !== "string") continue;
|
|
3647
|
+
const location = tag.file ?? head.file;
|
|
3648
|
+
const o = resolveRuleOptions(opts.id, spec, ctx.config, { route: head.route, file: location }, compiled);
|
|
3649
|
+
const min = intOption(o, "min", opts.min);
|
|
3650
|
+
const max = intOption(o, "max", opts.max);
|
|
3651
|
+
const recommendation11 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
|
|
3122
3652
|
const len = visibleLength(tag.text);
|
|
3123
3653
|
let problem;
|
|
3124
|
-
if (len <
|
|
3125
|
-
else if (len >
|
|
3654
|
+
if (len < min) problem = `${opts.noun} is too short (${len} chars; aim for ${min}\u2013${max})`;
|
|
3655
|
+
else if (len > max) problem = `${opts.noun} is too long (${len} chars; aim for ${min}\u2013${max})`;
|
|
3126
3656
|
out.push(
|
|
3127
3657
|
problem ? {
|
|
3128
3658
|
id: opts.id,
|
|
@@ -3130,10 +3660,10 @@ function lengthRule(opts) {
|
|
|
3130
3660
|
severity: "info",
|
|
3131
3661
|
detection: PENALIZED,
|
|
3132
3662
|
route: head.route,
|
|
3133
|
-
location
|
|
3663
|
+
location,
|
|
3134
3664
|
message: problem,
|
|
3135
|
-
recommendation:
|
|
3136
|
-
docsUrl:
|
|
3665
|
+
recommendation: recommendation11,
|
|
3666
|
+
docsUrl: docsUrl11
|
|
3137
3667
|
} : {
|
|
3138
3668
|
id: opts.id,
|
|
3139
3669
|
category: "seo",
|
|
@@ -3141,8 +3671,8 @@ function lengthRule(opts) {
|
|
|
3141
3671
|
detection: PASS,
|
|
3142
3672
|
route: head.route,
|
|
3143
3673
|
message: opts.label,
|
|
3144
|
-
recommendation:
|
|
3145
|
-
docsUrl:
|
|
3674
|
+
recommendation: recommendation11,
|
|
3675
|
+
docsUrl: docsUrl11
|
|
3146
3676
|
}
|
|
3147
3677
|
);
|
|
3148
3678
|
}
|
|
@@ -3152,28 +3682,32 @@ function lengthRule(opts) {
|
|
|
3152
3682
|
}
|
|
3153
3683
|
|
|
3154
3684
|
// src/rules/seo/title-length.ts
|
|
3685
|
+
var MIN = 30;
|
|
3686
|
+
var MAX = 60;
|
|
3155
3687
|
var seoTitleLength = lengthRule({
|
|
3156
3688
|
id: "seo/title-length",
|
|
3157
3689
|
title: "Title length",
|
|
3158
3690
|
label: "Title length",
|
|
3159
3691
|
noun: "Title",
|
|
3160
3692
|
match: (t) => t.kind === "title",
|
|
3161
|
-
min:
|
|
3162
|
-
max:
|
|
3163
|
-
recommendation:
|
|
3693
|
+
min: MIN,
|
|
3694
|
+
max: MAX,
|
|
3695
|
+
recommendation: (o) => `Aim for a title of ${intOption(o, "min", MIN)}\u2013${intOption(o, "max", MAX)} characters so it is not truncated in search results.`,
|
|
3164
3696
|
rationale: "A title that is too short wastes the strongest on-page signal; one that is too long is truncated in the SERP."
|
|
3165
3697
|
});
|
|
3166
3698
|
|
|
3167
3699
|
// src/rules/seo/description-length.ts
|
|
3700
|
+
var MIN2 = 70;
|
|
3701
|
+
var MAX2 = 160;
|
|
3168
3702
|
var seoDescriptionLength = lengthRule({
|
|
3169
3703
|
id: "seo/description-length",
|
|
3170
3704
|
title: "Description length",
|
|
3171
3705
|
label: "Description length",
|
|
3172
3706
|
noun: "Description",
|
|
3173
3707
|
match: (t) => t.kind === "meta" && t.name === "description",
|
|
3174
|
-
min:
|
|
3175
|
-
max:
|
|
3176
|
-
recommendation:
|
|
3708
|
+
min: MIN2,
|
|
3709
|
+
max: MAX2,
|
|
3710
|
+
recommendation: (o) => `Aim for a meta description of ${intOption(o, "min", MIN2)}\u2013${intOption(o, "max", MAX2)} characters so it is not truncated in search results.`,
|
|
3177
3711
|
rationale: "A description that is too short under-uses the SERP snippet; one that is too long is truncated by search engines."
|
|
3178
3712
|
});
|
|
3179
3713
|
|
|
@@ -3323,7 +3857,7 @@ var seoSingleH1 = {
|
|
|
3323
3857
|
|
|
3324
3858
|
// src/rules/seo/uniqueness-rule.ts
|
|
3325
3859
|
function uniquenessRule(opts) {
|
|
3326
|
-
const
|
|
3860
|
+
const docsUrl11 = docsUrlFor(opts.id);
|
|
3327
3861
|
return {
|
|
3328
3862
|
id: opts.id,
|
|
3329
3863
|
title: opts.title,
|
|
@@ -3353,7 +3887,7 @@ function uniquenessRule(opts) {
|
|
|
3353
3887
|
location: e.file,
|
|
3354
3888
|
message: `${opts.noun} is duplicated across ${n} routes`,
|
|
3355
3889
|
recommendation: opts.recommendation,
|
|
3356
|
-
docsUrl:
|
|
3890
|
+
docsUrl: docsUrl11
|
|
3357
3891
|
} : {
|
|
3358
3892
|
id: opts.id,
|
|
3359
3893
|
category: "seo",
|
|
@@ -3362,7 +3896,7 @@ function uniquenessRule(opts) {
|
|
|
3362
3896
|
route: e.route,
|
|
3363
3897
|
message: opts.label,
|
|
3364
3898
|
recommendation: opts.recommendation,
|
|
3365
|
-
docsUrl:
|
|
3899
|
+
docsUrl: docsUrl11
|
|
3366
3900
|
};
|
|
3367
3901
|
});
|
|
3368
3902
|
}
|
|
@@ -3450,7 +3984,7 @@ function isSuppressed(m, ruleId, line) {
|
|
|
3450
3984
|
return (m.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
|
|
3451
3985
|
}
|
|
3452
3986
|
function kitModuleRule(opts) {
|
|
3453
|
-
const
|
|
3987
|
+
const docsUrl11 = docsUrlFor(opts.id);
|
|
3454
3988
|
const severity = opts.severity ?? "warning";
|
|
3455
3989
|
return {
|
|
3456
3990
|
id: opts.id,
|
|
@@ -3474,7 +4008,7 @@ function kitModuleRule(opts) {
|
|
|
3474
4008
|
route: m.file,
|
|
3475
4009
|
message: opts.label,
|
|
3476
4010
|
recommendation: opts.recommendation,
|
|
3477
|
-
docsUrl:
|
|
4011
|
+
docsUrl: docsUrl11
|
|
3478
4012
|
});
|
|
3479
4013
|
continue;
|
|
3480
4014
|
}
|
|
@@ -3489,7 +4023,7 @@ function kitModuleRule(opts) {
|
|
|
3489
4023
|
...b.line > 0 ? { line: b.line } : {},
|
|
3490
4024
|
message: b.message,
|
|
3491
4025
|
recommendation: opts.recommendation,
|
|
3492
|
-
docsUrl:
|
|
4026
|
+
docsUrl: docsUrl11,
|
|
3493
4027
|
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
3494
4028
|
});
|
|
3495
4029
|
}
|
|
@@ -3525,7 +4059,7 @@ function isSuppressed2(c, ruleId, line) {
|
|
|
3525
4059
|
return (c.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
|
|
3526
4060
|
}
|
|
3527
4061
|
function componentRule(opts) {
|
|
3528
|
-
const
|
|
4062
|
+
const docsUrl11 = docsUrlFor(opts.id);
|
|
3529
4063
|
const severity = opts.severity ?? "warning";
|
|
3530
4064
|
return {
|
|
3531
4065
|
id: opts.id,
|
|
@@ -3535,11 +4069,15 @@ function componentRule(opts) {
|
|
|
3535
4069
|
scope: "component",
|
|
3536
4070
|
rationale: opts.rationale,
|
|
3537
4071
|
...opts.fix ? { fix: opts.fix } : {},
|
|
4072
|
+
...opts.options ? { options: opts.options } : {},
|
|
3538
4073
|
async check(ctx) {
|
|
3539
4074
|
const out = [];
|
|
4075
|
+
const compiled = compileOverrides(ctx.config);
|
|
3540
4076
|
for (const c of ctx.components ?? []) {
|
|
3541
|
-
|
|
3542
|
-
const
|
|
4077
|
+
const o = resolveRuleOptions(opts.id, opts.options, ctx.config, { route: c.file, file: c.file }, compiled);
|
|
4078
|
+
const recommendation11 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
|
|
4079
|
+
if (!opts.applies(c, o, ctx)) continue;
|
|
4080
|
+
const bad = opts.bad(c, o, ctx).filter((b) => !(b.line > 0 && isSuppressed2(c, opts.id, b.line)));
|
|
3543
4081
|
if (bad.length === 0) {
|
|
3544
4082
|
out.push({
|
|
3545
4083
|
id: opts.id,
|
|
@@ -3548,8 +4086,8 @@ function componentRule(opts) {
|
|
|
3548
4086
|
detection: PASS3,
|
|
3549
4087
|
route: c.file,
|
|
3550
4088
|
message: opts.label,
|
|
3551
|
-
recommendation:
|
|
3552
|
-
docsUrl:
|
|
4089
|
+
recommendation: recommendation11,
|
|
4090
|
+
docsUrl: docsUrl11
|
|
3553
4091
|
});
|
|
3554
4092
|
continue;
|
|
3555
4093
|
}
|
|
@@ -3563,8 +4101,8 @@ function componentRule(opts) {
|
|
|
3563
4101
|
location: c.file,
|
|
3564
4102
|
...b.line > 0 ? { line: b.line } : {},
|
|
3565
4103
|
message: b.message,
|
|
3566
|
-
recommendation:
|
|
3567
|
-
docsUrl:
|
|
4104
|
+
recommendation: recommendation11,
|
|
4105
|
+
docsUrl: docsUrl11,
|
|
3568
4106
|
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
3569
4107
|
});
|
|
3570
4108
|
}
|
|
@@ -3694,6 +4232,25 @@ var correctnessNonreactiveBuiltinState = componentRule({
|
|
|
3694
4232
|
}))
|
|
3695
4233
|
});
|
|
3696
4234
|
|
|
4235
|
+
// src/rules/correctness/checkable-bind-value.ts
|
|
4236
|
+
var correctnessCheckableBindValue = componentRule({
|
|
4237
|
+
id: "correctness/checkable-bind-value",
|
|
4238
|
+
title: "bind:value on a checkable input",
|
|
4239
|
+
category: "correctness",
|
|
4240
|
+
severity: "warning",
|
|
4241
|
+
label: "bind:checked / bind:group on checkable inputs",
|
|
4242
|
+
recommendation: "Replace bind:value with bind:checked (single checkbox) or bind:group (checkbox list / radio group).",
|
|
4243
|
+
rationale: "bind:value binds the DOM value property. A checkbox/radio's user interaction toggles checkedness, which bind:value never observes \u2014 the bound state is frozen at its initial value. Svelte's checked/grouped bindings (bind:checked, bind:group) are built for exactly this.",
|
|
4244
|
+
fix: {
|
|
4245
|
+
description: "For a single checkbox, replace bind:value={x} with bind:checked={x} (x becomes a boolean). For a checkbox list or radio group, replace bind:value={x} with bind:group={x} on every input sharing the group, keeping each input's static value attribute to identify the option."
|
|
4246
|
+
},
|
|
4247
|
+
applies: (c) => c.checkableBindValues.length > 0,
|
|
4248
|
+
bad: (c) => c.checkableBindValues.map((v) => ({
|
|
4249
|
+
line: v.line,
|
|
4250
|
+
message: v.kind === "checkbox" ? "bind:value on a checkbox does not track its checked state \u2014 the bound value silently never updates when the user toggles it. Use bind:checked (single checkbox) or bind:group (checkbox list) instead." : "bind:value on a radio input does not track which option is selected \u2014 the bound value silently never updates when the user picks one. Use bind:group with a shared group variable across the radio inputs instead."
|
|
4251
|
+
}))
|
|
4252
|
+
});
|
|
4253
|
+
|
|
3697
4254
|
// src/rules/correctness/orphan-effect.ts
|
|
3698
4255
|
var correctnessOrphanEffect = componentRule({
|
|
3699
4256
|
id: "correctness/orphan-effect",
|
|
@@ -3793,24 +4350,35 @@ var correctnessOrphanLifecycle = {
|
|
|
3793
4350
|
}
|
|
3794
4351
|
};
|
|
3795
4352
|
|
|
3796
|
-
// src/rules/correctness/
|
|
4353
|
+
// src/rules/correctness/base-path-navigation.ts
|
|
3797
4354
|
var PENALIZED5 = { presence: "none", value: "absent" };
|
|
3798
4355
|
var PASS5 = { presence: "own", value: "static" };
|
|
3799
|
-
var ID2 = "correctness/
|
|
4356
|
+
var ID2 = "correctness/base-path-navigation";
|
|
3800
4357
|
var DOCS_URL2 = docsUrlFor(ID2);
|
|
3801
|
-
var LABEL2 = "
|
|
3802
|
-
var RECOMMENDATION2 = "
|
|
3803
|
-
var
|
|
4358
|
+
var LABEL2 = "Base-path-aware navigation";
|
|
4359
|
+
var RECOMMENDATION2 = "Wrap root-relative paths in resolve() from '$app/paths' so they resolve against kit.paths.base.";
|
|
4360
|
+
var FIX7 = {
|
|
4361
|
+
description: "Import { resolve } from '$app/paths' and wrap the path: href={resolve('/about')}, goto(resolve('/about')), redirect(303, resolve('/login'))."
|
|
4362
|
+
};
|
|
4363
|
+
function messageFor2(link) {
|
|
4364
|
+
if (link.kind === "href") {
|
|
4365
|
+
return `<a href="${link.path}"> is root-relative \u2014 under this project's kit.paths.base it points at the domain root, outside the app, and 404s in production. Use resolve('${link.path}') from '$app/paths'.`;
|
|
4366
|
+
}
|
|
4367
|
+
if (link.kind === "goto") {
|
|
4368
|
+
return `goto('${link.path}') is root-relative \u2014 it navigates outside this project's kit.paths.base and 404s in production. Use goto(resolve('${link.path}')) with resolve from '$app/paths'.`;
|
|
4369
|
+
}
|
|
4370
|
+
return `redirect(\u2026, '${link.path}') is root-relative \u2014 the Location header points outside this project's kit.paths.base and 404s in production. Use resolve('${link.path}') from '$app/paths'.`;
|
|
4371
|
+
}
|
|
3804
4372
|
function isSuppressed4(suppressions, line) {
|
|
3805
4373
|
return (suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ID2)));
|
|
3806
4374
|
}
|
|
3807
|
-
function emitFile2(out, file,
|
|
3808
|
-
const bad =
|
|
4375
|
+
function emitFile2(out, file, links, suppressions) {
|
|
4376
|
+
const bad = links.filter((l) => !(l.line > 0 && isSuppressed4(suppressions, l.line)));
|
|
3809
4377
|
if (bad.length === 0) {
|
|
3810
4378
|
out.push({
|
|
3811
4379
|
id: ID2,
|
|
3812
4380
|
category: "correctness",
|
|
3813
|
-
severity: "
|
|
4381
|
+
severity: "warning",
|
|
3814
4382
|
detection: PASS5,
|
|
3815
4383
|
route: file,
|
|
3816
4384
|
message: LABEL2,
|
|
@@ -3819,23 +4387,90 @@ function emitFile2(out, file, issues, suppressions) {
|
|
|
3819
4387
|
});
|
|
3820
4388
|
return;
|
|
3821
4389
|
}
|
|
3822
|
-
for (const
|
|
4390
|
+
for (const l of bad) {
|
|
3823
4391
|
out.push({
|
|
3824
4392
|
id: ID2,
|
|
3825
4393
|
category: "correctness",
|
|
3826
|
-
severity: "
|
|
4394
|
+
severity: "warning",
|
|
3827
4395
|
detection: PENALIZED5,
|
|
3828
4396
|
route: file,
|
|
3829
4397
|
location: file,
|
|
4398
|
+
...l.line > 0 ? { line: l.line } : {},
|
|
4399
|
+
message: messageFor2(l),
|
|
4400
|
+
recommendation: RECOMMENDATION2,
|
|
4401
|
+
docsUrl: DOCS_URL2,
|
|
4402
|
+
fix: { ...FIX7 }
|
|
4403
|
+
});
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4406
|
+
var correctnessBasePathNavigation = {
|
|
4407
|
+
id: ID2,
|
|
4408
|
+
title: "Root-relative navigation under a base path",
|
|
4409
|
+
category: "correctness",
|
|
4410
|
+
severity: "warning",
|
|
4411
|
+
scope: "component",
|
|
4412
|
+
rationale: "A root-relative literal resolves against the domain root, not kit.paths.base, so navigation lands outside an app served from a sub-path. The break only appears once the app is deployed under its base \u2014 locally base is usually empty, so every such link works.",
|
|
4413
|
+
fix: { ...FIX7 },
|
|
4414
|
+
async check(ctx) {
|
|
4415
|
+
if (!ctx.project.kitPathsBase) return [];
|
|
4416
|
+
const out = [];
|
|
4417
|
+
for (const c of ctx.components ?? []) {
|
|
4418
|
+
const links = c.basePathLinks ?? [];
|
|
4419
|
+
if (links.length === 0) continue;
|
|
4420
|
+
emitFile2(out, c.file, links, c.suppressions);
|
|
4421
|
+
}
|
|
4422
|
+
for (const m of ctx.kitModules ?? []) {
|
|
4423
|
+
const links = m.basePathLinks ?? [];
|
|
4424
|
+
if (links.length === 0) continue;
|
|
4425
|
+
emitFile2(out, m.file, links, m.suppressions);
|
|
4426
|
+
}
|
|
4427
|
+
return out;
|
|
4428
|
+
}
|
|
4429
|
+
};
|
|
4430
|
+
|
|
4431
|
+
// src/rules/correctness/server-browser-global.ts
|
|
4432
|
+
var PENALIZED6 = { presence: "none", value: "absent" };
|
|
4433
|
+
var PASS6 = { presence: "own", value: "static" };
|
|
4434
|
+
var ID3 = "correctness/server-browser-global";
|
|
4435
|
+
var DOCS_URL3 = docsUrlFor(ID3);
|
|
4436
|
+
var LABEL3 = "Server-safe module code";
|
|
4437
|
+
var RECOMMENDATION3 = "Move browser-only code into onMount or $effect (they never run on the server), or guard it with browser from $app/environment (or a typeof check).";
|
|
4438
|
+
var moduleMessage = (name) => `${name} is accessed at module scope \u2014 it does not exist on the server, so importing this file crashes SSR with "${name} is not defined"`;
|
|
4439
|
+
function isSuppressed5(suppressions, line) {
|
|
4440
|
+
return (suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ID3)));
|
|
4441
|
+
}
|
|
4442
|
+
function emitFile3(out, file, issues, suppressions) {
|
|
4443
|
+
const bad = issues.filter((b) => !(b.line > 0 && isSuppressed5(suppressions, b.line)));
|
|
4444
|
+
if (bad.length === 0) {
|
|
4445
|
+
out.push({
|
|
4446
|
+
id: ID3,
|
|
4447
|
+
category: "correctness",
|
|
4448
|
+
severity: "critical",
|
|
4449
|
+
detection: PASS6,
|
|
4450
|
+
route: file,
|
|
4451
|
+
message: LABEL3,
|
|
4452
|
+
recommendation: RECOMMENDATION3,
|
|
4453
|
+
docsUrl: DOCS_URL3
|
|
4454
|
+
});
|
|
4455
|
+
return;
|
|
4456
|
+
}
|
|
4457
|
+
for (const b of bad) {
|
|
4458
|
+
out.push({
|
|
4459
|
+
id: ID3,
|
|
4460
|
+
category: "correctness",
|
|
4461
|
+
severity: "critical",
|
|
4462
|
+
detection: PENALIZED6,
|
|
4463
|
+
route: file,
|
|
4464
|
+
location: file,
|
|
3830
4465
|
...b.line > 0 ? { line: b.line } : {},
|
|
3831
4466
|
message: b.message,
|
|
3832
|
-
recommendation:
|
|
3833
|
-
docsUrl:
|
|
4467
|
+
recommendation: RECOMMENDATION3,
|
|
4468
|
+
docsUrl: DOCS_URL3
|
|
3834
4469
|
});
|
|
3835
4470
|
}
|
|
3836
4471
|
}
|
|
3837
4472
|
var correctnessServerBrowserGlobal = {
|
|
3838
|
-
id:
|
|
4473
|
+
id: ID3,
|
|
3839
4474
|
title: "Browser global in server module code",
|
|
3840
4475
|
category: "correctness",
|
|
3841
4476
|
severity: "critical",
|
|
@@ -3846,7 +4481,7 @@ var correctnessServerBrowserGlobal = {
|
|
|
3846
4481
|
for (const c of ctx.components ?? []) {
|
|
3847
4482
|
const refs = (c.browserGlobalRefs ?? []).filter((r) => r.context === "module");
|
|
3848
4483
|
if (refs.length === 0) continue;
|
|
3849
|
-
|
|
4484
|
+
emitFile3(
|
|
3850
4485
|
out,
|
|
3851
4486
|
c.file,
|
|
3852
4487
|
refs.map((r) => ({ line: r.line, message: moduleMessage(r.name) })),
|
|
@@ -3856,7 +4491,7 @@ var correctnessServerBrowserGlobal = {
|
|
|
3856
4491
|
for (const m of ctx.kitModules ?? []) {
|
|
3857
4492
|
const refs = m.browserGlobalRefs ?? [];
|
|
3858
4493
|
if (refs.length === 0) continue;
|
|
3859
|
-
|
|
4494
|
+
emitFile3(
|
|
3860
4495
|
out,
|
|
3861
4496
|
m.file,
|
|
3862
4497
|
refs.map((r) => ({
|
|
@@ -3972,33 +4607,733 @@ var securitySharedStateImport = kitModuleRule({
|
|
|
3972
4607
|
});
|
|
3973
4608
|
|
|
3974
4609
|
// src/rules/architecture/component-size.ts
|
|
3975
|
-
var MAX_LOC =
|
|
4610
|
+
var MAX_LOC = 200;
|
|
3976
4611
|
var architectureComponentSize = componentRule({
|
|
3977
4612
|
id: "architecture/component-size",
|
|
3978
4613
|
title: "Component size",
|
|
3979
4614
|
category: "architecture",
|
|
3980
4615
|
severity: "info",
|
|
3981
4616
|
label: "Component size",
|
|
3982
|
-
|
|
4617
|
+
options: { max: { kind: "integer", default: MAX_LOC, min: 1 } },
|
|
4618
|
+
recommendation: (o) => `Split components over ${intOption(o, "max", MAX_LOC)} lines into smaller, focused pieces.`,
|
|
3983
4619
|
rationale: "A very large component is hard to read, test, and reuse, and is a common sign that several responsibilities should be split out.",
|
|
3984
4620
|
applies: (c) => c.loc > 0,
|
|
3985
4621
|
// skip unanalyzable files (loc 0 = read/parse failure), don't PASS them
|
|
3986
|
-
bad: (c) =>
|
|
4622
|
+
bad: (c, o) => {
|
|
4623
|
+
const max = intOption(o, "max", MAX_LOC);
|
|
4624
|
+
return c.loc > max ? [{ line: 1, message: `Component is ${c.loc} lines (over ${max})` }] : [];
|
|
4625
|
+
}
|
|
3987
4626
|
});
|
|
3988
4627
|
|
|
3989
4628
|
// src/rules/architecture/prop-count.ts
|
|
3990
|
-
var MAX_PROPS =
|
|
4629
|
+
var MAX_PROPS = 6;
|
|
3991
4630
|
var architecturePropCount = componentRule({
|
|
3992
4631
|
id: "architecture/prop-count",
|
|
3993
4632
|
title: "Prop count",
|
|
3994
4633
|
category: "architecture",
|
|
3995
4634
|
severity: "info",
|
|
3996
4635
|
label: "Prop count",
|
|
3997
|
-
|
|
4636
|
+
options: { max: { kind: "integer", default: MAX_PROPS, min: 1 } },
|
|
4637
|
+
recommendation: (o) => `Group related props into an object, or split the component, when it takes more than ${intOption(o, "max", MAX_PROPS)} props.`,
|
|
3998
4638
|
rationale: "A component taking many props is usually doing too much; grouping or splitting keeps its API understandable.",
|
|
3999
4639
|
applies: (c) => c.propCount > 0,
|
|
4000
4640
|
// only components whose props we could count
|
|
4001
|
-
bad: (c) =>
|
|
4641
|
+
bad: (c, o) => {
|
|
4642
|
+
const max = intOption(o, "max", MAX_PROPS);
|
|
4643
|
+
return c.propCount > max ? [{ line: 1, message: `Component takes ${c.propCount} props (over ${max})` }] : [];
|
|
4644
|
+
}
|
|
4645
|
+
});
|
|
4646
|
+
|
|
4647
|
+
// src/rules/architecture/private-scope-import.ts
|
|
4648
|
+
var docsUrl7 = docsUrlFor("architecture/private-scope-import");
|
|
4649
|
+
var recommendation7 = "Move the unit to the directory shared by all of its importers, or import it only from inside its own scope.";
|
|
4650
|
+
var OPTIONS2 = { scopes: { kind: "string-list", default: [] } };
|
|
4651
|
+
function ancestorDirs(file) {
|
|
4652
|
+
const segments = file.split("/");
|
|
4653
|
+
const out = [];
|
|
4654
|
+
for (let i = segments.length - 1; i > 0; i--) out.push(segments.slice(0, i).join("/"));
|
|
4655
|
+
return out;
|
|
4656
|
+
}
|
|
4657
|
+
function privateScopeOf(target, patterns) {
|
|
4658
|
+
for (const dir of ancestorDirs(target)) {
|
|
4659
|
+
if (!patterns.some((p) => p.test(dir))) continue;
|
|
4660
|
+
const cut = dir.lastIndexOf("/");
|
|
4661
|
+
return cut === -1 ? "" : dir.slice(0, cut);
|
|
4662
|
+
}
|
|
4663
|
+
return void 0;
|
|
4664
|
+
}
|
|
4665
|
+
function isInside(file, boundary) {
|
|
4666
|
+
return boundary === "" || file.startsWith(`${boundary}/`);
|
|
4667
|
+
}
|
|
4668
|
+
var architecturePrivateScopeImport = {
|
|
4669
|
+
id: "architecture/private-scope-import",
|
|
4670
|
+
title: "Private-scope import",
|
|
4671
|
+
category: "architecture",
|
|
4672
|
+
severity: "info",
|
|
4673
|
+
scope: "component",
|
|
4674
|
+
rationale: "A unit placed inside a private directory is written for one owner; importing it from elsewhere couples two parts of the tree that were meant to move independently, and the unit belongs higher up instead.",
|
|
4675
|
+
fix: {
|
|
4676
|
+
description: "Move this unit out of its private scope, to the directory shared by all of its importers, and update this import."
|
|
4677
|
+
},
|
|
4678
|
+
options: OPTIONS2,
|
|
4679
|
+
async check(ctx) {
|
|
4680
|
+
const out = [];
|
|
4681
|
+
const compiled = compileOverrides(ctx.config);
|
|
4682
|
+
const patternCache = /* @__PURE__ */ new Map();
|
|
4683
|
+
const compileScopes = (scopes) => {
|
|
4684
|
+
const key = JSON.stringify(scopes);
|
|
4685
|
+
let patterns = patternCache.get(key);
|
|
4686
|
+
if (patterns === void 0) {
|
|
4687
|
+
patterns = scopes.map((scope) => {
|
|
4688
|
+
const marker = scope.endsWith("/**") ? scope.slice(0, -3) : scope;
|
|
4689
|
+
return routeGlobToRegExp(marker);
|
|
4690
|
+
});
|
|
4691
|
+
patternCache.set(key, patterns);
|
|
4692
|
+
}
|
|
4693
|
+
return patterns;
|
|
4694
|
+
};
|
|
4695
|
+
for (const c of ctx.components ?? []) {
|
|
4696
|
+
const o = resolveRuleOptions(
|
|
4697
|
+
"architecture/private-scope-import",
|
|
4698
|
+
OPTIONS2,
|
|
4699
|
+
ctx.config,
|
|
4700
|
+
{ route: c.file, file: c.file },
|
|
4701
|
+
compiled
|
|
4702
|
+
);
|
|
4703
|
+
const scopes = listOption(o, "scopes");
|
|
4704
|
+
if (scopes.length === 0) continue;
|
|
4705
|
+
const patterns = compileScopes(scopes);
|
|
4706
|
+
const spans = c.importSpans ?? c.imports.map((source) => ({ source, line: 0 }));
|
|
4707
|
+
let sawScopedImport = false;
|
|
4708
|
+
const violations = [];
|
|
4709
|
+
for (const { source, line } of spans) {
|
|
4710
|
+
const target = resolveRepoLocalPath(source, c.file, ctx.project.kitAliases);
|
|
4711
|
+
if (target === void 0) continue;
|
|
4712
|
+
const boundary = privateScopeOf(target, patterns);
|
|
4713
|
+
if (boundary === void 0) continue;
|
|
4714
|
+
sawScopedImport = true;
|
|
4715
|
+
if (isInside(c.file, boundary)) continue;
|
|
4716
|
+
violations.push({ line, message: `${target} is private to ${boundary}` });
|
|
4717
|
+
}
|
|
4718
|
+
if (!sawScopedImport) continue;
|
|
4719
|
+
const visible = violations.filter(
|
|
4720
|
+
(v) => !(v.line > 0 && isSuppressed2(c, "architecture/private-scope-import", v.line))
|
|
4721
|
+
);
|
|
4722
|
+
if (visible.length === 0) {
|
|
4723
|
+
out.push({
|
|
4724
|
+
id: "architecture/private-scope-import",
|
|
4725
|
+
category: "architecture",
|
|
4726
|
+
severity: "info",
|
|
4727
|
+
detection: { presence: "own", value: "static" },
|
|
4728
|
+
route: c.file,
|
|
4729
|
+
message: "No private-scope imports",
|
|
4730
|
+
recommendation: recommendation7,
|
|
4731
|
+
docsUrl: docsUrl7
|
|
4732
|
+
});
|
|
4733
|
+
continue;
|
|
4734
|
+
}
|
|
4735
|
+
for (const v of visible) {
|
|
4736
|
+
out.push({
|
|
4737
|
+
id: "architecture/private-scope-import",
|
|
4738
|
+
category: "architecture",
|
|
4739
|
+
severity: "info",
|
|
4740
|
+
detection: { presence: "none", value: "absent" },
|
|
4741
|
+
route: c.file,
|
|
4742
|
+
location: c.file,
|
|
4743
|
+
...v.line > 0 ? { line: v.line } : {},
|
|
4744
|
+
message: v.message,
|
|
4745
|
+
recommendation: recommendation7,
|
|
4746
|
+
docsUrl: docsUrl7,
|
|
4747
|
+
fix: { ...architecturePrivateScopeImport.fix }
|
|
4748
|
+
});
|
|
4749
|
+
}
|
|
4750
|
+
}
|
|
4751
|
+
return out;
|
|
4752
|
+
}
|
|
4753
|
+
};
|
|
4754
|
+
|
|
4755
|
+
// src/rules/architecture/declarations.ts
|
|
4756
|
+
function ancestorDirs2(file) {
|
|
4757
|
+
const segments = file.split("/");
|
|
4758
|
+
const out = [];
|
|
4759
|
+
for (let i = 1; i < segments.length; i++) out.push(segments.slice(0, i).join("/"));
|
|
4760
|
+
return out;
|
|
4761
|
+
}
|
|
4762
|
+
function baseName(dir) {
|
|
4763
|
+
const cut = dir.lastIndexOf("/");
|
|
4764
|
+
return cut === -1 ? dir : dir.slice(cut + 1);
|
|
4765
|
+
}
|
|
4766
|
+
function childDirs(dirs) {
|
|
4767
|
+
const out = /* @__PURE__ */ new Map();
|
|
4768
|
+
for (const dir of dirs) {
|
|
4769
|
+
const cut = dir.lastIndexOf("/");
|
|
4770
|
+
if (cut === -1) continue;
|
|
4771
|
+
const parent = dir.slice(0, cut);
|
|
4772
|
+
let kids = out.get(parent);
|
|
4773
|
+
if (kids === void 0) out.set(parent, kids = []);
|
|
4774
|
+
kids.push(dir);
|
|
4775
|
+
}
|
|
4776
|
+
for (const kids of out.values()) kids.sort();
|
|
4777
|
+
return out;
|
|
4778
|
+
}
|
|
4779
|
+
function childFiles(files) {
|
|
4780
|
+
const out = /* @__PURE__ */ new Map();
|
|
4781
|
+
for (const file of files) {
|
|
4782
|
+
const cut = file.lastIndexOf("/");
|
|
4783
|
+
if (cut === -1) continue;
|
|
4784
|
+
const dir = file.slice(0, cut);
|
|
4785
|
+
let own = out.get(dir);
|
|
4786
|
+
if (own === void 0) out.set(dir, own = []);
|
|
4787
|
+
own.push(file.slice(cut + 1));
|
|
4788
|
+
}
|
|
4789
|
+
for (const own of out.values()) own.sort();
|
|
4790
|
+
return out;
|
|
4791
|
+
}
|
|
4792
|
+
function splitNames(value) {
|
|
4793
|
+
const out = [];
|
|
4794
|
+
for (const raw of value.split("|")) {
|
|
4795
|
+
const token = raw.trim();
|
|
4796
|
+
if (token.length > 0) out.push(token);
|
|
4797
|
+
}
|
|
4798
|
+
return out;
|
|
4799
|
+
}
|
|
4800
|
+
function keyShape(key) {
|
|
4801
|
+
const parts = key.split("/");
|
|
4802
|
+
let doubleStars = 0;
|
|
4803
|
+
for (const p of parts) if (p === "**") doubleStars++;
|
|
4804
|
+
return { segments: parts.length, doubleStars };
|
|
4805
|
+
}
|
|
4806
|
+
function createKeyCompiler() {
|
|
4807
|
+
const cache = /* @__PURE__ */ new Map();
|
|
4808
|
+
return (globs, bareGuard = false) => {
|
|
4809
|
+
const cacheKey = JSON.stringify([globs, bareGuard]);
|
|
4810
|
+
let entry = cache.get(cacheKey);
|
|
4811
|
+
if (entry === void 0) {
|
|
4812
|
+
entry = globs.map((key) => ({
|
|
4813
|
+
key,
|
|
4814
|
+
re: routeGlobToRegExp(key),
|
|
4815
|
+
...keyShape(key),
|
|
4816
|
+
...bareGuard && key.endsWith("/**") ? { barePrefixRe: routeGlobToRegExp(key.slice(0, -3)) } : {}
|
|
4817
|
+
}));
|
|
4818
|
+
cache.set(cacheKey, entry);
|
|
4819
|
+
}
|
|
4820
|
+
return entry;
|
|
4821
|
+
};
|
|
4822
|
+
}
|
|
4823
|
+
function matchKeys(dir, compiled) {
|
|
4824
|
+
const matched = [];
|
|
4825
|
+
let best;
|
|
4826
|
+
for (const entry of compiled) {
|
|
4827
|
+
if (entry.barePrefixRe?.test(dir)) continue;
|
|
4828
|
+
if (!entry.re.test(dir)) continue;
|
|
4829
|
+
matched.push(entry.key);
|
|
4830
|
+
if (best === void 0 || moreSpecificShaped(entry, best)) best = entry;
|
|
4831
|
+
}
|
|
4832
|
+
return best === void 0 ? { matched } : { matched, best: best.key };
|
|
4833
|
+
}
|
|
4834
|
+
function moreSpecificShaped(a, b) {
|
|
4835
|
+
if (a.segments !== b.segments) return a.segments > b.segments;
|
|
4836
|
+
if (a.doubleStars !== b.doubleStars) return a.doubleStars < b.doubleStars;
|
|
4837
|
+
if (a.key.length !== b.key.length) return a.key.length > b.key.length;
|
|
4838
|
+
return a.key < b.key;
|
|
4839
|
+
}
|
|
4840
|
+
function moreSpecificGlob(a, b) {
|
|
4841
|
+
return moreSpecificShaped({ key: a, ...keyShape(a) }, { key: b, ...keyShape(b) });
|
|
4842
|
+
}
|
|
4843
|
+
function reportAt(dir, files) {
|
|
4844
|
+
const prefix = `${dir}/`;
|
|
4845
|
+
const under = files.filter((f) => f.startsWith(prefix)).sort();
|
|
4846
|
+
return under.find((f) => !f.slice(prefix.length).includes("/")) ?? under[0];
|
|
4847
|
+
}
|
|
4848
|
+
function isExcluded(dir, ancestors, excluded) {
|
|
4849
|
+
return excluded.some(({ re }) => re.test(dir) || ancestors.some((a) => re.test(a)));
|
|
4850
|
+
}
|
|
4851
|
+
function keysMatchingAny(keys, dirs, compile) {
|
|
4852
|
+
const hit = /* @__PURE__ */ new Set();
|
|
4853
|
+
if (keys.length === 0 || dirs.length === 0) return hit;
|
|
4854
|
+
for (const { key, re, barePrefixRe } of compile(keys, true)) {
|
|
4855
|
+
if (dirs.some((d) => !barePrefixRe?.test(d) && re.test(d))) hit.add(key);
|
|
4856
|
+
}
|
|
4857
|
+
return hit;
|
|
4858
|
+
}
|
|
4859
|
+
function classifyUnusedKeys(unused, excludedDirs, compile) {
|
|
4860
|
+
const out = /* @__PURE__ */ new Map();
|
|
4861
|
+
if (unused.length === 0) return out;
|
|
4862
|
+
const shadowed = keysMatchingAny(unused, excludedDirs, compile);
|
|
4863
|
+
for (const key of unused) out.set(key, shadowed.has(key) ? "only-excluded" : "no-match");
|
|
4864
|
+
return out;
|
|
4865
|
+
}
|
|
4866
|
+
|
|
4867
|
+
// src/rules/architecture/unit-entry-file.ts
|
|
4868
|
+
var ID4 = "architecture/unit-entry-file";
|
|
4869
|
+
var docsUrl8 = docsUrlFor(ID4);
|
|
4870
|
+
var recommendation8 = "Give every declared unit directory a file named after it, or stop declaring that directory a unit.";
|
|
4871
|
+
var OPTIONS3 = {
|
|
4872
|
+
units: { kind: "string-map", default: {} },
|
|
4873
|
+
pascalCaseUnits: { kind: "string-map", default: {} },
|
|
4874
|
+
exclude: { kind: "string-list", default: [] }
|
|
4875
|
+
};
|
|
4876
|
+
function isPascalCase(name) {
|
|
4877
|
+
const c = name.charCodeAt(0);
|
|
4878
|
+
return c >= 65 && c <= 90;
|
|
4879
|
+
}
|
|
4880
|
+
var architectureUnitEntryFile = {
|
|
4881
|
+
id: ID4,
|
|
4882
|
+
title: "Unit entry file",
|
|
4883
|
+
category: "architecture",
|
|
4884
|
+
severity: "info",
|
|
4885
|
+
scope: "component",
|
|
4886
|
+
rationale: "A directory named after a unit but missing that unit's entry file is either an incomplete unit or a grouping wearing the wrong name; either way the tree no longer says what it means, and tooling that resolves by convention starts guessing.",
|
|
4887
|
+
fix: {
|
|
4888
|
+
description: "Make the directory and its entry file agree \u2014 add the entry file, or stop declaring this directory a unit."
|
|
4889
|
+
},
|
|
4890
|
+
options: OPTIONS3,
|
|
4891
|
+
async check(ctx) {
|
|
4892
|
+
const files = ctx.sourceFiles;
|
|
4893
|
+
if (files === void 0) return [];
|
|
4894
|
+
if (!isMentionedAnywhere(ctx.config, ID4)) return [];
|
|
4895
|
+
const compiledOverrides = compileOverrides(ctx.config);
|
|
4896
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
4897
|
+
for (const f of files) for (const d of ancestorDirs2(f)) dirs.add(d);
|
|
4898
|
+
const fileSet = new Set(files);
|
|
4899
|
+
const compile = createKeyCompiler();
|
|
4900
|
+
const out = [];
|
|
4901
|
+
const globalOptions = resolveRuleOptions(ID4, OPTIONS3, ctx.config);
|
|
4902
|
+
const globalKeys = /* @__PURE__ */ new Set([
|
|
4903
|
+
...Object.keys(mapOption(globalOptions, "units")),
|
|
4904
|
+
...Object.keys(mapOption(globalOptions, "pascalCaseUnits"))
|
|
4905
|
+
]);
|
|
4906
|
+
const usedKeys = /* @__PURE__ */ new Set();
|
|
4907
|
+
const excludedDirs = [];
|
|
4908
|
+
const matchedSurviving = /* @__PURE__ */ new Set();
|
|
4909
|
+
for (const dir of [...dirs].sort()) {
|
|
4910
|
+
const o = resolveRuleOptions(ID4, OPTIONS3, ctx.config, { route: dir, file: dir }, compiledOverrides);
|
|
4911
|
+
const units = mapOption(o, "units");
|
|
4912
|
+
const pascalUnits = mapOption(o, "pascalCaseUnits");
|
|
4913
|
+
if (Object.keys(units).length === 0 && Object.keys(pascalUnits).length === 0) continue;
|
|
4914
|
+
const excluded = compile(listOption(o, "exclude"));
|
|
4915
|
+
const ancestors = ancestorDirs2(dir);
|
|
4916
|
+
if (isExcluded(dir, ancestors, excluded)) {
|
|
4917
|
+
excludedDirs.push(dir);
|
|
4918
|
+
continue;
|
|
4919
|
+
}
|
|
4920
|
+
const byPath = matchKeys(dir, compile(Object.keys(units), true));
|
|
4921
|
+
const byCasing = matchKeys(dir, compile(Object.keys(pascalUnits), true));
|
|
4922
|
+
for (const k of byPath.matched) if (globalKeys.has(k)) usedKeys.add(k);
|
|
4923
|
+
for (const k of byPath.matched) if (globalKeys.has(k)) matchedSurviving.add(k);
|
|
4924
|
+
for (const k of byCasing.matched) if (globalKeys.has(k)) matchedSurviving.add(k);
|
|
4925
|
+
if (isPascalCase(baseName(dir))) {
|
|
4926
|
+
for (const k of byCasing.matched) if (globalKeys.has(k)) usedKeys.add(k);
|
|
4927
|
+
}
|
|
4928
|
+
let ext = byPath.best === void 0 ? void 0 : units[byPath.best];
|
|
4929
|
+
const viaUnits = ext !== void 0;
|
|
4930
|
+
if (ext === void 0 && isPascalCase(baseName(dir))) {
|
|
4931
|
+
ext = byCasing.best === void 0 ? void 0 : pascalUnits[byCasing.best];
|
|
4932
|
+
}
|
|
4933
|
+
if (ext === void 0) continue;
|
|
4934
|
+
const expected = `${dir}/${baseName(dir)}${ext}`;
|
|
4935
|
+
if (fileSet.has(expected)) {
|
|
4936
|
+
out.push({
|
|
4937
|
+
id: ID4,
|
|
4938
|
+
category: "architecture",
|
|
4939
|
+
severity: "info",
|
|
4940
|
+
detection: { presence: "own", value: "static" },
|
|
4941
|
+
route: expected,
|
|
4942
|
+
message: "Unit entry file",
|
|
4943
|
+
recommendation: recommendation8,
|
|
4944
|
+
docsUrl: docsUrl8
|
|
4945
|
+
});
|
|
4946
|
+
continue;
|
|
4947
|
+
}
|
|
4948
|
+
const at = reportAt(dir, files);
|
|
4949
|
+
if (at === void 0) continue;
|
|
4950
|
+
out.push({
|
|
4951
|
+
id: ID4,
|
|
4952
|
+
category: "architecture",
|
|
4953
|
+
severity: "info",
|
|
4954
|
+
detection: { presence: "none", value: "absent" },
|
|
4955
|
+
route: dir,
|
|
4956
|
+
location: at,
|
|
4957
|
+
message: `${dir} declares a unit but has no ${expected}`,
|
|
4958
|
+
recommendation: recommendation8,
|
|
4959
|
+
docsUrl: docsUrl8,
|
|
4960
|
+
// Which declaration matched decides the wording: a `units` match like functions/getFoo/
|
|
4961
|
+
// is already camelCase, so telling its author to rename it would be nonsense.
|
|
4962
|
+
fix: {
|
|
4963
|
+
description: viaUnits ? `Add ${baseName(dir)}${ext} to this directory, or remove it from the units declaration.` : `Add the same-named entry file, or rename the directory to camelCase if it is a grouping.`
|
|
4964
|
+
}
|
|
4965
|
+
});
|
|
4966
|
+
}
|
|
4967
|
+
const inertKeys = [...globalKeys].filter((key) => !usedKeys.has(key)).sort();
|
|
4968
|
+
if (inertKeys.length > 0) {
|
|
4969
|
+
const shadowed = inertKeys.filter((k) => !matchedSurviving.has(k));
|
|
4970
|
+
const reasons = classifyUnusedKeys(shadowed, excludedDirs, compile);
|
|
4971
|
+
const why = (k) => reasons.get(k) === "only-excluded" ? "matched only excluded directories" : "matched no directory";
|
|
4972
|
+
const message = inertKeys.length === 1 ? `The declaration '${inertKeys[0]}' ${why(inertKeys[0])}, so it checks nothing.` : `These declarations check nothing: ${inertKeys.map((k) => `'${k}' (${why(k)})`).join(", ")}.`;
|
|
4973
|
+
out.push({
|
|
4974
|
+
id: ID4,
|
|
4975
|
+
category: "architecture",
|
|
4976
|
+
severity: "info",
|
|
4977
|
+
detection: { presence: "none", value: "absent" },
|
|
4978
|
+
message,
|
|
4979
|
+
recommendation: "Correct the glob, or remove the declaration.",
|
|
4980
|
+
docsUrl: docsUrl8
|
|
4981
|
+
});
|
|
4982
|
+
}
|
|
4983
|
+
return out;
|
|
4984
|
+
}
|
|
4985
|
+
};
|
|
4986
|
+
|
|
4987
|
+
// src/rules/architecture/casing.ts
|
|
4988
|
+
var CASINGS = {
|
|
4989
|
+
camelCase: /^[a-z][a-zA-Z0-9]*$/,
|
|
4990
|
+
PascalCase: /^[A-Z][a-zA-Z0-9]*$/,
|
|
4991
|
+
"kebab-case": /^[a-z0-9]+(-[a-z0-9]+)*$/,
|
|
4992
|
+
snake_case: /^[a-z0-9]+(_[a-z0-9]+)*$/
|
|
4993
|
+
};
|
|
4994
|
+
function parseCasings(value) {
|
|
4995
|
+
const known = [];
|
|
4996
|
+
const unknown = [];
|
|
4997
|
+
for (const name of splitNames(value)) {
|
|
4998
|
+
if (Object.hasOwn(CASINGS, name)) known.push(name);
|
|
4999
|
+
else unknown.push(name);
|
|
5000
|
+
}
|
|
5001
|
+
return { known, unknown };
|
|
5002
|
+
}
|
|
5003
|
+
function decodeSegment(name) {
|
|
5004
|
+
let inner = name;
|
|
5005
|
+
if (inner.length > 2 && inner.startsWith("(") && inner.endsWith(")")) inner = inner.slice(1, -1);
|
|
5006
|
+
else if (inner.length > 4 && inner.startsWith("[[") && inner.endsWith("]]")) inner = inner.slice(2, -2);
|
|
5007
|
+
else if (inner.length > 2 && inner.startsWith("[") && inner.endsWith("]")) inner = inner.slice(1, -1);
|
|
5008
|
+
if (inner.startsWith("...")) inner = inner.slice(3);
|
|
5009
|
+
const eq = inner.indexOf("=");
|
|
5010
|
+
if (eq !== -1) inner = inner.slice(0, eq);
|
|
5011
|
+
if (inner.length === 0 || /[[\]()]/.test(inner)) return void 0;
|
|
5012
|
+
return inner;
|
|
5013
|
+
}
|
|
5014
|
+
function satisfiesCasing(name, allowed) {
|
|
5015
|
+
if (!/[a-zA-Z]/.test(name)) return true;
|
|
5016
|
+
return allowed.some((c) => Object.hasOwn(CASINGS, c) && CASINGS[c].test(name));
|
|
5017
|
+
}
|
|
5018
|
+
|
|
5019
|
+
// src/rules/architecture/directory-naming.ts
|
|
5020
|
+
var ID5 = "architecture/directory-naming";
|
|
5021
|
+
var docsUrl9 = docsUrlFor(ID5);
|
|
5022
|
+
var recommendation9 = "Name each directory in the casing its location declares, or narrow the declaration.";
|
|
5023
|
+
var OPTIONS4 = {
|
|
5024
|
+
directories: { kind: "string-map", default: {} },
|
|
5025
|
+
exclude: { kind: "string-list", default: [] }
|
|
5026
|
+
};
|
|
5027
|
+
var architectureDirectoryNaming = {
|
|
5028
|
+
id: ID5,
|
|
5029
|
+
title: "Directory naming",
|
|
5030
|
+
category: "architecture",
|
|
5031
|
+
severity: "info",
|
|
5032
|
+
scope: "component",
|
|
5033
|
+
rationale: "A directory whose name breaks the convention its location declares stops carrying the meaning the convention gave it, and every reader \u2014 human or agent \u2014 has to open the directory to learn what it is.",
|
|
5034
|
+
fix: {
|
|
5035
|
+
description: "Rename the directory to the declared casing, or narrow the declaration that governs it."
|
|
5036
|
+
},
|
|
5037
|
+
options: OPTIONS4,
|
|
5038
|
+
async check(ctx) {
|
|
5039
|
+
const files = ctx.sourceFiles;
|
|
5040
|
+
if (files === void 0) return [];
|
|
5041
|
+
if (!isMentionedAnywhere(ctx.config, ID5)) return [];
|
|
5042
|
+
const compiledOverrides = compileOverrides(ctx.config);
|
|
5043
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
5044
|
+
for (const f of files) for (const d of ancestorDirs2(f)) dirs.add(d);
|
|
5045
|
+
const compile = createKeyCompiler();
|
|
5046
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
5047
|
+
const casingsOf = (value) => {
|
|
5048
|
+
let p = parsed.get(value);
|
|
5049
|
+
if (p === void 0) parsed.set(value, p = parseCasings(value));
|
|
5050
|
+
return p;
|
|
5051
|
+
};
|
|
5052
|
+
const out = [];
|
|
5053
|
+
const globalOptions = resolveRuleOptions(ID5, OPTIONS4, ctx.config);
|
|
5054
|
+
const globalMap = mapOption(globalOptions, "directories");
|
|
5055
|
+
const globalKeys = new Set(Object.keys(globalMap));
|
|
5056
|
+
const usedKeys = /* @__PURE__ */ new Set();
|
|
5057
|
+
const excludedDirs = [];
|
|
5058
|
+
for (const dir of [...dirs].sort()) {
|
|
5059
|
+
const o = resolveRuleOptions(ID5, OPTIONS4, ctx.config, { route: dir, file: dir }, compiledOverrides);
|
|
5060
|
+
const declared = mapOption(o, "directories");
|
|
5061
|
+
if (Object.keys(declared).length === 0) continue;
|
|
5062
|
+
const excluded = compile(listOption(o, "exclude"));
|
|
5063
|
+
if (isExcluded(dir, ancestorDirs2(dir), excluded)) {
|
|
5064
|
+
excludedDirs.push(dir);
|
|
5065
|
+
continue;
|
|
5066
|
+
}
|
|
5067
|
+
const live = Object.keys(declared).filter((k) => casingsOf(declared[k]).known.length > 0);
|
|
5068
|
+
const m = matchKeys(dir, compile(live, true));
|
|
5069
|
+
for (const k of m.matched) if (globalKeys.has(k)) usedKeys.add(k);
|
|
5070
|
+
if (m.best === void 0) continue;
|
|
5071
|
+
const decoded = decodeSegment(baseName(dir));
|
|
5072
|
+
if (decoded === void 0) continue;
|
|
5073
|
+
const allowed = casingsOf(declared[m.best]).known;
|
|
5074
|
+
if (satisfiesCasing(decoded, allowed)) continue;
|
|
5075
|
+
const at = reportAt(dir, files);
|
|
5076
|
+
if (at === void 0) continue;
|
|
5077
|
+
out.push({
|
|
5078
|
+
id: ID5,
|
|
5079
|
+
category: "architecture",
|
|
5080
|
+
severity: "info",
|
|
5081
|
+
detection: { presence: "none", value: "absent" },
|
|
5082
|
+
route: dir,
|
|
5083
|
+
location: at,
|
|
5084
|
+
message: `${dir} must be ${allowed.join(" or ")}.`,
|
|
5085
|
+
recommendation: recommendation9,
|
|
5086
|
+
docsUrl: docsUrl9,
|
|
5087
|
+
fix: { description: "Rename the directory, or narrow the declaration that governs it." }
|
|
5088
|
+
});
|
|
5089
|
+
}
|
|
5090
|
+
const notes = /* @__PURE__ */ new Map();
|
|
5091
|
+
for (const key of globalKeys) {
|
|
5092
|
+
const { known, unknown } = casingsOf(globalMap[key]);
|
|
5093
|
+
if (known.length > 0 && unknown.length === 0) continue;
|
|
5094
|
+
const names = unknown.map((u) => `'${u}'`).join(", ");
|
|
5095
|
+
notes.set(
|
|
5096
|
+
key,
|
|
5097
|
+
known.length === 0 ? unknown.length === 0 ? "the value names no casing at all, so it checks nothing" : `unknown casing name ${names}, so it checks nothing` : `unknown casing name ${names}; the rest of the value still applies`
|
|
5098
|
+
);
|
|
5099
|
+
}
|
|
5100
|
+
const unclassified = [...globalKeys].filter(
|
|
5101
|
+
(key) => !notes.has(key) && !usedKeys.has(key) && casingsOf(globalMap[key]).known.length > 0
|
|
5102
|
+
);
|
|
5103
|
+
const reasons = classifyUnusedKeys(unclassified, excludedDirs, compile);
|
|
5104
|
+
for (const [key, reason] of reasons) {
|
|
5105
|
+
notes.set(key, reason === "only-excluded" ? "matched only excluded directories" : "matched no directory");
|
|
5106
|
+
}
|
|
5107
|
+
const reported = [...notes.keys()].sort();
|
|
5108
|
+
if (reported.length > 0) {
|
|
5109
|
+
const message = reported.length === 1 ? `The declaration '${reported[0]}' does not check what it says: ${notes.get(reported[0])}.` : `These declarations do not check what they say: ${reported.map((k) => `'${k}' (${notes.get(k)})`).join(", ")}.`;
|
|
5110
|
+
out.push({
|
|
5111
|
+
id: ID5,
|
|
5112
|
+
category: "architecture",
|
|
5113
|
+
severity: "info",
|
|
5114
|
+
detection: { presence: "none", value: "absent" },
|
|
5115
|
+
message,
|
|
5116
|
+
recommendation: "Correct the glob or the casing name, or remove the declaration.",
|
|
5117
|
+
docsUrl: docsUrl9
|
|
5118
|
+
});
|
|
5119
|
+
}
|
|
5120
|
+
return out;
|
|
5121
|
+
}
|
|
5122
|
+
};
|
|
5123
|
+
|
|
5124
|
+
// src/rules/architecture/reserved-directory-names.ts
|
|
5125
|
+
var ID6 = "architecture/reserved-directory-names";
|
|
5126
|
+
var docsUrl10 = docsUrlFor(ID6);
|
|
5127
|
+
var recommendation10 = "Use one of the names this location declares, or add the new name to the declaration.";
|
|
5128
|
+
var OPTIONS5 = {
|
|
5129
|
+
scopes: { kind: "string-map", default: {} },
|
|
5130
|
+
unitScopes: { kind: "string-map", default: {} },
|
|
5131
|
+
exclude: { kind: "string-list", default: [] }
|
|
5132
|
+
};
|
|
5133
|
+
function stem(file) {
|
|
5134
|
+
const dot = file.indexOf(".");
|
|
5135
|
+
return dot === -1 ? file : file.slice(0, dot);
|
|
5136
|
+
}
|
|
5137
|
+
function isUnitDir(dir, filesIn) {
|
|
5138
|
+
const name = baseName(dir);
|
|
5139
|
+
const first = name.charCodeAt(0);
|
|
5140
|
+
if (!(first >= 65 && first <= 90)) return false;
|
|
5141
|
+
const own = filesIn.get(dir);
|
|
5142
|
+
return own !== void 0 && own.some((f) => stem(f) === name);
|
|
5143
|
+
}
|
|
5144
|
+
var architectureReservedDirectoryNames = {
|
|
5145
|
+
id: ID6,
|
|
5146
|
+
title: "Reserved directory names",
|
|
5147
|
+
category: "architecture",
|
|
5148
|
+
severity: "info",
|
|
5149
|
+
scope: "component",
|
|
5150
|
+
rationale: "A closed set of directory names is only worth writing down if it stays closed: one directory outside it and the table stops describing the tree, so every reader has to open a directory to learn what it holds.",
|
|
5151
|
+
fix: {
|
|
5152
|
+
description: "Rename the directory to a declared name, move it under one of them, or add its name to the declaration."
|
|
5153
|
+
},
|
|
5154
|
+
options: OPTIONS5,
|
|
5155
|
+
async check(ctx) {
|
|
5156
|
+
const files = ctx.sourceFiles;
|
|
5157
|
+
if (files === void 0) return [];
|
|
5158
|
+
if (!isMentionedAnywhere(ctx.config, ID6)) return [];
|
|
5159
|
+
const compiledOverrides = compileOverrides(ctx.config);
|
|
5160
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
5161
|
+
for (const f of files) for (const d of ancestorDirs2(f)) dirs.add(d);
|
|
5162
|
+
const kids = childDirs(dirs);
|
|
5163
|
+
const filesIn = childFiles(files);
|
|
5164
|
+
const compile = createKeyCompiler();
|
|
5165
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
5166
|
+
const namesOf = (value) => {
|
|
5167
|
+
let n = parsed.get(value);
|
|
5168
|
+
if (n === void 0) parsed.set(value, n = splitNames(value));
|
|
5169
|
+
return n;
|
|
5170
|
+
};
|
|
5171
|
+
const out = [];
|
|
5172
|
+
const globalOptions = resolveRuleOptions(ID6, OPTIONS5, ctx.config);
|
|
5173
|
+
const globalScopes = mapOption(globalOptions, "scopes");
|
|
5174
|
+
const globalUnits = mapOption(globalOptions, "unitScopes");
|
|
5175
|
+
const globalKeys = /* @__PURE__ */ new Set([...Object.keys(globalScopes), ...Object.keys(globalUnits)]);
|
|
5176
|
+
const usedKeys = /* @__PURE__ */ new Set();
|
|
5177
|
+
const excludedDirs = [];
|
|
5178
|
+
const nonUnitDirs = [];
|
|
5179
|
+
const collisions = /* @__PURE__ */ new Set();
|
|
5180
|
+
const noteCollisions = (scopesMap, unitMap) => {
|
|
5181
|
+
for (const key of Object.keys(scopesMap)) {
|
|
5182
|
+
if (!Object.hasOwn(unitMap, key)) continue;
|
|
5183
|
+
if (namesOf(scopesMap[key]).length === 0) continue;
|
|
5184
|
+
if (namesOf(unitMap[key]).length === 0) continue;
|
|
5185
|
+
collisions.add(key);
|
|
5186
|
+
}
|
|
5187
|
+
};
|
|
5188
|
+
noteCollisions(globalScopes, globalUnits);
|
|
5189
|
+
for (const dir of [...dirs].sort()) {
|
|
5190
|
+
const o = resolveRuleOptions(ID6, OPTIONS5, ctx.config, { route: dir, file: dir }, compiledOverrides);
|
|
5191
|
+
const scopes = mapOption(o, "scopes");
|
|
5192
|
+
const unitScopes = mapOption(o, "unitScopes");
|
|
5193
|
+
if (Object.keys(scopes).length === 0 && Object.keys(unitScopes).length === 0) continue;
|
|
5194
|
+
noteCollisions(scopes, unitScopes);
|
|
5195
|
+
const excluded = compile(listOption(o, "exclude"));
|
|
5196
|
+
if (isExcluded(dir, ancestorDirs2(dir), excluded)) {
|
|
5197
|
+
excludedDirs.push(dir);
|
|
5198
|
+
continue;
|
|
5199
|
+
}
|
|
5200
|
+
const liveScopes = Object.keys(scopes).filter((k) => namesOf(scopes[k]).length > 0);
|
|
5201
|
+
const isUnit = isUnitDir(dir, filesIn);
|
|
5202
|
+
const liveUnits = isUnit ? Object.keys(unitScopes).filter((k) => namesOf(unitScopes[k]).length > 0) : [];
|
|
5203
|
+
if (!isUnit) nonUnitDirs.push(dir);
|
|
5204
|
+
const byPosition = matchKeys(dir, compile(liveScopes, true));
|
|
5205
|
+
const byUnit = matchKeys(dir, compile(liveUnits, true));
|
|
5206
|
+
for (const k of byPosition.matched) if (globalKeys.has(k)) usedKeys.add(k);
|
|
5207
|
+
for (const k of byUnit.matched) if (globalKeys.has(k)) usedKeys.add(k);
|
|
5208
|
+
let governing;
|
|
5209
|
+
if (byPosition.best !== void 0 && byUnit.best !== void 0) {
|
|
5210
|
+
governing = moreSpecificGlob(byUnit.best, byPosition.best) ? namesOf(unitScopes[byUnit.best]) : namesOf(scopes[byPosition.best]);
|
|
5211
|
+
} else if (byPosition.best !== void 0) {
|
|
5212
|
+
governing = namesOf(scopes[byPosition.best]);
|
|
5213
|
+
} else if (byUnit.best !== void 0) {
|
|
5214
|
+
governing = namesOf(unitScopes[byUnit.best]);
|
|
5215
|
+
}
|
|
5216
|
+
if (governing === void 0) continue;
|
|
5217
|
+
const allowed = new Set(governing);
|
|
5218
|
+
for (const child of kids.get(dir) ?? []) {
|
|
5219
|
+
if (allowed.has(baseName(child))) continue;
|
|
5220
|
+
if (isExcluded(child, ancestorDirs2(child), excluded)) continue;
|
|
5221
|
+
const childOptions = resolveRuleOptions(
|
|
5222
|
+
ID6,
|
|
5223
|
+
OPTIONS5,
|
|
5224
|
+
ctx.config,
|
|
5225
|
+
{ route: child, file: child },
|
|
5226
|
+
compiledOverrides
|
|
5227
|
+
);
|
|
5228
|
+
if (isExcluded(child, ancestorDirs2(child), compile(listOption(childOptions, "exclude")))) continue;
|
|
5229
|
+
const at = reportAt(child, files);
|
|
5230
|
+
if (at === void 0) continue;
|
|
5231
|
+
out.push({
|
|
5232
|
+
id: ID6,
|
|
5233
|
+
category: "architecture",
|
|
5234
|
+
severity: "info",
|
|
5235
|
+
detection: { presence: "none", value: "absent" },
|
|
5236
|
+
route: child,
|
|
5237
|
+
location: at,
|
|
5238
|
+
message: `${child} is not one of the names declared here: ${governing.join(", ")}.`,
|
|
5239
|
+
recommendation: recommendation10,
|
|
5240
|
+
docsUrl: docsUrl10,
|
|
5241
|
+
fix: {
|
|
5242
|
+
description: "Rename it to a declared name, move it under one of them, or add its name to the declaration."
|
|
5243
|
+
}
|
|
5244
|
+
});
|
|
5245
|
+
}
|
|
5246
|
+
}
|
|
5247
|
+
const notes = /* @__PURE__ */ new Map();
|
|
5248
|
+
for (const key of collisions) {
|
|
5249
|
+
notes.set(key, "declared in both scopes and unitScopes, so the scopes entry wins wherever both apply");
|
|
5250
|
+
}
|
|
5251
|
+
for (const key of globalKeys) {
|
|
5252
|
+
if (notes.has(key)) continue;
|
|
5253
|
+
const scopesEmpty = Object.hasOwn(globalScopes, key) && namesOf(globalScopes[key]).length === 0;
|
|
5254
|
+
const unitsEmpty = Object.hasOwn(globalUnits, key) && namesOf(globalUnits[key]).length === 0;
|
|
5255
|
+
if (scopesEmpty || unitsEmpty) {
|
|
5256
|
+
notes.set(key, "names no directory name at all");
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5259
|
+
const unused = [...globalKeys].filter((k) => !notes.has(k) && !usedKeys.has(k));
|
|
5260
|
+
const unitOnly = unused.filter((k) => Object.hasOwn(globalUnits, k) && !Object.hasOwn(globalScopes, k));
|
|
5261
|
+
for (const key of keysMatchingAny(unitOnly, nonUnitDirs, compile)) {
|
|
5262
|
+
notes.set(key, "matched directories but never a unit");
|
|
5263
|
+
}
|
|
5264
|
+
for (const [key, reason] of classifyUnusedKeys(
|
|
5265
|
+
unused.filter((k) => !notes.has(k)),
|
|
5266
|
+
excludedDirs,
|
|
5267
|
+
compile
|
|
5268
|
+
)) {
|
|
5269
|
+
notes.set(key, reason === "only-excluded" ? "matched only excluded directories" : "matched no directory");
|
|
5270
|
+
}
|
|
5271
|
+
const reported = [...notes.keys()].sort();
|
|
5272
|
+
if (reported.length > 0) {
|
|
5273
|
+
const message = reported.length === 1 ? `The declaration '${reported[0]}' does not check what it says: ${notes.get(reported[0])}.` : `These declarations do not check what they say: ${reported.map((k) => `'${k}' (${notes.get(k)})`).join(", ")}.`;
|
|
5274
|
+
out.push({
|
|
5275
|
+
id: ID6,
|
|
5276
|
+
category: "architecture",
|
|
5277
|
+
severity: "info",
|
|
5278
|
+
detection: { presence: "none", value: "absent" },
|
|
5279
|
+
message,
|
|
5280
|
+
recommendation: "Correct the glob or the names, or remove the declaration.",
|
|
5281
|
+
docsUrl: docsUrl10
|
|
5282
|
+
});
|
|
5283
|
+
}
|
|
5284
|
+
return out;
|
|
5285
|
+
}
|
|
5286
|
+
};
|
|
5287
|
+
|
|
5288
|
+
// src/rules/architecture/route-component-import.ts
|
|
5289
|
+
var ID7 = "architecture/route-component-import";
|
|
5290
|
+
var EXEMPT_IMPORTERS = ["**/*.stories.svelte", "**/*.test.svelte", "**/*.spec.svelte"];
|
|
5291
|
+
var ROUTES_DIR = "src/routes/";
|
|
5292
|
+
var ROUTE_ENTRY = /^\+(page|layout)(@.*)?\.svelte$/;
|
|
5293
|
+
function isRouteEntry(path) {
|
|
5294
|
+
if (!path.startsWith(ROUTES_DIR)) return false;
|
|
5295
|
+
const base = path.slice(path.lastIndexOf("/") + 1);
|
|
5296
|
+
return base === "+error.svelte" || ROUTE_ENTRY.test(base);
|
|
5297
|
+
}
|
|
5298
|
+
function routeEntryImports(c, ctx) {
|
|
5299
|
+
const out = [];
|
|
5300
|
+
for (const { source, line, type } of c.importSpans ?? []) {
|
|
5301
|
+
if (type) continue;
|
|
5302
|
+
const target = resolveRepoLocalPath(source, c.file, ctx.project.kitAliases);
|
|
5303
|
+
if (target !== void 0 && isRouteEntry(target)) out.push({ line, target });
|
|
5304
|
+
}
|
|
5305
|
+
return out;
|
|
5306
|
+
}
|
|
5307
|
+
var routeEntryImportsCache = /* @__PURE__ */ new WeakMap();
|
|
5308
|
+
function cachedRouteEntryImports(c, ctx) {
|
|
5309
|
+
const cached = routeEntryImportsCache.get(c);
|
|
5310
|
+
if (cached !== void 0 && cached.aliases === ctx.project.kitAliases) return cached.result;
|
|
5311
|
+
const result = routeEntryImports(c, ctx);
|
|
5312
|
+
routeEntryImportsCache.set(c, { aliases: ctx.project.kitAliases, result });
|
|
5313
|
+
return result;
|
|
5314
|
+
}
|
|
5315
|
+
var architectureRouteComponentImport = componentRule({
|
|
5316
|
+
id: ID7,
|
|
5317
|
+
title: "Route component import",
|
|
5318
|
+
category: "architecture",
|
|
5319
|
+
severity: "info",
|
|
5320
|
+
label: "Route component imports",
|
|
5321
|
+
options: { exemptImporters: { kind: "string-list", default: EXEMPT_IMPORTERS } },
|
|
5322
|
+
recommendation: "Extract the shared markup into a component under $lib and import that from both places, leaving the route entry to SvelteKit.",
|
|
5323
|
+
rationale: "A route entry is written on the assumption that SvelteKit renders it: Kit hands a page its data and params, and an error page its page.error and page.status. Imported from somewhere else it receives none of that and renders against nothing, or against the importing page data standing in for its own.",
|
|
5324
|
+
// Signal present = this file imports a route entry, exempt or not. An exempt file therefore
|
|
5325
|
+
// reaches `bad` and earns a PASS, rather than being called signal-free.
|
|
5326
|
+
// `_o`: this rule's option is read only in `bad`, but `ctx` is the third parameter, so the
|
|
5327
|
+
// second cannot simply be omitted the way every other component rule omits what it does not use.
|
|
5328
|
+
applies: (c, _o, ctx) => cachedRouteEntryImports(c, ctx).length > 0,
|
|
5329
|
+
bad: (c, o, ctx) => {
|
|
5330
|
+
const exempt = listOption(o, "exemptImporters").map(routeGlobToRegExp);
|
|
5331
|
+
if (exempt.some((re) => re.test(c.file))) return [];
|
|
5332
|
+
return cachedRouteEntryImports(c, ctx).map(({ line, target }) => ({
|
|
5333
|
+
line,
|
|
5334
|
+
message: `${target} is a SvelteKit route entry \u2014 imported here it renders without the data Kit would give it`
|
|
5335
|
+
}));
|
|
5336
|
+
}
|
|
4002
5337
|
});
|
|
4003
5338
|
|
|
4004
5339
|
// src/rules/perf/heavy-import.ts
|
|
@@ -4014,18 +5349,20 @@ var performanceHeavyImport = componentRule({
|
|
|
4014
5349
|
label: "No heavy imports",
|
|
4015
5350
|
recommendation: "Import a submodule or switch to a lighter, tree-shakeable alternative.",
|
|
4016
5351
|
rationale: "Importing a large, non-tree-shakeable package pulls its whole weight into the bundle even when only a fraction is used, slowing load.",
|
|
5352
|
+
options: { packages: { kind: "string-map", default: HEAVY_PACKAGES } },
|
|
4017
5353
|
// ComponentFacts is a public @svelte-vitals/core export — an external caller compiled
|
|
4018
5354
|
// against an older version may still construct one without importSpans. Fall back to the
|
|
4019
5355
|
// line-less `imports` (line: 0, the pre-fix behavior) instead of crashing on `undefined`.
|
|
4020
5356
|
applies: (c) => (c.importSpans ?? c.imports).length > 0,
|
|
4021
|
-
bad: (c) => {
|
|
5357
|
+
bad: (c, o) => {
|
|
5358
|
+
const packages = mapOption(o, "packages");
|
|
4022
5359
|
const seen = /* @__PURE__ */ new Set();
|
|
4023
5360
|
const out = [];
|
|
4024
5361
|
const spans = c.importSpans ?? c.imports.map((source) => ({ source, line: 0 }));
|
|
4025
|
-
for (const { source: src, line } of spans) {
|
|
4026
|
-
if (!Object.hasOwn(
|
|
5362
|
+
for (const { source: src, line, type } of spans) {
|
|
5363
|
+
if (type || !Object.hasOwn(packages, src) || seen.has(src)) continue;
|
|
4027
5364
|
seen.add(src);
|
|
4028
|
-
out.push({ line, message: `Heavy import "${src}" \u2014 ${
|
|
5365
|
+
out.push({ line, message: `Heavy import "${src}" \u2014 ${packages[src]}` });
|
|
4029
5366
|
}
|
|
4030
5367
|
return out;
|
|
4031
5368
|
}
|
|
@@ -4055,13 +5392,13 @@ var performanceNamespaceImport = componentRule({
|
|
|
4055
5392
|
});
|
|
4056
5393
|
|
|
4057
5394
|
// src/rules/perf/minify-disabled.ts
|
|
4058
|
-
var
|
|
5395
|
+
var PENALIZED7 = { presence: "none", value: "absent" };
|
|
4059
5396
|
var MINIFY_DISABLED_FIX = {
|
|
4060
5397
|
description: "Remove the minify: false override from vite.config (Vite minifies with esbuild by default), or scope it to non-production builds.",
|
|
4061
5398
|
snippet: "export default defineConfig({\n build: {\n minify: 'esbuild'\n }\n});",
|
|
4062
5399
|
lang: "ts"
|
|
4063
5400
|
};
|
|
4064
|
-
var
|
|
5401
|
+
var RECOMMENDATION4 = "Remove build.minify: false from vite.config, or scope it to non-production builds if it is intentional.";
|
|
4065
5402
|
var performanceMinifyDisabled = {
|
|
4066
5403
|
id: "performance/minify-disabled",
|
|
4067
5404
|
title: "Minification disabled",
|
|
@@ -4079,11 +5416,11 @@ var performanceMinifyDisabled = {
|
|
|
4079
5416
|
id: "performance/minify-disabled",
|
|
4080
5417
|
category: "performance",
|
|
4081
5418
|
severity: "warning",
|
|
4082
|
-
detection:
|
|
5419
|
+
detection: PENALIZED7,
|
|
4083
5420
|
...hit.file !== void 0 ? { location: hit.file } : {},
|
|
4084
5421
|
...hit.line !== void 0 ? { line: hit.line } : {},
|
|
4085
5422
|
message: "JS/CSS minification is disabled (build.minify: false) \u2014 production bundles ship unminified and several times larger." + provenance,
|
|
4086
|
-
recommendation:
|
|
5423
|
+
recommendation: RECOMMENDATION4,
|
|
4087
5424
|
docsUrl: docsUrlFor("performance/minify-disabled"),
|
|
4088
5425
|
fix: { ...MINIFY_DISABLED_FIX }
|
|
4089
5426
|
}
|
|
@@ -4197,8 +5534,10 @@ var allRules = [
|
|
|
4197
5534
|
correctnessPropMutation,
|
|
4198
5535
|
correctnessStalePropDerivation,
|
|
4199
5536
|
correctnessNonreactiveBuiltinState,
|
|
5537
|
+
correctnessCheckableBindValue,
|
|
4200
5538
|
correctnessOrphanEffect,
|
|
4201
5539
|
correctnessOrphanLifecycle,
|
|
5540
|
+
correctnessBasePathNavigation,
|
|
4202
5541
|
correctnessServerBrowserGlobal,
|
|
4203
5542
|
correctnessInstanceBrowserGlobal,
|
|
4204
5543
|
securityRawHtml,
|
|
@@ -4208,6 +5547,11 @@ var allRules = [
|
|
|
4208
5547
|
securitySharedStateImport,
|
|
4209
5548
|
architectureComponentSize,
|
|
4210
5549
|
architecturePropCount,
|
|
5550
|
+
architecturePrivateScopeImport,
|
|
5551
|
+
architectureUnitEntryFile,
|
|
5552
|
+
architectureDirectoryNaming,
|
|
5553
|
+
architectureReservedDirectoryNames,
|
|
5554
|
+
architectureRouteComponentImport,
|
|
4211
5555
|
performanceHeavyImport,
|
|
4212
5556
|
performanceNamespaceImport,
|
|
4213
5557
|
performanceMinifyDisabled,
|
|
@@ -4215,6 +5559,15 @@ var allRules = [
|
|
|
4215
5559
|
performanceSequentialAwaits,
|
|
4216
5560
|
performanceStateRaw
|
|
4217
5561
|
];
|
|
5562
|
+
function optionInfos(spec) {
|
|
5563
|
+
return Object.entries(spec).map(([name, s]) => ({
|
|
5564
|
+
name,
|
|
5565
|
+
kind: s.kind,
|
|
5566
|
+
default: s.default,
|
|
5567
|
+
...s.kind === "integer" && s.min !== void 0 ? { min: s.min } : {},
|
|
5568
|
+
...s.kind === "integer" && s.max !== void 0 ? { max: s.max } : {}
|
|
5569
|
+
}));
|
|
5570
|
+
}
|
|
4218
5571
|
function explainRule(id) {
|
|
4219
5572
|
const rule = allRules.find((r) => r.id === id);
|
|
4220
5573
|
if (!rule) return void 0;
|
|
@@ -4225,7 +5578,8 @@ function explainRule(id) {
|
|
|
4225
5578
|
severity: rule.severity,
|
|
4226
5579
|
rationale: rule.rationale,
|
|
4227
5580
|
docsUrl: docsUrlFor(rule.id),
|
|
4228
|
-
...rule.fix ? { fix: rule.fix } : {}
|
|
5581
|
+
...rule.fix ? { fix: rule.fix } : {},
|
|
5582
|
+
...rule.options ? { options: optionInfos(rule.options) } : {}
|
|
4229
5583
|
};
|
|
4230
5584
|
}
|
|
4231
5585
|
|
|
@@ -5444,61 +6798,26 @@ function safeHref(url) {
|
|
|
5444
6798
|
const normalized = url.replace(/\s/g, "").toLowerCase();
|
|
5445
6799
|
return /^https?:\/\//.test(normalized) ? url : null;
|
|
5446
6800
|
}
|
|
5447
|
-
|
|
5448
|
-
// src/config-apply.ts
|
|
5449
|
-
function selectRules(rules, config) {
|
|
5450
|
-
return rules.filter((rule) => config.rules[rule.id] !== "off");
|
|
5451
|
-
}
|
|
5452
|
-
function applyRuleSeverities(results, config) {
|
|
5453
|
-
return results.map((result) => {
|
|
5454
|
-
const setting = config.rules[result.id];
|
|
5455
|
-
return setting && setting !== "off" ? { ...result, severity: setting } : result;
|
|
5456
|
-
});
|
|
5457
|
-
}
|
|
5458
|
-
function routeGlobToRegExp(pattern) {
|
|
5459
|
-
const body = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").split("\0").join(".*");
|
|
5460
|
-
const source = body.endsWith("/.*") ? `${body.slice(0, -3)}(/.*)?` : body;
|
|
5461
|
-
return new RegExp(`^${source}$`);
|
|
5462
|
-
}
|
|
5463
|
-
function toPatterns(globs) {
|
|
5464
|
-
if (globs === void 0) return [];
|
|
5465
|
-
return (Array.isArray(globs) ? globs : [globs]).map(routeGlobToRegExp);
|
|
5466
|
-
}
|
|
5467
|
-
function applyOverrides(results, config) {
|
|
5468
|
-
const overrides = config.overrides;
|
|
5469
|
-
if (!overrides || overrides.length === 0) return results;
|
|
5470
|
-
const compiled = overrides.map((o) => ({
|
|
5471
|
-
routes: toPatterns(o.route),
|
|
5472
|
-
files: toPatterns(o.files),
|
|
5473
|
-
rules: o.rules
|
|
5474
|
-
}));
|
|
5475
|
-
const out = [];
|
|
5476
|
-
for (const result of results) {
|
|
5477
|
-
const { route, location } = result;
|
|
5478
|
-
let setting;
|
|
5479
|
-
for (const o of compiled) {
|
|
5480
|
-
const matched = route !== void 0 && o.routes.some((p) => p.test(route)) || location !== void 0 && o.files.some((p) => p.test(location));
|
|
5481
|
-
if (!matched) continue;
|
|
5482
|
-
const s = o.rules[result.id] ?? o.rules[result.category ?? "seo"];
|
|
5483
|
-
if (s !== void 0) setting = s;
|
|
5484
|
-
}
|
|
5485
|
-
if (setting === void 0) out.push(result);
|
|
5486
|
-
else if (setting !== "off") out.push({ ...result, severity: setting });
|
|
5487
|
-
}
|
|
5488
|
-
return out;
|
|
5489
|
-
}
|
|
5490
6801
|
export {
|
|
5491
6802
|
APP_SCRIPT,
|
|
5492
6803
|
APP_STYLE,
|
|
5493
6804
|
BAND_COLOR,
|
|
6805
|
+
CATEGORIES,
|
|
5494
6806
|
CHILD_NODE_KEYS,
|
|
5495
6807
|
ROBOTS_SOURCE_PATHS,
|
|
5496
6808
|
SITEMAP_SOURCE_PATHS,
|
|
6809
|
+
SVELTE_CONFIG_FILES,
|
|
6810
|
+
VITE_CONFIG_FILES,
|
|
5497
6811
|
allRules,
|
|
5498
6812
|
applyOverrides,
|
|
5499
6813
|
applyRuleSeverities,
|
|
5500
6814
|
architectureComponentSize,
|
|
6815
|
+
architectureDirectoryNaming,
|
|
6816
|
+
architecturePrivateScopeImport,
|
|
5501
6817
|
architecturePropCount,
|
|
6818
|
+
architectureReservedDirectoryNames,
|
|
6819
|
+
architectureRouteComponentImport,
|
|
6820
|
+
architectureUnitEntryFile,
|
|
5502
6821
|
attrText,
|
|
5503
6822
|
attrTextOf,
|
|
5504
6823
|
attrValue,
|
|
@@ -5508,8 +6827,12 @@ export {
|
|
|
5508
6827
|
classify,
|
|
5509
6828
|
collectComponentFacts,
|
|
5510
6829
|
collectKitModuleFacts,
|
|
6830
|
+
collectSourceFiles,
|
|
6831
|
+
compileOverrides,
|
|
5511
6832
|
computeHealth,
|
|
5512
6833
|
computeScore,
|
|
6834
|
+
correctnessBasePathNavigation,
|
|
6835
|
+
correctnessCheckableBindValue,
|
|
5513
6836
|
correctnessEachIndexKey,
|
|
5514
6837
|
correctnessEachKey,
|
|
5515
6838
|
correctnessEffectAsDerived,
|
|
@@ -5532,6 +6855,9 @@ export {
|
|
|
5532
6855
|
escapeHtml,
|
|
5533
6856
|
explainRule,
|
|
5534
6857
|
findAttr,
|
|
6858
|
+
findKitAliasesInSvelteConfig,
|
|
6859
|
+
findKitPathsBaseInSvelteConfig,
|
|
6860
|
+
findKitPathsBaseInViteConfig,
|
|
5535
6861
|
findMinifyDisabled,
|
|
5536
6862
|
formatAgentReport,
|
|
5537
6863
|
formatConsoleReport,
|
|
@@ -5543,10 +6869,15 @@ export {
|
|
|
5543
6869
|
hasFailureAtOrAbove,
|
|
5544
6870
|
headTagRule,
|
|
5545
6871
|
imageRule,
|
|
6872
|
+
intOption,
|
|
6873
|
+
isMentionedAnywhere,
|
|
5546
6874
|
isPenalized,
|
|
5547
6875
|
lineOf,
|
|
5548
6876
|
linkRule,
|
|
6877
|
+
listOption,
|
|
6878
|
+
mapOption,
|
|
5549
6879
|
noColorPalette,
|
|
6880
|
+
overrideMatches,
|
|
5550
6881
|
parseComponentFacts,
|
|
5551
6882
|
parseKitModuleFacts,
|
|
5552
6883
|
performanceFontPreloadCrossorigin,
|
|
@@ -5564,6 +6895,9 @@ export {
|
|
|
5564
6895
|
performanceSequentialAwaits,
|
|
5565
6896
|
performanceStateRaw,
|
|
5566
6897
|
renderAppShell,
|
|
6898
|
+
resolveKitAliases,
|
|
6899
|
+
resolveKitPathsBase,
|
|
6900
|
+
resolveRuleOptions,
|
|
5567
6901
|
resolveRunesModuleSpecifier,
|
|
5568
6902
|
runRules,
|
|
5569
6903
|
safeHref,
|
|
@@ -5607,7 +6941,12 @@ export {
|
|
|
5607
6941
|
seoTitlePresence,
|
|
5608
6942
|
seoTwitterCard,
|
|
5609
6943
|
seoViewport,
|
|
6944
|
+
settingOptions,
|
|
6945
|
+
settingSeverity,
|
|
6946
|
+
shouldSkipRangeCheck,
|
|
5610
6947
|
summarize,
|
|
5611
6948
|
textFromNodes,
|
|
6949
|
+
validateRuleOptions,
|
|
6950
|
+
validateRuleSetting,
|
|
5612
6951
|
valueFromNodes
|
|
5613
6952
|
};
|