@svelte-vitals/core 0.10.1 → 0.12.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 +48 -1
- package/dist/index.js +283 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -105,6 +105,12 @@ interface HeadTag {
|
|
|
105
105
|
property?: string;
|
|
106
106
|
/** <link rel="...">. */
|
|
107
107
|
rel?: string;
|
|
108
|
+
/** <link as="..."> keyword (e.g. 'font') when statically literal; undefined when absent or dynamically bound. */
|
|
109
|
+
as?: string;
|
|
110
|
+
/** True when a <link> has an `as` attribute at all (literal or dynamic). Distinguishes "no as" from "dynamic as". */
|
|
111
|
+
hasAs?: boolean;
|
|
112
|
+
/** True when a <link> has a `crossorigin` attribute (presence only; value is irrelevant to the checks). */
|
|
113
|
+
hasCrossorigin?: boolean;
|
|
108
114
|
/** Where this tag was set relative to the route. Never 'none' (absence = no tag). */
|
|
109
115
|
presence: Exclude<Presence, 'none'>;
|
|
110
116
|
/** Whether the tag's value is static/dynamic/absent (design §4). */
|
|
@@ -229,6 +235,9 @@ declare const seo009HtmlLang: Rule;
|
|
|
229
235
|
declare const perf001ImageDimensions: Rule;
|
|
230
236
|
declare const perf002ImageLoading: Rule;
|
|
231
237
|
|
|
238
|
+
declare const perf003PreloadAs: Rule;
|
|
239
|
+
declare const perf004FontPreloadCrossorigin: Rule;
|
|
240
|
+
|
|
232
241
|
declare const allRules: Rule[];
|
|
233
242
|
|
|
234
243
|
interface RuleInfo {
|
|
@@ -275,6 +284,23 @@ interface ImageRuleOptions {
|
|
|
275
284
|
/** Build a route-scoped Performance rule that checks each <img> against `ok` (issue #10). */
|
|
276
285
|
declare function imageRule(opts: ImageRuleOptions): Rule;
|
|
277
286
|
|
|
287
|
+
interface LinkRuleOptions {
|
|
288
|
+
id: string;
|
|
289
|
+
title: string;
|
|
290
|
+
severity: Severity;
|
|
291
|
+
/** Noun phrase for messages, e.g. '`as` on a preloaded `<link>`'. */
|
|
292
|
+
label: string;
|
|
293
|
+
recommendation: string;
|
|
294
|
+
rationale: string;
|
|
295
|
+
fix?: Fix;
|
|
296
|
+
/** Which link tags this rule evaluates (e.g. rel === 'preload'). */
|
|
297
|
+
relevant: (tag: HeadTag) => boolean;
|
|
298
|
+
/** Returns true when a relevant link satisfies the rule (passes). */
|
|
299
|
+
ok: (tag: HeadTag) => boolean;
|
|
300
|
+
}
|
|
301
|
+
/** Build a route-scoped Performance rule that checks each relevant <link> in the effective head. */
|
|
302
|
+
declare function linkRule(opts: LinkRuleOptions): Rule;
|
|
303
|
+
|
|
278
304
|
interface Summary {
|
|
279
305
|
critical: number;
|
|
280
306
|
warning: number;
|
|
@@ -385,9 +411,30 @@ declare function formatSarifReport(results: Result[], config: Config, meta: {
|
|
|
385
411
|
*/
|
|
386
412
|
declare function formatGithubReport(results: Result[], config: Config): string;
|
|
387
413
|
|
|
414
|
+
type Band = 'good' | 'warn' | 'poor';
|
|
415
|
+
declare const BAND_COLOR: Record<Band, string>;
|
|
416
|
+
declare function scoreBand(score: number): Band;
|
|
417
|
+
declare function escapeHtml(s: string): string;
|
|
418
|
+
/**
|
|
419
|
+
* Return the URL only when it uses a safe http/https scheme, else null.
|
|
420
|
+
* Guards a finding's `docsUrl` against `javascript:`/`data:` hrefs — escapeHtml
|
|
421
|
+
* neutralizes attribute breakout but not a malicious scheme. Browsers strip
|
|
422
|
+
* ASCII whitespace (tab/newline/CR) from a URL before resolving its scheme (so
|
|
423
|
+
* `java\tscript:` runs as `javascript:`), so strip whitespace first; anything not
|
|
424
|
+
* plainly http(s):// afterward is rejected. Pure string work — no `URL` global,
|
|
425
|
+
* keeping core runtime-agnostic and lib-minimal.
|
|
426
|
+
*/
|
|
427
|
+
declare function safeHref(url: string): string | null;
|
|
428
|
+
declare function buildHtmlDocument(report: JsonReport, meta: {
|
|
429
|
+
version: string;
|
|
430
|
+
}): string;
|
|
431
|
+
declare function formatHtmlReport(results: Result[], config: Config, meta: {
|
|
432
|
+
version: string;
|
|
433
|
+
}): string;
|
|
434
|
+
|
|
388
435
|
/** Drop rules disabled via config (design §6). */
|
|
389
436
|
declare function selectRules(rules: Rule[], config: Config): Rule[];
|
|
390
437
|
/** Apply per-rule severity overrides to results (design §6). */
|
|
391
438
|
declare function applyRuleSeverities(results: Result[], config: Config): Result[];
|
|
392
439
|
|
|
393
|
-
export { type Category, type Classification, type Config, type ConsoleReportOptions, type Detection, type Fix, type HeadProvider, type HeadTag, type HealthResult, type ImageInfo, type JsonReport, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type ResolvedImages, type Result, type Rule, type RuleContext, type RuleInfo, type RuleSetting, type Runtime, SITEMAP_SOURCE_PATHS, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type Summary, type TreatDynamicAs, type Value, allRules, applyRuleSeverities, buildJsonReport, classify, computeHealth, computeScore, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, explainRule, formatAgentReport, formatConsoleReport, formatGithubReport, formatJsonReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, perf001ImageDimensions, perf002ImageLoading, runRules, scoresByCategory, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, summarize };
|
|
440
|
+
export { BAND_COLOR, type Category, type Classification, type Config, type ConsoleReportOptions, type Detection, type Fix, type HeadProvider, type HeadTag, type HealthResult, type ImageInfo, type JsonReport, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type ResolvedImages, type Result, type Rule, type RuleContext, type RuleInfo, type RuleSetting, type Runtime, SITEMAP_SOURCE_PATHS, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type Summary, type TreatDynamicAs, type Value, allRules, applyRuleSeverities, buildHtmlDocument, buildJsonReport, classify, computeHealth, computeScore, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, escapeHtml, explainRule, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, linkRule, perf001ImageDimensions, perf002ImageLoading, perf003PreloadAs, perf004FontPreloadCrossorigin, runRules, safeHref, scoreBand, scoresByCategory, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, summarize };
|
package/dist/index.js
CHANGED
|
@@ -373,6 +373,91 @@ var perf002ImageLoading = imageRule({
|
|
|
373
373
|
ok: (img) => img.hasLoading
|
|
374
374
|
});
|
|
375
375
|
|
|
376
|
+
// src/rules/perf/link-rule.ts
|
|
377
|
+
function linkRule(opts) {
|
|
378
|
+
const docsUrl = docsUrlFor(opts.id);
|
|
379
|
+
return {
|
|
380
|
+
id: opts.id,
|
|
381
|
+
title: opts.title,
|
|
382
|
+
category: "performance",
|
|
383
|
+
severity: opts.severity,
|
|
384
|
+
scope: "route",
|
|
385
|
+
rationale: opts.rationale,
|
|
386
|
+
...opts.fix ? { fix: opts.fix } : {},
|
|
387
|
+
async check(ctx) {
|
|
388
|
+
const out = [];
|
|
389
|
+
for (const head of ctx.heads) {
|
|
390
|
+
const links = head.tags.filter((t) => t.kind === "link" && opts.relevant(t));
|
|
391
|
+
if (links.length === 0) continue;
|
|
392
|
+
const bad = links.filter((t) => !opts.ok(t));
|
|
393
|
+
if (bad.length === 0) {
|
|
394
|
+
out.push({
|
|
395
|
+
id: opts.id,
|
|
396
|
+
category: "performance",
|
|
397
|
+
severity: opts.severity,
|
|
398
|
+
detection: { presence: "own", value: "static" },
|
|
399
|
+
route: head.route,
|
|
400
|
+
message: opts.label,
|
|
401
|
+
recommendation: opts.recommendation,
|
|
402
|
+
docsUrl
|
|
403
|
+
});
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
for (const tag of bad) {
|
|
407
|
+
out.push({
|
|
408
|
+
id: opts.id,
|
|
409
|
+
category: "performance",
|
|
410
|
+
severity: opts.severity,
|
|
411
|
+
detection: { presence: "none", value: "absent" },
|
|
412
|
+
route: head.route,
|
|
413
|
+
// Point at the file the link actually came from (a layout in static
|
|
414
|
+
// mode); fall back to the route's representative file when the tag
|
|
415
|
+
// carries no file (rendered mode).
|
|
416
|
+
location: tag.file ?? head.file,
|
|
417
|
+
message: `Missing ${opts.label}`,
|
|
418
|
+
recommendation: opts.recommendation,
|
|
419
|
+
docsUrl,
|
|
420
|
+
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/rules/perf/resource-hints.ts
|
|
430
|
+
var perf003PreloadAs = linkRule({
|
|
431
|
+
id: "PERF003",
|
|
432
|
+
title: "Preload missing as",
|
|
433
|
+
severity: "warning",
|
|
434
|
+
label: "`as` on a preloaded `<link>`",
|
|
435
|
+
recommendation: 'Add an `as` attribute to every `<link rel="preload">` so the browser knows the resource type and can prioritize it.',
|
|
436
|
+
rationale: 'A `<link rel="preload">` without an `as` attribute is ignored by the browser (or fetched a second time), wasting the preload.',
|
|
437
|
+
fix: {
|
|
438
|
+
description: "Add an `as` attribute matching the resource type to the preload link.",
|
|
439
|
+
snippet: '<link rel="preload" href="/app.css" as="style" />',
|
|
440
|
+
lang: "html"
|
|
441
|
+
},
|
|
442
|
+
relevant: (t) => t.rel === "preload",
|
|
443
|
+
ok: (t) => t.hasAs === true
|
|
444
|
+
});
|
|
445
|
+
var perf004FontPreloadCrossorigin = linkRule({
|
|
446
|
+
id: "PERF004",
|
|
447
|
+
title: "Font preload missing crossorigin",
|
|
448
|
+
severity: "warning",
|
|
449
|
+
label: "`crossorigin` on a font preload",
|
|
450
|
+
recommendation: 'Add `crossorigin` to `<link rel="preload" as="font">` \u2014 fonts are fetched in CORS mode, so without it the preload fetches a second, unused copy.',
|
|
451
|
+
rationale: "A font preload without `crossorigin` does not match the actual (CORS) font request, so the preloaded file is never used and the font downloads twice.",
|
|
452
|
+
fix: {
|
|
453
|
+
description: "Add the `crossorigin` attribute to the font preload link.",
|
|
454
|
+
snippet: '<link rel="preload" href="/inter.woff2" as="font" type="font/woff2" crossorigin />',
|
|
455
|
+
lang: "html"
|
|
456
|
+
},
|
|
457
|
+
relevant: (t) => t.rel === "preload" && t.as === "font",
|
|
458
|
+
ok: (t) => t.hasCrossorigin === true
|
|
459
|
+
});
|
|
460
|
+
|
|
376
461
|
// src/rules/index.ts
|
|
377
462
|
var allRules = [
|
|
378
463
|
seo001Title,
|
|
@@ -385,7 +470,9 @@ var allRules = [
|
|
|
385
470
|
seo008JsonLd,
|
|
386
471
|
seo009HtmlLang,
|
|
387
472
|
perf001ImageDimensions,
|
|
388
|
-
perf002ImageLoading
|
|
473
|
+
perf002ImageLoading,
|
|
474
|
+
perf003PreloadAs,
|
|
475
|
+
perf004FontPreloadCrossorigin
|
|
389
476
|
];
|
|
390
477
|
function explainRule(id) {
|
|
391
478
|
const target = id.toUpperCase();
|
|
@@ -769,6 +856,192 @@ function formatGithubReport(results, config) {
|
|
|
769
856
|
return lines.join("\n");
|
|
770
857
|
}
|
|
771
858
|
|
|
859
|
+
// src/reporter/html.ts
|
|
860
|
+
var BAND_COLOR = {
|
|
861
|
+
good: "#2FA968",
|
|
862
|
+
warn: "#E8A317",
|
|
863
|
+
poor: "#E5484D"
|
|
864
|
+
};
|
|
865
|
+
function scoreBand(score) {
|
|
866
|
+
return score >= 90 ? "good" : score >= 50 ? "warn" : "poor";
|
|
867
|
+
}
|
|
868
|
+
function escapeHtml(s) {
|
|
869
|
+
return s.replace(
|
|
870
|
+
/[&<>"']/g,
|
|
871
|
+
(c) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : c === '"' ? """ : "'"
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
function safeHref(url) {
|
|
875
|
+
const normalized = url.replace(/\s/g, "").toLowerCase();
|
|
876
|
+
return /^https?:\/\//.test(normalized) ? url : null;
|
|
877
|
+
}
|
|
878
|
+
var slug = (route) => "route-" + route.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase();
|
|
879
|
+
var categoryLabel = (cat) => cat === "seo" ? "SEO" : cat.charAt(0).toUpperCase() + cat.slice(1);
|
|
880
|
+
var SEVERITIES = ["critical", "warning", "info"];
|
|
881
|
+
function renderFinding(issue) {
|
|
882
|
+
const sev = SEVERITIES.includes(issue.severity) ? issue.severity : "info";
|
|
883
|
+
const dyn = issue.detection.value === "dynamic" ? ' <span class="dyn" title="set dynamically (verified at runtime)">\u21AF</span>' : "";
|
|
884
|
+
const line = issue.line !== void 0 ? `:${issue.line}` : "";
|
|
885
|
+
const fix = issue.fix?.snippet !== void 0 ? `<div class="fix"><div class="label">fix</div><pre><code>${escapeHtml(issue.fix.snippet)}</code></pre></div>` : "";
|
|
886
|
+
const href = issue.docsUrl ? safeHref(issue.docsUrl) : null;
|
|
887
|
+
const docs = href ? `<a class="f-link" href="${escapeHtml(href)}">Learn more</a>` : "";
|
|
888
|
+
return `<article class="finding sev-${sev}" data-severity="${sev}" data-category="${escapeHtml(issue.category)}"><div class="f-head"><span class="ruleid">${escapeHtml(issue.id)}</span><span class="f-title">${escapeHtml(issue.title)}</span><span class="sev-tag ${sev}">${sev}</span></div>` + (issue.location ? `<p class="f-loc">${escapeHtml(issue.location)}${line}${dyn}</p>` : "") + (issue.recommendation ? `<p class="f-rec">${escapeHtml(issue.recommendation)}</p>` : "") + fix + docs + `</article>`;
|
|
889
|
+
}
|
|
890
|
+
function renderTopbar(report, meta) {
|
|
891
|
+
const findings = report.routes.reduce((n, r) => n + r.issues.length, 0) + report.siteIssues.length;
|
|
892
|
+
return `<header class="topbar"><div class="brand"><span class="bolt">\u21AF</span>svelte-<span class="v">vitals</span></div><div class="meta"><span>v${escapeHtml(meta.version)}</span><span>${report.routes.length} routes</span><span>${findings} findings</span></div></header>`;
|
|
893
|
+
}
|
|
894
|
+
function renderHero(report) {
|
|
895
|
+
const C = 2 * Math.PI * 58;
|
|
896
|
+
const offset = (C * (1 - report.score / 100)).toFixed(1);
|
|
897
|
+
const hb = scoreBand(report.score);
|
|
898
|
+
const s = report.summary;
|
|
899
|
+
const dynNote = s.dynamic > 0 ? `<span class="tally"><span class="dot dyn-dot">\u21AF</span>Dynamic <span class="n">${s.dynamic}</span></span>` : "";
|
|
900
|
+
const cats = Object.entries(report.categories).map(([cat, { score }]) => {
|
|
901
|
+
const b = scoreBand(score);
|
|
902
|
+
const weight = report.weights[cat];
|
|
903
|
+
const w = weight !== void 0 ? `<span class="w">weight ${weight}</span>` : "";
|
|
904
|
+
const name = categoryLabel(cat);
|
|
905
|
+
return `<div class="cat"><div class="top"><span class="name">${escapeHtml(name)} ${w}</span><span class="sc" style="color:${BAND_COLOR[b]}">${score}</span></div><div class="bar"><i style="width:${score}%;background:${BAND_COLOR[b]}"></i></div></div>`;
|
|
906
|
+
}).join("");
|
|
907
|
+
return `<section class="hero"><div class="gauge"><svg width="132" height="132" viewBox="0 0 132 132" aria-hidden="true"><circle cx="66" cy="66" r="58" fill="none" stroke="#e4e7ec" stroke-width="11"></circle><circle id="arc" cx="66" cy="66" r="58" fill="none" stroke="${BAND_COLOR[hb]}" stroke-width="11" stroke-linecap="round" stroke-dasharray="${C.toFixed(1)}" stroke-dashoffset="${offset}"></circle></svg><div class="num"><strong id="hnum">${report.score}</strong><span>Health</span></div></div><div class="readout"><div class="eyebrow">SvelteKit \xB7 SEO & Performance</div><div class="tallies"><span class="tally"><span class="dot crit"></span>Critical <span class="n">${s.critical}</span></span><span class="tally"><span class="dot warn"></span>Warning <span class="n">${s.warning}</span></span><span class="tally"><span class="dot info"></span>Info <span class="n">${s.info}</span></span><span class="tally"><span class="dot pass"></span>Passed <span class="n">${s.passed}</span></span>` + dynNote + `</div><div class="cats">${cats}</div></div></section>`;
|
|
908
|
+
}
|
|
909
|
+
function renderRoutes(report) {
|
|
910
|
+
if (report.routes.length === 0) return "";
|
|
911
|
+
const rows = report.routes.map((r) => {
|
|
912
|
+
const b = scoreBand(r.score);
|
|
913
|
+
const crit = r.issues.filter((i) => i.severity === "critical").length;
|
|
914
|
+
const warn = r.issues.filter((i) => i.severity === "warning").length;
|
|
915
|
+
const info = r.issues.filter((i) => i.severity === "info").length;
|
|
916
|
+
const parts = [];
|
|
917
|
+
if (crit) parts.push(`${crit} critical`);
|
|
918
|
+
if (warn) parts.push(`${warn} warning${warn > 1 ? "s" : ""}`);
|
|
919
|
+
if (info) parts.push(`${info} info`);
|
|
920
|
+
const sum = parts.length ? parts.join(" \xB7 ") : '<span class="none">no issues</span>';
|
|
921
|
+
const body = r.issues.length ? r.issues.map(renderFinding).join("") : '<p class="empty">No issues found on this route.</p>';
|
|
922
|
+
return `<details class="route" id="${slug(r.route)}" data-score="${r.score}"${r.issues.length ? " open" : ""}><summary><span class="route-name"><span class="path">${escapeHtml(r.route)}</span></span><span class="issue-sum">${sum}</span><span class="score-chip"><span class="ring" style="background:${BAND_COLOR[b]}"></span>${r.score}</span><span class="chev">\u203A</span></summary><div class="route-body">${body}</div></details>`;
|
|
923
|
+
}).join("");
|
|
924
|
+
return `<section class="section"><h2>Routes</h2><div class="routes">${rows}</div></section>`;
|
|
925
|
+
}
|
|
926
|
+
function renderSiteChecks(report) {
|
|
927
|
+
if (report.siteIssues.length === 0) return "";
|
|
928
|
+
const cards = report.siteIssues.map(renderFinding).join("");
|
|
929
|
+
return `<section class="section"><h2>Site checks</h2>${cards}</section>`;
|
|
930
|
+
}
|
|
931
|
+
function renderFilters(report) {
|
|
932
|
+
const chip = (filter, label, pressed = false) => `<button class="chip" type="button" aria-pressed="${pressed}" data-filter="${escapeHtml(filter)}">${escapeHtml(label)}</button>`;
|
|
933
|
+
const catChips = Object.keys(report.categories).map((cat) => chip(cat, categoryLabel(cat))).join("");
|
|
934
|
+
return `<div class="filters" role="group" aria-label="Filter findings">` + chip("all", "All", true) + chip("critical", "Critical") + chip("warning", "Warning") + chip("info", "Info") + catChips + `</div>`;
|
|
935
|
+
}
|
|
936
|
+
var STYLE = `
|
|
937
|
+
:root{--ground: #f6f7f9;--panel: #fff;--ink: #0c1322;--muted: #5a6472;--faint: #8c95a3;--line: #e4e7ec;--line-strong: #d3d8e0;--accent: #ff3e00;--good: #2fa968;--warn: #e8a317;--poor: #e5484d;--code-bg: #0e1525;--code-ink: #e7ecf4;--radius: 12px;--mono: ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;--sans: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}
|
|
938
|
+
*{box-sizing:border-box}
|
|
939
|
+
body{margin:0;background:var(--ground);color:var(--ink);font-family:var(--sans);line-height:1.5;-webkit-font-smoothing:antialiased}
|
|
940
|
+
.wrap{max-width:960px;margin:0 auto;padding:0 20px 96px}
|
|
941
|
+
.topbar{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;padding:18px 0 16px;border-bottom:1px solid var(--line)}
|
|
942
|
+
.brand{display:flex;align-items:baseline;gap:8px;font-weight:700;font-size:18px;letter-spacing:-.02em}
|
|
943
|
+
.brand .bolt{color:var(--accent);font-size:20px}.brand .v{color:var(--accent)}
|
|
944
|
+
.meta{font-family:var(--mono);font-size:12.5px;color:var(--muted);display:flex;gap:14px;flex-wrap:wrap}
|
|
945
|
+
.hero{display:grid;grid-template-columns:auto 1fr;gap:28px;align-items:center;padding:30px 0 24px}
|
|
946
|
+
.gauge{position:relative;width:132px;height:132px}.gauge svg{transform:rotate(-90deg);display:block}
|
|
947
|
+
.gauge .num{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center}
|
|
948
|
+
.gauge .num strong{font-family:var(--mono);font-size:40px;font-weight:600;letter-spacing:-.03em;line-height:1;font-variant-numeric:tabular-nums}
|
|
949
|
+
.gauge .num span{font-size:11px;text-transform:uppercase;letter-spacing:.14em;color:var(--muted);margin-top:6px}
|
|
950
|
+
.readout{min-width:0}
|
|
951
|
+
.eyebrow{font-size:11px;text-transform:uppercase;letter-spacing:.16em;color:var(--accent);font-weight:700}
|
|
952
|
+
.eyebrow::before{content:"\u21AF "}
|
|
953
|
+
.tallies{display:flex;gap:8px;flex-wrap:wrap;margin:16px 0 18px}
|
|
954
|
+
.tally{display:inline-flex;align-items:center;gap:7px;font-size:13px;font-weight:600;background:var(--panel);border:1px solid var(--line);padding:5px 11px;border-radius:999px}
|
|
955
|
+
.tally .dot{width:9px;height:9px;border-radius:50%}.tally .n{font-family:var(--mono);font-variant-numeric:tabular-nums}
|
|
956
|
+
.dot.crit{background:var(--poor)}.dot.warn{background:var(--warn)}.dot.info{background:var(--faint)}.dot.pass{background:var(--good)}
|
|
957
|
+
.dot.dyn-dot{background:transparent;color:var(--accent);font-weight:700;width:auto;height:auto}
|
|
958
|
+
.cats{display:flex;gap:22px;flex-wrap:wrap}
|
|
959
|
+
.cat{min-width:190px;flex:1}.cat .top{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:6px}
|
|
960
|
+
.cat .name{font-size:13px;font-weight:600}.cat .name .w{color:var(--faint);font-family:var(--mono);font-size:11px;font-weight:500;margin-left:6px}
|
|
961
|
+
.cat .sc{font-family:var(--mono);font-weight:600;font-variant-numeric:tabular-nums}
|
|
962
|
+
.bar{height:7px;border-radius:999px;background:var(--line);overflow:hidden}.bar>i{display:block;height:100%;border-radius:999px}
|
|
963
|
+
.section{margin-top:40px}
|
|
964
|
+
.section>h2{font-size:13px;text-transform:uppercase;letter-spacing:.12em;color:var(--muted);font-weight:700;margin:0 0 14px}
|
|
965
|
+
.filters{display:flex;gap:8px;flex-wrap:wrap;margin-top:28px}
|
|
966
|
+
.chip{font:inherit;font-size:12.5px;font-weight:600;cursor:pointer;background:var(--panel);border:1px solid var(--line-strong);color:var(--muted);padding:5px 12px;border-radius:999px}
|
|
967
|
+
.chip:hover{border-color:var(--faint);color:var(--ink)}
|
|
968
|
+
.chip[aria-pressed="true"]{background:var(--ink);border-color:var(--ink);color:#fff}
|
|
969
|
+
.chip:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
970
|
+
.routes{border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;background:var(--panel)}
|
|
971
|
+
.route{border-top:1px solid var(--line)}.route:first-child{border-top:0}
|
|
972
|
+
.route>summary{display:grid;grid-template-columns:1fr auto auto auto;gap:16px;align-items:center;padding:13px 16px;cursor:pointer;list-style:none}
|
|
973
|
+
.route>summary::-webkit-details-marker{display:none}
|
|
974
|
+
.route>summary:hover{background:#fbfcfd}
|
|
975
|
+
.route-name{font-family:var(--mono);font-size:13.5px;min-width:0}
|
|
976
|
+
.route-name .path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
977
|
+
.issue-sum{font-size:12.5px;color:var(--muted);font-family:var(--mono);white-space:nowrap}.issue-sum .none{color:var(--good)}
|
|
978
|
+
.score-chip{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-weight:600;font-variant-numeric:tabular-nums;font-size:14px}
|
|
979
|
+
.score-chip .ring{width:10px;height:10px;border-radius:50%}
|
|
980
|
+
.chev{color:var(--faint);font-family:var(--mono);transition:transform .15s ease}
|
|
981
|
+
.route[open]>summary .chev{transform:rotate(90deg)}
|
|
982
|
+
.route-body{padding:4px 16px 16px}.empty{color:var(--muted);font-size:13px;margin:6px 0}
|
|
983
|
+
.finding{background:var(--panel);border:1px solid var(--line);border-left-width:3px;border-radius:10px;padding:16px 18px;margin:0 0 12px}
|
|
984
|
+
.finding.sev-critical{border-left-color:var(--poor)}.finding.sev-warning{border-left-color:var(--warn)}.finding.sev-info{border-left-color:var(--faint)}
|
|
985
|
+
.f-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
986
|
+
.ruleid{font-family:var(--mono);font-size:12px;font-weight:600;background:#eef1f5;padding:2px 8px;border-radius:6px}
|
|
987
|
+
.f-title{font-weight:650;font-size:15px}
|
|
988
|
+
.sev-tag{margin-left:auto;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}
|
|
989
|
+
.sev-tag.critical{color:var(--poor)}.sev-tag.warning{color:var(--warn)}.sev-tag.info{color:var(--faint)}
|
|
990
|
+
.f-loc{font-family:var(--mono);font-size:12.5px;color:var(--muted);margin:8px 0 0}.dyn{color:var(--accent);font-weight:700}
|
|
991
|
+
.f-rec{font-size:14px;color:#2b3340;margin:10px 0 0}
|
|
992
|
+
.fix{margin:12px 0 0;background:var(--code-bg);border-radius:8px;overflow:hidden}
|
|
993
|
+
.fix .label{font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:#8da0bd;padding:8px 14px 0}
|
|
994
|
+
.fix pre{margin:0;padding:8px 14px 14px;overflow-x:auto}
|
|
995
|
+
.fix code{font-family:var(--mono);font-size:12.5px;color:var(--code-ink);line-height:1.65;white-space:pre}
|
|
996
|
+
.f-link{display:inline-block;margin-top:12px;font-size:13px;font-weight:600;color:var(--accent);text-decoration:none}
|
|
997
|
+
.f-link:hover{text-decoration:underline}.f-link::after{content:" \u2192"}
|
|
998
|
+
.foot{margin-top:48px;padding-top:18px;border-top:1px solid var(--line);font-family:var(--mono);font-size:12px;color:var(--faint);display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px}
|
|
999
|
+
.foot a{color:var(--accent);text-decoration:none}
|
|
1000
|
+
@media (max-width:640px){.hero{grid-template-columns:1fr;justify-items:start}.route>summary{grid-template-columns:1fr auto}.chev{display:none}}
|
|
1001
|
+
@media (prefers-reduced-motion:reduce){*{transition:none!important}}
|
|
1002
|
+
`;
|
|
1003
|
+
var SCRIPT = `
|
|
1004
|
+
(function(){
|
|
1005
|
+
var arc=document.getElementById('arc'),num=document.getElementById('hnum');
|
|
1006
|
+
if(arc&&num){
|
|
1007
|
+
var C=2*Math.PI*58,score=parseInt(num.textContent,10)||0;
|
|
1008
|
+
var reduce=window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
1009
|
+
if(!reduce){
|
|
1010
|
+
arc.style.transition='stroke-dashoffset 1.1s cubic-bezier(.22,.61,.36,1)';
|
|
1011
|
+
arc.style.strokeDashoffset=C.toFixed(1);
|
|
1012
|
+
var start=null;
|
|
1013
|
+
requestAnimationFrame(function step(t){
|
|
1014
|
+
if(start===null)start=t;
|
|
1015
|
+
var p=Math.min((t-start)/1100,1);
|
|
1016
|
+
num.textContent=Math.round(score*(p<1?1-Math.pow(1-p,3):1));
|
|
1017
|
+
if(p<1)requestAnimationFrame(step);
|
|
1018
|
+
});
|
|
1019
|
+
requestAnimationFrame(function(){arc.style.strokeDashoffset=(C*(1-score/100)).toFixed(1);});
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
var chips=document.querySelectorAll('.chip'),findings=document.querySelectorAll('.finding');
|
|
1023
|
+
function apply(f){
|
|
1024
|
+
findings.forEach(function(el){
|
|
1025
|
+
var ok=f==='all'||el.getAttribute('data-severity')===f||el.getAttribute('data-category')===f;
|
|
1026
|
+
el.style.display=ok?'':'none';
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
chips.forEach(function(c){
|
|
1030
|
+
c.addEventListener('click',function(){
|
|
1031
|
+
chips.forEach(function(o){o.setAttribute('aria-pressed','false');});
|
|
1032
|
+
c.setAttribute('aria-pressed','true');
|
|
1033
|
+
apply(c.getAttribute('data-filter')||'all');
|
|
1034
|
+
});
|
|
1035
|
+
});
|
|
1036
|
+
})();
|
|
1037
|
+
`;
|
|
1038
|
+
function buildHtmlDocument(report, meta) {
|
|
1039
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>svelte-vitals report</title><style>${STYLE}</style></head><body><div class="wrap">` + renderTopbar(report, meta) + renderHero(report) + renderFilters(report) + renderRoutes(report) + renderSiteChecks(report) + `<footer class="foot"><span>Generated by svelte-vitals \xB7 static analysis, no browser</span><span>oekazuma.github.io/svelte-vitals</span></footer></div><script>${SCRIPT}</script></body></html>`;
|
|
1040
|
+
}
|
|
1041
|
+
function formatHtmlReport(results, config, meta) {
|
|
1042
|
+
return buildHtmlDocument(buildJsonReport(results, config, meta), meta);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
772
1045
|
// src/config-apply.ts
|
|
773
1046
|
function selectRules(rules, config) {
|
|
774
1047
|
return rules.filter((rule) => config.rules[rule.id] !== "off");
|
|
@@ -780,10 +1053,12 @@ function applyRuleSeverities(results, config) {
|
|
|
780
1053
|
});
|
|
781
1054
|
}
|
|
782
1055
|
export {
|
|
1056
|
+
BAND_COLOR,
|
|
783
1057
|
ROBOTS_SOURCE_PATHS,
|
|
784
1058
|
SITEMAP_SOURCE_PATHS,
|
|
785
1059
|
allRules,
|
|
786
1060
|
applyRuleSeverities,
|
|
1061
|
+
buildHtmlDocument,
|
|
787
1062
|
buildJsonReport,
|
|
788
1063
|
classify,
|
|
789
1064
|
computeHealth,
|
|
@@ -793,19 +1068,26 @@ export {
|
|
|
793
1068
|
defineConfig,
|
|
794
1069
|
docsUrlFor,
|
|
795
1070
|
effectiveSeverity,
|
|
1071
|
+
escapeHtml,
|
|
796
1072
|
explainRule,
|
|
797
1073
|
formatAgentReport,
|
|
798
1074
|
formatConsoleReport,
|
|
799
1075
|
formatGithubReport,
|
|
1076
|
+
formatHtmlReport,
|
|
800
1077
|
formatJsonReport,
|
|
801
1078
|
formatSarifReport,
|
|
802
1079
|
hasFailureAtOrAbove,
|
|
803
1080
|
headTagRule,
|
|
804
1081
|
imageRule,
|
|
805
1082
|
isPenalized,
|
|
1083
|
+
linkRule,
|
|
806
1084
|
perf001ImageDimensions,
|
|
807
1085
|
perf002ImageLoading,
|
|
1086
|
+
perf003PreloadAs,
|
|
1087
|
+
perf004FontPreloadCrossorigin,
|
|
808
1088
|
runRules,
|
|
1089
|
+
safeHref,
|
|
1090
|
+
scoreBand,
|
|
809
1091
|
scoresByCategory,
|
|
810
1092
|
selectRules,
|
|
811
1093
|
seo001Title,
|