@svelte-vitals/core 0.28.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 +337 -23
- package/dist/index.js +910 -135
- 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",
|
|
@@ -245,8 +251,10 @@ function scopeIntroducedNames(node) {
|
|
|
245
251
|
addBoundNames(node.param, introduced);
|
|
246
252
|
} else if (node.type === "BlockStatement") {
|
|
247
253
|
for (const stmt of node.body ?? []) {
|
|
248
|
-
if (stmt?.type === "VariableDeclaration"
|
|
254
|
+
if (stmt?.type === "VariableDeclaration") {
|
|
249
255
|
for (const d of stmt.declarations ?? []) addBoundNames(d.id, introduced);
|
|
256
|
+
} else if ((stmt?.type === "FunctionDeclaration" || stmt?.type === "ClassDeclaration") && typeof stmt.id?.name === "string") {
|
|
257
|
+
introduced.add(stmt.id.name);
|
|
250
258
|
}
|
|
251
259
|
}
|
|
252
260
|
} else if (node.type === "ForStatement" || node.type === "ForOfStatement" || node.type === "ForInStatement") {
|
|
@@ -262,6 +270,12 @@ function scopeIntroducedNames(node) {
|
|
|
262
270
|
} else if (node.type === "AwaitBlock") {
|
|
263
271
|
if (node.value) addBoundNames(node.value, introduced);
|
|
264
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
|
+
}
|
|
265
279
|
}
|
|
266
280
|
return introduced;
|
|
267
281
|
}
|
|
@@ -330,6 +344,81 @@ function isDeferredBody(n) {
|
|
|
330
344
|
function isPlainStateCall(node) {
|
|
331
345
|
return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "$state";
|
|
332
346
|
}
|
|
347
|
+
var BUILTIN_STATE_TYPES = /* @__PURE__ */ new Set(["Map", "Set", "Date", "URL", "URLSearchParams"]);
|
|
348
|
+
var BUILTIN_MUTATIONS = {
|
|
349
|
+
Map: /* @__PURE__ */ new Set(["set", "delete", "clear"]),
|
|
350
|
+
Set: /* @__PURE__ */ new Set(["add", "delete", "clear"]),
|
|
351
|
+
Date: /* @__PURE__ */ new Set([
|
|
352
|
+
"setTime",
|
|
353
|
+
"setFullYear",
|
|
354
|
+
"setMonth",
|
|
355
|
+
"setDate",
|
|
356
|
+
"setHours",
|
|
357
|
+
"setMinutes",
|
|
358
|
+
"setSeconds",
|
|
359
|
+
"setMilliseconds",
|
|
360
|
+
"setYear",
|
|
361
|
+
"setUTCFullYear",
|
|
362
|
+
"setUTCMonth",
|
|
363
|
+
"setUTCDate",
|
|
364
|
+
"setUTCHours",
|
|
365
|
+
"setUTCMinutes",
|
|
366
|
+
"setUTCSeconds",
|
|
367
|
+
"setUTCMilliseconds"
|
|
368
|
+
]),
|
|
369
|
+
URL: /* @__PURE__ */ new Set(),
|
|
370
|
+
URLSearchParams: /* @__PURE__ */ new Set(["append", "set", "delete", "sort"])
|
|
371
|
+
};
|
|
372
|
+
function collectBuiltinStateSignals(node, candidates, mutated, reassigned, shadowed = /* @__PURE__ */ new Set(), inFunction = false) {
|
|
373
|
+
if (Array.isArray(node)) {
|
|
374
|
+
for (const child of node) collectBuiltinStateSignals(child, candidates, mutated, reassigned, shadowed, inFunction);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
378
|
+
const introduced = scopeIntroducedNames(node);
|
|
379
|
+
const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
|
|
380
|
+
const boundary = isDeferredBody(node) || node.type === "ClassDeclaration" || node.type === "ClassExpression";
|
|
381
|
+
const nextInFunction = inFunction || boundary;
|
|
382
|
+
const hit = (name) => typeof name === "string" && candidates.has(name) && !scope.has(name) ? name : void 0;
|
|
383
|
+
if (node.type === "AssignmentExpression") {
|
|
384
|
+
if (node.left?.type === "Identifier") {
|
|
385
|
+
const n = hit(node.left.name);
|
|
386
|
+
const isBareSelfAssign = node.right?.type === "Identifier" && node.right.name === n;
|
|
387
|
+
if (n && !isBareSelfAssign) reassigned.add(n);
|
|
388
|
+
} else if (node.left?.type === "ObjectPattern" || node.left?.type === "ArrayPattern") {
|
|
389
|
+
const bound = /* @__PURE__ */ new Set();
|
|
390
|
+
addBoundNames(node.left, bound);
|
|
391
|
+
for (const name of bound) {
|
|
392
|
+
const n = hit(name);
|
|
393
|
+
if (n) reassigned.add(n);
|
|
394
|
+
}
|
|
395
|
+
} else if (node.left?.type === "MemberExpression" && inFunction) {
|
|
396
|
+
const n = hit(rootObjectName(node.left));
|
|
397
|
+
if (n && candidates.get(n) === "URL") mutated.add(n);
|
|
398
|
+
}
|
|
399
|
+
} else if (node.type === "UpdateExpression" && node.argument?.type === "MemberExpression" && inFunction) {
|
|
400
|
+
const n = hit(rootObjectName(node.argument));
|
|
401
|
+
if (n && candidates.get(n) === "URL") mutated.add(n);
|
|
402
|
+
} else if (node.type === "UnaryExpression" && node.operator === "delete" && inFunction) {
|
|
403
|
+
const n = hit(rootObjectName(node.argument));
|
|
404
|
+
if (n && candidates.get(n) === "URL") mutated.add(n);
|
|
405
|
+
} else if (node.type === "CallExpression" && node.callee?.type === "MemberExpression" && !node.callee.computed && inFunction) {
|
|
406
|
+
const method = node.callee.property?.name;
|
|
407
|
+
if (typeof method === "string") {
|
|
408
|
+
if (node.callee.object?.type === "Identifier") {
|
|
409
|
+
const n = hit(node.callee.object.name);
|
|
410
|
+
if (n && BUILTIN_MUTATIONS[candidates.get(n)]?.has(method)) mutated.add(n);
|
|
411
|
+
} else if (node.callee.object?.type === "MemberExpression") {
|
|
412
|
+
const n = hit(rootObjectName(node.callee));
|
|
413
|
+
if (n && candidates.get(n) === "URL" && BUILTIN_MUTATIONS.URLSearchParams.has(method)) mutated.add(n);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
for (const key of Object.keys(node)) {
|
|
418
|
+
if (WALK_IGNORED_KEYS.has(key)) continue;
|
|
419
|
+
collectBuiltinStateSignals(node[key], candidates, mutated, reassigned, scope, nextInFunction);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
333
422
|
function collectPatternAliasRefs(node, names, acc, scope, ownRhs) {
|
|
334
423
|
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
335
424
|
if (node.type === "Identifier") return;
|
|
@@ -594,6 +683,7 @@ function bodyIsEmpty(fn) {
|
|
|
594
683
|
return false;
|
|
595
684
|
}
|
|
596
685
|
var URL_ATTRS = ["href", "src", "action", "formaction"];
|
|
686
|
+
var CHECKABLE_INPUT_TYPES = /* @__PURE__ */ new Set(["checkbox", "radio"]);
|
|
597
687
|
function collectSecurityFacts(node, source, htmlTags, jsUrls) {
|
|
598
688
|
if (Array.isArray(node)) {
|
|
599
689
|
for (const child of node) collectSecurityFacts(child, source, htmlTags, jsUrls);
|
|
@@ -615,6 +705,59 @@ function collectSecurityFacts(node, source, htmlTags, jsUrls) {
|
|
|
615
705
|
if (key in node) collectSecurityFacts(node[key], source, htmlTags, jsUrls);
|
|
616
706
|
}
|
|
617
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
|
+
}
|
|
618
761
|
function isPropsCall(node) {
|
|
619
762
|
return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "$props";
|
|
620
763
|
}
|
|
@@ -651,6 +794,16 @@ function collectPropNames(program, includeBindable) {
|
|
|
651
794
|
});
|
|
652
795
|
return ambiguous || seen > 1 ? /* @__PURE__ */ new Set() : names;
|
|
653
796
|
}
|
|
797
|
+
function collectLegacyPropNames(program) {
|
|
798
|
+
const names = /* @__PURE__ */ new Set();
|
|
799
|
+
for (const stmt of program.body ?? []) {
|
|
800
|
+
if (stmt?.type !== "ExportNamedDeclaration" || stmt.declaration?.type !== "VariableDeclaration") continue;
|
|
801
|
+
for (const d of stmt.declaration.declarations ?? []) {
|
|
802
|
+
if (d?.id?.type === "Identifier") names.add(d.id.name);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return names;
|
|
806
|
+
}
|
|
654
807
|
var MUTATING_METHODS = /* @__PURE__ */ new Set([
|
|
655
808
|
"push",
|
|
656
809
|
"pop",
|
|
@@ -890,20 +1043,25 @@ var BROWSER_GLOBALS = /* @__PURE__ */ new Set([
|
|
|
890
1043
|
"confirm",
|
|
891
1044
|
"prompt"
|
|
892
1045
|
]);
|
|
893
|
-
function
|
|
1046
|
+
function collectNamedImportAliases(program, moduleSource, names) {
|
|
894
1047
|
const out = /* @__PURE__ */ new Set();
|
|
895
1048
|
for (const stmt of program.body ?? []) {
|
|
896
|
-
if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type" || stmt.source?.value !==
|
|
1049
|
+
if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type" || stmt.source?.value !== moduleSource) {
|
|
897
1050
|
continue;
|
|
1051
|
+
}
|
|
898
1052
|
for (const s of stmt.specifiers ?? []) {
|
|
899
1053
|
if (s?.importKind === "type" || s?.local?.type !== "Identifier") continue;
|
|
900
|
-
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)) {
|
|
901
1055
|
out.add(s.local.name);
|
|
902
1056
|
}
|
|
903
1057
|
}
|
|
904
1058
|
}
|
|
905
1059
|
return out;
|
|
906
1060
|
}
|
|
1061
|
+
var BROWSER_GUARD_NAMES = /* @__PURE__ */ new Set(["browser"]);
|
|
1062
|
+
function collectBrowserGuardImports(program) {
|
|
1063
|
+
return collectNamedImportAliases(program, "$app/environment", BROWSER_GUARD_NAMES);
|
|
1064
|
+
}
|
|
907
1065
|
function collectProgramBindings(program) {
|
|
908
1066
|
const bound = /* @__PURE__ */ new Set();
|
|
909
1067
|
for (const stmt of program.body ?? []) {
|
|
@@ -1081,6 +1239,14 @@ function parseModuleFacts(source, filename) {
|
|
|
1081
1239
|
const orphanLifecycleCalls = program ? collectOrphanLifecycleCalls(program, wrapped).map((f) => ({ ...f, line: shift(f.line) })) : [];
|
|
1082
1240
|
const browserGlobalRefs = program ? collectBrowserGlobalRefs(program, wrapped).map((r) => ({ ...r, line: shift(r.line), context: "module" })) : [];
|
|
1083
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
|
+
}
|
|
1084
1250
|
return {
|
|
1085
1251
|
eachBlocks: [],
|
|
1086
1252
|
effects: [],
|
|
@@ -1095,6 +1261,9 @@ function parseModuleFacts(source, filename) {
|
|
|
1095
1261
|
mutatedProps: [],
|
|
1096
1262
|
stalePropDerivations: [],
|
|
1097
1263
|
rawableStates: [],
|
|
1264
|
+
nonreactiveBuiltinStates: [],
|
|
1265
|
+
checkableBindValues: [],
|
|
1266
|
+
basePathLinks,
|
|
1098
1267
|
suppressions: collectSuppressions(source),
|
|
1099
1268
|
orphanEffects,
|
|
1100
1269
|
orphanLifecycleCalls,
|
|
@@ -1110,6 +1279,18 @@ function parseComponentFacts(source, filename) {
|
|
|
1110
1279
|
const htmlTags = [];
|
|
1111
1280
|
const javascriptUrls = [];
|
|
1112
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);
|
|
1113
1294
|
const loc = countLines(source);
|
|
1114
1295
|
const suppressions = collectSuppressions(source);
|
|
1115
1296
|
const moduleProgram = ast.module?.content;
|
|
@@ -1132,16 +1313,20 @@ function parseComponentFacts(source, filename) {
|
|
|
1132
1313
|
const mutatedProps = [];
|
|
1133
1314
|
const stalePropDerivations = [];
|
|
1134
1315
|
const rawableStates = [];
|
|
1316
|
+
const nonreactiveBuiltinStates = [];
|
|
1135
1317
|
let propCount = 0;
|
|
1136
1318
|
const program = ast.instance?.content;
|
|
1137
1319
|
if (program) {
|
|
1138
1320
|
collectImportSources(program, source, importSpans);
|
|
1139
1321
|
collectNamespaceImports(program, source, namespaceImports);
|
|
1140
1322
|
propCount = countProps(program);
|
|
1141
|
-
const
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1323
|
+
const legacyPropNames = collectLegacyPropNames(program);
|
|
1324
|
+
const nonBindableProps = /* @__PURE__ */ new Set([...collectPropNames(program, false), ...legacyPropNames]);
|
|
1325
|
+
const rawMutations = [];
|
|
1326
|
+
collectPropMutations(program, nonBindableProps, source, rawMutations);
|
|
1327
|
+
if (ast.fragment) collectPropMutations(ast.fragment, nonBindableProps, source, rawMutations);
|
|
1328
|
+
for (const m of rawMutations) mutatedProps.push(legacyPropNames.has(m.name) ? { ...m, legacy: true } : m);
|
|
1329
|
+
const allPropNames = /* @__PURE__ */ new Set([...collectPropNames(program, true), ...legacyPropNames]);
|
|
1145
1330
|
if (allPropNames.size > 0) {
|
|
1146
1331
|
const candidates = collectStalePropCandidates(program, allPropNames, source);
|
|
1147
1332
|
if (candidates.length > 0) {
|
|
@@ -1154,8 +1339,11 @@ function parseComponentFacts(source, filename) {
|
|
|
1154
1339
|
}
|
|
1155
1340
|
const referenced = /* @__PURE__ */ new Set();
|
|
1156
1341
|
if (ast.fragment) collectFragmentRefs(ast.fragment, candidateNames, referenced);
|
|
1342
|
+
const isLegacy = legacyPropNames.size > 0;
|
|
1157
1343
|
for (const c of candidates) {
|
|
1158
|
-
if (!disqualified.has(c.name) && referenced.has(c.name))
|
|
1344
|
+
if (!disqualified.has(c.name) && referenced.has(c.name)) {
|
|
1345
|
+
stalePropDerivations.push(isLegacy ? { ...c, legacy: true } : c);
|
|
1346
|
+
}
|
|
1159
1347
|
}
|
|
1160
1348
|
}
|
|
1161
1349
|
}
|
|
@@ -1186,6 +1374,7 @@ function parseComponentFacts(source, filename) {
|
|
|
1186
1374
|
if (ast.fragment) {
|
|
1187
1375
|
collectStateWrites(ast.fragment, stateNames, writtenOrEscaped);
|
|
1188
1376
|
collectTemplateEscapes(ast.fragment, stateNames, writtenOrEscaped);
|
|
1377
|
+
collectDirectiveEscapes(ast.fragment, stateNames, writtenOrEscaped);
|
|
1189
1378
|
}
|
|
1190
1379
|
for (const d of stateDecls) {
|
|
1191
1380
|
if (!writtenOrEscaped.has(d.name)) constableStates.push(d);
|
|
@@ -1225,6 +1414,29 @@ function parseComponentFacts(source, filename) {
|
|
|
1225
1414
|
if (reassigned && !dirty) rawableStates.push(c);
|
|
1226
1415
|
}
|
|
1227
1416
|
}
|
|
1417
|
+
const builtinCandidates = /* @__PURE__ */ new Map();
|
|
1418
|
+
for (const stmt of program.body ?? []) {
|
|
1419
|
+
if (stmt?.type !== "VariableDeclaration") continue;
|
|
1420
|
+
for (const d of stmt.declarations ?? []) {
|
|
1421
|
+
if (d?.id?.type !== "Identifier" || !d.init || !isPlainStateCall(d.init)) continue;
|
|
1422
|
+
const arg = unwrapTs(d.init.arguments?.[0]);
|
|
1423
|
+
if (arg?.type === "NewExpression" && arg.callee?.type === "Identifier" && BUILTIN_STATE_TYPES.has(arg.callee.name)) {
|
|
1424
|
+
builtinCandidates.set(d.id.name, { type: arg.callee.name, line: lineOf(source, d.start) });
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
if (builtinCandidates.size > 0) {
|
|
1429
|
+
const types = new Map([...builtinCandidates].map(([n, meta]) => [n, meta.type]));
|
|
1430
|
+
const mutatedBuiltins = /* @__PURE__ */ new Set();
|
|
1431
|
+
const reassignedBuiltins = /* @__PURE__ */ new Set();
|
|
1432
|
+
collectBuiltinStateSignals(program, types, mutatedBuiltins, reassignedBuiltins);
|
|
1433
|
+
if (ast.fragment) collectBuiltinStateSignals(ast.fragment, types, mutatedBuiltins, reassignedBuiltins);
|
|
1434
|
+
for (const [name, meta] of builtinCandidates) {
|
|
1435
|
+
if (mutatedBuiltins.has(name) && !reassignedBuiltins.has(name)) {
|
|
1436
|
+
nonreactiveBuiltinStates.push({ name, type: meta.type, line: meta.line });
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1228
1440
|
let moduleExtra;
|
|
1229
1441
|
if (moduleProgram) {
|
|
1230
1442
|
const moduleBrowserImports = collectBrowserGuardImports(moduleProgram);
|
|
@@ -1252,6 +1464,9 @@ function parseComponentFacts(source, filename) {
|
|
|
1252
1464
|
mutatedProps,
|
|
1253
1465
|
stalePropDerivations,
|
|
1254
1466
|
rawableStates,
|
|
1467
|
+
nonreactiveBuiltinStates,
|
|
1468
|
+
checkableBindValues,
|
|
1469
|
+
basePathLinks,
|
|
1255
1470
|
orphanEffects,
|
|
1256
1471
|
orphanLifecycleCalls,
|
|
1257
1472
|
browserGlobalRefs,
|
|
@@ -1277,6 +1492,9 @@ function emptyComponentFacts(file) {
|
|
|
1277
1492
|
mutatedProps: [],
|
|
1278
1493
|
stalePropDerivations: [],
|
|
1279
1494
|
rawableStates: [],
|
|
1495
|
+
nonreactiveBuiltinStates: [],
|
|
1496
|
+
checkableBindValues: [],
|
|
1497
|
+
basePathLinks: [],
|
|
1280
1498
|
orphanEffects: [],
|
|
1281
1499
|
orphanLifecycleCalls: [],
|
|
1282
1500
|
browserGlobalRefs: [],
|
|
@@ -1413,6 +1631,22 @@ function collectAwaits(node, out = []) {
|
|
|
1413
1631
|
}
|
|
1414
1632
|
return out;
|
|
1415
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
|
+
}
|
|
1416
1650
|
function isParentCall(arg) {
|
|
1417
1651
|
const e = unwrapTs(arg);
|
|
1418
1652
|
if (e?.type !== "CallExpression") return false;
|
|
@@ -1600,6 +1834,7 @@ function parseKitModuleFacts(source, filename) {
|
|
|
1600
1834
|
runesModuleImports,
|
|
1601
1835
|
lifecycleCalls,
|
|
1602
1836
|
browserGlobalRefs,
|
|
1837
|
+
basePathLinks: [],
|
|
1603
1838
|
suppressions
|
|
1604
1839
|
};
|
|
1605
1840
|
}
|
|
@@ -1724,6 +1959,15 @@ function parseKitModuleFacts(source, filename) {
|
|
|
1724
1959
|
}
|
|
1725
1960
|
});
|
|
1726
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
|
+
}
|
|
1727
1971
|
return {
|
|
1728
1972
|
moduleStateReassignments: byLine(moduleStateReassignments),
|
|
1729
1973
|
importedStateWrites: byLine(importedStateWrites),
|
|
@@ -1731,6 +1975,7 @@ function parseKitModuleFacts(source, filename) {
|
|
|
1731
1975
|
runesModuleImports: byLine(runesModuleImports),
|
|
1732
1976
|
lifecycleCalls: byLine(lifecycleCalls),
|
|
1733
1977
|
browserGlobalRefs: byLine(browserGlobalRefs),
|
|
1978
|
+
basePathLinks: byLine(basePathLinks),
|
|
1734
1979
|
...ssrOptOut ? { ssrDisabled: { line: Math.max(0, ssrOptOut.line - 1) } } : {},
|
|
1735
1980
|
...csrOptOut ? { csrDisabled: { line: Math.max(0, csrOptOut.line - 1) } } : {},
|
|
1736
1981
|
...waterfalls.dependentLines.length > 0 || waterfalls.independentLines.length > 0 ? { loadWaterfalls: waterfalls } : {},
|
|
@@ -1749,6 +1994,7 @@ function emptyKitModuleFacts(file, kind) {
|
|
|
1749
1994
|
runesModuleImports: [],
|
|
1750
1995
|
lifecycleCalls: [],
|
|
1751
1996
|
browserGlobalRefs: [],
|
|
1997
|
+
basePathLinks: [],
|
|
1752
1998
|
suppressions: []
|
|
1753
1999
|
};
|
|
1754
2000
|
}
|
|
@@ -1778,7 +2024,7 @@ async function collectKitModuleFacts(rt, cwd) {
|
|
|
1778
2024
|
);
|
|
1779
2025
|
}
|
|
1780
2026
|
|
|
1781
|
-
// src/
|
|
2027
|
+
// src/config-object.ts
|
|
1782
2028
|
function propOf(obj, name) {
|
|
1783
2029
|
let found;
|
|
1784
2030
|
for (const p of obj.properties) {
|
|
@@ -1833,6 +2079,8 @@ function resolveConfigObject(program) {
|
|
|
1833
2079
|
if (!exported) return void 0;
|
|
1834
2080
|
return unwrapToObjectExpression(exported, collectTopLevelBindings(program));
|
|
1835
2081
|
}
|
|
2082
|
+
|
|
2083
|
+
// src/vite-config-parse.ts
|
|
1836
2084
|
function findMinifyDisabled(source) {
|
|
1837
2085
|
let program;
|
|
1838
2086
|
let wrapped;
|
|
@@ -1853,6 +2101,79 @@ function findMinifyDisabled(source) {
|
|
|
1853
2101
|
return { line: Math.max(0, lineOf(wrapped, minify.start) - 1) };
|
|
1854
2102
|
}
|
|
1855
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
|
+
|
|
1856
2177
|
// src/project-paths.ts
|
|
1857
2178
|
var ROBOTS_SOURCE_PATHS = [
|
|
1858
2179
|
"static/robots.txt",
|
|
@@ -1864,6 +2185,15 @@ var SITEMAP_SOURCE_PATHS = [
|
|
|
1864
2185
|
"src/routes/sitemap.xml/+server.ts",
|
|
1865
2186
|
"src/routes/sitemap.xml/+server.js"
|
|
1866
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"];
|
|
1867
2197
|
|
|
1868
2198
|
// src/rule.ts
|
|
1869
2199
|
function docsUrlFor(id) {
|
|
@@ -1933,7 +2263,7 @@ function detect(head, match) {
|
|
|
1933
2263
|
return tag ? { presence: tag.presence, value: tag.value } : { presence: "none", value: "absent" };
|
|
1934
2264
|
}
|
|
1935
2265
|
function headTagRule(opts) {
|
|
1936
|
-
const
|
|
2266
|
+
const docsUrl8 = docsUrlFor(opts.id);
|
|
1937
2267
|
return {
|
|
1938
2268
|
id: opts.id,
|
|
1939
2269
|
title: opts.title,
|
|
@@ -1956,7 +2286,7 @@ function headTagRule(opts) {
|
|
|
1956
2286
|
location: head.file,
|
|
1957
2287
|
message,
|
|
1958
2288
|
recommendation: opts.recommendation,
|
|
1959
|
-
docsUrl:
|
|
2289
|
+
docsUrl: docsUrl8,
|
|
1960
2290
|
// Copy per finding: opts.fix is a rule-level template shared across all
|
|
1961
2291
|
// results this rule emits; a fresh object keeps findings independent.
|
|
1962
2292
|
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
@@ -2149,7 +2479,7 @@ var seoHtmlLang = {
|
|
|
2149
2479
|
|
|
2150
2480
|
// src/rules/perf/image-rule.ts
|
|
2151
2481
|
function imageRule(opts) {
|
|
2152
|
-
const
|
|
2482
|
+
const docsUrl8 = docsUrlFor(opts.id);
|
|
2153
2483
|
const category = opts.category ?? "performance";
|
|
2154
2484
|
return {
|
|
2155
2485
|
id: opts.id,
|
|
@@ -2173,7 +2503,7 @@ function imageRule(opts) {
|
|
|
2173
2503
|
route: route.route,
|
|
2174
2504
|
message: opts.label,
|
|
2175
2505
|
recommendation: opts.recommendation,
|
|
2176
|
-
docsUrl:
|
|
2506
|
+
docsUrl: docsUrl8
|
|
2177
2507
|
});
|
|
2178
2508
|
continue;
|
|
2179
2509
|
}
|
|
@@ -2188,7 +2518,7 @@ function imageRule(opts) {
|
|
|
2188
2518
|
...img.line > 0 ? { line: img.line } : {},
|
|
2189
2519
|
message: `Missing ${opts.label}`,
|
|
2190
2520
|
recommendation: opts.recommendation,
|
|
2191
|
-
docsUrl:
|
|
2521
|
+
docsUrl: docsUrl8,
|
|
2192
2522
|
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
2193
2523
|
});
|
|
2194
2524
|
}
|
|
@@ -2248,7 +2578,7 @@ var performanceResponsiveImage = imageRule({
|
|
|
2248
2578
|
|
|
2249
2579
|
// src/rules/perf/link-rule.ts
|
|
2250
2580
|
function linkRule(opts) {
|
|
2251
|
-
const
|
|
2581
|
+
const docsUrl8 = docsUrlFor(opts.id);
|
|
2252
2582
|
return {
|
|
2253
2583
|
id: opts.id,
|
|
2254
2584
|
title: opts.title,
|
|
@@ -2272,7 +2602,7 @@ function linkRule(opts) {
|
|
|
2272
2602
|
route: head.route,
|
|
2273
2603
|
message: opts.label,
|
|
2274
2604
|
recommendation: opts.recommendation,
|
|
2275
|
-
docsUrl:
|
|
2605
|
+
docsUrl: docsUrl8
|
|
2276
2606
|
});
|
|
2277
2607
|
continue;
|
|
2278
2608
|
}
|
|
@@ -2289,7 +2619,7 @@ function linkRule(opts) {
|
|
|
2289
2619
|
location: tag.file ?? head.file,
|
|
2290
2620
|
message: `Missing ${opts.label}`,
|
|
2291
2621
|
recommendation: opts.recommendation,
|
|
2292
|
-
docsUrl:
|
|
2622
|
+
docsUrl: docsUrl8,
|
|
2293
2623
|
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
2294
2624
|
});
|
|
2295
2625
|
}
|
|
@@ -2436,10 +2766,197 @@ var performanceRenderBlockingScript = {
|
|
|
2436
2766
|
}
|
|
2437
2767
|
};
|
|
2438
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
|
+
|
|
2439
2955
|
// src/rules/perf/preconnect.ts
|
|
2440
2956
|
var docsUrl3 = docsUrlFor("performance/preconnect");
|
|
2441
2957
|
var recommendation3 = 'Add <link rel="preconnect"> (or dns-prefetch) for the third-party origin so the connection is set up early.';
|
|
2442
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] } };
|
|
2443
2960
|
function hostOf(href) {
|
|
2444
2961
|
const m = /^(?:https?:)?\/\/([^/?#]+)/i.exec(href);
|
|
2445
2962
|
return m ? m[1].toLowerCase() : void 0;
|
|
@@ -2456,17 +2973,30 @@ var performancePreconnect = {
|
|
|
2456
2973
|
snippet: '<link rel="preconnect" href="https://fonts.googleapis.com" />',
|
|
2457
2974
|
lang: "html"
|
|
2458
2975
|
},
|
|
2976
|
+
options: OPTIONS,
|
|
2459
2977
|
async check(ctx) {
|
|
2460
2978
|
const out = [];
|
|
2979
|
+
const compiled = compileOverrides(ctx.config);
|
|
2461
2980
|
for (const head of ctx.heads) {
|
|
2462
2981
|
const referenced = /* @__PURE__ */ new Map();
|
|
2463
2982
|
const covered = /* @__PURE__ */ new Set();
|
|
2464
2983
|
for (const tag of head.tags) {
|
|
2465
2984
|
if (tag.kind !== "link" && tag.kind !== "script" || typeof tag.href !== "string") continue;
|
|
2466
2985
|
const host = hostOf(tag.href);
|
|
2467
|
-
if (!host
|
|
2468
|
-
if (tag.kind === "link" && (tag.rel === "preconnect" || tag.rel === "dns-prefetch"))
|
|
2469
|
-
|
|
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);
|
|
2470
3000
|
}
|
|
2471
3001
|
if (referenced.size === 0) continue;
|
|
2472
3002
|
const missing = [...referenced].filter(([host]) => !covered.has(host));
|
|
@@ -2517,7 +3047,7 @@ var seoIndexability = {
|
|
|
2517
3047
|
rationale: "A noindex directive removes the page from search results; an accidental noindex on a public route silently deindexes it.",
|
|
2518
3048
|
fix: FIX5,
|
|
2519
3049
|
async check(ctx) {
|
|
2520
|
-
const
|
|
3050
|
+
const docsUrl8 = docsUrlFor("seo/indexability");
|
|
2521
3051
|
const out = [];
|
|
2522
3052
|
for (const head of ctx.heads) {
|
|
2523
3053
|
const noindexed = head.tags.some((t) => t.kind === "meta" && t.name === "robots" && t.noindex === true);
|
|
@@ -2532,7 +3062,7 @@ var seoIndexability = {
|
|
|
2532
3062
|
location: head.file,
|
|
2533
3063
|
message: "Route is noindex \u2014 verify this is intentional",
|
|
2534
3064
|
recommendation: 'If this route should be indexed, remove noindex from its <meta name="robots">.',
|
|
2535
|
-
docsUrl:
|
|
3065
|
+
docsUrl: docsUrl8,
|
|
2536
3066
|
fix: { ...FIX5 }
|
|
2537
3067
|
});
|
|
2538
3068
|
}
|
|
@@ -2783,7 +3313,7 @@ function jsonldTags(head) {
|
|
|
2783
3313
|
return head.tags.filter((t) => t.kind === "jsonld" && typeof t.jsonld === "string");
|
|
2784
3314
|
}
|
|
2785
3315
|
function jsonldRule(opts) {
|
|
2786
|
-
const
|
|
3316
|
+
const docsUrl8 = docsUrlFor(opts.id);
|
|
2787
3317
|
return {
|
|
2788
3318
|
id: opts.id,
|
|
2789
3319
|
title: opts.title,
|
|
@@ -2811,7 +3341,7 @@ function jsonldRule(opts) {
|
|
|
2811
3341
|
location: head.file,
|
|
2812
3342
|
message: problem,
|
|
2813
3343
|
recommendation: opts.recommendation,
|
|
2814
|
-
docsUrl:
|
|
3344
|
+
docsUrl: docsUrl8,
|
|
2815
3345
|
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
2816
3346
|
} : {
|
|
2817
3347
|
id: opts.id,
|
|
@@ -2821,7 +3351,7 @@ function jsonldRule(opts) {
|
|
|
2821
3351
|
route: head.route,
|
|
2822
3352
|
message: opts.label,
|
|
2823
3353
|
recommendation: opts.recommendation,
|
|
2824
|
-
docsUrl:
|
|
3354
|
+
docsUrl: docsUrl8
|
|
2825
3355
|
}
|
|
2826
3356
|
);
|
|
2827
3357
|
}
|
|
@@ -2845,7 +3375,7 @@ var seoJsonLdValidity = {
|
|
|
2845
3375
|
lang: "svelte"
|
|
2846
3376
|
},
|
|
2847
3377
|
async check(ctx) {
|
|
2848
|
-
const
|
|
3378
|
+
const docsUrl8 = docsUrlFor("seo/json-ld-validity");
|
|
2849
3379
|
const out = [];
|
|
2850
3380
|
for (const head of ctx.heads) {
|
|
2851
3381
|
for (const tag of jsonldTags(head)) {
|
|
@@ -2864,7 +3394,7 @@ var seoJsonLdValidity = {
|
|
|
2864
3394
|
location: head.file,
|
|
2865
3395
|
message: problem,
|
|
2866
3396
|
recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
|
|
2867
|
-
docsUrl:
|
|
3397
|
+
docsUrl: docsUrl8,
|
|
2868
3398
|
fix: { ...seoJsonLdValidity.fix }
|
|
2869
3399
|
} : {
|
|
2870
3400
|
id: "seo/json-ld-validity",
|
|
@@ -2874,7 +3404,7 @@ var seoJsonLdValidity = {
|
|
|
2874
3404
|
route: head.route,
|
|
2875
3405
|
message: "JSON-LD validity",
|
|
2876
3406
|
recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
|
|
2877
|
-
docsUrl:
|
|
3407
|
+
docsUrl: docsUrl8
|
|
2878
3408
|
}
|
|
2879
3409
|
);
|
|
2880
3410
|
}
|
|
@@ -2985,7 +3515,11 @@ function visibleLength(s) {
|
|
|
2985
3515
|
|
|
2986
3516
|
// src/rules/seo/length-rule.ts
|
|
2987
3517
|
function lengthRule(opts) {
|
|
2988
|
-
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
|
+
};
|
|
2989
3523
|
return {
|
|
2990
3524
|
id: opts.id,
|
|
2991
3525
|
title: opts.title,
|
|
@@ -2993,15 +3527,22 @@ function lengthRule(opts) {
|
|
|
2993
3527
|
severity: "info",
|
|
2994
3528
|
scope: "route",
|
|
2995
3529
|
rationale: opts.rationale,
|
|
3530
|
+
options: spec,
|
|
2996
3531
|
async check(ctx) {
|
|
2997
3532
|
const out = [];
|
|
3533
|
+
const compiled = compileOverrides(ctx.config);
|
|
2998
3534
|
for (const head of ctx.heads) {
|
|
2999
3535
|
const tag = head.tags.find(opts.match);
|
|
3000
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;
|
|
3001
3542
|
const len = visibleLength(tag.text);
|
|
3002
3543
|
let problem;
|
|
3003
|
-
if (len <
|
|
3004
|
-
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})`;
|
|
3005
3546
|
out.push(
|
|
3006
3547
|
problem ? {
|
|
3007
3548
|
id: opts.id,
|
|
@@ -3009,10 +3550,10 @@ function lengthRule(opts) {
|
|
|
3009
3550
|
severity: "info",
|
|
3010
3551
|
detection: PENALIZED,
|
|
3011
3552
|
route: head.route,
|
|
3012
|
-
location
|
|
3553
|
+
location,
|
|
3013
3554
|
message: problem,
|
|
3014
|
-
recommendation:
|
|
3015
|
-
docsUrl:
|
|
3555
|
+
recommendation: recommendation8,
|
|
3556
|
+
docsUrl: docsUrl8
|
|
3016
3557
|
} : {
|
|
3017
3558
|
id: opts.id,
|
|
3018
3559
|
category: "seo",
|
|
@@ -3020,8 +3561,8 @@ function lengthRule(opts) {
|
|
|
3020
3561
|
detection: PASS,
|
|
3021
3562
|
route: head.route,
|
|
3022
3563
|
message: opts.label,
|
|
3023
|
-
recommendation:
|
|
3024
|
-
docsUrl:
|
|
3564
|
+
recommendation: recommendation8,
|
|
3565
|
+
docsUrl: docsUrl8
|
|
3025
3566
|
}
|
|
3026
3567
|
);
|
|
3027
3568
|
}
|
|
@@ -3031,28 +3572,32 @@ function lengthRule(opts) {
|
|
|
3031
3572
|
}
|
|
3032
3573
|
|
|
3033
3574
|
// src/rules/seo/title-length.ts
|
|
3575
|
+
var MIN = 30;
|
|
3576
|
+
var MAX = 60;
|
|
3034
3577
|
var seoTitleLength = lengthRule({
|
|
3035
3578
|
id: "seo/title-length",
|
|
3036
3579
|
title: "Title length",
|
|
3037
3580
|
label: "Title length",
|
|
3038
3581
|
noun: "Title",
|
|
3039
3582
|
match: (t) => t.kind === "title",
|
|
3040
|
-
min:
|
|
3041
|
-
max:
|
|
3042
|
-
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.`,
|
|
3043
3586
|
rationale: "A title that is too short wastes the strongest on-page signal; one that is too long is truncated in the SERP."
|
|
3044
3587
|
});
|
|
3045
3588
|
|
|
3046
3589
|
// src/rules/seo/description-length.ts
|
|
3590
|
+
var MIN2 = 70;
|
|
3591
|
+
var MAX2 = 160;
|
|
3047
3592
|
var seoDescriptionLength = lengthRule({
|
|
3048
3593
|
id: "seo/description-length",
|
|
3049
3594
|
title: "Description length",
|
|
3050
3595
|
label: "Description length",
|
|
3051
3596
|
noun: "Description",
|
|
3052
3597
|
match: (t) => t.kind === "meta" && t.name === "description",
|
|
3053
|
-
min:
|
|
3054
|
-
max:
|
|
3055
|
-
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.`,
|
|
3056
3601
|
rationale: "A description that is too short under-uses the SERP snippet; one that is too long is truncated by search engines."
|
|
3057
3602
|
});
|
|
3058
3603
|
|
|
@@ -3202,7 +3747,7 @@ var seoSingleH1 = {
|
|
|
3202
3747
|
|
|
3203
3748
|
// src/rules/seo/uniqueness-rule.ts
|
|
3204
3749
|
function uniquenessRule(opts) {
|
|
3205
|
-
const
|
|
3750
|
+
const docsUrl8 = docsUrlFor(opts.id);
|
|
3206
3751
|
return {
|
|
3207
3752
|
id: opts.id,
|
|
3208
3753
|
title: opts.title,
|
|
@@ -3232,7 +3777,7 @@ function uniquenessRule(opts) {
|
|
|
3232
3777
|
location: e.file,
|
|
3233
3778
|
message: `${opts.noun} is duplicated across ${n} routes`,
|
|
3234
3779
|
recommendation: opts.recommendation,
|
|
3235
|
-
docsUrl:
|
|
3780
|
+
docsUrl: docsUrl8
|
|
3236
3781
|
} : {
|
|
3237
3782
|
id: opts.id,
|
|
3238
3783
|
category: "seo",
|
|
@@ -3241,7 +3786,7 @@ function uniquenessRule(opts) {
|
|
|
3241
3786
|
route: e.route,
|
|
3242
3787
|
message: opts.label,
|
|
3243
3788
|
recommendation: opts.recommendation,
|
|
3244
|
-
docsUrl:
|
|
3789
|
+
docsUrl: docsUrl8
|
|
3245
3790
|
};
|
|
3246
3791
|
});
|
|
3247
3792
|
}
|
|
@@ -3329,7 +3874,7 @@ function isSuppressed(m, ruleId, line) {
|
|
|
3329
3874
|
return (m.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
|
|
3330
3875
|
}
|
|
3331
3876
|
function kitModuleRule(opts) {
|
|
3332
|
-
const
|
|
3877
|
+
const docsUrl8 = docsUrlFor(opts.id);
|
|
3333
3878
|
const severity = opts.severity ?? "warning";
|
|
3334
3879
|
return {
|
|
3335
3880
|
id: opts.id,
|
|
@@ -3353,7 +3898,7 @@ function kitModuleRule(opts) {
|
|
|
3353
3898
|
route: m.file,
|
|
3354
3899
|
message: opts.label,
|
|
3355
3900
|
recommendation: opts.recommendation,
|
|
3356
|
-
docsUrl:
|
|
3901
|
+
docsUrl: docsUrl8
|
|
3357
3902
|
});
|
|
3358
3903
|
continue;
|
|
3359
3904
|
}
|
|
@@ -3368,7 +3913,7 @@ function kitModuleRule(opts) {
|
|
|
3368
3913
|
...b.line > 0 ? { line: b.line } : {},
|
|
3369
3914
|
message: b.message,
|
|
3370
3915
|
recommendation: opts.recommendation,
|
|
3371
|
-
docsUrl:
|
|
3916
|
+
docsUrl: docsUrl8,
|
|
3372
3917
|
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
3373
3918
|
});
|
|
3374
3919
|
}
|
|
@@ -3404,7 +3949,7 @@ function isSuppressed2(c, ruleId, line) {
|
|
|
3404
3949
|
return (c.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
|
|
3405
3950
|
}
|
|
3406
3951
|
function componentRule(opts) {
|
|
3407
|
-
const
|
|
3952
|
+
const docsUrl8 = docsUrlFor(opts.id);
|
|
3408
3953
|
const severity = opts.severity ?? "warning";
|
|
3409
3954
|
return {
|
|
3410
3955
|
id: opts.id,
|
|
@@ -3414,11 +3959,15 @@ function componentRule(opts) {
|
|
|
3414
3959
|
scope: "component",
|
|
3415
3960
|
rationale: opts.rationale,
|
|
3416
3961
|
...opts.fix ? { fix: opts.fix } : {},
|
|
3962
|
+
...opts.options ? { options: opts.options } : {},
|
|
3417
3963
|
async check(ctx) {
|
|
3418
3964
|
const out = [];
|
|
3965
|
+
const compiled = compileOverrides(ctx.config);
|
|
3419
3966
|
for (const c of ctx.components ?? []) {
|
|
3420
|
-
|
|
3421
|
-
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)));
|
|
3422
3971
|
if (bad.length === 0) {
|
|
3423
3972
|
out.push({
|
|
3424
3973
|
id: opts.id,
|
|
@@ -3427,8 +3976,8 @@ function componentRule(opts) {
|
|
|
3427
3976
|
detection: PASS3,
|
|
3428
3977
|
route: c.file,
|
|
3429
3978
|
message: opts.label,
|
|
3430
|
-
recommendation:
|
|
3431
|
-
docsUrl:
|
|
3979
|
+
recommendation: recommendation8,
|
|
3980
|
+
docsUrl: docsUrl8
|
|
3432
3981
|
});
|
|
3433
3982
|
continue;
|
|
3434
3983
|
}
|
|
@@ -3442,8 +3991,8 @@ function componentRule(opts) {
|
|
|
3442
3991
|
location: c.file,
|
|
3443
3992
|
...b.line > 0 ? { line: b.line } : {},
|
|
3444
3993
|
message: b.message,
|
|
3445
|
-
recommendation:
|
|
3446
|
-
docsUrl:
|
|
3994
|
+
recommendation: recommendation8,
|
|
3995
|
+
docsUrl: docsUrl8,
|
|
3447
3996
|
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
3448
3997
|
});
|
|
3449
3998
|
}
|
|
@@ -3526,12 +4075,12 @@ var correctnessPropMutation = componentRule({
|
|
|
3526
4075
|
title: "Mutated non-bindable prop",
|
|
3527
4076
|
category: "correctness",
|
|
3528
4077
|
label: "Prop mutation",
|
|
3529
|
-
recommendation: "
|
|
3530
|
-
rationale: "Svelte's docs say plainly: don't mutate props unless they are $bindable. A plain-object prop mutation is a silent no-op (the object isn't a state proxy); a reactive-state-proxy prop mutation works but triggers the ownership_invalid_mutation dev warning only when that code path actually runs. Neither is caught by the compiler, so this rule catches both statically.",
|
|
4078
|
+
recommendation: "Runes mode: clone the value before mutating it, communicate the change via a callback prop, or declare the prop $bindable if the parent and child should share it. Legacy mode: reassign the prop after mutating it (e.g. `list = list`) so Svelte's assignment-based reactivity picks up the change.",
|
|
4079
|
+
rationale: "Svelte's docs say plainly: don't mutate props unless they are $bindable. A plain-object prop mutation is a silent no-op (the object isn't a state proxy); a reactive-state-proxy prop mutation works but triggers the ownership_invalid_mutation dev warning only when that code path actually runs. In legacy mode, mutating methods like .push()/.splice() never trigger an update on their own \u2014 Svelte's reactivity there is based on assignments, not mutations. Neither case is caught by the compiler, so this rule catches both statically.",
|
|
3531
4080
|
applies: (c) => c.mutatedProps.length > 0,
|
|
3532
4081
|
bad: (c) => c.mutatedProps.map((m) => ({
|
|
3533
4082
|
line: m.line,
|
|
3534
|
-
message: `Prop "${m.name}" is mutated, but it is not declared $bindable`
|
|
4083
|
+
message: m.legacy ? `Prop "${m.name}" is mutated directly \u2014 Svelte's legacy-mode reactivity is assignment-based, so this alone will not update the UI. Reassign it after mutating (e.g. "${m.name} = ${m.name}").` : `Prop "${m.name}" is mutated, but it is not declared $bindable`
|
|
3535
4084
|
}))
|
|
3536
4085
|
});
|
|
3537
4086
|
|
|
@@ -3542,15 +4091,53 @@ var correctnessStalePropDerivation = componentRule({
|
|
|
3542
4091
|
category: "correctness",
|
|
3543
4092
|
severity: "warning",
|
|
3544
4093
|
label: "Props derived reactively",
|
|
3545
|
-
recommendation: "Wrap the computation in $derived(...)
|
|
3546
|
-
rationale: "Svelte's guidance is to treat props as though they will change: a plain `let color = type === 'danger' ? 'red' : 'green'` freezes the first render's value, so the UI silently stops tracking the parent when the prop changes. $derived keeps the computation live at no cost.",
|
|
4094
|
+
recommendation: "Wrap the computation in $derived(...) (or $derived.by(() => ...) for a function body) in runes-mode components; prefix the assignment with $: in legacy-mode components.",
|
|
4095
|
+
rationale: "Svelte's guidance is to treat props as though they will change: a plain `let color = type === 'danger' ? 'red' : 'green'` freezes the first render's value, so the UI silently stops tracking the parent when the prop changes. In runes mode, $derived keeps the computation live at no cost; in legacy mode (export let props), a $: reactive statement does the same job.",
|
|
3547
4096
|
fix: {
|
|
3548
|
-
description: "Wrap the prop-derived computation in $derived(...) (or $derived.by(() => ...) for a function body), keeping the same expression."
|
|
4097
|
+
description: "Wrap the prop-derived computation in $derived(...) (or $derived.by(() => ...) for a function body) in runes mode, or prefix the assignment with $: in legacy mode, keeping the same expression."
|
|
3549
4098
|
},
|
|
3550
4099
|
applies: (c) => c.stalePropDerivations.length > 0,
|
|
3551
4100
|
bad: (c) => c.stalePropDerivations.map((s) => ({
|
|
3552
4101
|
line: s.line,
|
|
3553
|
-
message: `"${s.name}" is computed from a prop once, at initialization \u2014 it will not update when the prop changes. Wrap it in $derived.`
|
|
4102
|
+
message: s.legacy ? `"${s.name}" is computed from a prop once, at initialization \u2014 it will not update when the prop changes. Prefix the assignment with $: to make it a reactive statement.` : `"${s.name}" is computed from a prop once, at initialization \u2014 it will not update when the prop changes. Wrap it in $derived.`
|
|
4103
|
+
}))
|
|
4104
|
+
});
|
|
4105
|
+
|
|
4106
|
+
// src/rules/correctness/nonreactive-builtin-state.ts
|
|
4107
|
+
var correctnessNonreactiveBuiltinState = componentRule({
|
|
4108
|
+
id: "correctness/nonreactive-builtin-state",
|
|
4109
|
+
title: "Non-reactive built-in in $state",
|
|
4110
|
+
category: "correctness",
|
|
4111
|
+
severity: "warning",
|
|
4112
|
+
label: "Reactive collections in $state",
|
|
4113
|
+
recommendation: "Import the reactive equivalent from 'svelte/reactivity' (SvelteMap, SvelteSet, SvelteDate, SvelteURL, SvelteURLSearchParams) and construct that instead.",
|
|
4114
|
+
rationale: "$state deep-proxies plain objects and arrays only; built-in collection, date, and URL instances stay untracked, so property-level changes never reach effects, deriveds, or the template. Svelte's own answer is the drop-in classes in svelte/reactivity.",
|
|
4115
|
+
fix: {
|
|
4116
|
+
description: "Import Svelte<Type> from 'svelte/reactivity' and replace new <Type>(...) with new Svelte<Type>(...) \u2014 the API is identical."
|
|
4117
|
+
},
|
|
4118
|
+
applies: (c) => c.nonreactiveBuiltinStates.length > 0,
|
|
4119
|
+
bad: (c) => c.nonreactiveBuiltinStates.map((s) => ({
|
|
4120
|
+
line: s.line,
|
|
4121
|
+
message: `"${s.name}" is a plain ${s.type} in $state \u2014 its mutations are not tracked, so the UI silently stops updating when it changes. Use Svelte${s.type} from 'svelte/reactivity'.`
|
|
4122
|
+
}))
|
|
4123
|
+
});
|
|
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."
|
|
3554
4141
|
}))
|
|
3555
4142
|
});
|
|
3556
4143
|
|
|
@@ -3653,24 +4240,35 @@ var correctnessOrphanLifecycle = {
|
|
|
3653
4240
|
}
|
|
3654
4241
|
};
|
|
3655
4242
|
|
|
3656
|
-
// src/rules/correctness/
|
|
4243
|
+
// src/rules/correctness/base-path-navigation.ts
|
|
3657
4244
|
var PENALIZED5 = { presence: "none", value: "absent" };
|
|
3658
4245
|
var PASS5 = { presence: "own", value: "static" };
|
|
3659
|
-
var ID2 = "correctness/
|
|
4246
|
+
var ID2 = "correctness/base-path-navigation";
|
|
3660
4247
|
var DOCS_URL2 = docsUrlFor(ID2);
|
|
3661
|
-
var LABEL2 = "
|
|
3662
|
-
var RECOMMENDATION2 = "
|
|
3663
|
-
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
|
+
}
|
|
3664
4262
|
function isSuppressed4(suppressions, line) {
|
|
3665
4263
|
return (suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ID2)));
|
|
3666
4264
|
}
|
|
3667
|
-
function emitFile2(out, file,
|
|
3668
|
-
const bad =
|
|
4265
|
+
function emitFile2(out, file, links, suppressions) {
|
|
4266
|
+
const bad = links.filter((l) => !(l.line > 0 && isSuppressed4(suppressions, l.line)));
|
|
3669
4267
|
if (bad.length === 0) {
|
|
3670
4268
|
out.push({
|
|
3671
4269
|
id: ID2,
|
|
3672
4270
|
category: "correctness",
|
|
3673
|
-
severity: "
|
|
4271
|
+
severity: "warning",
|
|
3674
4272
|
detection: PASS5,
|
|
3675
4273
|
route: file,
|
|
3676
4274
|
message: LABEL2,
|
|
@@ -3679,23 +4277,90 @@ function emitFile2(out, file, issues, suppressions) {
|
|
|
3679
4277
|
});
|
|
3680
4278
|
return;
|
|
3681
4279
|
}
|
|
3682
|
-
for (const
|
|
4280
|
+
for (const l of bad) {
|
|
3683
4281
|
out.push({
|
|
3684
4282
|
id: ID2,
|
|
3685
4283
|
category: "correctness",
|
|
3686
|
-
severity: "
|
|
4284
|
+
severity: "warning",
|
|
3687
4285
|
detection: PENALIZED5,
|
|
3688
4286
|
route: file,
|
|
3689
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,
|
|
3690
4355
|
...b.line > 0 ? { line: b.line } : {},
|
|
3691
4356
|
message: b.message,
|
|
3692
|
-
recommendation:
|
|
3693
|
-
docsUrl:
|
|
4357
|
+
recommendation: RECOMMENDATION3,
|
|
4358
|
+
docsUrl: DOCS_URL3
|
|
3694
4359
|
});
|
|
3695
4360
|
}
|
|
3696
4361
|
}
|
|
3697
4362
|
var correctnessServerBrowserGlobal = {
|
|
3698
|
-
id:
|
|
4363
|
+
id: ID3,
|
|
3699
4364
|
title: "Browser global in server module code",
|
|
3700
4365
|
category: "correctness",
|
|
3701
4366
|
severity: "critical",
|
|
@@ -3706,7 +4371,7 @@ var correctnessServerBrowserGlobal = {
|
|
|
3706
4371
|
for (const c of ctx.components ?? []) {
|
|
3707
4372
|
const refs = (c.browserGlobalRefs ?? []).filter((r) => r.context === "module");
|
|
3708
4373
|
if (refs.length === 0) continue;
|
|
3709
|
-
|
|
4374
|
+
emitFile3(
|
|
3710
4375
|
out,
|
|
3711
4376
|
c.file,
|
|
3712
4377
|
refs.map((r) => ({ line: r.line, message: moduleMessage(r.name) })),
|
|
@@ -3716,7 +4381,7 @@ var correctnessServerBrowserGlobal = {
|
|
|
3716
4381
|
for (const m of ctx.kitModules ?? []) {
|
|
3717
4382
|
const refs = m.browserGlobalRefs ?? [];
|
|
3718
4383
|
if (refs.length === 0) continue;
|
|
3719
|
-
|
|
4384
|
+
emitFile3(
|
|
3720
4385
|
out,
|
|
3721
4386
|
m.file,
|
|
3722
4387
|
refs.map((r) => ({
|
|
@@ -3832,35 +4497,151 @@ var securitySharedStateImport = kitModuleRule({
|
|
|
3832
4497
|
});
|
|
3833
4498
|
|
|
3834
4499
|
// src/rules/architecture/component-size.ts
|
|
3835
|
-
var MAX_LOC =
|
|
4500
|
+
var MAX_LOC = 200;
|
|
3836
4501
|
var architectureComponentSize = componentRule({
|
|
3837
4502
|
id: "architecture/component-size",
|
|
3838
4503
|
title: "Component size",
|
|
3839
4504
|
category: "architecture",
|
|
3840
4505
|
severity: "info",
|
|
3841
4506
|
label: "Component size",
|
|
3842
|
-
|
|
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.`,
|
|
3843
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.",
|
|
3844
4510
|
applies: (c) => c.loc > 0,
|
|
3845
4511
|
// skip unanalyzable files (loc 0 = read/parse failure), don't PASS them
|
|
3846
|
-
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
|
+
}
|
|
3847
4516
|
});
|
|
3848
4517
|
|
|
3849
4518
|
// src/rules/architecture/prop-count.ts
|
|
3850
|
-
var MAX_PROPS =
|
|
4519
|
+
var MAX_PROPS = 6;
|
|
3851
4520
|
var architecturePropCount = componentRule({
|
|
3852
4521
|
id: "architecture/prop-count",
|
|
3853
4522
|
title: "Prop count",
|
|
3854
4523
|
category: "architecture",
|
|
3855
4524
|
severity: "info",
|
|
3856
4525
|
label: "Prop count",
|
|
3857
|
-
|
|
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.`,
|
|
3858
4528
|
rationale: "A component taking many props is usually doing too much; grouping or splitting keeps its API understandable.",
|
|
3859
4529
|
applies: (c) => c.propCount > 0,
|
|
3860
4530
|
// only components whose props we could count
|
|
3861
|
-
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
|
+
}
|
|
3862
4535
|
});
|
|
3863
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
|
+
|
|
3864
4645
|
// src/rules/perf/heavy-import.ts
|
|
3865
4646
|
var HEAVY_PACKAGES = {
|
|
3866
4647
|
lodash: "import a submodule (lodash/debounce) or use lodash-es for tree-shaking",
|
|
@@ -3874,18 +4655,20 @@ var performanceHeavyImport = componentRule({
|
|
|
3874
4655
|
label: "No heavy imports",
|
|
3875
4656
|
recommendation: "Import a submodule or switch to a lighter, tree-shakeable alternative.",
|
|
3876
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 } },
|
|
3877
4659
|
// ComponentFacts is a public @svelte-vitals/core export — an external caller compiled
|
|
3878
4660
|
// against an older version may still construct one without importSpans. Fall back to the
|
|
3879
4661
|
// line-less `imports` (line: 0, the pre-fix behavior) instead of crashing on `undefined`.
|
|
3880
4662
|
applies: (c) => (c.importSpans ?? c.imports).length > 0,
|
|
3881
|
-
bad: (c) => {
|
|
4663
|
+
bad: (c, o) => {
|
|
4664
|
+
const packages = mapOption(o, "packages");
|
|
3882
4665
|
const seen = /* @__PURE__ */ new Set();
|
|
3883
4666
|
const out = [];
|
|
3884
4667
|
const spans = c.importSpans ?? c.imports.map((source) => ({ source, line: 0 }));
|
|
3885
4668
|
for (const { source: src, line } of spans) {
|
|
3886
|
-
if (!Object.hasOwn(
|
|
4669
|
+
if (!Object.hasOwn(packages, src) || seen.has(src)) continue;
|
|
3887
4670
|
seen.add(src);
|
|
3888
|
-
out.push({ line, message: `Heavy import "${src}" \u2014 ${
|
|
4671
|
+
out.push({ line, message: `Heavy import "${src}" \u2014 ${packages[src]}` });
|
|
3889
4672
|
}
|
|
3890
4673
|
return out;
|
|
3891
4674
|
}
|
|
@@ -3915,13 +4698,13 @@ var performanceNamespaceImport = componentRule({
|
|
|
3915
4698
|
});
|
|
3916
4699
|
|
|
3917
4700
|
// src/rules/perf/minify-disabled.ts
|
|
3918
|
-
var
|
|
4701
|
+
var PENALIZED7 = { presence: "none", value: "absent" };
|
|
3919
4702
|
var MINIFY_DISABLED_FIX = {
|
|
3920
4703
|
description: "Remove the minify: false override from vite.config (Vite minifies with esbuild by default), or scope it to non-production builds.",
|
|
3921
4704
|
snippet: "export default defineConfig({\n build: {\n minify: 'esbuild'\n }\n});",
|
|
3922
4705
|
lang: "ts"
|
|
3923
4706
|
};
|
|
3924
|
-
var
|
|
4707
|
+
var RECOMMENDATION4 = "Remove build.minify: false from vite.config, or scope it to non-production builds if it is intentional.";
|
|
3925
4708
|
var performanceMinifyDisabled = {
|
|
3926
4709
|
id: "performance/minify-disabled",
|
|
3927
4710
|
title: "Minification disabled",
|
|
@@ -3939,11 +4722,11 @@ var performanceMinifyDisabled = {
|
|
|
3939
4722
|
id: "performance/minify-disabled",
|
|
3940
4723
|
category: "performance",
|
|
3941
4724
|
severity: "warning",
|
|
3942
|
-
detection:
|
|
4725
|
+
detection: PENALIZED7,
|
|
3943
4726
|
...hit.file !== void 0 ? { location: hit.file } : {},
|
|
3944
4727
|
...hit.line !== void 0 ? { line: hit.line } : {},
|
|
3945
4728
|
message: "JS/CSS minification is disabled (build.minify: false) \u2014 production bundles ship unminified and several times larger." + provenance,
|
|
3946
|
-
recommendation:
|
|
4729
|
+
recommendation: RECOMMENDATION4,
|
|
3947
4730
|
docsUrl: docsUrlFor("performance/minify-disabled"),
|
|
3948
4731
|
fix: { ...MINIFY_DISABLED_FIX }
|
|
3949
4732
|
}
|
|
@@ -4056,8 +4839,11 @@ var allRules = [
|
|
|
4056
4839
|
correctnessUnmutatedState,
|
|
4057
4840
|
correctnessPropMutation,
|
|
4058
4841
|
correctnessStalePropDerivation,
|
|
4842
|
+
correctnessNonreactiveBuiltinState,
|
|
4843
|
+
correctnessCheckableBindValue,
|
|
4059
4844
|
correctnessOrphanEffect,
|
|
4060
4845
|
correctnessOrphanLifecycle,
|
|
4846
|
+
correctnessBasePathNavigation,
|
|
4061
4847
|
correctnessServerBrowserGlobal,
|
|
4062
4848
|
correctnessInstanceBrowserGlobal,
|
|
4063
4849
|
securityRawHtml,
|
|
@@ -4067,6 +4853,7 @@ var allRules = [
|
|
|
4067
4853
|
securitySharedStateImport,
|
|
4068
4854
|
architectureComponentSize,
|
|
4069
4855
|
architecturePropCount,
|
|
4856
|
+
architecturePrivateScopeImport,
|
|
4070
4857
|
performanceHeavyImport,
|
|
4071
4858
|
performanceNamespaceImport,
|
|
4072
4859
|
performanceMinifyDisabled,
|
|
@@ -4074,6 +4861,15 @@ var allRules = [
|
|
|
4074
4861
|
performanceSequentialAwaits,
|
|
4075
4862
|
performanceStateRaw
|
|
4076
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
|
+
}
|
|
4077
4873
|
function explainRule(id) {
|
|
4078
4874
|
const rule = allRules.find((r) => r.id === id);
|
|
4079
4875
|
if (!rule) return void 0;
|
|
@@ -4084,7 +4880,8 @@ function explainRule(id) {
|
|
|
4084
4880
|
severity: rule.severity,
|
|
4085
4881
|
rationale: rule.rationale,
|
|
4086
4882
|
docsUrl: docsUrlFor(rule.id),
|
|
4087
|
-
...rule.fix ? { fix: rule.fix } : {}
|
|
4883
|
+
...rule.fix ? { fix: rule.fix } : {},
|
|
4884
|
+
...rule.options ? { options: optionInfos(rule.options) } : {}
|
|
4088
4885
|
};
|
|
4089
4886
|
}
|
|
4090
4887
|
|
|
@@ -5303,60 +6100,21 @@ function safeHref(url) {
|
|
|
5303
6100
|
const normalized = url.replace(/\s/g, "").toLowerCase();
|
|
5304
6101
|
return /^https?:\/\//.test(normalized) ? url : null;
|
|
5305
6102
|
}
|
|
5306
|
-
|
|
5307
|
-
// src/config-apply.ts
|
|
5308
|
-
function selectRules(rules, config) {
|
|
5309
|
-
return rules.filter((rule) => config.rules[rule.id] !== "off");
|
|
5310
|
-
}
|
|
5311
|
-
function applyRuleSeverities(results, config) {
|
|
5312
|
-
return results.map((result) => {
|
|
5313
|
-
const setting = config.rules[result.id];
|
|
5314
|
-
return setting && setting !== "off" ? { ...result, severity: setting } : result;
|
|
5315
|
-
});
|
|
5316
|
-
}
|
|
5317
|
-
function routeGlobToRegExp(pattern) {
|
|
5318
|
-
const body = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").split("\0").join(".*");
|
|
5319
|
-
const source = body.endsWith("/.*") ? `${body.slice(0, -3)}(/.*)?` : body;
|
|
5320
|
-
return new RegExp(`^${source}$`);
|
|
5321
|
-
}
|
|
5322
|
-
function toPatterns(globs) {
|
|
5323
|
-
if (globs === void 0) return [];
|
|
5324
|
-
return (Array.isArray(globs) ? globs : [globs]).map(routeGlobToRegExp);
|
|
5325
|
-
}
|
|
5326
|
-
function applyOverrides(results, config) {
|
|
5327
|
-
const overrides = config.overrides;
|
|
5328
|
-
if (!overrides || overrides.length === 0) return results;
|
|
5329
|
-
const compiled = overrides.map((o) => ({
|
|
5330
|
-
routes: toPatterns(o.route),
|
|
5331
|
-
files: toPatterns(o.files),
|
|
5332
|
-
rules: o.rules
|
|
5333
|
-
}));
|
|
5334
|
-
const out = [];
|
|
5335
|
-
for (const result of results) {
|
|
5336
|
-
const { route, location } = result;
|
|
5337
|
-
let setting;
|
|
5338
|
-
for (const o of compiled) {
|
|
5339
|
-
const matched = route !== void 0 && o.routes.some((p) => p.test(route)) || location !== void 0 && o.files.some((p) => p.test(location));
|
|
5340
|
-
if (!matched) continue;
|
|
5341
|
-
const s = o.rules[result.id] ?? o.rules[result.category ?? "seo"];
|
|
5342
|
-
if (s !== void 0) setting = s;
|
|
5343
|
-
}
|
|
5344
|
-
if (setting === void 0) out.push(result);
|
|
5345
|
-
else if (setting !== "off") out.push({ ...result, severity: setting });
|
|
5346
|
-
}
|
|
5347
|
-
return out;
|
|
5348
|
-
}
|
|
5349
6103
|
export {
|
|
5350
6104
|
APP_SCRIPT,
|
|
5351
6105
|
APP_STYLE,
|
|
5352
6106
|
BAND_COLOR,
|
|
6107
|
+
CATEGORIES,
|
|
5353
6108
|
CHILD_NODE_KEYS,
|
|
5354
6109
|
ROBOTS_SOURCE_PATHS,
|
|
5355
6110
|
SITEMAP_SOURCE_PATHS,
|
|
6111
|
+
SVELTE_CONFIG_FILES,
|
|
6112
|
+
VITE_CONFIG_FILES,
|
|
5356
6113
|
allRules,
|
|
5357
6114
|
applyOverrides,
|
|
5358
6115
|
applyRuleSeverities,
|
|
5359
6116
|
architectureComponentSize,
|
|
6117
|
+
architecturePrivateScopeImport,
|
|
5360
6118
|
architecturePropCount,
|
|
5361
6119
|
attrText,
|
|
5362
6120
|
attrTextOf,
|
|
@@ -5367,13 +6125,17 @@ export {
|
|
|
5367
6125
|
classify,
|
|
5368
6126
|
collectComponentFacts,
|
|
5369
6127
|
collectKitModuleFacts,
|
|
6128
|
+
compileOverrides,
|
|
5370
6129
|
computeHealth,
|
|
5371
6130
|
computeScore,
|
|
6131
|
+
correctnessBasePathNavigation,
|
|
6132
|
+
correctnessCheckableBindValue,
|
|
5372
6133
|
correctnessEachIndexKey,
|
|
5373
6134
|
correctnessEachKey,
|
|
5374
6135
|
correctnessEffectAsDerived,
|
|
5375
6136
|
correctnessEffectAsOnMount,
|
|
5376
6137
|
correctnessInstanceBrowserGlobal,
|
|
6138
|
+
correctnessNonreactiveBuiltinState,
|
|
5377
6139
|
correctnessOrphanEffect,
|
|
5378
6140
|
correctnessOrphanLifecycle,
|
|
5379
6141
|
correctnessPropMutation,
|
|
@@ -5390,6 +6152,8 @@ export {
|
|
|
5390
6152
|
escapeHtml,
|
|
5391
6153
|
explainRule,
|
|
5392
6154
|
findAttr,
|
|
6155
|
+
findKitPathsBaseInSvelteConfig,
|
|
6156
|
+
findKitPathsBaseInViteConfig,
|
|
5393
6157
|
findMinifyDisabled,
|
|
5394
6158
|
formatAgentReport,
|
|
5395
6159
|
formatConsoleReport,
|
|
@@ -5401,10 +6165,14 @@ export {
|
|
|
5401
6165
|
hasFailureAtOrAbove,
|
|
5402
6166
|
headTagRule,
|
|
5403
6167
|
imageRule,
|
|
6168
|
+
intOption,
|
|
5404
6169
|
isPenalized,
|
|
5405
6170
|
lineOf,
|
|
5406
6171
|
linkRule,
|
|
6172
|
+
listOption,
|
|
6173
|
+
mapOption,
|
|
5407
6174
|
noColorPalette,
|
|
6175
|
+
overrideMatches,
|
|
5408
6176
|
parseComponentFacts,
|
|
5409
6177
|
parseKitModuleFacts,
|
|
5410
6178
|
performanceFontPreloadCrossorigin,
|
|
@@ -5422,6 +6190,8 @@ export {
|
|
|
5422
6190
|
performanceSequentialAwaits,
|
|
5423
6191
|
performanceStateRaw,
|
|
5424
6192
|
renderAppShell,
|
|
6193
|
+
resolveKitPathsBase,
|
|
6194
|
+
resolveRuleOptions,
|
|
5425
6195
|
resolveRunesModuleSpecifier,
|
|
5426
6196
|
runRules,
|
|
5427
6197
|
safeHref,
|
|
@@ -5465,7 +6235,12 @@ export {
|
|
|
5465
6235
|
seoTitlePresence,
|
|
5466
6236
|
seoTwitterCard,
|
|
5467
6237
|
seoViewport,
|
|
6238
|
+
settingOptions,
|
|
6239
|
+
settingSeverity,
|
|
6240
|
+
shouldSkipRangeCheck,
|
|
5468
6241
|
summarize,
|
|
5469
6242
|
textFromNodes,
|
|
6243
|
+
validateRuleOptions,
|
|
6244
|
+
validateRuleSetting,
|
|
5470
6245
|
valueFromNodes
|
|
5471
6246
|
};
|