@svelte-vitals/core 0.39.0 → 0.40.1
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 +23 -5
- package/dist/index.js +129 -79
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -309,6 +309,13 @@ interface HeadingInfo {
|
|
|
309
309
|
interface ResolvedHeadings {
|
|
310
310
|
route: string;
|
|
311
311
|
headings: HeadingInfo[];
|
|
312
|
+
/**
|
|
313
|
+
* Headings found in child components rendered (transitively) by this route's
|
|
314
|
+
* chain files — source mode only; absent in rendered mode. Kept separate from
|
|
315
|
+
* `headings` because their position in document order is unknown: safe for
|
|
316
|
+
* counting (seo/single-h1), unusable for outline order (seo/heading-level-skip).
|
|
317
|
+
*/
|
|
318
|
+
componentHeadings?: HeadingInfo[];
|
|
312
319
|
}
|
|
313
320
|
|
|
314
321
|
/**
|
|
@@ -479,6 +486,8 @@ interface ComponentFacts {
|
|
|
479
486
|
url: string;
|
|
480
487
|
line: number;
|
|
481
488
|
}[];
|
|
489
|
+
/** Set when the file failed to read or parse and these facts are the empty fallback — the file was NOT analyzed. */
|
|
490
|
+
parseFailed?: true;
|
|
482
491
|
}
|
|
483
492
|
|
|
484
493
|
/** What the per-file parsers produce — `ComponentFacts` minus `file`, with `suppressions` always present. */
|
|
@@ -599,6 +608,8 @@ interface KitModuleFacts {
|
|
|
599
608
|
};
|
|
600
609
|
/** Inline `svelte-vitals-disable-next-line` directives in this file. */
|
|
601
610
|
suppressions: SuppressionDirective[];
|
|
611
|
+
/** Set when the file failed to read or parse and these facts are the empty fallback — the file was NOT analyzed. */
|
|
612
|
+
parseFailed?: true;
|
|
602
613
|
}
|
|
603
614
|
|
|
604
615
|
/**
|
|
@@ -1119,9 +1130,15 @@ declare const seoHreflang: Rule;
|
|
|
1119
1130
|
|
|
1120
1131
|
/**
|
|
1121
1132
|
* seo/single-h1 — Heading hierarchy (single H1). Reads the per-route page-body headings
|
|
1122
|
-
* channel (collected by both providers)
|
|
1123
|
-
*
|
|
1124
|
-
*
|
|
1133
|
+
* channel (collected by both providers), counting `headings` plus `componentHeadings`
|
|
1134
|
+
* (static mode only — headings found transitively in rendered child components) as one
|
|
1135
|
+
* combined list. Zero <h1> (no primary heading) is a `warning`: defensible, a page needs
|
|
1136
|
+
* a primary heading. Two or more is only `info`: a single <h1> is the conventional
|
|
1137
|
+
* signal, but no official source documents a ranking penalty for several (2026-08-09 v1
|
|
1138
|
+
* rule-validity review, P2 #11) — so it's flagged as a style nit, not a defect. Exactly
|
|
1139
|
+
* one passes. A route whose headings were not collected (channel unset) emits nothing. A
|
|
1140
|
+
* global `rules: { 'seo/single-h1': <severity> }` override flattens both arms to one
|
|
1141
|
+
* severity (design, `applyRuleSeverities`).
|
|
1125
1142
|
*/
|
|
1126
1143
|
declare const seoSingleH1: Rule;
|
|
1127
1144
|
|
|
@@ -1179,8 +1196,9 @@ declare const correctnessNonreactiveBuiltinState: Rule;
|
|
|
1179
1196
|
|
|
1180
1197
|
/**
|
|
1181
1198
|
* correctness/checkable-bind-value — bind:value binds the DOM value property. A
|
|
1182
|
-
* checkbox/radio's user interaction toggles checkedness, which bind:value never observes
|
|
1183
|
-
*
|
|
1199
|
+
* checkbox/radio's user interaction toggles checkedness, which bind:value never observes. A
|
|
1200
|
+
* checkbox throws bind_invalid_checkbox_value in dev (silently tracks value instead of
|
|
1201
|
+
* checkedness in prod); a radio throws nothing and its bound state silently never updates.
|
|
1184
1202
|
* bind:checked (single checkbox) / bind:group (checkbox list, radio group) are the correct
|
|
1185
1203
|
* bindings.
|
|
1186
1204
|
*/
|
package/dist/index.js
CHANGED
|
@@ -94,7 +94,7 @@ function attrTextOf(attr) {
|
|
|
94
94
|
// src/component-parse.ts
|
|
95
95
|
function unwrapTs(expr) {
|
|
96
96
|
let cur = expr;
|
|
97
|
-
while (cur.type === "TSSatisfiesExpression" || cur.type === "TSAsExpression" || cur.type === "TSNonNullExpression")
|
|
97
|
+
while (cur !== void 0 && (cur.type === "TSSatisfiesExpression" || cur.type === "TSAsExpression" || cur.type === "TSNonNullExpression"))
|
|
98
98
|
cur = cur.expression;
|
|
99
99
|
return cur;
|
|
100
100
|
}
|
|
@@ -623,6 +623,21 @@ function collectDirectiveEscapes(node, names, acc) {
|
|
|
623
623
|
}
|
|
624
624
|
}
|
|
625
625
|
var RUNE_NAMES = /* @__PURE__ */ new Set(["$state", "$derived", "$effect", "$props", "$bindable", "$inspect", "$host"]);
|
|
626
|
+
function collectImportedLocalNames(program, acc) {
|
|
627
|
+
for (const stmt of program?.body ?? []) {
|
|
628
|
+
if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type") continue;
|
|
629
|
+
for (const s of stmt.specifiers ?? []) {
|
|
630
|
+
if (s?.importKind === "type" || s?.local?.type !== "Identifier") continue;
|
|
631
|
+
acc.add(s.local.name);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
function collectNewExprLocalNames(program, acc) {
|
|
636
|
+
walkEstree(program, (n) => {
|
|
637
|
+
if (n.type !== "VariableDeclarator" || n.id?.type !== "Identifier" || !n.init) return;
|
|
638
|
+
if (unwrapTs(n.init).type === "NewExpression") acc.add(n.id.name);
|
|
639
|
+
});
|
|
640
|
+
}
|
|
626
641
|
function bodyReadsReactive(fn, reactiveNames) {
|
|
627
642
|
let reads = false;
|
|
628
643
|
const visit = (n) => {
|
|
@@ -1398,6 +1413,12 @@ function parseComponentFacts(source, filename) {
|
|
|
1398
1413
|
}
|
|
1399
1414
|
const stateNames = /* @__PURE__ */ new Set();
|
|
1400
1415
|
const reactiveNames = /* @__PURE__ */ new Set();
|
|
1416
|
+
collectImportedLocalNames(program, reactiveNames);
|
|
1417
|
+
collectNewExprLocalNames(program, reactiveNames);
|
|
1418
|
+
if (moduleProgram) {
|
|
1419
|
+
collectImportedLocalNames(moduleProgram, reactiveNames);
|
|
1420
|
+
collectNewExprLocalNames(moduleProgram, reactiveNames);
|
|
1421
|
+
}
|
|
1401
1422
|
const stateDecls = [];
|
|
1402
1423
|
walkEstree(program, (n) => {
|
|
1403
1424
|
if (n.type !== "VariableDeclarator" || !n.init) return;
|
|
@@ -1566,7 +1587,7 @@ async function collectComponentFacts(rt, cwd) {
|
|
|
1566
1587
|
const source = await rt.readFile(rt.join(cwd, rel));
|
|
1567
1588
|
return { file: rel, ...parseComponentFacts(source, rel) };
|
|
1568
1589
|
} catch {
|
|
1569
|
-
return emptyComponentFacts(rel);
|
|
1590
|
+
return { ...emptyComponentFacts(rel), parseFailed: true };
|
|
1570
1591
|
}
|
|
1571
1592
|
})
|
|
1572
1593
|
);
|
|
@@ -2146,7 +2167,7 @@ async function collectKitModuleFacts(rt, cwd, aliases) {
|
|
|
2146
2167
|
const source = await rt.readFile(rt.join(cwd, rel));
|
|
2147
2168
|
return { file: rel, kind, ...parseKitModuleFacts(source, rel, aliases) };
|
|
2148
2169
|
} catch {
|
|
2149
|
-
return emptyKitModuleFacts(rel, kind);
|
|
2170
|
+
return { ...emptyKitModuleFacts(rel, kind), parseFailed: true };
|
|
2150
2171
|
}
|
|
2151
2172
|
})
|
|
2152
2173
|
);
|
|
@@ -2536,7 +2557,7 @@ function headTagRule(opts) {
|
|
|
2536
2557
|
var seoDescriptionPresence = headTagRule({
|
|
2537
2558
|
id: "seo/description-presence",
|
|
2538
2559
|
title: "Description presence",
|
|
2539
|
-
severity: "
|
|
2560
|
+
severity: "warning",
|
|
2540
2561
|
match: (t) => t.kind === "meta" && t.name === "description",
|
|
2541
2562
|
label: '<meta name="description">',
|
|
2542
2563
|
recommendation: 'Add a <meta name="description"> in <svelte:head>, or set the description on your meta component.',
|
|
@@ -2556,7 +2577,7 @@ var seoCanonicalUrl = headTagRule({
|
|
|
2556
2577
|
match: (t) => t.kind === "link" && t.rel === "canonical",
|
|
2557
2578
|
label: '<link rel="canonical">',
|
|
2558
2579
|
recommendation: 'Add <link rel="canonical"> in <svelte:head>, or set the canonical prop on your meta component.',
|
|
2559
|
-
rationale: "A canonical URL tells search engines which URL is authoritative, preventing duplicate-content dilution across query
|
|
2580
|
+
rationale: "A canonical URL tells search engines which URL is authoritative, preventing duplicate-content dilution across query-string variants of the same page.",
|
|
2560
2581
|
fix: {
|
|
2561
2582
|
description: 'Add <link rel="canonical"> inside <svelte:head>, or set the canonical prop on your meta component.',
|
|
2562
2583
|
snippet: '<svelte:head>\n <link rel="canonical" href="https://example.com/this-page" />\n</svelte:head>',
|
|
@@ -2693,7 +2714,7 @@ var seoHtmlLang = {
|
|
|
2693
2714
|
category: "seo",
|
|
2694
2715
|
severity: "warning",
|
|
2695
2716
|
scope: "project",
|
|
2696
|
-
rationale: "The <html lang> attribute
|
|
2717
|
+
rationale: "The <html lang> attribute tells screen readers how to pronounce the page, browsers whether to offer translation, and other assistive tools how to handle the content \u2014 Google has said it does not use lang for ranking.",
|
|
2697
2718
|
fix: FIX4,
|
|
2698
2719
|
async check(ctx) {
|
|
2699
2720
|
const detection = ctx.project.htmlLang;
|
|
@@ -2778,7 +2799,7 @@ var performanceImageDimensions = imageRule({
|
|
|
2778
2799
|
severity: "warning",
|
|
2779
2800
|
label: "<img> width/height",
|
|
2780
2801
|
recommendation: "Set explicit width and height on <img> to reserve space and avoid layout shift (CLS).",
|
|
2781
|
-
rationale: "An <img> without explicit width and height
|
|
2802
|
+
rationale: "An <img> without explicit width and height can trigger layout shift (CLS) as it loads, hurting Core Web Vitals and visual stability \u2014 unless the box is reserved another way, e.g. CSS aspect-ratio.",
|
|
2782
2803
|
fix: {
|
|
2783
2804
|
description: "Add explicit width and height attributes to the <img>.",
|
|
2784
2805
|
snippet: '<img src="/hero.jpg" width="1200" height="630" alt="\u2026" />',
|
|
@@ -3233,7 +3254,7 @@ var performancePreconnect = {
|
|
|
3233
3254
|
rationale: "Connecting to a third-party origin (DNS + TCP + TLS) is costly; a preconnect/dns-prefetch hint starts it early so the resource arrives sooner.",
|
|
3234
3255
|
fix: {
|
|
3235
3256
|
description: "Add a preconnect hint for the third-party origin.",
|
|
3236
|
-
snippet: '<link rel="preconnect" href="https://fonts.googleapis.com" />',
|
|
3257
|
+
snippet: '<link rel="preconnect" href="https://fonts.googleapis.com" />\n<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />',
|
|
3237
3258
|
lang: "html"
|
|
3238
3259
|
},
|
|
3239
3260
|
options: OPTIONS,
|
|
@@ -3357,11 +3378,11 @@ var seoTwitterCard = headTagRule({
|
|
|
3357
3378
|
var seoOgDescription = headTagRule({
|
|
3358
3379
|
id: "seo/og-description",
|
|
3359
3380
|
title: "Open Graph description",
|
|
3360
|
-
severity: "
|
|
3381
|
+
severity: "info",
|
|
3361
3382
|
match: (t) => t.kind === "meta" && t.property === "og:description",
|
|
3362
3383
|
label: '<meta property="og:description">',
|
|
3363
3384
|
recommendation: 'Add <meta property="og:description">, or set openGraph.description on your meta component.',
|
|
3364
|
-
rationale: "og:description is the summary shown under the title in social previews; without it platforms guess or show nothing, lowering click-through.",
|
|
3385
|
+
rationale: "og:description is the summary shown under the title in social previews; without it platforms guess or show nothing, lowering click-through. The Open Graph protocol lists it as an optional property.",
|
|
3365
3386
|
fix: {
|
|
3366
3387
|
description: "Add an og:description meta tag in <svelte:head>.",
|
|
3367
3388
|
snippet: '<svelte:head>\n <meta property="og:description" content="A concise page summary." />\n</svelte:head>',
|
|
@@ -3373,11 +3394,11 @@ var seoOgDescription = headTagRule({
|
|
|
3373
3394
|
var seoOgUrl = headTagRule({
|
|
3374
3395
|
id: "seo/og-url",
|
|
3375
3396
|
title: "Open Graph URL",
|
|
3376
|
-
severity: "
|
|
3397
|
+
severity: "warning",
|
|
3377
3398
|
match: (t) => t.kind === "meta" && t.property === "og:url",
|
|
3378
3399
|
label: '<meta property="og:url">',
|
|
3379
3400
|
recommendation: 'Add <meta property="og:url"> with the canonical URL, or set openGraph.url on your meta component.',
|
|
3380
|
-
rationale: "og:url tells social platforms the canonical address to attribute shares and likes to, consolidating engagement on one URL.",
|
|
3401
|
+
rationale: "og:url tells social platforms the canonical address to attribute shares and likes to, consolidating engagement on one URL. The Open Graph protocol lists it as a required property.",
|
|
3381
3402
|
fix: {
|
|
3382
3403
|
description: "Add an og:url meta tag in <svelte:head>.",
|
|
3383
3404
|
snippet: '<svelte:head>\n <meta property="og:url" content="https://example.com/this-page" />\n</svelte:head>',
|
|
@@ -3397,7 +3418,7 @@ var seoViewport = headTagRule({
|
|
|
3397
3418
|
// silent there instead of false-flagging "missing" on every route.
|
|
3398
3419
|
appliesTo: (head) => head.source === "rendered",
|
|
3399
3420
|
recommendation: 'Add <meta name="viewport" content="width=device-width, initial-scale=1"> (usually in app.html).',
|
|
3400
|
-
rationale: "Without a viewport meta tag the page
|
|
3421
|
+
rationale: "Without a viewport meta tag mobile browsers render the page at a fixed ~980px layout viewport and scale it to fit, so text and controls end up too small to read or tap without pinch-zooming.",
|
|
3401
3422
|
fix: {
|
|
3402
3423
|
description: "Add the viewport meta tag (typically in src/app.html <head>).",
|
|
3403
3424
|
snippet: '<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
@@ -3562,19 +3583,25 @@ function hasNonEmpty(node, key) {
|
|
|
3562
3583
|
return true;
|
|
3563
3584
|
}
|
|
3564
3585
|
var REQUIRED_PROPS = {
|
|
3565
|
-
|
|
3566
|
-
BlogPosting: ["headline"],
|
|
3567
|
-
NewsArticle: ["headline"],
|
|
3568
|
-
Product: ["name", "offers"],
|
|
3586
|
+
Product: { all: ["name"], oneOf: ["review", "aggregateRating", "offers"] },
|
|
3569
3587
|
BreadcrumbList: ["itemListElement"],
|
|
3570
|
-
Organization: ["name", "url"],
|
|
3571
3588
|
WebSite: ["name", "url"],
|
|
3572
3589
|
Event: ["name", "startDate", "location"],
|
|
3573
|
-
Recipe: ["name", "image"
|
|
3574
|
-
|
|
3575
|
-
VideoObject: ["name", "description", "thumbnailUrl", "uploadDate"],
|
|
3590
|
+
Recipe: ["name", "image"],
|
|
3591
|
+
VideoObject: ["name", "thumbnailUrl", "uploadDate"],
|
|
3576
3592
|
LocalBusiness: ["name", "address"]
|
|
3577
3593
|
};
|
|
3594
|
+
function oneOfLabel(props) {
|
|
3595
|
+
const last = props.at(-1) ?? "";
|
|
3596
|
+
return props.length <= 1 ? last : `one of ${props.slice(0, -1).join(", ")} or ${last}`;
|
|
3597
|
+
}
|
|
3598
|
+
function missingRequiredProps(node, row) {
|
|
3599
|
+
const all = Array.isArray(row) ? row : row.all;
|
|
3600
|
+
const missing = all.filter((p) => !hasNonEmpty(node, p));
|
|
3601
|
+
const oneOf = Array.isArray(row) ? void 0 : row.oneOf;
|
|
3602
|
+
if (oneOf && !oneOf.some((p) => hasNonEmpty(node, p))) missing.push(oneOfLabel(oneOf));
|
|
3603
|
+
return missing;
|
|
3604
|
+
}
|
|
3578
3605
|
function jsonldTags(head) {
|
|
3579
3606
|
return head.tags.filter((t) => t.kind === "jsonld" && typeof t.jsonld === "string");
|
|
3580
3607
|
}
|
|
@@ -3766,7 +3793,7 @@ var seoJsonLdRequiredProps = jsonldRule({
|
|
|
3766
3793
|
const required = REQUIRED_PROPS[t];
|
|
3767
3794
|
if (!required) continue;
|
|
3768
3795
|
hasKnownType = true;
|
|
3769
|
-
const missing =
|
|
3796
|
+
const missing = missingRequiredProps(node, required);
|
|
3770
3797
|
if (missing.length > 0) return `${t} JSON-LD is missing required ${missing.join(", ")}`;
|
|
3771
3798
|
}
|
|
3772
3799
|
}
|
|
@@ -3808,7 +3835,7 @@ function lengthRule(opts) {
|
|
|
3808
3835
|
const o = resolveRuleOptions(opts.id, spec, ctx.config, { route: head.route, file: location }, compiled);
|
|
3809
3836
|
const min = intOption(o, "min", opts.min);
|
|
3810
3837
|
const max = intOption(o, "max", opts.max);
|
|
3811
|
-
const
|
|
3838
|
+
const recommendation10 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
|
|
3812
3839
|
const len = visibleLength(tag.text);
|
|
3813
3840
|
let problem;
|
|
3814
3841
|
if (len < min) problem = `${opts.noun} is too short (${len} chars; aim for ${min}\u2013${max})`;
|
|
@@ -3822,7 +3849,7 @@ function lengthRule(opts) {
|
|
|
3822
3849
|
route: head.route,
|
|
3823
3850
|
location,
|
|
3824
3851
|
message: problem,
|
|
3825
|
-
recommendation:
|
|
3852
|
+
recommendation: recommendation10,
|
|
3826
3853
|
docsUrl: docsUrl12
|
|
3827
3854
|
} : {
|
|
3828
3855
|
id: opts.id,
|
|
@@ -3836,7 +3863,7 @@ function lengthRule(opts) {
|
|
|
3836
3863
|
// it to also apply `severity: 'off'`.
|
|
3837
3864
|
location,
|
|
3838
3865
|
message: opts.label,
|
|
3839
|
-
recommendation:
|
|
3866
|
+
recommendation: recommendation10,
|
|
3840
3867
|
docsUrl: docsUrl12
|
|
3841
3868
|
}
|
|
3842
3869
|
);
|
|
@@ -3904,7 +3931,7 @@ var seoImageAlt = imageRule({
|
|
|
3904
3931
|
rationale: "An <img> with no alt attribute is invisible to image search and assistive technology; a descriptive alt is an image-SEO signal.",
|
|
3905
3932
|
fix: {
|
|
3906
3933
|
description: 'Add a descriptive alt attribute to the <img> (or alt="" if purely decorative).',
|
|
3907
|
-
snippet: '<img src="/photo.jpg" width="800" height="600" alt="
|
|
3934
|
+
snippet: '<img src="/photo.jpg" width="800" height="600" alt="Golden retriever catching a frisbee in a park" />',
|
|
3908
3935
|
lang: "svelte"
|
|
3909
3936
|
},
|
|
3910
3937
|
ok: (img) => img.hasAlt
|
|
@@ -3912,7 +3939,9 @@ var seoImageAlt = imageRule({
|
|
|
3912
3939
|
|
|
3913
3940
|
// src/rules/seo/hreflang.ts
|
|
3914
3941
|
var docsUrl4 = docsUrlFor("seo/hreflang");
|
|
3915
|
-
var
|
|
3942
|
+
var malformedRecommendation = 'Use valid hreflang codes, e.g. "en", "en-US", or the literal "x-default".';
|
|
3943
|
+
var noDefaultRecommendation = "x-default is a Google recommendation, not a requirement \u2014 most useful for a language-selector or auto-redirecting page. Add one if this page behaves that way; otherwise search engines fall back to matching the individual alternates.";
|
|
3944
|
+
var passRecommendation = 'Use valid hreflang codes (e.g. "en", "en-US", "x-default") for your language alternates.';
|
|
3916
3945
|
var HREFLANG_RE = /^[a-z]{2,3}(-[a-z]{4})?(-([a-z]{2}|\d{3}))?$/i;
|
|
3917
3946
|
function isValidHreflang(v) {
|
|
3918
3947
|
return v.toLowerCase() === "x-default" || HREFLANG_RE.test(v);
|
|
@@ -3923,7 +3952,7 @@ var seoHreflang = {
|
|
|
3923
3952
|
category: "seo",
|
|
3924
3953
|
severity: "warning",
|
|
3925
3954
|
scope: "route",
|
|
3926
|
-
rationale: "A malformed hreflang code
|
|
3955
|
+
rationale: "A malformed hreflang code breaks international targeting outright. A missing x-default is a Google recommendation for language-selector or auto-redirecting pages, not a defect on every multilingual site.",
|
|
3927
3956
|
async check(ctx) {
|
|
3928
3957
|
const out = [];
|
|
3929
3958
|
for (const head of ctx.heads) {
|
|
@@ -3936,10 +3965,13 @@ var seoHreflang = {
|
|
|
3936
3965
|
let problem;
|
|
3937
3966
|
let location = head.file;
|
|
3938
3967
|
if (badTag) {
|
|
3939
|
-
problem = `Invalid hreflang value "${badTag.hreflang}"
|
|
3968
|
+
problem = { message: `Invalid hreflang value "${badTag.hreflang}"`, recommendation: malformedRecommendation };
|
|
3940
3969
|
location = badTag.file ?? head.file;
|
|
3941
3970
|
} else if (values.length >= 2 && !values.some((v) => v.toLowerCase() === "x-default")) {
|
|
3942
|
-
problem =
|
|
3971
|
+
problem = {
|
|
3972
|
+
message: "Multiple hreflang alternates with no x-default declared",
|
|
3973
|
+
recommendation: noDefaultRecommendation
|
|
3974
|
+
};
|
|
3943
3975
|
}
|
|
3944
3976
|
out.push(
|
|
3945
3977
|
problem ? {
|
|
@@ -3949,8 +3981,8 @@ var seoHreflang = {
|
|
|
3949
3981
|
detection: PENALIZED,
|
|
3950
3982
|
route: head.route,
|
|
3951
3983
|
location,
|
|
3952
|
-
message: problem,
|
|
3953
|
-
recommendation:
|
|
3984
|
+
message: problem.message,
|
|
3985
|
+
recommendation: problem.recommendation,
|
|
3954
3986
|
docsUrl: docsUrl4
|
|
3955
3987
|
} : {
|
|
3956
3988
|
id: "seo/hreflang",
|
|
@@ -3962,7 +3994,7 @@ var seoHreflang = {
|
|
|
3962
3994
|
// 2026-08-08-pass-result-location-design.md).
|
|
3963
3995
|
location,
|
|
3964
3996
|
message: "hreflang",
|
|
3965
|
-
recommendation:
|
|
3997
|
+
recommendation: passRecommendation,
|
|
3966
3998
|
docsUrl: docsUrl4
|
|
3967
3999
|
}
|
|
3968
4000
|
);
|
|
@@ -3973,26 +4005,33 @@ var seoHreflang = {
|
|
|
3973
4005
|
|
|
3974
4006
|
// src/rules/seo/single-h1.ts
|
|
3975
4007
|
var docsUrl5 = docsUrlFor("seo/single-h1");
|
|
3976
|
-
var
|
|
4008
|
+
var passRecommendation2 = "Use exactly one <h1> per page for its main topic; demote extra top-level headings to <h2>+.";
|
|
4009
|
+
var missingRecommendation = "Add a single, descriptive <h1> naming the page's main topic.";
|
|
4010
|
+
var multipleRecommendation = "A single, clear <h1> is the conventional signal for a page \u2014 consider demoting extra top-level headings to <h2>+.";
|
|
3977
4011
|
var seoSingleH1 = {
|
|
3978
4012
|
id: "seo/single-h1",
|
|
3979
4013
|
title: "Heading hierarchy",
|
|
3980
4014
|
category: "seo",
|
|
3981
4015
|
severity: "warning",
|
|
3982
4016
|
scope: "route",
|
|
3983
|
-
rationale: "
|
|
4017
|
+
rationale: "A page should have a primary heading naming its main topic. Zero <h1> leaves the page without one; a single, clear <h1> is the conventional signal, though multiple <h1>s are tolerated by modern heading algorithms.",
|
|
3984
4018
|
async check(ctx) {
|
|
3985
4019
|
const out = [];
|
|
3986
4020
|
for (const route of ctx.headings ?? []) {
|
|
3987
|
-
const
|
|
4021
|
+
const combined = [...route.headings, ...route.componentHeadings ?? []];
|
|
4022
|
+
const h1 = combined.filter((h) => h.level === 1);
|
|
3988
4023
|
let problem;
|
|
3989
4024
|
let where = {};
|
|
3990
4025
|
if (h1.length === 0) {
|
|
3991
|
-
problem = "Missing <h1>";
|
|
4026
|
+
problem = { message: "Missing <h1>", severity: "warning", recommendation: missingRecommendation };
|
|
3992
4027
|
const first = route.headings[0];
|
|
3993
4028
|
if (first) where = { location: first.file, ...first.line > 0 ? { line: first.line } : {} };
|
|
3994
4029
|
} else if (h1.length > 1) {
|
|
3995
|
-
problem =
|
|
4030
|
+
problem = {
|
|
4031
|
+
message: `Multiple <h1> (${h1.length}); a single <h1> is the conventional signal`,
|
|
4032
|
+
severity: "info",
|
|
4033
|
+
recommendation: multipleRecommendation
|
|
4034
|
+
};
|
|
3996
4035
|
const extra = h1[1];
|
|
3997
4036
|
where = { location: extra.file, ...extra.line > 0 ? { line: extra.line } : {} };
|
|
3998
4037
|
}
|
|
@@ -4000,12 +4039,12 @@ var seoSingleH1 = {
|
|
|
4000
4039
|
problem ? {
|
|
4001
4040
|
id: "seo/single-h1",
|
|
4002
4041
|
category: "seo",
|
|
4003
|
-
severity:
|
|
4042
|
+
severity: problem.severity,
|
|
4004
4043
|
detection: PENALIZED,
|
|
4005
4044
|
route: route.route,
|
|
4006
4045
|
...where,
|
|
4007
|
-
message: problem,
|
|
4008
|
-
recommendation:
|
|
4046
|
+
message: problem.message,
|
|
4047
|
+
recommendation: problem.recommendation,
|
|
4009
4048
|
docsUrl: docsUrl5
|
|
4010
4049
|
} : {
|
|
4011
4050
|
id: "seo/single-h1",
|
|
@@ -4018,7 +4057,7 @@ var seoSingleH1 = {
|
|
|
4018
4057
|
// 2026-08-08-pass-result-location-design.md). Only reached when h1.length === 1.
|
|
4019
4058
|
location: h1[0].file,
|
|
4020
4059
|
message: "Heading hierarchy",
|
|
4021
|
-
recommendation:
|
|
4060
|
+
recommendation: passRecommendation2,
|
|
4022
4061
|
docsUrl: docsUrl5
|
|
4023
4062
|
}
|
|
4024
4063
|
);
|
|
@@ -4102,14 +4141,14 @@ var seoDuplicateDescription = uniquenessRule({
|
|
|
4102
4141
|
|
|
4103
4142
|
// src/rules/seo/heading-level-skip.ts
|
|
4104
4143
|
var docsUrl6 = docsUrlFor("seo/heading-level-skip");
|
|
4105
|
-
var
|
|
4144
|
+
var recommendation4 = "Increase heading levels one step at a time (do not jump, e.g. from <h2> straight to <h4>).";
|
|
4106
4145
|
var seoHeadingLevelSkip = {
|
|
4107
4146
|
id: "seo/heading-level-skip",
|
|
4108
4147
|
title: "Heading order",
|
|
4109
4148
|
category: "seo",
|
|
4110
4149
|
severity: "info",
|
|
4111
4150
|
scope: "route",
|
|
4112
|
-
rationale: "Skipping a heading level breaks the document outline that
|
|
4151
|
+
rationale: "Skipping a heading level breaks the document outline that assistive tech relies on to navigate page structure, and that search engines use as a structural signal.",
|
|
4113
4152
|
async check(ctx) {
|
|
4114
4153
|
const out = [];
|
|
4115
4154
|
for (const route of ctx.headings ?? []) {
|
|
@@ -4134,7 +4173,7 @@ var seoHeadingLevelSkip = {
|
|
|
4134
4173
|
location: skip.file,
|
|
4135
4174
|
...skip.line > 0 ? { line: skip.line } : {},
|
|
4136
4175
|
message: `Heading level skipped (<h${skip.prev}> to <h${skip.level}>)`,
|
|
4137
|
-
recommendation:
|
|
4176
|
+
recommendation: recommendation4,
|
|
4138
4177
|
docsUrl: docsUrl6
|
|
4139
4178
|
} : {
|
|
4140
4179
|
id: "seo/heading-level-skip",
|
|
@@ -4148,7 +4187,7 @@ var seoHeadingLevelSkip = {
|
|
|
4148
4187
|
// already continued above, so `[0]` is always defined here.
|
|
4149
4188
|
location: route.headings[0].file,
|
|
4150
4189
|
message: "Heading order",
|
|
4151
|
-
recommendation:
|
|
4190
|
+
recommendation: recommendation4,
|
|
4152
4191
|
docsUrl: docsUrl6
|
|
4153
4192
|
}
|
|
4154
4193
|
);
|
|
@@ -4258,7 +4297,7 @@ function componentRule(opts) {
|
|
|
4258
4297
|
const compiled = compileOverrides(ctx.config);
|
|
4259
4298
|
for (const c of ctx.components ?? []) {
|
|
4260
4299
|
const o = resolveRuleOptions(opts.id, opts.options, ctx.config, { route: c.file, file: c.file }, compiled);
|
|
4261
|
-
const
|
|
4300
|
+
const recommendation10 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
|
|
4262
4301
|
if (!opts.applies(c, o, ctx)) continue;
|
|
4263
4302
|
const bad = opts.bad(c, o, ctx).filter((b) => !(b.line > 0 && isSuppressed2(c, opts.id, b.line)));
|
|
4264
4303
|
if (bad.length === 0) {
|
|
@@ -4272,7 +4311,7 @@ function componentRule(opts) {
|
|
|
4272
4311
|
// same location a penalized result for this file would carry.
|
|
4273
4312
|
location: c.file,
|
|
4274
4313
|
message: opts.label,
|
|
4275
|
-
recommendation:
|
|
4314
|
+
recommendation: recommendation10,
|
|
4276
4315
|
docsUrl: docsUrl12
|
|
4277
4316
|
});
|
|
4278
4317
|
continue;
|
|
@@ -4287,7 +4326,7 @@ function componentRule(opts) {
|
|
|
4287
4326
|
location: c.file,
|
|
4288
4327
|
...b.line > 0 ? { line: b.line } : {},
|
|
4289
4328
|
message: b.message,
|
|
4290
|
-
recommendation:
|
|
4329
|
+
recommendation: recommendation10,
|
|
4291
4330
|
docsUrl: docsUrl12,
|
|
4292
4331
|
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
4293
4332
|
});
|
|
@@ -4343,10 +4382,13 @@ var correctnessEffectAsOnMount = componentRule({
|
|
|
4343
4382
|
title: "Effect used as onMount",
|
|
4344
4383
|
category: "correctness",
|
|
4345
4384
|
label: "$effect usage",
|
|
4346
|
-
recommendation: "
|
|
4347
|
-
rationale: "An $effect
|
|
4385
|
+
recommendation: "If this runs in response to a user interaction, use an event handler; to sync an element with an external library, use {@attach} instead. For genuine one-time mount work, use onMount (import { onMount } from 'svelte'). Reserve $effect for logic that reacts to $state/$derived/$props.",
|
|
4386
|
+
rationale: "An $effect whose body reads no reactive value visible to this analysis runs once after mount and never re-runs on the paths it can see \u2014 usually a sign the code belongs in an event handler, {@attach}, or onMount instead of $effect. This can't see a reactive value reached only through a plain function's return value, so a genuinely reactive effect built that way can still be flagged.",
|
|
4348
4387
|
applies: (c) => c.effects.length > 0,
|
|
4349
|
-
bad: (c) => c.effects.filter((e) => e.mountOnly).map((e) => ({
|
|
4388
|
+
bad: (c) => c.effects.filter((e) => e.mountOnly).map((e) => ({
|
|
4389
|
+
line: e.line,
|
|
4390
|
+
message: "$effect reads no reactive value this analysis can see \u2014 consider an event handler, {@attach}, or onMount instead"
|
|
4391
|
+
}))
|
|
4350
4392
|
});
|
|
4351
4393
|
|
|
4352
4394
|
// src/rules/correctness/unmutated-state.ts
|
|
@@ -4426,14 +4468,14 @@ var correctnessCheckableBindValue = componentRule({
|
|
|
4426
4468
|
severity: "warning",
|
|
4427
4469
|
label: "bind:checked / bind:group on checkable inputs",
|
|
4428
4470
|
recommendation: "Replace bind:value with bind:checked (single checkbox) or bind:group (checkbox list / radio group).",
|
|
4429
|
-
rationale: "bind:value binds the DOM value property. A checkbox/radio's user interaction toggles checkedness, which bind:value never observes
|
|
4471
|
+
rationale: "bind:value binds the DOM value property. A checkbox/radio's user interaction toggles checkedness, which bind:value never observes. On a checkbox this throws bind_invalid_checkbox_value in a development build; in production the check is skipped and the binding silently tracks the value attribute instead of checkedness. On a radio it throws nothing in either build \u2014 it renders once with the initial value, then silently never updates. Svelte's checked/grouped bindings (bind:checked, bind:group) are built for exactly this.",
|
|
4430
4472
|
fix: {
|
|
4431
4473
|
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."
|
|
4432
4474
|
},
|
|
4433
4475
|
applies: (c) => c.checkableBindValues.length > 0,
|
|
4434
4476
|
bad: (c) => c.checkableBindValues.map((v) => ({
|
|
4435
4477
|
line: v.line,
|
|
4436
|
-
message: v.kind === "checkbox" ? "bind:value on a checkbox does not track its checked state \u2014
|
|
4478
|
+
message: v.kind === "checkbox" ? "bind:value on a checkbox does not track its checked state \u2014 it throws bind_invalid_checkbox_value in development; in a production build it silently tracks the value attribute instead of checkedness. 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."
|
|
4437
4479
|
}))
|
|
4438
4480
|
});
|
|
4439
4481
|
|
|
@@ -4445,7 +4487,7 @@ var correctnessOrphanEffect = componentRule({
|
|
|
4445
4487
|
severity: "critical",
|
|
4446
4488
|
label: "$effect context",
|
|
4447
4489
|
recommendation: "Wrap the effect in $effect.root (and own the returned cleanup), or restructure so the effect is created during component initialisation (e.g. call a setup method from a component).",
|
|
4448
|
-
rationale: "An $effect created outside component initialisation throws effect_orphan at runtime \u2014 the compiler
|
|
4490
|
+
rationale: "An $effect created outside component initialisation throws effect_orphan at runtime. The compiler does not catch it \u2014 the server compiler deletes $effect calls entirely, so SSR renders without error \u2014 and the crash happens client-side, when the module evaluates in the browser, breaking hydration rather than producing a server error.",
|
|
4449
4491
|
// `orphanEffects` is typed required, but a facts object built by an older/external
|
|
4450
4492
|
// constructor may omit it — default to empty rather than let `applies` throw and
|
|
4451
4493
|
// take the whole `runRules` Promise.all down with it.
|
|
@@ -4464,6 +4506,14 @@ var DOCS_URL = docsUrlFor(ID);
|
|
|
4464
4506
|
var LABEL = "Lifecycle-call context";
|
|
4465
4507
|
var RECOMMENDATION = "Call lifecycle/context functions during component initialisation (the top level of a component's <script>). In load, return the data and call setContext in a layout/page component; in shared modules, expose a setup function that components call during init.";
|
|
4466
4508
|
var topLevelMessage = (name) => `${name}() runs at module evaluation, outside component initialisation \u2014 it throws lifecycle_outside_component at runtime`;
|
|
4509
|
+
var ALWAYS_THROWS = /* @__PURE__ */ new Set(["getContext", "setContext", "hasContext", "getAllContexts"]);
|
|
4510
|
+
function kitLifecycleMessage(name, kind, inHandler) {
|
|
4511
|
+
if (kind === "server" && !ALWAYS_THROWS.has(name)) {
|
|
4512
|
+
const where = inHandler ? `${name}() is called in a load/handler, outside component initialisation` : `${name}() runs outside component initialisation (module evaluation or the init hook)`;
|
|
4513
|
+
return name === "onDestroy" ? `${where} \u2014 on the server it still crashes, but with a plain TypeError, not lifecycle_outside_component (onDestroy has no component-context guard there)` : `${where} \u2014 on the server this is a silent no-op (it throws lifecycle_outside_component only if this module also runs in the browser)`;
|
|
4514
|
+
}
|
|
4515
|
+
return inHandler ? `${name}() is called in a load/handler \u2014 it runs on every request, outside component initialisation, and throws lifecycle_outside_component at runtime` : `${name}() runs outside component initialisation (module evaluation or the init hook) \u2014 it throws lifecycle_outside_component at runtime`;
|
|
4516
|
+
}
|
|
4467
4517
|
function isSuppressed3(suppressions, line) {
|
|
4468
4518
|
return (suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ID)));
|
|
4469
4519
|
}
|
|
@@ -4506,7 +4556,7 @@ var correctnessOrphanLifecycle = {
|
|
|
4506
4556
|
category: "correctness",
|
|
4507
4557
|
severity: "critical",
|
|
4508
4558
|
scope: "component",
|
|
4509
|
-
rationale: "Svelte lifecycle and context functions require an active component context; called at module scope, in a shared-state class constructor, or in a load/handler they throw lifecycle_outside_component at runtime \u2014 the compiler does not catch it, and it surfaces as a production crash.",
|
|
4559
|
+
rationale: "Svelte lifecycle and context functions require an active component context; called at module scope, in a shared-state class constructor, or in a load/handler they throw lifecycle_outside_component at runtime \u2014 the compiler does not catch it, and it surfaces as a production crash. Exception: in a Kit module that only ever runs on the server (+page.server.ts, +server.ts, hooks.server.ts), onMount/beforeUpdate/afterUpdate/createEventDispatcher are silent no-ops there instead, and onDestroy throws a plain TypeError rather than lifecycle_outside_component \u2014 only getContext/setContext/hasContext/getAllContexts still throw in that channel.",
|
|
4510
4560
|
async check(ctx) {
|
|
4511
4561
|
const out = [];
|
|
4512
4562
|
for (const c of ctx.components ?? []) {
|
|
@@ -4530,7 +4580,7 @@ var correctnessOrphanLifecycle = {
|
|
|
4530
4580
|
m.file,
|
|
4531
4581
|
calls.map((l) => ({
|
|
4532
4582
|
line: l.line,
|
|
4533
|
-
message: l.
|
|
4583
|
+
message: kitLifecycleMessage(l.name, m.kind, l.inHandler)
|
|
4534
4584
|
})),
|
|
4535
4585
|
m.suppressions
|
|
4536
4586
|
);
|
|
@@ -4721,7 +4771,7 @@ var securityRawHtml = componentRule({
|
|
|
4721
4771
|
title: "Raw HTML render",
|
|
4722
4772
|
category: "security",
|
|
4723
4773
|
label: "{@html} usage",
|
|
4724
|
-
recommendation: "Sanitize the value before {@html} (e.g. DOMPurify), or render it as text/markup instead.",
|
|
4774
|
+
recommendation: "Sanitize the value before {@html} (e.g. DOMPurify), or render it as text/markup instead. A sanitizer keeps {@html} in the source, so the finding persists by design \u2014 once reviewed, suppress it with the inline directive.",
|
|
4725
4775
|
rationale: "{@html} renders its value as unescaped HTML; if the value can contain user input and is not sanitized, it is a cross-site-scripting (XSS) vector.",
|
|
4726
4776
|
applies: (c) => c.htmlTags.length > 0,
|
|
4727
4777
|
bad: (c) => c.htmlTags.map((h) => ({ line: h.line, message: "{@html} renders unescaped HTML \u2014 ensure it is sanitized" }))
|
|
@@ -4734,7 +4784,7 @@ var securityJavascriptUrl = componentRule({
|
|
|
4734
4784
|
category: "security",
|
|
4735
4785
|
label: "No javascript: URLs",
|
|
4736
4786
|
recommendation: "Use an event handler or a real URL instead of a javascript: URL.",
|
|
4737
|
-
rationale: "A javascript: URL in href/src/action
|
|
4787
|
+
rationale: "A javascript: URL in href/src/action/formaction breaks under a strict Content-Security-Policy and turns what should be a real navigation into inline script execution on activation \u2014 use an event handler on a <button> instead (the same shape is also a classic XSS vector, though detection here is literal-only, so every flagged URL is author-written, not injected).",
|
|
4738
4788
|
applies: (c) => c.javascriptUrls.length > 0,
|
|
4739
4789
|
bad: (c) => c.javascriptUrls.map((u) => ({ line: u.line, message: "javascript: URL in an attribute" }))
|
|
4740
4790
|
});
|
|
@@ -4746,12 +4796,12 @@ var securityHandlerStateWrite = kitModuleRule({
|
|
|
4746
4796
|
category: "security",
|
|
4747
4797
|
severity: "critical",
|
|
4748
4798
|
label: "Load/handler purity",
|
|
4749
|
-
recommendation: "Return the data from load (or the action) and pass it via page data instead of writing it to module state; per-user data belongs in cookies/locals plus a database.",
|
|
4750
|
-
rationale: "SvelteKit's docs mark this NEVER-DO-THIS: the server is one long-lived process shared by every user, so module state written during a request is visible to ALL later requests
|
|
4751
|
-
applies: (m) => m.importedStateWrites.length > 0,
|
|
4799
|
+
recommendation: "Return the data from load (or the action) and pass it via page data instead of writing it to module state; per-user data belongs in cookies/locals plus a database. Caches and rate limiters keyed by non-personal data are the benign shape \u2014 if that describes this write, add an inline suppression.",
|
|
4800
|
+
rationale: "SvelteKit's docs mark this NEVER-DO-THIS: the server is one long-lived process shared by every user, so module state written during a request is visible to ALL later requests.",
|
|
4801
|
+
applies: (m) => m.importedStateWrites.length > 0 && !(m.kind === "universal" && m.ssrDisabled),
|
|
4752
4802
|
bad: (m) => m.importedStateWrites.map((w) => ({
|
|
4753
4803
|
line: w.line,
|
|
4754
|
-
message: `a server-executed handler writes imported module state "${w.name}" \u2014 shared across all requests on the server, one user's data can leak to another`
|
|
4804
|
+
message: `a server-executed handler writes imported module state "${w.name}" \u2014 module state is shared across all requests on the server; if this holds per-request or per-user data, one user's data can leak to another. Caches and rate limiters keyed by non-personal data are the benign shape \u2014 verify which this is`
|
|
4755
4805
|
}))
|
|
4756
4806
|
});
|
|
4757
4807
|
|
|
@@ -4781,7 +4831,7 @@ var securitySharedStateImport = kitModuleRule({
|
|
|
4781
4831
|
label: "Server state imports",
|
|
4782
4832
|
recommendation: "Keep module-scope $state out of server-executed code: return data from load and share it via page data or the context API. If the module is genuinely client-only, restructure so server files do not import it, or add an inline suppression.",
|
|
4783
4833
|
rationale: "A .svelte.ts module with module-scope $state is one shared instance on the server: mutated, it leaks data between users; read-only, every request sees the same boot-time value instead of per-user data.",
|
|
4784
|
-
applies: (m) => m.runesModuleImports.length > 0,
|
|
4834
|
+
applies: (m) => m.runesModuleImports.length > 0 && !(m.kind === "universal" && m.ssrDisabled),
|
|
4785
4835
|
bad: (m, ctx) => {
|
|
4786
4836
|
const stateFiles = new Set((ctx.components ?? []).filter((c) => c.moduleStateDecls.length > 0).map((c) => c.file));
|
|
4787
4837
|
const writtenOutside = new Set(m.importedStateWritesOutsideHandlers.map((w) => w.name));
|
|
@@ -4841,7 +4891,7 @@ var architecturePropCount = componentRule({
|
|
|
4841
4891
|
|
|
4842
4892
|
// src/rules/architecture/private-scope-import.ts
|
|
4843
4893
|
var docsUrl7 = docsUrlFor("architecture/private-scope-import");
|
|
4844
|
-
var
|
|
4894
|
+
var recommendation5 = "Move the unit to the directory shared by all of its importers, or import it only from inside its own scope.";
|
|
4845
4895
|
var OPTIONS2 = { scopes: { kind: "string-list", default: [] } };
|
|
4846
4896
|
function ancestorDirs(file) {
|
|
4847
4897
|
const segments = file.split("/");
|
|
@@ -4927,7 +4977,7 @@ var architecturePrivateScopeImport = {
|
|
|
4927
4977
|
// afterward (maintainer ruling, same date).
|
|
4928
4978
|
location: c.file,
|
|
4929
4979
|
message: "No private-scope imports",
|
|
4930
|
-
recommendation:
|
|
4980
|
+
recommendation: recommendation5,
|
|
4931
4981
|
docsUrl: docsUrl7
|
|
4932
4982
|
});
|
|
4933
4983
|
continue;
|
|
@@ -4942,7 +4992,7 @@ var architecturePrivateScopeImport = {
|
|
|
4942
4992
|
location: c.file,
|
|
4943
4993
|
...v.line > 0 ? { line: v.line } : {},
|
|
4944
4994
|
message: v.message,
|
|
4945
|
-
recommendation:
|
|
4995
|
+
recommendation: recommendation5,
|
|
4946
4996
|
docsUrl: docsUrl7,
|
|
4947
4997
|
fix: { ...architecturePrivateScopeImport.fix }
|
|
4948
4998
|
});
|
|
@@ -5067,7 +5117,7 @@ function classifyUnusedKeys(unused, excludedDirs, compile) {
|
|
|
5067
5117
|
// src/rules/architecture/unit-entry-file.ts
|
|
5068
5118
|
var ID4 = "architecture/unit-entry-file";
|
|
5069
5119
|
var docsUrl8 = docsUrlFor(ID4);
|
|
5070
|
-
var
|
|
5120
|
+
var recommendation6 = "Give every declared unit directory a file named after it, or stop declaring that directory a unit.";
|
|
5071
5121
|
var OPTIONS3 = {
|
|
5072
5122
|
units: { kind: "string-map", default: {} },
|
|
5073
5123
|
pascalCaseUnits: { kind: "string-map", default: {} },
|
|
@@ -5146,7 +5196,7 @@ var architectureUnitEntryFile = {
|
|
|
5146
5196
|
detection: { presence: "own", value: "static" },
|
|
5147
5197
|
location: expected,
|
|
5148
5198
|
message: "Unit entry file",
|
|
5149
|
-
recommendation:
|
|
5199
|
+
recommendation: recommendation6,
|
|
5150
5200
|
docsUrl: docsUrl8
|
|
5151
5201
|
});
|
|
5152
5202
|
continue;
|
|
@@ -5161,7 +5211,7 @@ var architectureUnitEntryFile = {
|
|
|
5161
5211
|
route: dir,
|
|
5162
5212
|
location: at,
|
|
5163
5213
|
message: `${dir} declares a unit but has no ${expected}`,
|
|
5164
|
-
recommendation:
|
|
5214
|
+
recommendation: recommendation6,
|
|
5165
5215
|
docsUrl: docsUrl8,
|
|
5166
5216
|
// Which declaration matched decides the wording: a `units` match like functions/getFoo/
|
|
5167
5217
|
// is already camelCase, so telling its author to rename it would be nonsense.
|
|
@@ -5226,7 +5276,7 @@ function satisfiesCasing(name, allowed) {
|
|
|
5226
5276
|
// src/rules/architecture/directory-naming.ts
|
|
5227
5277
|
var ID5 = "architecture/directory-naming";
|
|
5228
5278
|
var docsUrl9 = docsUrlFor(ID5);
|
|
5229
|
-
var
|
|
5279
|
+
var recommendation7 = "Name each directory in the casing its location declares, or narrow the declaration.";
|
|
5230
5280
|
var OPTIONS4 = {
|
|
5231
5281
|
directories: { kind: "string-map", default: {} },
|
|
5232
5282
|
exclude: { kind: "string-list", default: [] }
|
|
@@ -5292,7 +5342,7 @@ var architectureDirectoryNaming = {
|
|
|
5292
5342
|
route: dir,
|
|
5293
5343
|
location: at,
|
|
5294
5344
|
message: `${dir} must be ${allowed.join(" or ")}.`,
|
|
5295
|
-
recommendation:
|
|
5345
|
+
recommendation: recommendation7,
|
|
5296
5346
|
docsUrl: docsUrl9,
|
|
5297
5347
|
fix: { description: "Rename the directory, or narrow the declaration that governs it." }
|
|
5298
5348
|
});
|
|
@@ -5335,7 +5385,7 @@ var architectureDirectoryNaming = {
|
|
|
5335
5385
|
// src/rules/architecture/reserved-directory-names.ts
|
|
5336
5386
|
var ID6 = "architecture/reserved-directory-names";
|
|
5337
5387
|
var docsUrl10 = docsUrlFor(ID6);
|
|
5338
|
-
var
|
|
5388
|
+
var recommendation8 = "Use one of the names this location declares, or add the new name to the declaration.";
|
|
5339
5389
|
var OPTIONS5 = {
|
|
5340
5390
|
scopes: { kind: "string-map", default: {} },
|
|
5341
5391
|
unitScopes: { kind: "string-map", default: {} },
|
|
@@ -5494,7 +5544,7 @@ var architectureReservedDirectoryNames = {
|
|
|
5494
5544
|
route: child,
|
|
5495
5545
|
location: at,
|
|
5496
5546
|
message: `${child} is not one of the names declared here: ${winner.names.join(", ")}.`,
|
|
5497
|
-
recommendation:
|
|
5547
|
+
recommendation: recommendation8,
|
|
5498
5548
|
docsUrl: docsUrl10,
|
|
5499
5549
|
fix: {
|
|
5500
5550
|
description: "Rename it to a declared name, move it under one of them, or add its name to the declaration."
|
|
@@ -5552,7 +5602,7 @@ var architectureReservedDirectoryNames = {
|
|
|
5552
5602
|
// src/rules/architecture/reserved-name-placement.ts
|
|
5553
5603
|
var ID7 = "architecture/reserved-name-placement";
|
|
5554
5604
|
var docsUrl11 = docsUrlFor(ID7);
|
|
5555
|
-
var
|
|
5605
|
+
var recommendation9 = "Move it to one of the places declared for this name, or declare this place for it.";
|
|
5556
5606
|
var fixDescription = "Move the directory to one of the places declared for its name, rename it, or declare this place for the name.";
|
|
5557
5607
|
var OPTIONS6 = {
|
|
5558
5608
|
placements: { kind: "string-map", default: {} },
|
|
@@ -5670,7 +5720,7 @@ var architectureReservedNamePlacement = {
|
|
|
5670
5720
|
route: dir,
|
|
5671
5721
|
location: at,
|
|
5672
5722
|
message: `${dir} is not one of the places declared for '${name}'.`,
|
|
5673
|
-
recommendation:
|
|
5723
|
+
recommendation: recommendation9,
|
|
5674
5724
|
docsUrl: docsUrl11,
|
|
5675
5725
|
fix: {
|
|
5676
5726
|
description: fixDescription
|
|
@@ -5884,8 +5934,8 @@ var performanceNamespaceImport = componentRule({
|
|
|
5884
5934
|
// src/rules/perf/minify-disabled.ts
|
|
5885
5935
|
var PENALIZED7 = { presence: "none", value: "absent" };
|
|
5886
5936
|
var MINIFY_DISABLED_FIX = {
|
|
5887
|
-
description: "Remove the minify: false override from vite.config (Vite minifies
|
|
5888
|
-
snippet: "export default defineConfig({\n build: {\n minify:
|
|
5937
|
+
description: "Remove the minify: false override from vite.config (Vite minifies by default), or scope it to non-production builds.",
|
|
5938
|
+
snippet: "export default defineConfig({\n build: {\n // minify: false \u2014 removed; Vite minifies production builds by default\n }\n});",
|
|
5889
5939
|
lang: "ts"
|
|
5890
5940
|
};
|
|
5891
5941
|
var RECOMMENDATION4 = "Remove build.minify: false from vite.config, or scope it to non-production builds if it is intentional.";
|
|
@@ -6465,7 +6515,7 @@ function formatAgentReport(results, config) {
|
|
|
6465
6515
|
return lines.join("\n").replace(/\n+$/, "\n");
|
|
6466
6516
|
}
|
|
6467
6517
|
lines.push(
|
|
6468
|
-
`${failing.length} issue(s) to fix, ordered most-severe first. Fix critical issues first; warning and info items improve SEO but do not fail the default build. Apply each fix below, then re-run \`svelte-vitals\` (or the build) to confirm each rule passes.`,
|
|
6518
|
+
`${failing.length} issue(s) to fix, ordered most-severe first. Fix critical issues first; warning and info items improve SEO but do not fail the default build. Apply each fix below, then re-run \`svelte-vitals\` (or the build) to confirm each rule passes. Run \`svelte-vitals explain <rule-id>\` for any rule's rationale and options (works offline).`,
|
|
6469
6519
|
""
|
|
6470
6520
|
);
|
|
6471
6521
|
const groups = /* @__PURE__ */ new Map();
|