@svelte-vitals/core 0.29.0 → 0.30.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 +305 -18
- package/dist/index.js +755 -122
- 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
|
}
|
|
@@ -977,20 +1043,25 @@ var BROWSER_GLOBALS = /* @__PURE__ */ new Set([
|
|
|
977
1043
|
"confirm",
|
|
978
1044
|
"prompt"
|
|
979
1045
|
]);
|
|
980
|
-
function
|
|
1046
|
+
function collectNamedImportAliases(program, moduleSource, names) {
|
|
981
1047
|
const out = /* @__PURE__ */ new Set();
|
|
982
1048
|
for (const stmt of program.body ?? []) {
|
|
983
|
-
if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type" || stmt.source?.value !==
|
|
1049
|
+
if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type" || stmt.source?.value !== moduleSource) {
|
|
984
1050
|
continue;
|
|
1051
|
+
}
|
|
985
1052
|
for (const s of stmt.specifiers ?? []) {
|
|
986
1053
|
if (s?.importKind === "type" || s?.local?.type !== "Identifier") continue;
|
|
987
|
-
if (s.type === "ImportSpecifier" && s.imported?.type === "Identifier" && s.imported.name
|
|
1054
|
+
if (s.type === "ImportSpecifier" && s.imported?.type === "Identifier" && names.has(s.imported.name)) {
|
|
988
1055
|
out.add(s.local.name);
|
|
989
1056
|
}
|
|
990
1057
|
}
|
|
991
1058
|
}
|
|
992
1059
|
return out;
|
|
993
1060
|
}
|
|
1061
|
+
var BROWSER_GUARD_NAMES = /* @__PURE__ */ new Set(["browser"]);
|
|
1062
|
+
function collectBrowserGuardImports(program) {
|
|
1063
|
+
return collectNamedImportAliases(program, "$app/environment", BROWSER_GUARD_NAMES);
|
|
1064
|
+
}
|
|
994
1065
|
function collectProgramBindings(program) {
|
|
995
1066
|
const bound = /* @__PURE__ */ new Set();
|
|
996
1067
|
for (const stmt of program.body ?? []) {
|
|
@@ -1168,6 +1239,14 @@ function parseModuleFacts(source, filename) {
|
|
|
1168
1239
|
const orphanLifecycleCalls = program ? collectOrphanLifecycleCalls(program, wrapped).map((f) => ({ ...f, line: shift(f.line) })) : [];
|
|
1169
1240
|
const browserGlobalRefs = program ? collectBrowserGlobalRefs(program, wrapped).map((r) => ({ ...r, line: shift(r.line), context: "module" })) : [];
|
|
1170
1241
|
const moduleStateDecls = program ? collectModuleStateDecls(program, wrapped).map((d) => ({ ...d, line: shift(d.line) })) : [];
|
|
1242
|
+
const basePathLinks = [];
|
|
1243
|
+
if (program) {
|
|
1244
|
+
const locals = collectNamedImportAliases(program, "$app/navigation", GOTO_NAMES);
|
|
1245
|
+
const raw = [];
|
|
1246
|
+
collectGotoLinks(locals, [program], wrapped, raw);
|
|
1247
|
+
for (const l of raw) basePathLinks.push({ ...l, line: shift(l.line) });
|
|
1248
|
+
basePathLinks.sort((a, b) => a.line - b.line);
|
|
1249
|
+
}
|
|
1171
1250
|
return {
|
|
1172
1251
|
eachBlocks: [],
|
|
1173
1252
|
effects: [],
|
|
@@ -1183,6 +1262,8 @@ function parseModuleFacts(source, filename) {
|
|
|
1183
1262
|
stalePropDerivations: [],
|
|
1184
1263
|
rawableStates: [],
|
|
1185
1264
|
nonreactiveBuiltinStates: [],
|
|
1265
|
+
checkableBindValues: [],
|
|
1266
|
+
basePathLinks,
|
|
1186
1267
|
suppressions: collectSuppressions(source),
|
|
1187
1268
|
orphanEffects,
|
|
1188
1269
|
orphanLifecycleCalls,
|
|
@@ -1198,6 +1279,18 @@ function parseComponentFacts(source, filename) {
|
|
|
1198
1279
|
const htmlTags = [];
|
|
1199
1280
|
const javascriptUrls = [];
|
|
1200
1281
|
collectSecurityFacts(ast.fragment ?? ast, source, htmlTags, javascriptUrls);
|
|
1282
|
+
const checkableBindValues = [];
|
|
1283
|
+
collectCheckableBindValues(ast.fragment ?? ast, source, checkableBindValues);
|
|
1284
|
+
const basePathLinks = [];
|
|
1285
|
+
collectHrefLinks(ast.fragment ?? ast, source, basePathLinks);
|
|
1286
|
+
const gotoPrograms = [ast.module?.content, ast.instance?.content].filter(Boolean);
|
|
1287
|
+
const gotoLocals = /* @__PURE__ */ new Set();
|
|
1288
|
+
for (const p of gotoPrograms)
|
|
1289
|
+
for (const n of collectNamedImportAliases(p, "$app/navigation", GOTO_NAMES)) {
|
|
1290
|
+
gotoLocals.add(n);
|
|
1291
|
+
}
|
|
1292
|
+
collectGotoLinks(gotoLocals, [...gotoPrograms, ast.fragment], source, basePathLinks);
|
|
1293
|
+
basePathLinks.sort((a, b) => a.line - b.line);
|
|
1201
1294
|
const loc = countLines(source);
|
|
1202
1295
|
const suppressions = collectSuppressions(source);
|
|
1203
1296
|
const moduleProgram = ast.module?.content;
|
|
@@ -1372,6 +1465,8 @@ function parseComponentFacts(source, filename) {
|
|
|
1372
1465
|
stalePropDerivations,
|
|
1373
1466
|
rawableStates,
|
|
1374
1467
|
nonreactiveBuiltinStates,
|
|
1468
|
+
checkableBindValues,
|
|
1469
|
+
basePathLinks,
|
|
1375
1470
|
orphanEffects,
|
|
1376
1471
|
orphanLifecycleCalls,
|
|
1377
1472
|
browserGlobalRefs,
|
|
@@ -1398,6 +1493,8 @@ function emptyComponentFacts(file) {
|
|
|
1398
1493
|
stalePropDerivations: [],
|
|
1399
1494
|
rawableStates: [],
|
|
1400
1495
|
nonreactiveBuiltinStates: [],
|
|
1496
|
+
checkableBindValues: [],
|
|
1497
|
+
basePathLinks: [],
|
|
1401
1498
|
orphanEffects: [],
|
|
1402
1499
|
orphanLifecycleCalls: [],
|
|
1403
1500
|
browserGlobalRefs: [],
|
|
@@ -1534,6 +1631,22 @@ function collectAwaits(node, out = []) {
|
|
|
1534
1631
|
}
|
|
1535
1632
|
return out;
|
|
1536
1633
|
}
|
|
1634
|
+
var REDIRECT_NAMES = /* @__PURE__ */ new Set(["redirect"]);
|
|
1635
|
+
function collectRedirectCalls(node, locals, out = []) {
|
|
1636
|
+
if (Array.isArray(node)) {
|
|
1637
|
+
for (const child of node) collectRedirectCalls(child, locals, out);
|
|
1638
|
+
return out;
|
|
1639
|
+
}
|
|
1640
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return out;
|
|
1641
|
+
if (node.type === "CallExpression" && node.callee?.type === "Identifier" && locals.has(node.callee.name)) {
|
|
1642
|
+
out.push(node);
|
|
1643
|
+
}
|
|
1644
|
+
for (const key of Object.keys(node)) {
|
|
1645
|
+
if (WALK_IGNORED_KEYS.has(key)) continue;
|
|
1646
|
+
collectRedirectCalls(node[key], locals, out);
|
|
1647
|
+
}
|
|
1648
|
+
return out;
|
|
1649
|
+
}
|
|
1537
1650
|
function isParentCall(arg) {
|
|
1538
1651
|
const e = unwrapTs(arg);
|
|
1539
1652
|
if (e?.type !== "CallExpression") return false;
|
|
@@ -1721,6 +1834,7 @@ function parseKitModuleFacts(source, filename) {
|
|
|
1721
1834
|
runesModuleImports,
|
|
1722
1835
|
lifecycleCalls,
|
|
1723
1836
|
browserGlobalRefs,
|
|
1837
|
+
basePathLinks: [],
|
|
1724
1838
|
suppressions
|
|
1725
1839
|
};
|
|
1726
1840
|
}
|
|
@@ -1845,6 +1959,15 @@ function parseKitModuleFacts(source, filename) {
|
|
|
1845
1959
|
}
|
|
1846
1960
|
});
|
|
1847
1961
|
const byLine = (arr) => arr.sort((a, b) => a.line - b.line);
|
|
1962
|
+
const basePathLinks = [];
|
|
1963
|
+
const redirectLocals = collectNamedImportAliases(program, "@sveltejs/kit", REDIRECT_NAMES);
|
|
1964
|
+
if (redirectLocals.size > 0) {
|
|
1965
|
+
for (const call of collectRedirectCalls(program, redirectLocals)) {
|
|
1966
|
+
const arg = call.arguments?.[1];
|
|
1967
|
+
if (arg?.type !== "Literal" || typeof arg.value !== "string" || !isRootRelativePath(arg.value)) continue;
|
|
1968
|
+
basePathLinks.push({ kind: "redirect", path: arg.value, line: line(call.start) });
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1848
1971
|
return {
|
|
1849
1972
|
moduleStateReassignments: byLine(moduleStateReassignments),
|
|
1850
1973
|
importedStateWrites: byLine(importedStateWrites),
|
|
@@ -1852,6 +1975,7 @@ function parseKitModuleFacts(source, filename) {
|
|
|
1852
1975
|
runesModuleImports: byLine(runesModuleImports),
|
|
1853
1976
|
lifecycleCalls: byLine(lifecycleCalls),
|
|
1854
1977
|
browserGlobalRefs: byLine(browserGlobalRefs),
|
|
1978
|
+
basePathLinks: byLine(basePathLinks),
|
|
1855
1979
|
...ssrOptOut ? { ssrDisabled: { line: Math.max(0, ssrOptOut.line - 1) } } : {},
|
|
1856
1980
|
...csrOptOut ? { csrDisabled: { line: Math.max(0, csrOptOut.line - 1) } } : {},
|
|
1857
1981
|
...waterfalls.dependentLines.length > 0 || waterfalls.independentLines.length > 0 ? { loadWaterfalls: waterfalls } : {},
|
|
@@ -1870,6 +1994,7 @@ function emptyKitModuleFacts(file, kind) {
|
|
|
1870
1994
|
runesModuleImports: [],
|
|
1871
1995
|
lifecycleCalls: [],
|
|
1872
1996
|
browserGlobalRefs: [],
|
|
1997
|
+
basePathLinks: [],
|
|
1873
1998
|
suppressions: []
|
|
1874
1999
|
};
|
|
1875
2000
|
}
|
|
@@ -1899,7 +2024,7 @@ async function collectKitModuleFacts(rt, cwd) {
|
|
|
1899
2024
|
);
|
|
1900
2025
|
}
|
|
1901
2026
|
|
|
1902
|
-
// src/
|
|
2027
|
+
// src/config-object.ts
|
|
1903
2028
|
function propOf(obj, name) {
|
|
1904
2029
|
let found;
|
|
1905
2030
|
for (const p of obj.properties) {
|
|
@@ -1954,6 +2079,8 @@ function resolveConfigObject(program) {
|
|
|
1954
2079
|
if (!exported) return void 0;
|
|
1955
2080
|
return unwrapToObjectExpression(exported, collectTopLevelBindings(program));
|
|
1956
2081
|
}
|
|
2082
|
+
|
|
2083
|
+
// src/vite-config-parse.ts
|
|
1957
2084
|
function findMinifyDisabled(source) {
|
|
1958
2085
|
let program;
|
|
1959
2086
|
let wrapped;
|
|
@@ -1974,6 +2101,79 @@ function findMinifyDisabled(source) {
|
|
|
1974
2101
|
return { line: Math.max(0, lineOf(wrapped, minify.start) - 1) };
|
|
1975
2102
|
}
|
|
1976
2103
|
|
|
2104
|
+
// src/svelte-config-parse.ts
|
|
2105
|
+
function basePathOf(kitConfig, bindings) {
|
|
2106
|
+
const paths = propOf(kitConfig, "paths");
|
|
2107
|
+
const pathsObj = paths ? unwrapToObjectExpression(paths.value, bindings) : void 0;
|
|
2108
|
+
if (!pathsObj) return void 0;
|
|
2109
|
+
const base = propOf(pathsObj, "base");
|
|
2110
|
+
if (!base) return void 0;
|
|
2111
|
+
const value = unwrapTs(base.value);
|
|
2112
|
+
if (value.type === "Literal") {
|
|
2113
|
+
return typeof value.value === "string" && value.value !== "" ? { value: value.value } : void 0;
|
|
2114
|
+
}
|
|
2115
|
+
return {};
|
|
2116
|
+
}
|
|
2117
|
+
function programOf(source, filename) {
|
|
2118
|
+
try {
|
|
2119
|
+
return parseModuleProgram(source, filename).program ?? void 0;
|
|
2120
|
+
} catch {
|
|
2121
|
+
return void 0;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
function findKitPathsBaseInSvelteConfig(source) {
|
|
2125
|
+
const program = programOf(source, "svelte.config.js");
|
|
2126
|
+
if (!program) return void 0;
|
|
2127
|
+
const config = resolveConfigObject(program);
|
|
2128
|
+
if (!config) return void 0;
|
|
2129
|
+
const bindings = collectTopLevelBindings(program);
|
|
2130
|
+
const kit = propOf(config, "kit");
|
|
2131
|
+
const kitObj = kit ? unwrapToObjectExpression(kit.value, bindings) : void 0;
|
|
2132
|
+
return kitObj ? basePathOf(kitObj, bindings) : void 0;
|
|
2133
|
+
}
|
|
2134
|
+
function sveltekitLocalNames(program) {
|
|
2135
|
+
const out = collectNamedImportAliases(program, "@sveltejs/kit/vite", /* @__PURE__ */ new Set(["sveltekit"]));
|
|
2136
|
+
if (out.size === 0) out.add("sveltekit");
|
|
2137
|
+
return out;
|
|
2138
|
+
}
|
|
2139
|
+
function findKitPathsBaseInViteConfig(source) {
|
|
2140
|
+
const none = { kind: "no-plugin-config" };
|
|
2141
|
+
const program = programOf(source, "vite.config.ts");
|
|
2142
|
+
if (!program) return none;
|
|
2143
|
+
const config = resolveConfigObject(program);
|
|
2144
|
+
if (!config) return none;
|
|
2145
|
+
const bindings = collectTopLevelBindings(program);
|
|
2146
|
+
const plugins = propOf(config, "plugins");
|
|
2147
|
+
const pluginsValue = plugins ? unwrapTs(plugins.value) : void 0;
|
|
2148
|
+
if (pluginsValue?.type !== "ArrayExpression") return none;
|
|
2149
|
+
const locals = sveltekitLocalNames(program);
|
|
2150
|
+
for (const el of pluginsValue.elements) {
|
|
2151
|
+
if (!el || el.type === "SpreadElement") continue;
|
|
2152
|
+
const call = unwrapTs(el);
|
|
2153
|
+
if (call.type !== "CallExpression") continue;
|
|
2154
|
+
if (call.callee.type !== "Identifier" || !locals.has(call.callee.name)) continue;
|
|
2155
|
+
const arg = call.arguments[0];
|
|
2156
|
+
if (arg === void 0) return none;
|
|
2157
|
+
const kitConfig = unwrapToObjectExpression(arg, bindings);
|
|
2158
|
+
if (!kitConfig) return { kind: "unresolvable" };
|
|
2159
|
+
const base = basePathOf(kitConfig, bindings);
|
|
2160
|
+
return base ? { kind: "resolved", base } : { kind: "resolved" };
|
|
2161
|
+
}
|
|
2162
|
+
return none;
|
|
2163
|
+
}
|
|
2164
|
+
function resolveKitPathsBase(viteConfig, svelteConfig) {
|
|
2165
|
+
if (viteConfig) {
|
|
2166
|
+
const result = findKitPathsBaseInViteConfig(viteConfig.source);
|
|
2167
|
+
if (result.kind === "unresolvable") return void 0;
|
|
2168
|
+
if (result.kind === "resolved") {
|
|
2169
|
+
return result.base ? { ...result.base, file: viteConfig.file } : void 0;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
if (!svelteConfig) return void 0;
|
|
2173
|
+
const base = findKitPathsBaseInSvelteConfig(svelteConfig.source);
|
|
2174
|
+
return base ? { ...base, file: svelteConfig.file } : void 0;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
1977
2177
|
// src/project-paths.ts
|
|
1978
2178
|
var ROBOTS_SOURCE_PATHS = [
|
|
1979
2179
|
"static/robots.txt",
|
|
@@ -1985,6 +2185,15 @@ var SITEMAP_SOURCE_PATHS = [
|
|
|
1985
2185
|
"src/routes/sitemap.xml/+server.ts",
|
|
1986
2186
|
"src/routes/sitemap.xml/+server.js"
|
|
1987
2187
|
];
|
|
2188
|
+
var VITE_CONFIG_FILES = [
|
|
2189
|
+
"vite.config.js",
|
|
2190
|
+
"vite.config.mjs",
|
|
2191
|
+
"vite.config.ts",
|
|
2192
|
+
"vite.config.cjs",
|
|
2193
|
+
"vite.config.mts",
|
|
2194
|
+
"vite.config.cts"
|
|
2195
|
+
];
|
|
2196
|
+
var SVELTE_CONFIG_FILES = ["svelte.config.js", "svelte.config.ts"];
|
|
1988
2197
|
|
|
1989
2198
|
// src/rule.ts
|
|
1990
2199
|
function docsUrlFor(id) {
|
|
@@ -2054,7 +2263,7 @@ function detect(head, match) {
|
|
|
2054
2263
|
return tag ? { presence: tag.presence, value: tag.value } : { presence: "none", value: "absent" };
|
|
2055
2264
|
}
|
|
2056
2265
|
function headTagRule(opts) {
|
|
2057
|
-
const
|
|
2266
|
+
const docsUrl8 = docsUrlFor(opts.id);
|
|
2058
2267
|
return {
|
|
2059
2268
|
id: opts.id,
|
|
2060
2269
|
title: opts.title,
|
|
@@ -2077,7 +2286,7 @@ function headTagRule(opts) {
|
|
|
2077
2286
|
location: head.file,
|
|
2078
2287
|
message,
|
|
2079
2288
|
recommendation: opts.recommendation,
|
|
2080
|
-
docsUrl:
|
|
2289
|
+
docsUrl: docsUrl8,
|
|
2081
2290
|
// Copy per finding: opts.fix is a rule-level template shared across all
|
|
2082
2291
|
// results this rule emits; a fresh object keeps findings independent.
|
|
2083
2292
|
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
@@ -2270,7 +2479,7 @@ var seoHtmlLang = {
|
|
|
2270
2479
|
|
|
2271
2480
|
// src/rules/perf/image-rule.ts
|
|
2272
2481
|
function imageRule(opts) {
|
|
2273
|
-
const
|
|
2482
|
+
const docsUrl8 = docsUrlFor(opts.id);
|
|
2274
2483
|
const category = opts.category ?? "performance";
|
|
2275
2484
|
return {
|
|
2276
2485
|
id: opts.id,
|
|
@@ -2294,7 +2503,7 @@ function imageRule(opts) {
|
|
|
2294
2503
|
route: route.route,
|
|
2295
2504
|
message: opts.label,
|
|
2296
2505
|
recommendation: opts.recommendation,
|
|
2297
|
-
docsUrl:
|
|
2506
|
+
docsUrl: docsUrl8
|
|
2298
2507
|
});
|
|
2299
2508
|
continue;
|
|
2300
2509
|
}
|
|
@@ -2309,7 +2518,7 @@ function imageRule(opts) {
|
|
|
2309
2518
|
...img.line > 0 ? { line: img.line } : {},
|
|
2310
2519
|
message: `Missing ${opts.label}`,
|
|
2311
2520
|
recommendation: opts.recommendation,
|
|
2312
|
-
docsUrl:
|
|
2521
|
+
docsUrl: docsUrl8,
|
|
2313
2522
|
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
2314
2523
|
});
|
|
2315
2524
|
}
|
|
@@ -2369,7 +2578,7 @@ var performanceResponsiveImage = imageRule({
|
|
|
2369
2578
|
|
|
2370
2579
|
// src/rules/perf/link-rule.ts
|
|
2371
2580
|
function linkRule(opts) {
|
|
2372
|
-
const
|
|
2581
|
+
const docsUrl8 = docsUrlFor(opts.id);
|
|
2373
2582
|
return {
|
|
2374
2583
|
id: opts.id,
|
|
2375
2584
|
title: opts.title,
|
|
@@ -2393,7 +2602,7 @@ function linkRule(opts) {
|
|
|
2393
2602
|
route: head.route,
|
|
2394
2603
|
message: opts.label,
|
|
2395
2604
|
recommendation: opts.recommendation,
|
|
2396
|
-
docsUrl:
|
|
2605
|
+
docsUrl: docsUrl8
|
|
2397
2606
|
});
|
|
2398
2607
|
continue;
|
|
2399
2608
|
}
|
|
@@ -2410,7 +2619,7 @@ function linkRule(opts) {
|
|
|
2410
2619
|
location: tag.file ?? head.file,
|
|
2411
2620
|
message: `Missing ${opts.label}`,
|
|
2412
2621
|
recommendation: opts.recommendation,
|
|
2413
|
-
docsUrl:
|
|
2622
|
+
docsUrl: docsUrl8,
|
|
2414
2623
|
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
2415
2624
|
});
|
|
2416
2625
|
}
|
|
@@ -2557,10 +2766,197 @@ var performanceRenderBlockingScript = {
|
|
|
2557
2766
|
}
|
|
2558
2767
|
};
|
|
2559
2768
|
|
|
2769
|
+
// src/config-apply.ts
|
|
2770
|
+
function settingSeverity(setting) {
|
|
2771
|
+
if (setting === void 0) return void 0;
|
|
2772
|
+
if (typeof setting === "string") return setting;
|
|
2773
|
+
return setting.severity;
|
|
2774
|
+
}
|
|
2775
|
+
function settingOptions(setting) {
|
|
2776
|
+
return setting !== void 0 && typeof setting !== "string" ? setting.options : void 0;
|
|
2777
|
+
}
|
|
2778
|
+
function selectRules(rules, config) {
|
|
2779
|
+
return rules.filter((rule) => settingSeverity(config.rules[rule.id]) !== "off");
|
|
2780
|
+
}
|
|
2781
|
+
function applyRuleSeverities(results, config) {
|
|
2782
|
+
return results.map((result) => {
|
|
2783
|
+
const severity = settingSeverity(config.rules[result.id]);
|
|
2784
|
+
return severity !== void 0 && severity !== "off" ? { ...result, severity } : result;
|
|
2785
|
+
});
|
|
2786
|
+
}
|
|
2787
|
+
function routeGlobToRegExp(pattern) {
|
|
2788
|
+
const body = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").split("\0").join(".*");
|
|
2789
|
+
const source = body.endsWith("/.*") ? `${body.slice(0, -3)}(/.*)?` : body;
|
|
2790
|
+
return new RegExp(`^${source}$`);
|
|
2791
|
+
}
|
|
2792
|
+
function toPatterns(globs) {
|
|
2793
|
+
if (globs === void 0) return [];
|
|
2794
|
+
return (Array.isArray(globs) ? globs : [globs]).map(routeGlobToRegExp);
|
|
2795
|
+
}
|
|
2796
|
+
function compileOverrides(config) {
|
|
2797
|
+
return (config.overrides ?? []).map((o) => ({
|
|
2798
|
+
routes: toPatterns(o.route),
|
|
2799
|
+
files: toPatterns(o.files),
|
|
2800
|
+
rules: o.rules
|
|
2801
|
+
}));
|
|
2802
|
+
}
|
|
2803
|
+
function overrideMatches(o, target) {
|
|
2804
|
+
const { route, file } = target;
|
|
2805
|
+
return route !== void 0 && o.routes.some((p) => p.test(route)) || file !== void 0 && o.files.some((p) => p.test(file));
|
|
2806
|
+
}
|
|
2807
|
+
function applyOverrides(results, config) {
|
|
2808
|
+
const compiled = compileOverrides(config);
|
|
2809
|
+
if (compiled.length === 0) return results;
|
|
2810
|
+
const out = [];
|
|
2811
|
+
for (const result of results) {
|
|
2812
|
+
let severity;
|
|
2813
|
+
for (const o of compiled) {
|
|
2814
|
+
if (!overrideMatches(o, { route: result.route, file: result.location })) continue;
|
|
2815
|
+
const sev = settingSeverity(o.rules[result.id]) ?? settingSeverity(o.rules[result.category ?? "seo"]);
|
|
2816
|
+
if (sev !== void 0) severity = sev;
|
|
2817
|
+
}
|
|
2818
|
+
if (severity === void 0) out.push(result);
|
|
2819
|
+
else if (severity !== "off") out.push({ ...result, severity });
|
|
2820
|
+
}
|
|
2821
|
+
return out;
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
// src/rule-options.ts
|
|
2825
|
+
var RULE_SETTING_VALUES = ["off", "critical", "warning", "info"];
|
|
2826
|
+
function defaultsOf(spec) {
|
|
2827
|
+
const out = {};
|
|
2828
|
+
for (const [key, s] of Object.entries(spec)) {
|
|
2829
|
+
out[key] = s.kind === "integer" ? s.default : s.kind === "string-list" ? [...s.default] : { ...s.default };
|
|
2830
|
+
}
|
|
2831
|
+
return out;
|
|
2832
|
+
}
|
|
2833
|
+
function intOption(options, key, fallback = 0) {
|
|
2834
|
+
const v = options[key];
|
|
2835
|
+
return typeof v === "number" ? v : fallback;
|
|
2836
|
+
}
|
|
2837
|
+
function listOption(options, key) {
|
|
2838
|
+
const v = options[key];
|
|
2839
|
+
return Array.isArray(v) ? v : [];
|
|
2840
|
+
}
|
|
2841
|
+
function mapOption(options, key) {
|
|
2842
|
+
const v = options[key];
|
|
2843
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
|
|
2844
|
+
}
|
|
2845
|
+
function resolveRuleOptions(ruleId, spec, config, target, compiled) {
|
|
2846
|
+
if (!spec) return {};
|
|
2847
|
+
const out = defaultsOf(spec);
|
|
2848
|
+
const layers = [settingOptions(config.rules[ruleId])];
|
|
2849
|
+
if (target) {
|
|
2850
|
+
for (const o of compiled ?? compileOverrides(config)) {
|
|
2851
|
+
if (overrideMatches(o, target)) layers.push(settingOptions(o.rules[ruleId]));
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
for (const layer of layers) {
|
|
2855
|
+
if (!layer) continue;
|
|
2856
|
+
for (const [key, value] of Object.entries(layer)) {
|
|
2857
|
+
const s = spec[key];
|
|
2858
|
+
if (!s) continue;
|
|
2859
|
+
if (s.kind === "integer") out[key] = value;
|
|
2860
|
+
else if (s.kind === "string-list") out[key] = [...out[key], ...value];
|
|
2861
|
+
else out[key] = { ...out[key], ...value };
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
return out;
|
|
2865
|
+
}
|
|
2866
|
+
function validateRuleOptions(ruleId, spec, options, baseline, skipRangeCheck) {
|
|
2867
|
+
if (!spec) return Object.keys(options).length === 0 ? [] : [`${ruleId} takes no options.`];
|
|
2868
|
+
const errors = [];
|
|
2869
|
+
const badKeys = /* @__PURE__ */ new Set();
|
|
2870
|
+
const isNonEmptyString = (v) => typeof v === "string" && v.length > 0;
|
|
2871
|
+
for (const [key, value] of Object.entries(options)) {
|
|
2872
|
+
const s = spec[key];
|
|
2873
|
+
if (!s) {
|
|
2874
|
+
errors.push(`${ruleId}: unknown option '${key}'. Known options: ${Object.keys(spec).join(", ")}.`);
|
|
2875
|
+
continue;
|
|
2876
|
+
}
|
|
2877
|
+
if (s.kind === "integer") {
|
|
2878
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
2879
|
+
errors.push(`${ruleId}.${key} must be an integer.`);
|
|
2880
|
+
badKeys.add(key);
|
|
2881
|
+
} else if (s.min !== void 0 && value < s.min) {
|
|
2882
|
+
errors.push(`${ruleId}.${key} must be >= ${s.min}.`);
|
|
2883
|
+
badKeys.add(key);
|
|
2884
|
+
} else if (s.max !== void 0 && value > s.max) {
|
|
2885
|
+
errors.push(`${ruleId}.${key} must be <= ${s.max}.`);
|
|
2886
|
+
badKeys.add(key);
|
|
2887
|
+
}
|
|
2888
|
+
} else if (s.kind === "string-list") {
|
|
2889
|
+
if (!Array.isArray(value) || !value.every(isNonEmptyString)) {
|
|
2890
|
+
errors.push(`${ruleId}.${key} must be an array of non-empty strings.`);
|
|
2891
|
+
}
|
|
2892
|
+
} else if (typeof value !== "object" || value === null || Array.isArray(value) || !Object.values(value).every(isNonEmptyString)) {
|
|
2893
|
+
errors.push(`${ruleId}.${key} must be an object of string \u2192 non-empty string.`);
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
const minSpec = spec.min;
|
|
2897
|
+
const maxSpec = spec.max;
|
|
2898
|
+
if (minSpec?.kind === "integer" && maxSpec?.kind === "integer" && !badKeys.has("min") && !badKeys.has("max") && !skipRangeCheck) {
|
|
2899
|
+
const base = baseline ?? defaultsOf(spec);
|
|
2900
|
+
const minVal = "min" in options ? options.min : base.min;
|
|
2901
|
+
const maxVal = "max" in options ? options.max : base.max;
|
|
2902
|
+
if (typeof minVal === "number" && typeof maxVal === "number" && minVal > maxVal) {
|
|
2903
|
+
errors.push(`${ruleId}: min (${minVal}) must be <= max (${maxVal}).`);
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
return errors;
|
|
2907
|
+
}
|
|
2908
|
+
function isPlainObject(value) {
|
|
2909
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2910
|
+
}
|
|
2911
|
+
function otherOverrideNarrowsOppositeSide(overrides, selfIndex, key, side) {
|
|
2912
|
+
return overrides.some((entry, i) => {
|
|
2913
|
+
if (i === selfIndex || !isPlainObject(entry) || !isPlainObject(entry.rules)) return false;
|
|
2914
|
+
const setting = entry.rules[key];
|
|
2915
|
+
return isPlainObject(setting) && isPlainObject(setting.options) && side in setting.options;
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2918
|
+
function shouldSkipRangeCheck(overrides, selfIndex, key, setting) {
|
|
2919
|
+
if (!isPlainObject(setting) || !isPlainObject(setting.options)) return false;
|
|
2920
|
+
const setsMin = "min" in setting.options;
|
|
2921
|
+
const setsMax = "max" in setting.options;
|
|
2922
|
+
if (setsMin === setsMax) return false;
|
|
2923
|
+
return otherOverrideNarrowsOppositeSide(overrides, selfIndex, key, setsMin ? "max" : "min");
|
|
2924
|
+
}
|
|
2925
|
+
function validateRuleSetting(label, ruleId, setting, spec, opts) {
|
|
2926
|
+
const expected = RULE_SETTING_VALUES.join("|");
|
|
2927
|
+
if (typeof setting === "string") {
|
|
2928
|
+
return RULE_SETTING_VALUES.includes(setting) ? [] : [`${label}: invalid setting '${setting}'; expected ${expected}.`];
|
|
2929
|
+
}
|
|
2930
|
+
if (!isPlainObject(setting)) {
|
|
2931
|
+
return [`${label}: must be ${expected} or an object with 'severity' and/or 'options'.`];
|
|
2932
|
+
}
|
|
2933
|
+
const errors = [];
|
|
2934
|
+
const unknownKeys = Object.keys(setting).filter((k) => k !== "severity" && k !== "options");
|
|
2935
|
+
if (unknownKeys.length > 0) {
|
|
2936
|
+
errors.push(`${label}: unknown key(s) ${unknownKeys.join(", ")}; expected severity, options.`);
|
|
2937
|
+
}
|
|
2938
|
+
if (setting.severity !== void 0 && !RULE_SETTING_VALUES.includes(setting.severity)) {
|
|
2939
|
+
errors.push(`${label}.severity: invalid setting '${String(setting.severity)}'; expected ${expected}.`);
|
|
2940
|
+
}
|
|
2941
|
+
if (setting.options === void 0) return errors;
|
|
2942
|
+
if (!opts.allowOptions) {
|
|
2943
|
+
errors.push(`${label}: options are not allowed on a category key.`);
|
|
2944
|
+
return errors;
|
|
2945
|
+
}
|
|
2946
|
+
if (!isPlainObject(setting.options)) {
|
|
2947
|
+
errors.push(`${label}.options: must be an object.`);
|
|
2948
|
+
return errors;
|
|
2949
|
+
}
|
|
2950
|
+
const optionErrors = validateRuleOptions(ruleId, spec, setting.options, opts.baseline, opts.skipRangeCheck);
|
|
2951
|
+
if (optionErrors.length > 0) errors.push(`${label}: ${optionErrors.join(" ")}`);
|
|
2952
|
+
return errors;
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2560
2955
|
// src/rules/perf/preconnect.ts
|
|
2561
2956
|
var docsUrl3 = docsUrlFor("performance/preconnect");
|
|
2562
2957
|
var recommendation3 = 'Add <link rel="preconnect"> (or dns-prefetch) for the third-party origin so the connection is set up early.';
|
|
2563
2958
|
var THIRD_PARTY_ORIGINS = /* @__PURE__ */ new Set(["fonts.googleapis.com", "fonts.gstatic.com"]);
|
|
2959
|
+
var OPTIONS = { origins: { kind: "string-list", default: [...THIRD_PARTY_ORIGINS] } };
|
|
2564
2960
|
function hostOf(href) {
|
|
2565
2961
|
const m = /^(?:https?:)?\/\/([^/?#]+)/i.exec(href);
|
|
2566
2962
|
return m ? m[1].toLowerCase() : void 0;
|
|
@@ -2577,17 +2973,30 @@ var performancePreconnect = {
|
|
|
2577
2973
|
snippet: '<link rel="preconnect" href="https://fonts.googleapis.com" />',
|
|
2578
2974
|
lang: "html"
|
|
2579
2975
|
},
|
|
2976
|
+
options: OPTIONS,
|
|
2580
2977
|
async check(ctx) {
|
|
2581
2978
|
const out = [];
|
|
2979
|
+
const compiled = compileOverrides(ctx.config);
|
|
2582
2980
|
for (const head of ctx.heads) {
|
|
2583
2981
|
const referenced = /* @__PURE__ */ new Map();
|
|
2584
2982
|
const covered = /* @__PURE__ */ new Set();
|
|
2585
2983
|
for (const tag of head.tags) {
|
|
2586
2984
|
if (tag.kind !== "link" && tag.kind !== "script" || typeof tag.href !== "string") continue;
|
|
2587
2985
|
const host = hostOf(tag.href);
|
|
2588
|
-
if (!host
|
|
2589
|
-
if (tag.kind === "link" && (tag.rel === "preconnect" || tag.rel === "dns-prefetch"))
|
|
2590
|
-
|
|
2986
|
+
if (!host) continue;
|
|
2987
|
+
if (tag.kind === "link" && (tag.rel === "preconnect" || tag.rel === "dns-prefetch")) {
|
|
2988
|
+
covered.add(host);
|
|
2989
|
+
continue;
|
|
2990
|
+
}
|
|
2991
|
+
const o = resolveRuleOptions(
|
|
2992
|
+
"performance/preconnect",
|
|
2993
|
+
OPTIONS,
|
|
2994
|
+
ctx.config,
|
|
2995
|
+
{ route: head.route, file: tag.file ?? head.file },
|
|
2996
|
+
compiled
|
|
2997
|
+
);
|
|
2998
|
+
if (!listOption(o, "origins").includes(host)) continue;
|
|
2999
|
+
if (!referenced.has(host)) referenced.set(host, tag.file);
|
|
2591
3000
|
}
|
|
2592
3001
|
if (referenced.size === 0) continue;
|
|
2593
3002
|
const missing = [...referenced].filter(([host]) => !covered.has(host));
|
|
@@ -2638,7 +3047,7 @@ var seoIndexability = {
|
|
|
2638
3047
|
rationale: "A noindex directive removes the page from search results; an accidental noindex on a public route silently deindexes it.",
|
|
2639
3048
|
fix: FIX5,
|
|
2640
3049
|
async check(ctx) {
|
|
2641
|
-
const
|
|
3050
|
+
const docsUrl8 = docsUrlFor("seo/indexability");
|
|
2642
3051
|
const out = [];
|
|
2643
3052
|
for (const head of ctx.heads) {
|
|
2644
3053
|
const noindexed = head.tags.some((t) => t.kind === "meta" && t.name === "robots" && t.noindex === true);
|
|
@@ -2653,7 +3062,7 @@ var seoIndexability = {
|
|
|
2653
3062
|
location: head.file,
|
|
2654
3063
|
message: "Route is noindex \u2014 verify this is intentional",
|
|
2655
3064
|
recommendation: 'If this route should be indexed, remove noindex from its <meta name="robots">.',
|
|
2656
|
-
docsUrl:
|
|
3065
|
+
docsUrl: docsUrl8,
|
|
2657
3066
|
fix: { ...FIX5 }
|
|
2658
3067
|
});
|
|
2659
3068
|
}
|
|
@@ -2904,7 +3313,7 @@ function jsonldTags(head) {
|
|
|
2904
3313
|
return head.tags.filter((t) => t.kind === "jsonld" && typeof t.jsonld === "string");
|
|
2905
3314
|
}
|
|
2906
3315
|
function jsonldRule(opts) {
|
|
2907
|
-
const
|
|
3316
|
+
const docsUrl8 = docsUrlFor(opts.id);
|
|
2908
3317
|
return {
|
|
2909
3318
|
id: opts.id,
|
|
2910
3319
|
title: opts.title,
|
|
@@ -2932,7 +3341,7 @@ function jsonldRule(opts) {
|
|
|
2932
3341
|
location: head.file,
|
|
2933
3342
|
message: problem,
|
|
2934
3343
|
recommendation: opts.recommendation,
|
|
2935
|
-
docsUrl:
|
|
3344
|
+
docsUrl: docsUrl8,
|
|
2936
3345
|
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
2937
3346
|
} : {
|
|
2938
3347
|
id: opts.id,
|
|
@@ -2942,7 +3351,7 @@ function jsonldRule(opts) {
|
|
|
2942
3351
|
route: head.route,
|
|
2943
3352
|
message: opts.label,
|
|
2944
3353
|
recommendation: opts.recommendation,
|
|
2945
|
-
docsUrl:
|
|
3354
|
+
docsUrl: docsUrl8
|
|
2946
3355
|
}
|
|
2947
3356
|
);
|
|
2948
3357
|
}
|
|
@@ -2966,7 +3375,7 @@ var seoJsonLdValidity = {
|
|
|
2966
3375
|
lang: "svelte"
|
|
2967
3376
|
},
|
|
2968
3377
|
async check(ctx) {
|
|
2969
|
-
const
|
|
3378
|
+
const docsUrl8 = docsUrlFor("seo/json-ld-validity");
|
|
2970
3379
|
const out = [];
|
|
2971
3380
|
for (const head of ctx.heads) {
|
|
2972
3381
|
for (const tag of jsonldTags(head)) {
|
|
@@ -2985,7 +3394,7 @@ var seoJsonLdValidity = {
|
|
|
2985
3394
|
location: head.file,
|
|
2986
3395
|
message: problem,
|
|
2987
3396
|
recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
|
|
2988
|
-
docsUrl:
|
|
3397
|
+
docsUrl: docsUrl8,
|
|
2989
3398
|
fix: { ...seoJsonLdValidity.fix }
|
|
2990
3399
|
} : {
|
|
2991
3400
|
id: "seo/json-ld-validity",
|
|
@@ -2995,7 +3404,7 @@ var seoJsonLdValidity = {
|
|
|
2995
3404
|
route: head.route,
|
|
2996
3405
|
message: "JSON-LD validity",
|
|
2997
3406
|
recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
|
|
2998
|
-
docsUrl:
|
|
3407
|
+
docsUrl: docsUrl8
|
|
2999
3408
|
}
|
|
3000
3409
|
);
|
|
3001
3410
|
}
|
|
@@ -3106,7 +3515,11 @@ function visibleLength(s) {
|
|
|
3106
3515
|
|
|
3107
3516
|
// src/rules/seo/length-rule.ts
|
|
3108
3517
|
function lengthRule(opts) {
|
|
3109
|
-
const
|
|
3518
|
+
const docsUrl8 = docsUrlFor(opts.id);
|
|
3519
|
+
const spec = {
|
|
3520
|
+
min: { kind: "integer", default: opts.min, min: 0 },
|
|
3521
|
+
max: { kind: "integer", default: opts.max, min: 1 }
|
|
3522
|
+
};
|
|
3110
3523
|
return {
|
|
3111
3524
|
id: opts.id,
|
|
3112
3525
|
title: opts.title,
|
|
@@ -3114,15 +3527,22 @@ function lengthRule(opts) {
|
|
|
3114
3527
|
severity: "info",
|
|
3115
3528
|
scope: "route",
|
|
3116
3529
|
rationale: opts.rationale,
|
|
3530
|
+
options: spec,
|
|
3117
3531
|
async check(ctx) {
|
|
3118
3532
|
const out = [];
|
|
3533
|
+
const compiled = compileOverrides(ctx.config);
|
|
3119
3534
|
for (const head of ctx.heads) {
|
|
3120
3535
|
const tag = head.tags.find(opts.match);
|
|
3121
3536
|
if (!tag || typeof tag.text !== "string") continue;
|
|
3537
|
+
const location = tag.file ?? head.file;
|
|
3538
|
+
const o = resolveRuleOptions(opts.id, spec, ctx.config, { route: head.route, file: location }, compiled);
|
|
3539
|
+
const min = intOption(o, "min", opts.min);
|
|
3540
|
+
const max = intOption(o, "max", opts.max);
|
|
3541
|
+
const recommendation8 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
|
|
3122
3542
|
const len = visibleLength(tag.text);
|
|
3123
3543
|
let problem;
|
|
3124
|
-
if (len <
|
|
3125
|
-
else if (len >
|
|
3544
|
+
if (len < min) problem = `${opts.noun} is too short (${len} chars; aim for ${min}\u2013${max})`;
|
|
3545
|
+
else if (len > max) problem = `${opts.noun} is too long (${len} chars; aim for ${min}\u2013${max})`;
|
|
3126
3546
|
out.push(
|
|
3127
3547
|
problem ? {
|
|
3128
3548
|
id: opts.id,
|
|
@@ -3130,10 +3550,10 @@ function lengthRule(opts) {
|
|
|
3130
3550
|
severity: "info",
|
|
3131
3551
|
detection: PENALIZED,
|
|
3132
3552
|
route: head.route,
|
|
3133
|
-
location
|
|
3553
|
+
location,
|
|
3134
3554
|
message: problem,
|
|
3135
|
-
recommendation:
|
|
3136
|
-
docsUrl:
|
|
3555
|
+
recommendation: recommendation8,
|
|
3556
|
+
docsUrl: docsUrl8
|
|
3137
3557
|
} : {
|
|
3138
3558
|
id: opts.id,
|
|
3139
3559
|
category: "seo",
|
|
@@ -3141,8 +3561,8 @@ function lengthRule(opts) {
|
|
|
3141
3561
|
detection: PASS,
|
|
3142
3562
|
route: head.route,
|
|
3143
3563
|
message: opts.label,
|
|
3144
|
-
recommendation:
|
|
3145
|
-
docsUrl:
|
|
3564
|
+
recommendation: recommendation8,
|
|
3565
|
+
docsUrl: docsUrl8
|
|
3146
3566
|
}
|
|
3147
3567
|
);
|
|
3148
3568
|
}
|
|
@@ -3152,28 +3572,32 @@ function lengthRule(opts) {
|
|
|
3152
3572
|
}
|
|
3153
3573
|
|
|
3154
3574
|
// src/rules/seo/title-length.ts
|
|
3575
|
+
var MIN = 30;
|
|
3576
|
+
var MAX = 60;
|
|
3155
3577
|
var seoTitleLength = lengthRule({
|
|
3156
3578
|
id: "seo/title-length",
|
|
3157
3579
|
title: "Title length",
|
|
3158
3580
|
label: "Title length",
|
|
3159
3581
|
noun: "Title",
|
|
3160
3582
|
match: (t) => t.kind === "title",
|
|
3161
|
-
min:
|
|
3162
|
-
max:
|
|
3163
|
-
recommendation:
|
|
3583
|
+
min: MIN,
|
|
3584
|
+
max: MAX,
|
|
3585
|
+
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
3586
|
rationale: "A title that is too short wastes the strongest on-page signal; one that is too long is truncated in the SERP."
|
|
3165
3587
|
});
|
|
3166
3588
|
|
|
3167
3589
|
// src/rules/seo/description-length.ts
|
|
3590
|
+
var MIN2 = 70;
|
|
3591
|
+
var MAX2 = 160;
|
|
3168
3592
|
var seoDescriptionLength = lengthRule({
|
|
3169
3593
|
id: "seo/description-length",
|
|
3170
3594
|
title: "Description length",
|
|
3171
3595
|
label: "Description length",
|
|
3172
3596
|
noun: "Description",
|
|
3173
3597
|
match: (t) => t.kind === "meta" && t.name === "description",
|
|
3174
|
-
min:
|
|
3175
|
-
max:
|
|
3176
|
-
recommendation:
|
|
3598
|
+
min: MIN2,
|
|
3599
|
+
max: MAX2,
|
|
3600
|
+
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
3601
|
rationale: "A description that is too short under-uses the SERP snippet; one that is too long is truncated by search engines."
|
|
3178
3602
|
});
|
|
3179
3603
|
|
|
@@ -3323,7 +3747,7 @@ var seoSingleH1 = {
|
|
|
3323
3747
|
|
|
3324
3748
|
// src/rules/seo/uniqueness-rule.ts
|
|
3325
3749
|
function uniquenessRule(opts) {
|
|
3326
|
-
const
|
|
3750
|
+
const docsUrl8 = docsUrlFor(opts.id);
|
|
3327
3751
|
return {
|
|
3328
3752
|
id: opts.id,
|
|
3329
3753
|
title: opts.title,
|
|
@@ -3353,7 +3777,7 @@ function uniquenessRule(opts) {
|
|
|
3353
3777
|
location: e.file,
|
|
3354
3778
|
message: `${opts.noun} is duplicated across ${n} routes`,
|
|
3355
3779
|
recommendation: opts.recommendation,
|
|
3356
|
-
docsUrl:
|
|
3780
|
+
docsUrl: docsUrl8
|
|
3357
3781
|
} : {
|
|
3358
3782
|
id: opts.id,
|
|
3359
3783
|
category: "seo",
|
|
@@ -3362,7 +3786,7 @@ function uniquenessRule(opts) {
|
|
|
3362
3786
|
route: e.route,
|
|
3363
3787
|
message: opts.label,
|
|
3364
3788
|
recommendation: opts.recommendation,
|
|
3365
|
-
docsUrl:
|
|
3789
|
+
docsUrl: docsUrl8
|
|
3366
3790
|
};
|
|
3367
3791
|
});
|
|
3368
3792
|
}
|
|
@@ -3450,7 +3874,7 @@ function isSuppressed(m, ruleId, line) {
|
|
|
3450
3874
|
return (m.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
|
|
3451
3875
|
}
|
|
3452
3876
|
function kitModuleRule(opts) {
|
|
3453
|
-
const
|
|
3877
|
+
const docsUrl8 = docsUrlFor(opts.id);
|
|
3454
3878
|
const severity = opts.severity ?? "warning";
|
|
3455
3879
|
return {
|
|
3456
3880
|
id: opts.id,
|
|
@@ -3474,7 +3898,7 @@ function kitModuleRule(opts) {
|
|
|
3474
3898
|
route: m.file,
|
|
3475
3899
|
message: opts.label,
|
|
3476
3900
|
recommendation: opts.recommendation,
|
|
3477
|
-
docsUrl:
|
|
3901
|
+
docsUrl: docsUrl8
|
|
3478
3902
|
});
|
|
3479
3903
|
continue;
|
|
3480
3904
|
}
|
|
@@ -3489,7 +3913,7 @@ function kitModuleRule(opts) {
|
|
|
3489
3913
|
...b.line > 0 ? { line: b.line } : {},
|
|
3490
3914
|
message: b.message,
|
|
3491
3915
|
recommendation: opts.recommendation,
|
|
3492
|
-
docsUrl:
|
|
3916
|
+
docsUrl: docsUrl8,
|
|
3493
3917
|
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
3494
3918
|
});
|
|
3495
3919
|
}
|
|
@@ -3525,7 +3949,7 @@ function isSuppressed2(c, ruleId, line) {
|
|
|
3525
3949
|
return (c.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
|
|
3526
3950
|
}
|
|
3527
3951
|
function componentRule(opts) {
|
|
3528
|
-
const
|
|
3952
|
+
const docsUrl8 = docsUrlFor(opts.id);
|
|
3529
3953
|
const severity = opts.severity ?? "warning";
|
|
3530
3954
|
return {
|
|
3531
3955
|
id: opts.id,
|
|
@@ -3535,11 +3959,15 @@ function componentRule(opts) {
|
|
|
3535
3959
|
scope: "component",
|
|
3536
3960
|
rationale: opts.rationale,
|
|
3537
3961
|
...opts.fix ? { fix: opts.fix } : {},
|
|
3962
|
+
...opts.options ? { options: opts.options } : {},
|
|
3538
3963
|
async check(ctx) {
|
|
3539
3964
|
const out = [];
|
|
3965
|
+
const compiled = compileOverrides(ctx.config);
|
|
3540
3966
|
for (const c of ctx.components ?? []) {
|
|
3541
|
-
|
|
3542
|
-
const
|
|
3967
|
+
const o = resolveRuleOptions(opts.id, opts.options, ctx.config, { route: c.file, file: c.file }, compiled);
|
|
3968
|
+
const recommendation8 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
|
|
3969
|
+
if (!opts.applies(c, o)) continue;
|
|
3970
|
+
const bad = opts.bad(c, o).filter((b) => !(b.line > 0 && isSuppressed2(c, opts.id, b.line)));
|
|
3543
3971
|
if (bad.length === 0) {
|
|
3544
3972
|
out.push({
|
|
3545
3973
|
id: opts.id,
|
|
@@ -3548,8 +3976,8 @@ function componentRule(opts) {
|
|
|
3548
3976
|
detection: PASS3,
|
|
3549
3977
|
route: c.file,
|
|
3550
3978
|
message: opts.label,
|
|
3551
|
-
recommendation:
|
|
3552
|
-
docsUrl:
|
|
3979
|
+
recommendation: recommendation8,
|
|
3980
|
+
docsUrl: docsUrl8
|
|
3553
3981
|
});
|
|
3554
3982
|
continue;
|
|
3555
3983
|
}
|
|
@@ -3563,8 +3991,8 @@ function componentRule(opts) {
|
|
|
3563
3991
|
location: c.file,
|
|
3564
3992
|
...b.line > 0 ? { line: b.line } : {},
|
|
3565
3993
|
message: b.message,
|
|
3566
|
-
recommendation:
|
|
3567
|
-
docsUrl:
|
|
3994
|
+
recommendation: recommendation8,
|
|
3995
|
+
docsUrl: docsUrl8,
|
|
3568
3996
|
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
3569
3997
|
});
|
|
3570
3998
|
}
|
|
@@ -3694,6 +4122,25 @@ var correctnessNonreactiveBuiltinState = componentRule({
|
|
|
3694
4122
|
}))
|
|
3695
4123
|
});
|
|
3696
4124
|
|
|
4125
|
+
// src/rules/correctness/checkable-bind-value.ts
|
|
4126
|
+
var correctnessCheckableBindValue = componentRule({
|
|
4127
|
+
id: "correctness/checkable-bind-value",
|
|
4128
|
+
title: "bind:value on a checkable input",
|
|
4129
|
+
category: "correctness",
|
|
4130
|
+
severity: "warning",
|
|
4131
|
+
label: "bind:checked / bind:group on checkable inputs",
|
|
4132
|
+
recommendation: "Replace bind:value with bind:checked (single checkbox) or bind:group (checkbox list / radio group).",
|
|
4133
|
+
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.",
|
|
4134
|
+
fix: {
|
|
4135
|
+
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."
|
|
4136
|
+
},
|
|
4137
|
+
applies: (c) => c.checkableBindValues.length > 0,
|
|
4138
|
+
bad: (c) => c.checkableBindValues.map((v) => ({
|
|
4139
|
+
line: v.line,
|
|
4140
|
+
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."
|
|
4141
|
+
}))
|
|
4142
|
+
});
|
|
4143
|
+
|
|
3697
4144
|
// src/rules/correctness/orphan-effect.ts
|
|
3698
4145
|
var correctnessOrphanEffect = componentRule({
|
|
3699
4146
|
id: "correctness/orphan-effect",
|
|
@@ -3793,24 +4240,35 @@ var correctnessOrphanLifecycle = {
|
|
|
3793
4240
|
}
|
|
3794
4241
|
};
|
|
3795
4242
|
|
|
3796
|
-
// src/rules/correctness/
|
|
4243
|
+
// src/rules/correctness/base-path-navigation.ts
|
|
3797
4244
|
var PENALIZED5 = { presence: "none", value: "absent" };
|
|
3798
4245
|
var PASS5 = { presence: "own", value: "static" };
|
|
3799
|
-
var ID2 = "correctness/
|
|
4246
|
+
var ID2 = "correctness/base-path-navigation";
|
|
3800
4247
|
var DOCS_URL2 = docsUrlFor(ID2);
|
|
3801
|
-
var LABEL2 = "
|
|
3802
|
-
var RECOMMENDATION2 = "
|
|
3803
|
-
var
|
|
4248
|
+
var LABEL2 = "Base-path-aware navigation";
|
|
4249
|
+
var RECOMMENDATION2 = "Wrap root-relative paths in resolve() from '$app/paths' so they resolve against kit.paths.base.";
|
|
4250
|
+
var FIX7 = {
|
|
4251
|
+
description: "Import { resolve } from '$app/paths' and wrap the path: href={resolve('/about')}, goto(resolve('/about')), redirect(303, resolve('/login'))."
|
|
4252
|
+
};
|
|
4253
|
+
function messageFor2(link) {
|
|
4254
|
+
if (link.kind === "href") {
|
|
4255
|
+
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'.`;
|
|
4256
|
+
}
|
|
4257
|
+
if (link.kind === "goto") {
|
|
4258
|
+
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'.`;
|
|
4259
|
+
}
|
|
4260
|
+
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'.`;
|
|
4261
|
+
}
|
|
3804
4262
|
function isSuppressed4(suppressions, line) {
|
|
3805
4263
|
return (suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ID2)));
|
|
3806
4264
|
}
|
|
3807
|
-
function emitFile2(out, file,
|
|
3808
|
-
const bad =
|
|
4265
|
+
function emitFile2(out, file, links, suppressions) {
|
|
4266
|
+
const bad = links.filter((l) => !(l.line > 0 && isSuppressed4(suppressions, l.line)));
|
|
3809
4267
|
if (bad.length === 0) {
|
|
3810
4268
|
out.push({
|
|
3811
4269
|
id: ID2,
|
|
3812
4270
|
category: "correctness",
|
|
3813
|
-
severity: "
|
|
4271
|
+
severity: "warning",
|
|
3814
4272
|
detection: PASS5,
|
|
3815
4273
|
route: file,
|
|
3816
4274
|
message: LABEL2,
|
|
@@ -3819,23 +4277,90 @@ function emitFile2(out, file, issues, suppressions) {
|
|
|
3819
4277
|
});
|
|
3820
4278
|
return;
|
|
3821
4279
|
}
|
|
3822
|
-
for (const
|
|
4280
|
+
for (const l of bad) {
|
|
3823
4281
|
out.push({
|
|
3824
4282
|
id: ID2,
|
|
3825
4283
|
category: "correctness",
|
|
3826
|
-
severity: "
|
|
4284
|
+
severity: "warning",
|
|
3827
4285
|
detection: PENALIZED5,
|
|
3828
4286
|
route: file,
|
|
3829
4287
|
location: file,
|
|
4288
|
+
...l.line > 0 ? { line: l.line } : {},
|
|
4289
|
+
message: messageFor2(l),
|
|
4290
|
+
recommendation: RECOMMENDATION2,
|
|
4291
|
+
docsUrl: DOCS_URL2,
|
|
4292
|
+
fix: { ...FIX7 }
|
|
4293
|
+
});
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
var correctnessBasePathNavigation = {
|
|
4297
|
+
id: ID2,
|
|
4298
|
+
title: "Root-relative navigation under a base path",
|
|
4299
|
+
category: "correctness",
|
|
4300
|
+
severity: "warning",
|
|
4301
|
+
scope: "component",
|
|
4302
|
+
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.",
|
|
4303
|
+
fix: { ...FIX7 },
|
|
4304
|
+
async check(ctx) {
|
|
4305
|
+
if (!ctx.project.kitPathsBase) return [];
|
|
4306
|
+
const out = [];
|
|
4307
|
+
for (const c of ctx.components ?? []) {
|
|
4308
|
+
const links = c.basePathLinks ?? [];
|
|
4309
|
+
if (links.length === 0) continue;
|
|
4310
|
+
emitFile2(out, c.file, links, c.suppressions);
|
|
4311
|
+
}
|
|
4312
|
+
for (const m of ctx.kitModules ?? []) {
|
|
4313
|
+
const links = m.basePathLinks ?? [];
|
|
4314
|
+
if (links.length === 0) continue;
|
|
4315
|
+
emitFile2(out, m.file, links, m.suppressions);
|
|
4316
|
+
}
|
|
4317
|
+
return out;
|
|
4318
|
+
}
|
|
4319
|
+
};
|
|
4320
|
+
|
|
4321
|
+
// src/rules/correctness/server-browser-global.ts
|
|
4322
|
+
var PENALIZED6 = { presence: "none", value: "absent" };
|
|
4323
|
+
var PASS6 = { presence: "own", value: "static" };
|
|
4324
|
+
var ID3 = "correctness/server-browser-global";
|
|
4325
|
+
var DOCS_URL3 = docsUrlFor(ID3);
|
|
4326
|
+
var LABEL3 = "Server-safe module code";
|
|
4327
|
+
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).";
|
|
4328
|
+
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"`;
|
|
4329
|
+
function isSuppressed5(suppressions, line) {
|
|
4330
|
+
return (suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ID3)));
|
|
4331
|
+
}
|
|
4332
|
+
function emitFile3(out, file, issues, suppressions) {
|
|
4333
|
+
const bad = issues.filter((b) => !(b.line > 0 && isSuppressed5(suppressions, b.line)));
|
|
4334
|
+
if (bad.length === 0) {
|
|
4335
|
+
out.push({
|
|
4336
|
+
id: ID3,
|
|
4337
|
+
category: "correctness",
|
|
4338
|
+
severity: "critical",
|
|
4339
|
+
detection: PASS6,
|
|
4340
|
+
route: file,
|
|
4341
|
+
message: LABEL3,
|
|
4342
|
+
recommendation: RECOMMENDATION3,
|
|
4343
|
+
docsUrl: DOCS_URL3
|
|
4344
|
+
});
|
|
4345
|
+
return;
|
|
4346
|
+
}
|
|
4347
|
+
for (const b of bad) {
|
|
4348
|
+
out.push({
|
|
4349
|
+
id: ID3,
|
|
4350
|
+
category: "correctness",
|
|
4351
|
+
severity: "critical",
|
|
4352
|
+
detection: PENALIZED6,
|
|
4353
|
+
route: file,
|
|
4354
|
+
location: file,
|
|
3830
4355
|
...b.line > 0 ? { line: b.line } : {},
|
|
3831
4356
|
message: b.message,
|
|
3832
|
-
recommendation:
|
|
3833
|
-
docsUrl:
|
|
4357
|
+
recommendation: RECOMMENDATION3,
|
|
4358
|
+
docsUrl: DOCS_URL3
|
|
3834
4359
|
});
|
|
3835
4360
|
}
|
|
3836
4361
|
}
|
|
3837
4362
|
var correctnessServerBrowserGlobal = {
|
|
3838
|
-
id:
|
|
4363
|
+
id: ID3,
|
|
3839
4364
|
title: "Browser global in server module code",
|
|
3840
4365
|
category: "correctness",
|
|
3841
4366
|
severity: "critical",
|
|
@@ -3846,7 +4371,7 @@ var correctnessServerBrowserGlobal = {
|
|
|
3846
4371
|
for (const c of ctx.components ?? []) {
|
|
3847
4372
|
const refs = (c.browserGlobalRefs ?? []).filter((r) => r.context === "module");
|
|
3848
4373
|
if (refs.length === 0) continue;
|
|
3849
|
-
|
|
4374
|
+
emitFile3(
|
|
3850
4375
|
out,
|
|
3851
4376
|
c.file,
|
|
3852
4377
|
refs.map((r) => ({ line: r.line, message: moduleMessage(r.name) })),
|
|
@@ -3856,7 +4381,7 @@ var correctnessServerBrowserGlobal = {
|
|
|
3856
4381
|
for (const m of ctx.kitModules ?? []) {
|
|
3857
4382
|
const refs = m.browserGlobalRefs ?? [];
|
|
3858
4383
|
if (refs.length === 0) continue;
|
|
3859
|
-
|
|
4384
|
+
emitFile3(
|
|
3860
4385
|
out,
|
|
3861
4386
|
m.file,
|
|
3862
4387
|
refs.map((r) => ({
|
|
@@ -3972,35 +4497,151 @@ var securitySharedStateImport = kitModuleRule({
|
|
|
3972
4497
|
});
|
|
3973
4498
|
|
|
3974
4499
|
// src/rules/architecture/component-size.ts
|
|
3975
|
-
var MAX_LOC =
|
|
4500
|
+
var MAX_LOC = 200;
|
|
3976
4501
|
var architectureComponentSize = componentRule({
|
|
3977
4502
|
id: "architecture/component-size",
|
|
3978
4503
|
title: "Component size",
|
|
3979
4504
|
category: "architecture",
|
|
3980
4505
|
severity: "info",
|
|
3981
4506
|
label: "Component size",
|
|
3982
|
-
|
|
4507
|
+
options: { max: { kind: "integer", default: MAX_LOC, min: 1 } },
|
|
4508
|
+
recommendation: (o) => `Split components over ${intOption(o, "max", MAX_LOC)} lines into smaller, focused pieces.`,
|
|
3983
4509
|
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
4510
|
applies: (c) => c.loc > 0,
|
|
3985
4511
|
// skip unanalyzable files (loc 0 = read/parse failure), don't PASS them
|
|
3986
|
-
bad: (c) =>
|
|
4512
|
+
bad: (c, o) => {
|
|
4513
|
+
const max = intOption(o, "max", MAX_LOC);
|
|
4514
|
+
return c.loc > max ? [{ line: 1, message: `Component is ${c.loc} lines (over ${max})` }] : [];
|
|
4515
|
+
}
|
|
3987
4516
|
});
|
|
3988
4517
|
|
|
3989
4518
|
// src/rules/architecture/prop-count.ts
|
|
3990
|
-
var MAX_PROPS =
|
|
4519
|
+
var MAX_PROPS = 6;
|
|
3991
4520
|
var architecturePropCount = componentRule({
|
|
3992
4521
|
id: "architecture/prop-count",
|
|
3993
4522
|
title: "Prop count",
|
|
3994
4523
|
category: "architecture",
|
|
3995
4524
|
severity: "info",
|
|
3996
4525
|
label: "Prop count",
|
|
3997
|
-
|
|
4526
|
+
options: { max: { kind: "integer", default: MAX_PROPS, min: 1 } },
|
|
4527
|
+
recommendation: (o) => `Group related props into an object, or split the component, when it takes more than ${intOption(o, "max", MAX_PROPS)} props.`,
|
|
3998
4528
|
rationale: "A component taking many props is usually doing too much; grouping or splitting keeps its API understandable.",
|
|
3999
4529
|
applies: (c) => c.propCount > 0,
|
|
4000
4530
|
// only components whose props we could count
|
|
4001
|
-
bad: (c) =>
|
|
4531
|
+
bad: (c, o) => {
|
|
4532
|
+
const max = intOption(o, "max", MAX_PROPS);
|
|
4533
|
+
return c.propCount > max ? [{ line: 1, message: `Component takes ${c.propCount} props (over ${max})` }] : [];
|
|
4534
|
+
}
|
|
4002
4535
|
});
|
|
4003
4536
|
|
|
4537
|
+
// src/rules/architecture/private-scope-import.ts
|
|
4538
|
+
var docsUrl7 = docsUrlFor("architecture/private-scope-import");
|
|
4539
|
+
var recommendation7 = "Move the unit to the directory shared by all of its importers, or import it only from inside its own scope.";
|
|
4540
|
+
var OPTIONS2 = { scopes: { kind: "string-list", default: [] } };
|
|
4541
|
+
function ancestorDirs(file) {
|
|
4542
|
+
const segments = file.split("/");
|
|
4543
|
+
const out = [];
|
|
4544
|
+
for (let i = segments.length - 1; i > 0; i--) out.push(segments.slice(0, i).join("/"));
|
|
4545
|
+
return out;
|
|
4546
|
+
}
|
|
4547
|
+
function privateScopeOf(target, patterns) {
|
|
4548
|
+
for (const dir of ancestorDirs(target)) {
|
|
4549
|
+
if (!patterns.some((p) => p.test(dir))) continue;
|
|
4550
|
+
const cut = dir.lastIndexOf("/");
|
|
4551
|
+
return cut === -1 ? "" : dir.slice(0, cut);
|
|
4552
|
+
}
|
|
4553
|
+
return void 0;
|
|
4554
|
+
}
|
|
4555
|
+
function isInside(file, boundary) {
|
|
4556
|
+
return boundary === "" || file.startsWith(`${boundary}/`);
|
|
4557
|
+
}
|
|
4558
|
+
var architecturePrivateScopeImport = {
|
|
4559
|
+
id: "architecture/private-scope-import",
|
|
4560
|
+
title: "Private-scope import",
|
|
4561
|
+
category: "architecture",
|
|
4562
|
+
severity: "info",
|
|
4563
|
+
scope: "component",
|
|
4564
|
+
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.",
|
|
4565
|
+
fix: {
|
|
4566
|
+
description: "Move this unit out of its private scope, to the directory shared by all of its importers, and update this import."
|
|
4567
|
+
},
|
|
4568
|
+
options: OPTIONS2,
|
|
4569
|
+
async check(ctx) {
|
|
4570
|
+
const out = [];
|
|
4571
|
+
const compiled = compileOverrides(ctx.config);
|
|
4572
|
+
const patternCache = /* @__PURE__ */ new Map();
|
|
4573
|
+
const compileScopes = (scopes) => {
|
|
4574
|
+
const key = JSON.stringify(scopes);
|
|
4575
|
+
let patterns = patternCache.get(key);
|
|
4576
|
+
if (patterns === void 0) {
|
|
4577
|
+
patterns = scopes.map((scope) => {
|
|
4578
|
+
const marker = scope.endsWith("/**") ? scope.slice(0, -3) : scope;
|
|
4579
|
+
return routeGlobToRegExp(marker);
|
|
4580
|
+
});
|
|
4581
|
+
patternCache.set(key, patterns);
|
|
4582
|
+
}
|
|
4583
|
+
return patterns;
|
|
4584
|
+
};
|
|
4585
|
+
for (const c of ctx.components ?? []) {
|
|
4586
|
+
const o = resolveRuleOptions(
|
|
4587
|
+
"architecture/private-scope-import",
|
|
4588
|
+
OPTIONS2,
|
|
4589
|
+
ctx.config,
|
|
4590
|
+
{ route: c.file, file: c.file },
|
|
4591
|
+
compiled
|
|
4592
|
+
);
|
|
4593
|
+
const scopes = listOption(o, "scopes");
|
|
4594
|
+
if (scopes.length === 0) continue;
|
|
4595
|
+
const patterns = compileScopes(scopes);
|
|
4596
|
+
const spans = c.importSpans ?? c.imports.map((source) => ({ source, line: 0 }));
|
|
4597
|
+
let sawScopedImport = false;
|
|
4598
|
+
const violations = [];
|
|
4599
|
+
for (const { source, line } of spans) {
|
|
4600
|
+
const target = resolveRepoLocalPath(source, c.file);
|
|
4601
|
+
if (target === void 0) continue;
|
|
4602
|
+
const boundary = privateScopeOf(target, patterns);
|
|
4603
|
+
if (boundary === void 0) continue;
|
|
4604
|
+
sawScopedImport = true;
|
|
4605
|
+
if (isInside(c.file, boundary)) continue;
|
|
4606
|
+
violations.push({ line, message: `${target} is private to ${boundary}` });
|
|
4607
|
+
}
|
|
4608
|
+
if (!sawScopedImport) continue;
|
|
4609
|
+
const visible = violations.filter(
|
|
4610
|
+
(v) => !(v.line > 0 && isSuppressed2(c, "architecture/private-scope-import", v.line))
|
|
4611
|
+
);
|
|
4612
|
+
if (visible.length === 0) {
|
|
4613
|
+
out.push({
|
|
4614
|
+
id: "architecture/private-scope-import",
|
|
4615
|
+
category: "architecture",
|
|
4616
|
+
severity: "info",
|
|
4617
|
+
detection: { presence: "own", value: "static" },
|
|
4618
|
+
route: c.file,
|
|
4619
|
+
message: "No private-scope imports",
|
|
4620
|
+
recommendation: recommendation7,
|
|
4621
|
+
docsUrl: docsUrl7
|
|
4622
|
+
});
|
|
4623
|
+
continue;
|
|
4624
|
+
}
|
|
4625
|
+
for (const v of visible) {
|
|
4626
|
+
out.push({
|
|
4627
|
+
id: "architecture/private-scope-import",
|
|
4628
|
+
category: "architecture",
|
|
4629
|
+
severity: "info",
|
|
4630
|
+
detection: { presence: "none", value: "absent" },
|
|
4631
|
+
route: c.file,
|
|
4632
|
+
location: c.file,
|
|
4633
|
+
...v.line > 0 ? { line: v.line } : {},
|
|
4634
|
+
message: v.message,
|
|
4635
|
+
recommendation: recommendation7,
|
|
4636
|
+
docsUrl: docsUrl7,
|
|
4637
|
+
fix: { ...architecturePrivateScopeImport.fix }
|
|
4638
|
+
});
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4641
|
+
return out;
|
|
4642
|
+
}
|
|
4643
|
+
};
|
|
4644
|
+
|
|
4004
4645
|
// src/rules/perf/heavy-import.ts
|
|
4005
4646
|
var HEAVY_PACKAGES = {
|
|
4006
4647
|
lodash: "import a submodule (lodash/debounce) or use lodash-es for tree-shaking",
|
|
@@ -4014,18 +4655,20 @@ var performanceHeavyImport = componentRule({
|
|
|
4014
4655
|
label: "No heavy imports",
|
|
4015
4656
|
recommendation: "Import a submodule or switch to a lighter, tree-shakeable alternative.",
|
|
4016
4657
|
rationale: "Importing a large, non-tree-shakeable package pulls its whole weight into the bundle even when only a fraction is used, slowing load.",
|
|
4658
|
+
options: { packages: { kind: "string-map", default: HEAVY_PACKAGES } },
|
|
4017
4659
|
// ComponentFacts is a public @svelte-vitals/core export — an external caller compiled
|
|
4018
4660
|
// against an older version may still construct one without importSpans. Fall back to the
|
|
4019
4661
|
// line-less `imports` (line: 0, the pre-fix behavior) instead of crashing on `undefined`.
|
|
4020
4662
|
applies: (c) => (c.importSpans ?? c.imports).length > 0,
|
|
4021
|
-
bad: (c) => {
|
|
4663
|
+
bad: (c, o) => {
|
|
4664
|
+
const packages = mapOption(o, "packages");
|
|
4022
4665
|
const seen = /* @__PURE__ */ new Set();
|
|
4023
4666
|
const out = [];
|
|
4024
4667
|
const spans = c.importSpans ?? c.imports.map((source) => ({ source, line: 0 }));
|
|
4025
4668
|
for (const { source: src, line } of spans) {
|
|
4026
|
-
if (!Object.hasOwn(
|
|
4669
|
+
if (!Object.hasOwn(packages, src) || seen.has(src)) continue;
|
|
4027
4670
|
seen.add(src);
|
|
4028
|
-
out.push({ line, message: `Heavy import "${src}" \u2014 ${
|
|
4671
|
+
out.push({ line, message: `Heavy import "${src}" \u2014 ${packages[src]}` });
|
|
4029
4672
|
}
|
|
4030
4673
|
return out;
|
|
4031
4674
|
}
|
|
@@ -4055,13 +4698,13 @@ var performanceNamespaceImport = componentRule({
|
|
|
4055
4698
|
});
|
|
4056
4699
|
|
|
4057
4700
|
// src/rules/perf/minify-disabled.ts
|
|
4058
|
-
var
|
|
4701
|
+
var PENALIZED7 = { presence: "none", value: "absent" };
|
|
4059
4702
|
var MINIFY_DISABLED_FIX = {
|
|
4060
4703
|
description: "Remove the minify: false override from vite.config (Vite minifies with esbuild by default), or scope it to non-production builds.",
|
|
4061
4704
|
snippet: "export default defineConfig({\n build: {\n minify: 'esbuild'\n }\n});",
|
|
4062
4705
|
lang: "ts"
|
|
4063
4706
|
};
|
|
4064
|
-
var
|
|
4707
|
+
var RECOMMENDATION4 = "Remove build.minify: false from vite.config, or scope it to non-production builds if it is intentional.";
|
|
4065
4708
|
var performanceMinifyDisabled = {
|
|
4066
4709
|
id: "performance/minify-disabled",
|
|
4067
4710
|
title: "Minification disabled",
|
|
@@ -4079,11 +4722,11 @@ var performanceMinifyDisabled = {
|
|
|
4079
4722
|
id: "performance/minify-disabled",
|
|
4080
4723
|
category: "performance",
|
|
4081
4724
|
severity: "warning",
|
|
4082
|
-
detection:
|
|
4725
|
+
detection: PENALIZED7,
|
|
4083
4726
|
...hit.file !== void 0 ? { location: hit.file } : {},
|
|
4084
4727
|
...hit.line !== void 0 ? { line: hit.line } : {},
|
|
4085
4728
|
message: "JS/CSS minification is disabled (build.minify: false) \u2014 production bundles ship unminified and several times larger." + provenance,
|
|
4086
|
-
recommendation:
|
|
4729
|
+
recommendation: RECOMMENDATION4,
|
|
4087
4730
|
docsUrl: docsUrlFor("performance/minify-disabled"),
|
|
4088
4731
|
fix: { ...MINIFY_DISABLED_FIX }
|
|
4089
4732
|
}
|
|
@@ -4197,8 +4840,10 @@ var allRules = [
|
|
|
4197
4840
|
correctnessPropMutation,
|
|
4198
4841
|
correctnessStalePropDerivation,
|
|
4199
4842
|
correctnessNonreactiveBuiltinState,
|
|
4843
|
+
correctnessCheckableBindValue,
|
|
4200
4844
|
correctnessOrphanEffect,
|
|
4201
4845
|
correctnessOrphanLifecycle,
|
|
4846
|
+
correctnessBasePathNavigation,
|
|
4202
4847
|
correctnessServerBrowserGlobal,
|
|
4203
4848
|
correctnessInstanceBrowserGlobal,
|
|
4204
4849
|
securityRawHtml,
|
|
@@ -4208,6 +4853,7 @@ var allRules = [
|
|
|
4208
4853
|
securitySharedStateImport,
|
|
4209
4854
|
architectureComponentSize,
|
|
4210
4855
|
architecturePropCount,
|
|
4856
|
+
architecturePrivateScopeImport,
|
|
4211
4857
|
performanceHeavyImport,
|
|
4212
4858
|
performanceNamespaceImport,
|
|
4213
4859
|
performanceMinifyDisabled,
|
|
@@ -4215,6 +4861,15 @@ var allRules = [
|
|
|
4215
4861
|
performanceSequentialAwaits,
|
|
4216
4862
|
performanceStateRaw
|
|
4217
4863
|
];
|
|
4864
|
+
function optionInfos(spec) {
|
|
4865
|
+
return Object.entries(spec).map(([name, s]) => ({
|
|
4866
|
+
name,
|
|
4867
|
+
kind: s.kind,
|
|
4868
|
+
default: s.default,
|
|
4869
|
+
...s.kind === "integer" && s.min !== void 0 ? { min: s.min } : {},
|
|
4870
|
+
...s.kind === "integer" && s.max !== void 0 ? { max: s.max } : {}
|
|
4871
|
+
}));
|
|
4872
|
+
}
|
|
4218
4873
|
function explainRule(id) {
|
|
4219
4874
|
const rule = allRules.find((r) => r.id === id);
|
|
4220
4875
|
if (!rule) return void 0;
|
|
@@ -4225,7 +4880,8 @@ function explainRule(id) {
|
|
|
4225
4880
|
severity: rule.severity,
|
|
4226
4881
|
rationale: rule.rationale,
|
|
4227
4882
|
docsUrl: docsUrlFor(rule.id),
|
|
4228
|
-
...rule.fix ? { fix: rule.fix } : {}
|
|
4883
|
+
...rule.fix ? { fix: rule.fix } : {},
|
|
4884
|
+
...rule.options ? { options: optionInfos(rule.options) } : {}
|
|
4229
4885
|
};
|
|
4230
4886
|
}
|
|
4231
4887
|
|
|
@@ -5444,60 +6100,21 @@ function safeHref(url) {
|
|
|
5444
6100
|
const normalized = url.replace(/\s/g, "").toLowerCase();
|
|
5445
6101
|
return /^https?:\/\//.test(normalized) ? url : null;
|
|
5446
6102
|
}
|
|
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
6103
|
export {
|
|
5491
6104
|
APP_SCRIPT,
|
|
5492
6105
|
APP_STYLE,
|
|
5493
6106
|
BAND_COLOR,
|
|
6107
|
+
CATEGORIES,
|
|
5494
6108
|
CHILD_NODE_KEYS,
|
|
5495
6109
|
ROBOTS_SOURCE_PATHS,
|
|
5496
6110
|
SITEMAP_SOURCE_PATHS,
|
|
6111
|
+
SVELTE_CONFIG_FILES,
|
|
6112
|
+
VITE_CONFIG_FILES,
|
|
5497
6113
|
allRules,
|
|
5498
6114
|
applyOverrides,
|
|
5499
6115
|
applyRuleSeverities,
|
|
5500
6116
|
architectureComponentSize,
|
|
6117
|
+
architecturePrivateScopeImport,
|
|
5501
6118
|
architecturePropCount,
|
|
5502
6119
|
attrText,
|
|
5503
6120
|
attrTextOf,
|
|
@@ -5508,8 +6125,11 @@ export {
|
|
|
5508
6125
|
classify,
|
|
5509
6126
|
collectComponentFacts,
|
|
5510
6127
|
collectKitModuleFacts,
|
|
6128
|
+
compileOverrides,
|
|
5511
6129
|
computeHealth,
|
|
5512
6130
|
computeScore,
|
|
6131
|
+
correctnessBasePathNavigation,
|
|
6132
|
+
correctnessCheckableBindValue,
|
|
5513
6133
|
correctnessEachIndexKey,
|
|
5514
6134
|
correctnessEachKey,
|
|
5515
6135
|
correctnessEffectAsDerived,
|
|
@@ -5532,6 +6152,8 @@ export {
|
|
|
5532
6152
|
escapeHtml,
|
|
5533
6153
|
explainRule,
|
|
5534
6154
|
findAttr,
|
|
6155
|
+
findKitPathsBaseInSvelteConfig,
|
|
6156
|
+
findKitPathsBaseInViteConfig,
|
|
5535
6157
|
findMinifyDisabled,
|
|
5536
6158
|
formatAgentReport,
|
|
5537
6159
|
formatConsoleReport,
|
|
@@ -5543,10 +6165,14 @@ export {
|
|
|
5543
6165
|
hasFailureAtOrAbove,
|
|
5544
6166
|
headTagRule,
|
|
5545
6167
|
imageRule,
|
|
6168
|
+
intOption,
|
|
5546
6169
|
isPenalized,
|
|
5547
6170
|
lineOf,
|
|
5548
6171
|
linkRule,
|
|
6172
|
+
listOption,
|
|
6173
|
+
mapOption,
|
|
5549
6174
|
noColorPalette,
|
|
6175
|
+
overrideMatches,
|
|
5550
6176
|
parseComponentFacts,
|
|
5551
6177
|
parseKitModuleFacts,
|
|
5552
6178
|
performanceFontPreloadCrossorigin,
|
|
@@ -5564,6 +6190,8 @@ export {
|
|
|
5564
6190
|
performanceSequentialAwaits,
|
|
5565
6191
|
performanceStateRaw,
|
|
5566
6192
|
renderAppShell,
|
|
6193
|
+
resolveKitPathsBase,
|
|
6194
|
+
resolveRuleOptions,
|
|
5567
6195
|
resolveRunesModuleSpecifier,
|
|
5568
6196
|
runRules,
|
|
5569
6197
|
safeHref,
|
|
@@ -5607,7 +6235,12 @@ export {
|
|
|
5607
6235
|
seoTitlePresence,
|
|
5608
6236
|
seoTwitterCard,
|
|
5609
6237
|
seoViewport,
|
|
6238
|
+
settingOptions,
|
|
6239
|
+
settingSeverity,
|
|
6240
|
+
shouldSkipRangeCheck,
|
|
5610
6241
|
summarize,
|
|
5611
6242
|
textFromNodes,
|
|
6243
|
+
validateRuleOptions,
|
|
6244
|
+
validateRuleSetting,
|
|
5612
6245
|
valueFromNodes
|
|
5613
6246
|
};
|