@svelte-vitals/core 0.23.0 → 0.25.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/README.md +2 -2
- package/dist/index.d.ts +61 -11
- package/dist/index.js +689 -184
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@svelte-vitals/core)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
Runtime-agnostic core for [svelte-vitals](https://github.com/oekazuma/svelte-vitals): shared types, the rule engine, scorer,
|
|
6
|
+
Runtime-agnostic core for [svelte-vitals](https://github.com/oekazuma/svelte-vitals): shared types, the rule engine, scorer, and reporters, plus the full rule set across five categories — SEO, Performance, Correctness, Security, Architecture.
|
|
7
7
|
|
|
8
8
|
This package is **mode-independent** and contains no I/O — it operates on a normalized `ResolvedHead[]` intermediate representation, so the same rules run unchanged whether heads come from static source analysis (the `svelte-vitals` CLI) or from prerendered HTML (the `@svelte-vitals/vite` plugin). It has zero runtime dependencies and no `node:` imports.
|
|
9
9
|
|
|
10
10
|
> Most users don't depend on this directly — install [`svelte-vitals`](https://www.npmjs.com/package/svelte-vitals) instead. This package is for building tools on top of the shared engine.
|
|
11
11
|
|
|
12
|
-
> **ESM-only** (Node
|
|
12
|
+
> **ESM-only** (Node 22.13+). Ships ES modules only; `require()` is unsupported by design.
|
|
13
13
|
|
|
14
14
|
## License
|
|
15
15
|
|
package/dist/index.d.ts
CHANGED
|
@@ -247,6 +247,11 @@ interface ComponentFacts {
|
|
|
247
247
|
propCount: number;
|
|
248
248
|
/** Module specifiers of every `import` in the instance + module scripts (Bundle PERF009). */
|
|
249
249
|
imports: string[];
|
|
250
|
+
/** Module specifiers of every `import`, each with its source line (Bundle PERF009). */
|
|
251
|
+
importSpans: {
|
|
252
|
+
source: string;
|
|
253
|
+
line: number;
|
|
254
|
+
}[];
|
|
250
255
|
/** Value `import * as X from '<bare pkg>'` namespace imports (type-only excluded) — Bundle PERF010. */
|
|
251
256
|
namespaceImports: {
|
|
252
257
|
source: string;
|
|
@@ -275,6 +280,10 @@ declare function parseComponentFacts(source: string, filename: string): {
|
|
|
275
280
|
loc: number;
|
|
276
281
|
propCount: number;
|
|
277
282
|
imports: string[];
|
|
283
|
+
importSpans: {
|
|
284
|
+
source: string;
|
|
285
|
+
line: number;
|
|
286
|
+
}[];
|
|
278
287
|
namespaceImports: {
|
|
279
288
|
source: string;
|
|
280
289
|
line: number;
|
|
@@ -731,6 +740,57 @@ declare function formatMarkdownReport(results: Result[], config: Config, meta: {
|
|
|
731
740
|
version: string;
|
|
732
741
|
}): string;
|
|
733
742
|
|
|
743
|
+
/** Provenance of a route's findings: real rendered page vs. source-only analysis. */
|
|
744
|
+
type RouteBadge = 'measured' | 'static';
|
|
745
|
+
interface AppSnapshot {
|
|
746
|
+
report: JsonReport;
|
|
747
|
+
badges: Record<string, RouteBadge>;
|
|
748
|
+
analyzing: boolean;
|
|
749
|
+
/** Monotonically increasing; lets the client discard an out-of-order /data.json response. */
|
|
750
|
+
sequence: number;
|
|
751
|
+
/** Whether a dev server is behind this page (SSE updates, /data.json refetch, connection dot). */
|
|
752
|
+
live: boolean;
|
|
753
|
+
meta: {
|
|
754
|
+
version: string;
|
|
755
|
+
coreVersion?: string;
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Hand-authored CSS for the master/detail shell. Reuses the same design-token
|
|
760
|
+
* names/values as the rest of the project, and adds a dark theme via
|
|
761
|
+
* `:root[data-theme="dark"]` plus a `prefers-color-scheme` fallback for a
|
|
762
|
+
* first-ever visit with no stored preference.
|
|
763
|
+
*/
|
|
764
|
+
declare const APP_STYLE: string;
|
|
765
|
+
/**
|
|
766
|
+
* Hand-authored client script for the shell — no bundler, no framework. Parses the
|
|
767
|
+
* AppSnapshot embedded by renderAppShell, then owns all rendering: sidebar
|
|
768
|
+
* (search/sort/route list) and detail pane (Overview or a selected route). When the
|
|
769
|
+
* snapshot says `live`, it additionally re-fetches /data.json on every SSE `update`
|
|
770
|
+
* and on the EventSource's `open` event (covers the initial connection and every
|
|
771
|
+
* auto-reconnect, since EventSource replays no missed events) — discarding any
|
|
772
|
+
* response whose `sequence` isn't newer than what's already rendered.
|
|
773
|
+
*/
|
|
774
|
+
declare const APP_SCRIPT: string;
|
|
775
|
+
/** The shell HTML: empty sidebar/detail/topbar containers, the stylesheet, the
|
|
776
|
+
* client script, and the snapshot embedded as JSON for the client's first paint. */
|
|
777
|
+
declare function renderAppShell(snapshot: AppSnapshot): string;
|
|
778
|
+
/**
|
|
779
|
+
* Static (non-live) document over a prebuilt JsonReport — kept as the public name the
|
|
780
|
+
* html reporter has always exported. `routeBadges` preserves the old opts shape.
|
|
781
|
+
*/
|
|
782
|
+
declare function buildHtmlDocument(report: JsonReport, meta: {
|
|
783
|
+
version: string;
|
|
784
|
+
coreVersion?: string;
|
|
785
|
+
}, opts?: {
|
|
786
|
+
routeBadges?: Record<string, RouteBadge>;
|
|
787
|
+
}): string;
|
|
788
|
+
/** Render results as the self-contained HTML report (the CLI's `--reporter html`). */
|
|
789
|
+
declare function formatHtmlReport(results: Result[], config: Config, meta: {
|
|
790
|
+
version: string;
|
|
791
|
+
coreVersion?: string;
|
|
792
|
+
}): string;
|
|
793
|
+
|
|
734
794
|
type Band = 'good' | 'warn' | 'poor';
|
|
735
795
|
declare const BAND_COLOR: Record<Band, string>;
|
|
736
796
|
declare function scoreBand(score: number): Band;
|
|
@@ -745,20 +805,10 @@ declare function escapeHtml(s: string): string;
|
|
|
745
805
|
* keeping core runtime-agnostic and lib-minimal.
|
|
746
806
|
*/
|
|
747
807
|
declare function safeHref(url: string): string | null;
|
|
748
|
-
declare function buildHtmlDocument(report: JsonReport, meta: {
|
|
749
|
-
version: string;
|
|
750
|
-
coreVersion?: string;
|
|
751
|
-
}, opts?: {
|
|
752
|
-
routeBadges?: Record<string, 'measured' | 'static'>;
|
|
753
|
-
}): string;
|
|
754
|
-
declare function formatHtmlReport(results: Result[], config: Config, meta: {
|
|
755
|
-
version: string;
|
|
756
|
-
coreVersion?: string;
|
|
757
|
-
}): string;
|
|
758
808
|
|
|
759
809
|
/** Drop rules disabled via config (design §6). */
|
|
760
810
|
declare function selectRules(rules: Rule[], config: Config): Rule[];
|
|
761
811
|
/** Apply per-rule severity overrides to results (design §6). */
|
|
762
812
|
declare function applyRuleSeverities(results: Result[], config: Config): Result[];
|
|
763
813
|
|
|
764
|
-
export { BAND_COLOR, CHILD_NODE_KEYS, type Category, type Classification, type ComponentFacts, type Config, type ConsoleReportOptions, type Detection, type EachBlockFact, type EffectFact, type Fix, type HeadProvider, type HeadTag, type HeadingInfo, type HealthResult, type ImageInfo, type JsonReport, type Palette, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type ResolvedHeadings, 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 SourceSpan, type Summary, type SuppressionDirective, type TreatDynamicAs, type Value, allRules, applyRuleSeverities, arch001ComponentSize, arch002PropCount, attrText, attrTextOf, attrValue, attrValueOf, buildHtmlDocument, buildJsonReport, classify, collectComponentFacts, computeHealth, computeScore, correct001EachKey, correct002EffectDerived, correct003EffectAsOnMount, correct004UnmutatedState, correct005PropMutation, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, emptyComponentFacts, escapeHtml, explainRule, findAttr, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatMarkdownReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, lineOf, linkRule, noColorPalette, parseComponentFacts, perf001ImageDimensions, perf002ImageLoading, perf003PreloadAs, perf004FontPreloadCrossorigin, perf005LcpImage, perf006ResponsiveImage, perf007RenderBlockingScript, perf008Preconnect, perf009HeavyImport, perf010NamespaceImport, runRules, safeHref, scoreBand, scoreColor, scoresByCategory, sec001Html, sec002JavascriptUrl, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, seo010Indexability, seo011TwitterCard, seo012OgDescription, seo013OgUrl, seo014Viewport, seo015SitemapInRobots, seo016JsonLdValidity, seo017DeprecatedType, seo018RelativeUrl, seo019DateFormat, seo020Placeholder, seo021RequiredProps, seo022TitleLength, seo023DescriptionLength, seo024Charset, seo025ImageAlt, seo026Hreflang, seo027Heading, seo028TitleUnique, seo029DescriptionUnique, seo030HeadingOrder, summarize, textFromNodes, valueFromNodes };
|
|
814
|
+
export { APP_SCRIPT, APP_STYLE, type AppSnapshot, BAND_COLOR, CHILD_NODE_KEYS, type Category, type Classification, type ComponentFacts, type Config, type ConsoleReportOptions, type Detection, type EachBlockFact, type EffectFact, type Fix, type HeadProvider, type HeadTag, type HeadingInfo, type HealthResult, type ImageInfo, type JsonReport, type Palette, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type ResolvedHeadings, type ResolvedImages, type Result, type RouteBadge, type Rule, type RuleContext, type RuleInfo, type RuleSetting, type Runtime, SITEMAP_SOURCE_PATHS, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type SourceSpan, type Summary, type SuppressionDirective, type TreatDynamicAs, type Value, allRules, applyRuleSeverities, arch001ComponentSize, arch002PropCount, attrText, attrTextOf, attrValue, attrValueOf, buildHtmlDocument, buildJsonReport, classify, collectComponentFacts, computeHealth, computeScore, correct001EachKey, correct002EffectDerived, correct003EffectAsOnMount, correct004UnmutatedState, correct005PropMutation, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, emptyComponentFacts, escapeHtml, explainRule, findAttr, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatMarkdownReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, lineOf, linkRule, noColorPalette, parseComponentFacts, perf001ImageDimensions, perf002ImageLoading, perf003PreloadAs, perf004FontPreloadCrossorigin, perf005LcpImage, perf006ResponsiveImage, perf007RenderBlockingScript, perf008Preconnect, perf009HeavyImport, perf010NamespaceImport, renderAppShell, runRules, safeHref, scoreBand, scoreColor, scoresByCategory, sec001Html, sec002JavascriptUrl, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, seo010Indexability, seo011TwitterCard, seo012OgDescription, seo013OgUrl, seo014Viewport, seo015SitemapInRobots, seo016JsonLdValidity, seo017DeprecatedType, seo018RelativeUrl, seo019DateFormat, seo020Placeholder, seo021RequiredProps, seo022TitleLength, seo023DescriptionLength, seo024Charset, seo025ImageAlt, seo026Hreflang, seo027Heading, seo028TitleUnique, seo029DescriptionUnique, seo030HeadingOrder, summarize, textFromNodes, valueFromNodes };
|
package/dist/index.js
CHANGED
|
@@ -419,9 +419,11 @@ function countLines(source) {
|
|
|
419
419
|
if (source.length === 0) return 0;
|
|
420
420
|
return source.split("\n").length - (source.endsWith("\n") ? 1 : 0);
|
|
421
421
|
}
|
|
422
|
-
function collectImportSources(program, acc) {
|
|
422
|
+
function collectImportSources(program, source, acc) {
|
|
423
423
|
walkEstree(program, (n) => {
|
|
424
|
-
if (n.type === "ImportDeclaration" && typeof n.source?.value === "string")
|
|
424
|
+
if (n.type === "ImportDeclaration" && typeof n.source?.value === "string") {
|
|
425
|
+
acc.push({ source: n.source.value, line: lineOf(source, n.start) });
|
|
426
|
+
}
|
|
425
427
|
});
|
|
426
428
|
}
|
|
427
429
|
function isBareSpecifier(s) {
|
|
@@ -459,10 +461,10 @@ function parseComponentFacts(source, filename) {
|
|
|
459
461
|
collectSecurityFacts(ast.fragment ?? ast, source, htmlTags, javascriptUrls);
|
|
460
462
|
const loc = countLines(source);
|
|
461
463
|
const suppressions = collectSuppressions(source);
|
|
462
|
-
const
|
|
464
|
+
const importSpans = [];
|
|
463
465
|
const namespaceImports = [];
|
|
464
466
|
if (ast.module?.content) {
|
|
465
|
-
collectImportSources(ast.module.content,
|
|
467
|
+
collectImportSources(ast.module.content, source, importSpans);
|
|
466
468
|
collectNamespaceImports(ast.module.content, source, namespaceImports);
|
|
467
469
|
}
|
|
468
470
|
const effects = [];
|
|
@@ -471,7 +473,7 @@ function parseComponentFacts(source, filename) {
|
|
|
471
473
|
let propCount = 0;
|
|
472
474
|
const program = ast.instance?.content;
|
|
473
475
|
if (program) {
|
|
474
|
-
collectImportSources(program,
|
|
476
|
+
collectImportSources(program, source, importSpans);
|
|
475
477
|
collectNamespaceImports(program, source, namespaceImports);
|
|
476
478
|
propCount = countProps(program);
|
|
477
479
|
const nonBindableProps = collectNonBindableProps(program);
|
|
@@ -509,6 +511,7 @@ function parseComponentFacts(source, filename) {
|
|
|
509
511
|
if (!writtenOrEscaped.has(d.name)) constableStates.push(d);
|
|
510
512
|
}
|
|
511
513
|
}
|
|
514
|
+
const imports = importSpans.map((s) => s.source);
|
|
512
515
|
return {
|
|
513
516
|
eachBlocks,
|
|
514
517
|
effects,
|
|
@@ -517,6 +520,7 @@ function parseComponentFacts(source, filename) {
|
|
|
517
520
|
loc,
|
|
518
521
|
propCount,
|
|
519
522
|
imports,
|
|
523
|
+
importSpans,
|
|
520
524
|
namespaceImports,
|
|
521
525
|
constableStates,
|
|
522
526
|
mutatedProps,
|
|
@@ -535,6 +539,7 @@ function emptyComponentFacts(file) {
|
|
|
535
539
|
loc: 0,
|
|
536
540
|
propCount: 0,
|
|
537
541
|
imports: [],
|
|
542
|
+
importSpans: [],
|
|
538
543
|
namespaceImports: [],
|
|
539
544
|
constableStates: [],
|
|
540
545
|
mutatedProps: [],
|
|
@@ -2143,7 +2148,7 @@ var arch002PropCount = componentRule({
|
|
|
2143
2148
|
bad: (c) => c.propCount > MAX_PROPS ? [{ line: 1, message: `Component takes ${c.propCount} props (over ${MAX_PROPS})` }] : []
|
|
2144
2149
|
});
|
|
2145
2150
|
|
|
2146
|
-
// src/rules/
|
|
2151
|
+
// src/rules/perf/perf009-heavy-import.ts
|
|
2147
2152
|
var HEAVY_PACKAGES = {
|
|
2148
2153
|
lodash: "import a submodule (lodash/debounce) or use lodash-es for tree-shaking",
|
|
2149
2154
|
moment: "use a lighter date library (date-fns or dayjs) \u2014 moment is large and not tree-shakeable"
|
|
@@ -2156,20 +2161,24 @@ var perf009HeavyImport = componentRule({
|
|
|
2156
2161
|
label: "No heavy imports",
|
|
2157
2162
|
recommendation: "Import a submodule or switch to a lighter, tree-shakeable alternative.",
|
|
2158
2163
|
rationale: "Importing a large, non-tree-shakeable package pulls its whole weight into the bundle even when only a fraction is used, slowing load.",
|
|
2159
|
-
|
|
2164
|
+
// ComponentFacts is a public @svelte-vitals/core export — an external caller compiled
|
|
2165
|
+
// against an older version may still construct one without importSpans. Fall back to the
|
|
2166
|
+
// line-less `imports` (line: 0, the pre-fix behavior) instead of crashing on `undefined`.
|
|
2167
|
+
applies: (c) => (c.importSpans ?? c.imports).length > 0,
|
|
2160
2168
|
bad: (c) => {
|
|
2161
2169
|
const seen = /* @__PURE__ */ new Set();
|
|
2162
2170
|
const out = [];
|
|
2163
|
-
|
|
2171
|
+
const spans = c.importSpans ?? c.imports.map((source) => ({ source, line: 0 }));
|
|
2172
|
+
for (const { source: src, line } of spans) {
|
|
2164
2173
|
if (!Object.hasOwn(HEAVY_PACKAGES, src) || seen.has(src)) continue;
|
|
2165
2174
|
seen.add(src);
|
|
2166
|
-
out.push({ line
|
|
2175
|
+
out.push({ line, message: `Heavy import "${src}" \u2014 ${HEAVY_PACKAGES[src]}` });
|
|
2167
2176
|
}
|
|
2168
2177
|
return out;
|
|
2169
2178
|
}
|
|
2170
2179
|
});
|
|
2171
2180
|
|
|
2172
|
-
// src/rules/
|
|
2181
|
+
// src/rules/perf/perf010-namespace-import.ts
|
|
2173
2182
|
var perf010NamespaceImport = componentRule({
|
|
2174
2183
|
id: "PERF010",
|
|
2175
2184
|
title: "Namespace import",
|
|
@@ -2516,7 +2525,7 @@ function formatConsoleReport(results, config, options = {}) {
|
|
|
2516
2525
|
lines.push("");
|
|
2517
2526
|
}
|
|
2518
2527
|
if (options.byRoute) lines.push(...byRouteTree(p, results, config, options.verbose ?? false));
|
|
2519
|
-
if (summary.dynamic > 0) lines.push(p.dim("\u21AF = set dynamically (verified at runtime)."));
|
|
2528
|
+
if (options.verbose && summary.dynamic > 0) lines.push(p.dim("\u21AF = set dynamically (verified at runtime)."));
|
|
2520
2529
|
return lines.join("\n").replace(/\n+$/, "\n");
|
|
2521
2530
|
}
|
|
2522
2531
|
|
|
@@ -2784,200 +2793,693 @@ function formatMarkdownReport(results, config, meta) {
|
|
|
2784
2793
|
return lines.join("\n");
|
|
2785
2794
|
}
|
|
2786
2795
|
|
|
2787
|
-
// src/reporter/
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
warn: "#E8A317",
|
|
2791
|
-
poor: "#E5484D"
|
|
2792
|
-
};
|
|
2793
|
-
function scoreBand(score) {
|
|
2794
|
-
return score >= 90 ? "good" : score >= 50 ? "warn" : "poor";
|
|
2796
|
+
// src/reporter/app-shell.ts
|
|
2797
|
+
function embedJson(value) {
|
|
2798
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
2795
2799
|
}
|
|
2796
|
-
function
|
|
2797
|
-
return
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
);
|
|
2800
|
+
function sanitizeDocsUrl(issue) {
|
|
2801
|
+
if (issue.docsUrl === void 0) return issue;
|
|
2802
|
+
if (safeHref(issue.docsUrl) !== null) return issue;
|
|
2803
|
+
return { ...issue, docsUrl: void 0 };
|
|
2801
2804
|
}
|
|
2802
|
-
function
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
+
function sanitizeReport(report) {
|
|
2806
|
+
return {
|
|
2807
|
+
...report,
|
|
2808
|
+
routes: report.routes.map((route) => ({ ...route, issues: route.issues.map(sanitizeDocsUrl) })),
|
|
2809
|
+
siteIssues: report.siteIssues.map(sanitizeDocsUrl)
|
|
2810
|
+
};
|
|
2805
2811
|
}
|
|
2806
|
-
var
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
const sev = SEVERITIES.includes(issue.severity) ? issue.severity : "info";
|
|
2811
|
-
const dyn = issue.detection.value === "dynamic" ? ' <span class="dyn" title="set dynamically (verified at runtime)">\u21AF</span>' : "";
|
|
2812
|
-
const line = issue.line !== void 0 ? `:${issue.line}` : "";
|
|
2813
|
-
const fix = issue.fix?.snippet !== void 0 ? `<div class="fix"><div class="label">fix</div><pre><code>${escapeHtml(issue.fix.snippet)}</code></pre></div>` : "";
|
|
2814
|
-
const href = issue.docsUrl ? safeHref(issue.docsUrl) : null;
|
|
2815
|
-
const docs = href ? `<a class="f-link" href="${escapeHtml(href)}">Learn more</a>` : "";
|
|
2816
|
-
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>`;
|
|
2817
|
-
}
|
|
2818
|
-
function renderTopbar(report, meta) {
|
|
2819
|
-
const findings = report.routes.reduce((n, r) => n + r.issues.length, 0) + report.siteIssues.length;
|
|
2820
|
-
const core = meta.coreVersion ? `<span title="@svelte-vitals/core version">core v${escapeHtml(meta.coreVersion)}</span>` : "";
|
|
2821
|
-
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>` + core + `<span>${report.routes.length} routes</span><span>${findings} findings</span></div></header>`;
|
|
2822
|
-
}
|
|
2823
|
-
function renderHero(report) {
|
|
2824
|
-
const C = 2 * Math.PI * 58;
|
|
2825
|
-
const offset = (C * (1 - report.score / 100)).toFixed(1);
|
|
2826
|
-
const hb = scoreBand(report.score);
|
|
2827
|
-
const s = report.summary;
|
|
2828
|
-
const dynNote = s.dynamic > 0 ? `<span class="tally"><span class="dot dyn-dot">\u21AF</span>Dynamic <span class="n">${s.dynamic}</span></span>` : "";
|
|
2829
|
-
const cats = Object.entries(report.categories).map(([cat, { score }]) => {
|
|
2830
|
-
const b = scoreBand(score);
|
|
2831
|
-
const weight = report.weights[cat];
|
|
2832
|
-
const w = weight !== void 0 ? `<span class="w">weight ${weight}</span>` : "";
|
|
2833
|
-
const name = categoryLabel(cat);
|
|
2834
|
-
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>`;
|
|
2835
|
-
}).join("");
|
|
2836
|
-
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>`;
|
|
2837
|
-
}
|
|
2838
|
-
var ROUTE_BADGES = ["measured", "static"];
|
|
2839
|
-
function renderRoutes(report, routeBadges) {
|
|
2840
|
-
if (report.routes.length === 0) return "";
|
|
2841
|
-
const rows = report.routes.map((r) => {
|
|
2842
|
-
const b = scoreBand(r.score);
|
|
2843
|
-
const crit = r.issues.filter((i) => i.severity === "critical").length;
|
|
2844
|
-
const warn = r.issues.filter((i) => i.severity === "warning").length;
|
|
2845
|
-
const info = r.issues.filter((i) => i.severity === "info").length;
|
|
2846
|
-
const parts = [];
|
|
2847
|
-
if (crit) parts.push(`${crit} critical`);
|
|
2848
|
-
if (warn) parts.push(`${warn} warning${warn > 1 ? "s" : ""}`);
|
|
2849
|
-
if (info) parts.push(`${info} info`);
|
|
2850
|
-
const sum = parts.length ? parts.join(" \xB7 ") : '<span class="none">no issues</span>';
|
|
2851
|
-
const body = r.issues.length ? r.issues.map(renderFinding).join("") : '<p class="empty">No issues found on this route.</p>';
|
|
2852
|
-
const rawBadge = routeBadges?.[r.route];
|
|
2853
|
-
const badge = rawBadge && ROUTE_BADGES.includes(rawBadge) ? rawBadge : void 0;
|
|
2854
|
-
const badgeHtml = badge ? ` <span class="badge badge-${badge}">${escapeHtml(badge)}</span>` : "";
|
|
2855
|
-
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>${badgeHtml}</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>`;
|
|
2856
|
-
}).join("");
|
|
2857
|
-
return `<section class="section"><h2>Routes</h2><div class="routes">${rows}</div></section>`;
|
|
2858
|
-
}
|
|
2859
|
-
function renderSiteChecks(report) {
|
|
2860
|
-
if (report.siteIssues.length === 0) return "";
|
|
2861
|
-
const cards = report.siteIssues.map(renderFinding).join("");
|
|
2862
|
-
return `<section class="section"><h2>Site checks</h2>${cards}</section>`;
|
|
2863
|
-
}
|
|
2864
|
-
function renderFilters(report) {
|
|
2865
|
-
const chip = (filter, label, pressed = false) => `<button class="chip" type="button" aria-pressed="${pressed}" data-filter="${escapeHtml(filter)}">${escapeHtml(label)}</button>`;
|
|
2866
|
-
const catChips = Object.keys(report.categories).map((cat) => chip(cat, categoryLabel(cat))).join("");
|
|
2867
|
-
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>`;
|
|
2868
|
-
}
|
|
2869
|
-
var STYLE = `
|
|
2870
|
-
: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}
|
|
2812
|
+
var APP_STYLE = `
|
|
2813
|
+
: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;--active-bg:#0c1322;--active-ink:#fff;--mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;--sans:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}
|
|
2814
|
+
:root[data-theme="dark"]{--ground:#0b0e14;--panel:#12161f;--ink:#e7ecf4;--muted:#9aa4b2;--faint:#6b7484;--line:#232838;--line-strong:#2d3345;--code-bg:#05070c;--code-ink:#e7ecf4;--active-bg:#e7ecf4;--active-ink:#0b0e14}
|
|
2815
|
+
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--ground:#0b0e14;--panel:#12161f;--ink:#e7ecf4;--muted:#9aa4b2;--faint:#6b7484;--line:#232838;--line-strong:#2d3345;--code-bg:#05070c;--code-ink:#e7ecf4;--active-bg:#e7ecf4;--active-ink:#0b0e14}}
|
|
2871
2816
|
*{box-sizing:border-box}
|
|
2872
|
-
body{margin:0;
|
|
2873
|
-
|
|
2874
|
-
.
|
|
2875
|
-
.
|
|
2876
|
-
.
|
|
2877
|
-
.
|
|
2878
|
-
.
|
|
2879
|
-
.
|
|
2880
|
-
.
|
|
2881
|
-
.
|
|
2882
|
-
.
|
|
2883
|
-
.
|
|
2884
|
-
.
|
|
2885
|
-
.
|
|
2886
|
-
.
|
|
2887
|
-
.
|
|
2888
|
-
.
|
|
2889
|
-
.
|
|
2890
|
-
.
|
|
2891
|
-
.
|
|
2892
|
-
.
|
|
2893
|
-
.
|
|
2894
|
-
.
|
|
2895
|
-
.
|
|
2896
|
-
.
|
|
2897
|
-
.
|
|
2898
|
-
.
|
|
2899
|
-
.
|
|
2900
|
-
.
|
|
2901
|
-
.
|
|
2902
|
-
.
|
|
2903
|
-
.
|
|
2904
|
-
.
|
|
2905
|
-
.
|
|
2906
|
-
.
|
|
2907
|
-
.
|
|
2908
|
-
.
|
|
2909
|
-
.
|
|
2910
|
-
.
|
|
2911
|
-
.
|
|
2912
|
-
.
|
|
2913
|
-
.
|
|
2914
|
-
.
|
|
2915
|
-
.
|
|
2916
|
-
.
|
|
2917
|
-
.
|
|
2918
|
-
.
|
|
2919
|
-
.
|
|
2920
|
-
.
|
|
2921
|
-
.
|
|
2922
|
-
.
|
|
2923
|
-
.
|
|
2924
|
-
.
|
|
2925
|
-
.
|
|
2926
|
-
.
|
|
2927
|
-
.
|
|
2928
|
-
.
|
|
2929
|
-
.
|
|
2930
|
-
.
|
|
2931
|
-
.
|
|
2932
|
-
.
|
|
2933
|
-
.
|
|
2934
|
-
.
|
|
2935
|
-
.
|
|
2936
|
-
|
|
2817
|
+
html,body{margin:0;height:100%}
|
|
2818
|
+
body{background:var(--ground);color:var(--ink);font-family:var(--sans);line-height:1.5;-webkit-font-smoothing:antialiased}
|
|
2819
|
+
.dv-app{display:grid;grid-template-rows:auto 1fr;grid-template-columns:280px 1fr;grid-template-areas:"top top" "side main";height:100vh}
|
|
2820
|
+
.dv-topbar{grid-area:top;border-bottom:1px solid var(--line);background:var(--panel)}
|
|
2821
|
+
.dv-topbar-inner{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;padding:12px 20px}
|
|
2822
|
+
.dv-brand{display:flex;align-items:center;background:none;border:none;padding:0;cursor:pointer;border-radius:6px}
|
|
2823
|
+
.dv-brand svg{height:28px;width:auto;display:block}
|
|
2824
|
+
.dv-brand:focus-visible{outline:2px solid var(--accent);outline-offset:4px}
|
|
2825
|
+
.dv-meta{font-family:var(--mono);font-size:12px;color:var(--muted);display:flex;gap:12px;flex-wrap:wrap}
|
|
2826
|
+
.dv-status{display:flex;align-items:center;gap:10px}
|
|
2827
|
+
.dv-analyzing{font-size:12px;color:var(--accent);font-weight:600}
|
|
2828
|
+
.dv-conn{width:8px;height:8px;border-radius:50%;background:var(--faint);display:inline-block}
|
|
2829
|
+
.dv-conn-connected{background:var(--good)}
|
|
2830
|
+
.dv-conn-reconnecting{background:var(--warn)}
|
|
2831
|
+
.dv-menu-toggle{display:none;border:1px solid var(--line-strong);background:var(--panel);color:var(--ink);border-radius:8px;width:28px;height:28px;cursor:pointer}
|
|
2832
|
+
.dv-theme-toggle{border:1px solid var(--line-strong);background:var(--panel);color:var(--ink);border-radius:999px;width:28px;height:28px;cursor:pointer}
|
|
2833
|
+
.dv-theme-toggle:focus-visible,.dv-menu-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
2834
|
+
.dv-sidebar{grid-area:side;border-right:1px solid var(--line);background:var(--panel);overflow-y:auto}
|
|
2835
|
+
.dv-sidebar-inner{display:flex;flex-direction:column;gap:10px;padding:14px}
|
|
2836
|
+
.dv-search{font:inherit;font-size:13px;padding:7px 10px;border:1px solid var(--line-strong);border-radius:8px;background:var(--ground);color:var(--ink)}
|
|
2837
|
+
.dv-sort{font:inherit;font-size:12.5px;padding:6px 8px;border:1px solid var(--line-strong);border-radius:8px;background:var(--ground);color:var(--ink)}
|
|
2838
|
+
.dv-nav{display:flex;flex-direction:column;gap:2px}
|
|
2839
|
+
.dv-nav-item{display:flex;flex-direction:column;gap:4px;padding:8px 10px;border-radius:8px;cursor:pointer}
|
|
2840
|
+
.dv-nav-item:hover{background:var(--ground)}
|
|
2841
|
+
.dv-nav-item.active{background:var(--active-bg);color:var(--active-ink)}
|
|
2842
|
+
.dv-nav-item:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
|
|
2843
|
+
.dv-nav-label{font-family:var(--mono);font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
2844
|
+
.dv-nav-meta{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--muted)}
|
|
2845
|
+
.dv-nav-item.active .dv-nav-meta{color:inherit}
|
|
2846
|
+
.dv-nav-score{font-family:var(--mono);font-weight:700}
|
|
2847
|
+
.dv-badge{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:1px 6px;border-radius:999px}
|
|
2848
|
+
.dv-badge-measured{background:rgba(47,169,104,.16);color:var(--good)}
|
|
2849
|
+
.dv-badge-static{background:rgba(140,149,163,.2);color:var(--muted)}
|
|
2850
|
+
.dv-detail{grid-area:main;overflow-y:auto;padding:24px 28px 80px}
|
|
2851
|
+
.dv-gauge{position:relative;width:132px;height:132px;margin-bottom:20px}
|
|
2852
|
+
.dv-gauge svg{position:absolute;inset:0;transform:rotate(-90deg)}
|
|
2853
|
+
.dv-gauge-track{stroke:var(--line)}
|
|
2854
|
+
.dv-gauge-num{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center}
|
|
2855
|
+
.dv-gauge-num strong{font-family:var(--mono);font-size:36px;font-weight:600}
|
|
2856
|
+
.dv-gauge-num span{font-size:10px;text-transform:uppercase;letter-spacing:.14em;color:var(--muted)}
|
|
2857
|
+
.dv-cats{display:flex;gap:22px;flex-wrap:wrap;margin-bottom:20px}
|
|
2858
|
+
.dv-cat{min-width:180px;flex:1}
|
|
2859
|
+
.dv-cat-top{display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px}
|
|
2860
|
+
.dv-bar{height:7px;border-radius:999px;background:var(--line);overflow:hidden}
|
|
2861
|
+
.dv-bar>i{display:block;height:100%;border-radius:999px}
|
|
2862
|
+
.dv-filters{display:flex;gap:8px;flex-wrap:wrap;margin:16px 0}
|
|
2863
|
+
.dv-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}
|
|
2864
|
+
.dv-chip[aria-pressed="true"]{background:var(--active-bg);border-color:var(--active-bg);color:var(--active-ink)}
|
|
2865
|
+
.dv-chip:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
2866
|
+
.dv-section h2{font-size:12px;text-transform:uppercase;letter-spacing:.12em;color:var(--muted);margin:24px 0 12px}
|
|
2867
|
+
.dv-detail-header{display:flex;align-items:center;gap:12px;margin-bottom:14px;flex-wrap:wrap}
|
|
2868
|
+
.dv-route-path{font-family:var(--mono);font-size:16px;font-weight:600}
|
|
2869
|
+
.dv-score-chip{font-family:var(--mono);font-weight:700}
|
|
2870
|
+
.dv-finding{background:var(--panel);border:1px solid var(--line);border-left-width:3px;border-radius:10px;padding:16px 18px;margin:0 0 12px}
|
|
2871
|
+
.dv-finding-critical{border-left-color:var(--poor)}
|
|
2872
|
+
.dv-finding-warning{border-left-color:var(--warn)}
|
|
2873
|
+
.dv-finding-info{border-left-color:var(--faint)}
|
|
2874
|
+
.dv-f-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
2875
|
+
.dv-ruleid{font-family:var(--mono);font-size:12px;font-weight:600;background:var(--ground);padding:2px 8px;border-radius:6px}
|
|
2876
|
+
.dv-f-title{font-weight:650;font-size:15px}
|
|
2877
|
+
.dv-sev-tag{margin-left:auto;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}
|
|
2878
|
+
.dv-sev-critical{color:var(--poor)}
|
|
2879
|
+
.dv-sev-warning{color:var(--warn)}
|
|
2880
|
+
.dv-sev-info{color:var(--faint)}
|
|
2881
|
+
.dv-f-route{display:block;font:inherit;font-family:var(--mono);font-size:12.5px;font-weight:600;color:var(--accent);background:none;border:none;padding:0;margin:8px 0 0;cursor:pointer;text-align:left}
|
|
2882
|
+
.dv-f-route:hover{text-decoration:underline}
|
|
2883
|
+
.dv-f-route:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
2884
|
+
.dv-f-loc{font-family:var(--mono);font-size:12.5px;color:var(--muted);margin:8px 0 0}
|
|
2885
|
+
.dv-f-rec{font-size:14px;margin:10px 0 0}
|
|
2886
|
+
.dv-fix{margin:12px 0 0;background:var(--code-bg);border-radius:8px;overflow:hidden}
|
|
2887
|
+
.dv-fix-label{font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:#8da0bd;padding:8px 14px 0}
|
|
2888
|
+
.dv-fix pre{margin:0;padding:8px 14px 14px;overflow-x:auto}
|
|
2889
|
+
.dv-fix code{font-family:var(--mono);font-size:12.5px;color:var(--code-ink);line-height:1.65;white-space:pre}
|
|
2890
|
+
.tok-kw{color:#ff7ab8}
|
|
2891
|
+
.tok-str{color:#9ece6a}
|
|
2892
|
+
.tok-num{color:#ff9e64}
|
|
2893
|
+
.tok-cm{color:#6b7280;font-style:italic}
|
|
2894
|
+
.tok-id{color:var(--code-ink)}
|
|
2895
|
+
.tok-pn{color:#8da0bd}
|
|
2896
|
+
.dv-f-link{display:inline-block;margin-top:12px;font-size:13px;font-weight:600;color:var(--accent);text-decoration:none}
|
|
2897
|
+
.dv-f-link:hover{text-decoration:underline}
|
|
2898
|
+
.dv-ai-prompt{margin-top:12px;border:1px solid var(--line);border-radius:8px;overflow:hidden}
|
|
2899
|
+
.dv-ai-prompt>summary{cursor:pointer;list-style:none;padding:8px 12px;font-size:12px;font-weight:600;color:var(--muted);display:flex;align-items:center;gap:6px;user-select:none}
|
|
2900
|
+
.dv-ai-prompt>summary::-webkit-details-marker{display:none}
|
|
2901
|
+
.dv-ai-prompt>summary::before{content:"\u25B8";display:inline-block;transition:transform .15s ease}
|
|
2902
|
+
.dv-ai-prompt[open]>summary::before{transform:rotate(90deg)}
|
|
2903
|
+
.dv-ai-prompt-body{padding:0 12px 12px;display:flex;flex-direction:column;gap:8px}
|
|
2904
|
+
.dv-ai-prompt-pre{margin:0;padding:10px 12px;background:var(--code-bg);color:var(--code-ink);border-radius:8px;font-family:var(--mono);font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-word;max-height:280px;overflow-y:auto}
|
|
2905
|
+
.dv-ai-copy-btn{align-self:flex-start;font:inherit;font-size:12px;font-weight:600;cursor:pointer;background:var(--panel);border:1px solid var(--line-strong);color:var(--ink);padding:5px 12px;border-radius:999px}
|
|
2906
|
+
.dv-ai-copy-btn:hover{border-color:var(--faint)}
|
|
2907
|
+
.dv-ai-copy-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
2908
|
+
.dv-empty{color:var(--muted);font-size:13px}
|
|
2909
|
+
@media (max-width:640px){.dv-app{grid-template-columns:1fr;grid-template-areas:"top" "main"}.dv-menu-toggle{display:inline-flex}.dv-sidebar{position:fixed;inset:0 20% 0 0;transform:translateX(-100%);transition:transform .2s ease;z-index:10}.dv-sidebar.open{transform:translateX(0)}}
|
|
2937
2910
|
@media (prefers-reduced-motion:reduce){*{transition:none!important}}
|
|
2938
2911
|
`;
|
|
2939
|
-
var
|
|
2912
|
+
var APP_SCRIPT = `
|
|
2940
2913
|
(function(){
|
|
2941
|
-
var
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2914
|
+
var BAND_COLOR = { good: '#2fa968', warn: '#e8a317', poor: '#e5484d' };
|
|
2915
|
+
function scoreBand(score) { return score >= 90 ? 'good' : score >= 50 ? 'warn' : 'poor'; }
|
|
2916
|
+
|
|
2917
|
+
// Same mark as the docs site's hero wordmark (docs/public/wordmark.svg) \u2014 an inline
|
|
2918
|
+
// copy, not an <img src>, since the dashboard is a single self-contained HTML response
|
|
2919
|
+
// with no other static assets to serve alongside it. Fixed brand colors (not CSS custom
|
|
2920
|
+
// properties), matching the docs usage: the wordmark reads the same in both themes.
|
|
2921
|
+
var WORDMARK_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 380 56" role="img" aria-labelledby="dv-wordmark-title"><title id="dv-wordmark-title">svelte-vitals</title><defs><clipPath id="dv-wordmark-clip"><rect x="2" y="2" width="52" height="52" rx="14"/></clipPath></defs><rect x="2" y="2" width="52" height="52" rx="14" fill="#FF3E00"/><polyline clip-path="url(#dv-wordmark-clip)" points="4,28 15,28 17.5,23.5 20,28 23,28 26,7 29,49 32,28 35,28 37,24.5 39.5,28 52,28" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><text x="70" y="38" font-family="ui-sans-serif, system-ui, -apple-system, \\'Segoe UI\\', Roboto, sans-serif" font-size="30" font-weight="700" fill="#FF3E00">svelte-vitals</text></svg>';
|
|
2922
|
+
|
|
2923
|
+
function h(tag, attrs, kids) {
|
|
2924
|
+
var n = document.createElement(tag);
|
|
2925
|
+
if (attrs) {
|
|
2926
|
+
for (var k in attrs) {
|
|
2927
|
+
if (!Object.prototype.hasOwnProperty.call(attrs, k)) continue;
|
|
2928
|
+
var v = attrs[k];
|
|
2929
|
+
if (v === undefined || v === null || v === false) continue;
|
|
2930
|
+
if (k === 'class') n.className = v;
|
|
2931
|
+
else if (k === 'text') n.textContent = v;
|
|
2932
|
+
else if (k.indexOf('on') === 0 && typeof v === 'function') n.addEventListener(k.slice(2), v);
|
|
2933
|
+
else n.setAttribute(k, v === true ? '' : String(v));
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
(kids || []).forEach(function (c) {
|
|
2937
|
+
if (c === undefined || c === null || c === false) return;
|
|
2938
|
+
n.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
|
|
2939
|
+
});
|
|
2940
|
+
return n;
|
|
2941
|
+
}
|
|
2942
|
+
function clear(n) { while (n.firstChild) n.removeChild(n.firstChild); }
|
|
2943
|
+
function mount(id, node) { var el = document.getElementById(id); clear(el); el.appendChild(node); }
|
|
2944
|
+
|
|
2945
|
+
var HL_KEYWORDS = ['import','export','from','const','let','var','function','return','if','else','for','while','class','new','await','async','default','type','interface','extends','implements','this','typeof','instanceof','of','in','true','false','null','undefined'];
|
|
2946
|
+
var HL_LANGS = { js: 1, javascript: 1, ts: 1, typescript: 1, svelte: 1, html: 1, css: 1 };
|
|
2947
|
+
|
|
2948
|
+
function highlightTokens(code) {
|
|
2949
|
+
var tokens = [];
|
|
2950
|
+
var i = 0;
|
|
2951
|
+
var n = code.length;
|
|
2952
|
+
var reIdent = /[A-Za-z_$][A-Za-z0-9_$]*/y;
|
|
2953
|
+
var reNum = /\\d+(\\.\\d+)?/y;
|
|
2954
|
+
while (i < n) {
|
|
2955
|
+
var ch = code[i];
|
|
2956
|
+
if (ch === '/' && code[i + 1] === '/') {
|
|
2957
|
+
var end = code.indexOf('\\n', i);
|
|
2958
|
+
if (end === -1) end = n;
|
|
2959
|
+
tokens.push({ text: code.slice(i, end), cls: 'cm' });
|
|
2960
|
+
i = end;
|
|
2961
|
+
continue;
|
|
2962
|
+
}
|
|
2963
|
+
if (ch === '/' && code[i + 1] === '*') {
|
|
2964
|
+
var end2 = code.indexOf('*/', i + 2);
|
|
2965
|
+
end2 = end2 === -1 ? n : end2 + 2;
|
|
2966
|
+
tokens.push({ text: code.slice(i, end2), cls: 'cm' });
|
|
2967
|
+
i = end2;
|
|
2968
|
+
continue;
|
|
2969
|
+
}
|
|
2970
|
+
if (ch === '"' || ch === "'" || ch === '\`') {
|
|
2971
|
+
var quote = ch;
|
|
2972
|
+
var j = i + 1;
|
|
2973
|
+
while (j < n && code[j] !== quote) {
|
|
2974
|
+
if (code[j] === '\\\\') j++;
|
|
2975
|
+
j++;
|
|
2976
|
+
}
|
|
2977
|
+
j = Math.min(j + 1, n);
|
|
2978
|
+
tokens.push({ text: code.slice(i, j), cls: 'str' });
|
|
2979
|
+
i = j;
|
|
2980
|
+
continue;
|
|
2981
|
+
}
|
|
2982
|
+
reIdent.lastIndex = i;
|
|
2983
|
+
var mIdent = reIdent.exec(code);
|
|
2984
|
+
if (mIdent && mIdent.index === i) {
|
|
2985
|
+
var word = mIdent[0];
|
|
2986
|
+
tokens.push({ text: word, cls: HL_KEYWORDS.indexOf(word) !== -1 ? 'kw' : 'id' });
|
|
2987
|
+
i += word.length;
|
|
2988
|
+
continue;
|
|
2989
|
+
}
|
|
2990
|
+
reNum.lastIndex = i;
|
|
2991
|
+
var mNum = reNum.exec(code);
|
|
2992
|
+
if (mNum && mNum.index === i) {
|
|
2993
|
+
tokens.push({ text: mNum[0], cls: 'num' });
|
|
2994
|
+
i += mNum[0].length;
|
|
2995
|
+
continue;
|
|
2996
|
+
}
|
|
2997
|
+
tokens.push({ text: ch, cls: 'pn' });
|
|
2998
|
+
i += 1;
|
|
2999
|
+
}
|
|
3000
|
+
return tokens;
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
function renderFixSnippet(fix) {
|
|
3004
|
+
var pre = h('pre', null, []);
|
|
3005
|
+
var code = h('code', null, []);
|
|
3006
|
+
var lang = (fix.lang || 'svelte').toLowerCase();
|
|
3007
|
+
if (HL_LANGS[lang]) {
|
|
3008
|
+
highlightTokens(fix.snippet).forEach(function (t) {
|
|
3009
|
+
code.appendChild(h('span', { class: 'tok-' + t.cls, text: t.text }, []));
|
|
2954
3010
|
});
|
|
2955
|
-
|
|
3011
|
+
} else {
|
|
3012
|
+
code.textContent = fix.snippet;
|
|
3013
|
+
}
|
|
3014
|
+
pre.appendChild(code);
|
|
3015
|
+
return pre;
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
// Plain-text, copy-pasteable prompt for a single finding \u2014 same ingredients as the
|
|
3019
|
+
// agent reporter's per-finding block (rule id, location, recommendation, fix, docs),
|
|
3020
|
+
// reshaped for a standalone request rather than a whole-project remediation doc.
|
|
3021
|
+
function buildAiPrompt(issue, route) {
|
|
3022
|
+
var lines = ['Fix this svelte-vitals finding:', ''];
|
|
3023
|
+
lines.push('- Rule: ' + issue.id + ' \u2014 ' + issue.title + ' (' + issue.severity + ')');
|
|
3024
|
+
if (route) lines.push('- Route: ' + route);
|
|
3025
|
+
if (issue.location) {
|
|
3026
|
+
lines.push('- Location: ' + issue.location + (issue.line !== undefined ? ':' + issue.line : ''));
|
|
3027
|
+
}
|
|
3028
|
+
if (issue.recommendation) lines.push('- Recommendation: ' + issue.recommendation);
|
|
3029
|
+
if (issue.fix) {
|
|
3030
|
+
lines.push('- Fix: ' + issue.fix.description);
|
|
3031
|
+
if (issue.fix.snippet) {
|
|
3032
|
+
lines.push('', '\`\`\`' + (issue.fix.lang || 'svelte'), issue.fix.snippet, '\`\`\`');
|
|
3033
|
+
}
|
|
2956
3034
|
}
|
|
3035
|
+
if (issue.docsUrl) lines.push('- Docs: ' + issue.docsUrl);
|
|
3036
|
+
lines.push(
|
|
3037
|
+
'',
|
|
3038
|
+
'After fixing, re-run \`svelte-vitals --diff\` (or revisit this route) to confirm ' +
|
|
3039
|
+
issue.id +
|
|
3040
|
+
' passes' +
|
|
3041
|
+
(route ? ' for ' + route : '') +
|
|
3042
|
+
'.'
|
|
3043
|
+
);
|
|
3044
|
+
return lines.join('\\n');
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
function copyToClipboard(text, btn) {
|
|
3048
|
+
var original = 'Copy';
|
|
3049
|
+
function reset(label) {
|
|
3050
|
+
btn.textContent = label;
|
|
3051
|
+
setTimeout(function () { btn.textContent = original; }, 1500);
|
|
3052
|
+
}
|
|
3053
|
+
function done() { reset('Copied!'); }
|
|
3054
|
+
function fail() { reset('Copy failed'); }
|
|
3055
|
+
function fallbackCopy() {
|
|
3056
|
+
var ta = document.createElement('textarea');
|
|
3057
|
+
ta.value = text;
|
|
3058
|
+
ta.setAttribute('readonly', '');
|
|
3059
|
+
ta.style.position = 'fixed';
|
|
3060
|
+
ta.style.opacity = '0';
|
|
3061
|
+
document.body.appendChild(ta);
|
|
3062
|
+
ta.select();
|
|
3063
|
+
var ok = false;
|
|
3064
|
+
try { ok = document.execCommand('copy'); } catch (e) {}
|
|
3065
|
+
document.body.removeChild(ta);
|
|
3066
|
+
return ok;
|
|
3067
|
+
}
|
|
3068
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
3069
|
+
navigator.clipboard.writeText(text).then(done, function () { fallbackCopy() ? done() : fail(); });
|
|
3070
|
+
} else {
|
|
3071
|
+
fallbackCopy() ? done() : fail();
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
function renderAiPrompt(issue, route) {
|
|
3076
|
+
var text = buildAiPrompt(issue, route);
|
|
3077
|
+
var btn = h('button', { type: 'button', class: 'dv-ai-copy-btn', text: 'Copy' }, []);
|
|
3078
|
+
btn.addEventListener('click', function () { copyToClipboard(text, btn); });
|
|
3079
|
+
return h('details', { class: 'dv-ai-prompt' }, [
|
|
3080
|
+
h('summary', { text: 'AI Prompt' }, []),
|
|
3081
|
+
h('div', { class: 'dv-ai-prompt-body' }, [
|
|
3082
|
+
h('pre', { class: 'dv-ai-prompt-pre', text: text }, []),
|
|
3083
|
+
btn
|
|
3084
|
+
])
|
|
3085
|
+
]);
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
var state = {
|
|
3089
|
+
snapshot: null,
|
|
3090
|
+
selected: 'overview',
|
|
3091
|
+
search: '',
|
|
3092
|
+
sort: 'score-asc',
|
|
3093
|
+
filter: 'all',
|
|
3094
|
+
theme: initialTheme(),
|
|
3095
|
+
connection: 'connecting',
|
|
3096
|
+
routeBySlug: {}
|
|
3097
|
+
};
|
|
3098
|
+
|
|
3099
|
+
function initialTheme() {
|
|
3100
|
+
try {
|
|
3101
|
+
var stored = localStorage.getItem('svelte-vitals-theme');
|
|
3102
|
+
if (stored === 'dark' || stored === 'light') return stored;
|
|
3103
|
+
} catch (e) {}
|
|
3104
|
+
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
3105
|
+
}
|
|
3106
|
+
function applyTheme() { document.documentElement.setAttribute('data-theme', state.theme); }
|
|
3107
|
+
function toggleTheme() {
|
|
3108
|
+
state.theme = state.theme === 'dark' ? 'light' : 'dark';
|
|
3109
|
+
try { localStorage.setItem('svelte-vitals-theme', state.theme); } catch (e) {}
|
|
3110
|
+
applyTheme();
|
|
3111
|
+
renderTopbar();
|
|
3112
|
+
}
|
|
3113
|
+
function toggleSidebar() {
|
|
3114
|
+
var sb = document.getElementById('dv-sidebar');
|
|
3115
|
+
if (sb) sb.classList.toggle('open');
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
function brandEl() {
|
|
3119
|
+
var el = h('button', { type: 'button', class: 'dv-brand', 'aria-label': 'Go to Overview', onclick: function () { selectItem('overview'); } }, []);
|
|
3120
|
+
el.innerHTML = WORDMARK_SVG;
|
|
3121
|
+
return el;
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
function renderTopbar() {
|
|
3125
|
+
var s = state.snapshot;
|
|
3126
|
+
var findings = s.report.routes.reduce(function (n, r) { return n + r.issues.length; }, 0) + s.report.siteIssues.length;
|
|
3127
|
+
var kids = [
|
|
3128
|
+
h('button', { type: 'button', class: 'dv-menu-toggle', 'aria-label': 'Toggle route list', onclick: toggleSidebar, text: '\u2261' }, []),
|
|
3129
|
+
brandEl(),
|
|
3130
|
+
h('div', { class: 'dv-meta' }, [
|
|
3131
|
+
h('span', { text: 'v' + s.meta.version }, []),
|
|
3132
|
+
s.meta.coreVersion ? h('span', { title: '@svelte-vitals/core version', text: 'core v' + s.meta.coreVersion }, []) : null,
|
|
3133
|
+
h('span', { text: s.report.routes.length + ' routes' }, []),
|
|
3134
|
+
h('span', { text: findings + ' findings' }, [])
|
|
3135
|
+
].filter(Boolean)),
|
|
3136
|
+
h('div', { class: 'dv-status' }, [
|
|
3137
|
+
s.live && s.analyzing ? h('span', { class: 'dv-analyzing', text: 'Analyzing\u2026' }, []) : null,
|
|
3138
|
+
s.live ? h('span', { class: 'dv-conn dv-conn-' + state.connection, title: state.connection }, []) : null,
|
|
3139
|
+
h('button', { type: 'button', class: 'dv-theme-toggle', 'aria-label': 'Toggle dark mode', onclick: toggleTheme, text: state.theme === 'dark' ? '\u2600' : '\u263E' }, [])
|
|
3140
|
+
].filter(Boolean))
|
|
3141
|
+
];
|
|
3142
|
+
mount('dv-topbar', h('div', { class: 'dv-topbar-inner' }, kids));
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
function slugify(route) {
|
|
3146
|
+
return 'route-' + route.replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '').toLowerCase();
|
|
2957
3147
|
}
|
|
2958
|
-
|
|
2959
|
-
function
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
3148
|
+
|
|
3149
|
+
function matchesSearch(route, q) {
|
|
3150
|
+
if (!q) return true;
|
|
3151
|
+
q = q.toLowerCase();
|
|
3152
|
+
if (route.route.toLowerCase().indexOf(q) !== -1) return true;
|
|
3153
|
+
return route.issues.some(function (iss) {
|
|
3154
|
+
return (iss.id + ' ' + iss.title + ' ' + (iss.location || '')).toLowerCase().indexOf(q) !== -1;
|
|
2963
3155
|
});
|
|
2964
3156
|
}
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
3157
|
+
|
|
3158
|
+
function sortedRoutes() {
|
|
3159
|
+
var s = state.snapshot;
|
|
3160
|
+
var q = state.search.trim();
|
|
3161
|
+
var list = s.report.routes.filter(function (r) { return matchesSearch(r, q); }).slice();
|
|
3162
|
+
var sort = state.sort;
|
|
3163
|
+
if (sort === 'score-asc') list.sort(function (a, b) { return a.score - b.score; });
|
|
3164
|
+
else if (sort === 'score-desc') list.sort(function (a, b) { return b.score - a.score; });
|
|
3165
|
+
else if (sort === 'alpha') list.sort(function (a, b) { return a.route.localeCompare(b.route); });
|
|
3166
|
+
else if (sort === 'most-findings') list.sort(function (a, b) { return b.issues.length - a.issues.length; });
|
|
3167
|
+
return list;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
function renderNavItem(label, key, route, active) {
|
|
3171
|
+
var kids = [h('span', { class: 'dv-nav-label', text: label }, [])];
|
|
3172
|
+
if (route) {
|
|
3173
|
+
var band = scoreBand(route.score);
|
|
3174
|
+
var crit = route.issues.filter(function (i) { return i.severity === 'critical'; }).length;
|
|
3175
|
+
var warn = route.issues.filter(function (i) { return i.severity === 'warning'; }).length;
|
|
3176
|
+
var info = route.issues.filter(function (i) { return i.severity === 'info'; }).length;
|
|
3177
|
+
var summary = [];
|
|
3178
|
+
if (crit) summary.push(crit + ' critical');
|
|
3179
|
+
if (warn) summary.push(warn + ' warning' + (warn > 1 ? 's' : ''));
|
|
3180
|
+
if (info) summary.push(info + ' info');
|
|
3181
|
+
var badge = state.snapshot.badges[route.route];
|
|
3182
|
+
kids.push(h('span', { class: 'dv-nav-meta' }, [
|
|
3183
|
+
badge ? h('span', { class: 'dv-badge dv-badge-' + badge, text: badge }, []) : null,
|
|
3184
|
+
h('span', { class: 'dv-nav-score', style: 'color:' + BAND_COLOR[band], text: String(route.score) }, []),
|
|
3185
|
+
h('span', { class: 'dv-nav-sum', text: summary.length ? summary.join(' \xB7 ') : 'no issues' }, [])
|
|
3186
|
+
].filter(Boolean)));
|
|
3187
|
+
}
|
|
3188
|
+
return h('div', {
|
|
3189
|
+
class: 'dv-nav-item' + (active ? ' active' : ''),
|
|
3190
|
+
role: 'option',
|
|
3191
|
+
'aria-selected': active ? 'true' : 'false',
|
|
3192
|
+
tabindex: '0',
|
|
3193
|
+
onclick: function () { selectItem(key); },
|
|
3194
|
+
onkeydown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectItem(key); } }
|
|
3195
|
+
}, kids);
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
function selectItem(key) {
|
|
3199
|
+
state.selected = key;
|
|
3200
|
+
location.hash = key === 'overview' ? 'overview' : 'route/' + slugify(key);
|
|
3201
|
+
var sb = document.getElementById('dv-sidebar');
|
|
3202
|
+
if (sb) sb.classList.remove('open');
|
|
3203
|
+
renderSidebar();
|
|
3204
|
+
renderDetail();
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
function renderSidebar() {
|
|
3208
|
+
var s = state.snapshot;
|
|
3209
|
+
state.routeBySlug = {};
|
|
3210
|
+
|
|
3211
|
+
// mount() clears and rebuilds the whole sidebar, including the <input> itself, so
|
|
3212
|
+
// a naive re-render on every keystroke drops focus and cursor position \u2014 capture
|
|
3213
|
+
// them before rebuilding and restore them on the freshly-created input afterward.
|
|
3214
|
+
var prevSearch = document.querySelector('.dv-search');
|
|
3215
|
+
var hadFocus = !!prevSearch && document.activeElement === prevSearch;
|
|
3216
|
+
var selStart = hadFocus ? prevSearch.selectionStart : null;
|
|
3217
|
+
var selEnd = hadFocus ? prevSearch.selectionEnd : null;
|
|
3218
|
+
|
|
3219
|
+
var searchInput = h('input', {
|
|
3220
|
+
type: 'search',
|
|
3221
|
+
class: 'dv-search',
|
|
3222
|
+
placeholder: 'Search routes or rules\u2026',
|
|
3223
|
+
value: state.search,
|
|
3224
|
+
oninput: function (e) { state.search = e.target.value; renderSidebar(); }
|
|
3225
|
+
}, []);
|
|
3226
|
+
|
|
3227
|
+
var sortSelect = h('select', { class: 'dv-sort', 'aria-label': 'Sort routes', onchange: function (e) { state.sort = e.target.value; renderSidebar(); } }, [
|
|
3228
|
+
h('option', { value: 'score-asc', selected: state.sort === 'score-asc' || undefined, text: 'Score (worst first)' }, []),
|
|
3229
|
+
h('option', { value: 'score-desc', selected: state.sort === 'score-desc' || undefined, text: 'Score (best first)' }, []),
|
|
3230
|
+
h('option', { value: 'alpha', selected: state.sort === 'alpha' || undefined, text: 'Alphabetical' }, []),
|
|
3231
|
+
h('option', { value: 'most-findings', selected: state.sort === 'most-findings' || undefined, text: 'Most findings' }, [])
|
|
3232
|
+
]);
|
|
3233
|
+
|
|
3234
|
+
var items = [renderNavItem('Overview', 'overview', null, state.selected === 'overview')];
|
|
3235
|
+
sortedRoutes().forEach(function (r) {
|
|
3236
|
+
var slug = slugify(r.route);
|
|
3237
|
+
state.routeBySlug[slug] = r.route;
|
|
3238
|
+
items.push(renderNavItem(r.route, r.route, r, state.selected === r.route));
|
|
2970
3239
|
});
|
|
2971
|
-
|
|
3240
|
+
|
|
3241
|
+
var nav = h('div', { class: 'dv-nav', role: 'listbox', 'aria-label': 'Routes' }, items);
|
|
3242
|
+
mount('dv-sidebar', h('div', { class: 'dv-sidebar-inner' }, [searchInput, sortSelect, nav]));
|
|
3243
|
+
|
|
3244
|
+
if (hadFocus) {
|
|
3245
|
+
searchInput.focus();
|
|
3246
|
+
if (selStart !== null && searchInput.setSelectionRange) {
|
|
3247
|
+
searchInput.setSelectionRange(selStart, selEnd);
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
function renderFilterChips(categories) {
|
|
3253
|
+
var chip = function (filter, label) {
|
|
3254
|
+
return h('button', {
|
|
3255
|
+
type: 'button', class: 'dv-chip', 'aria-pressed': state.filter === filter ? 'true' : 'false',
|
|
3256
|
+
onclick: function () { state.filter = filter; renderDetail(); },
|
|
3257
|
+
text: label
|
|
3258
|
+
}, []);
|
|
3259
|
+
};
|
|
3260
|
+
var catChips = Object.keys(categories).map(function (cat) {
|
|
3261
|
+
var name = cat === 'seo' ? 'SEO' : cat.charAt(0).toUpperCase() + cat.slice(1);
|
|
3262
|
+
return chip(cat, name);
|
|
3263
|
+
});
|
|
3264
|
+
return h('div', { class: 'dv-filters', role: 'group', 'aria-label': 'Filter findings' },
|
|
3265
|
+
[chip('all', 'All'), chip('critical', 'Critical'), chip('warning', 'Warning'), chip('info', 'Info')].concat(catChips));
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
function passesFilter(issue) {
|
|
3269
|
+
var f = state.filter;
|
|
3270
|
+
return f === 'all' || issue.severity === f || issue.category === f;
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
var SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 };
|
|
3274
|
+
|
|
3275
|
+
function renderFinding(issue, route, promptRoute) {
|
|
3276
|
+
var kids = [
|
|
3277
|
+
h('div', { class: 'dv-f-head' }, [
|
|
3278
|
+
h('span', { class: 'dv-ruleid', text: issue.id }, []),
|
|
3279
|
+
h('span', { class: 'dv-f-title', text: issue.title }, []),
|
|
3280
|
+
h('span', { class: 'dv-sev-tag dv-sev-' + issue.severity, text: issue.severity }, [])
|
|
3281
|
+
])
|
|
3282
|
+
];
|
|
3283
|
+
if (route) {
|
|
3284
|
+
kids.push(h('button', { type: 'button', class: 'dv-f-route', onclick: function () { selectItem(route); }, text: route }, []));
|
|
3285
|
+
}
|
|
3286
|
+
if (issue.location) {
|
|
3287
|
+
kids.push(h('p', { class: 'dv-f-loc', text: issue.location + (issue.line !== undefined ? ':' + issue.line : '') }, []));
|
|
3288
|
+
}
|
|
3289
|
+
if (issue.recommendation) {
|
|
3290
|
+
kids.push(h('p', { class: 'dv-f-rec', text: issue.recommendation }, []));
|
|
3291
|
+
}
|
|
3292
|
+
if (issue.fix && issue.fix.snippet) {
|
|
3293
|
+
kids.push(h('div', { class: 'dv-fix' }, [h('div', { class: 'dv-fix-label', text: 'fix' }, []), renderFixSnippet(issue.fix)]));
|
|
3294
|
+
}
|
|
3295
|
+
if (issue.docsUrl) {
|
|
3296
|
+
kids.push(h('a', { class: 'dv-f-link', href: issue.docsUrl, text: 'Learn more' }, []));
|
|
3297
|
+
}
|
|
3298
|
+
kids.push(renderAiPrompt(issue, promptRoute !== undefined ? promptRoute : route));
|
|
3299
|
+
return h('article', { class: 'dv-finding dv-finding-' + issue.severity }, kids);
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
function renderGauge(score) {
|
|
3303
|
+
var band = scoreBand(score);
|
|
3304
|
+
var svgNs = 'http://www.w3.org/2000/svg';
|
|
3305
|
+
var C = 2 * Math.PI * 58;
|
|
3306
|
+
var offset = (C * (1 - score / 100)).toFixed(1);
|
|
3307
|
+
var svg = document.createElementNS(svgNs, 'svg');
|
|
3308
|
+
svg.setAttribute('width', '132');
|
|
3309
|
+
svg.setAttribute('height', '132');
|
|
3310
|
+
svg.setAttribute('viewBox', '0 0 132 132');
|
|
3311
|
+
var bg = document.createElementNS(svgNs, 'circle');
|
|
3312
|
+
bg.setAttribute('cx', '66'); bg.setAttribute('cy', '66'); bg.setAttribute('r', '58');
|
|
3313
|
+
bg.setAttribute('fill', 'none'); bg.setAttribute('class', 'dv-gauge-track'); bg.setAttribute('stroke-width', '11');
|
|
3314
|
+
var arc = document.createElementNS(svgNs, 'circle');
|
|
3315
|
+
arc.setAttribute('cx', '66'); arc.setAttribute('cy', '66'); arc.setAttribute('r', '58');
|
|
3316
|
+
arc.setAttribute('fill', 'none'); arc.setAttribute('stroke', BAND_COLOR[band]); arc.setAttribute('stroke-width', '11');
|
|
3317
|
+
arc.setAttribute('stroke-linecap', 'round');
|
|
3318
|
+
arc.setAttribute('stroke-dasharray', C.toFixed(1));
|
|
3319
|
+
arc.setAttribute('stroke-dashoffset', offset);
|
|
3320
|
+
svg.appendChild(bg);
|
|
3321
|
+
svg.appendChild(arc);
|
|
3322
|
+
var wrap = h('div', { class: 'dv-gauge' }, [h('div', { class: 'dv-gauge-num' }, [h('strong', { text: String(score) }, []), h('span', { text: 'Health' }, [])])]);
|
|
3323
|
+
wrap.insertBefore(svg, wrap.firstChild);
|
|
3324
|
+
return wrap;
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
function renderOverview(s) {
|
|
3328
|
+
var gauge = renderGauge(s.report.score);
|
|
3329
|
+
var cats = Object.keys(s.report.categories).map(function (cat) {
|
|
3330
|
+
var c = s.report.categories[cat];
|
|
3331
|
+
var band = scoreBand(c.score);
|
|
3332
|
+
var weight = s.report.weights[cat];
|
|
3333
|
+
var name = cat === 'seo' ? 'SEO' : cat.charAt(0).toUpperCase() + cat.slice(1);
|
|
3334
|
+
return h('div', { class: 'dv-cat' }, [
|
|
3335
|
+
h('div', { class: 'dv-cat-top' }, [
|
|
3336
|
+
h('span', { text: name + (weight !== undefined ? ' (weight ' + weight + ')' : '') }, []),
|
|
3337
|
+
h('span', { style: 'color:' + BAND_COLOR[band], text: String(c.score) }, [])
|
|
3338
|
+
]),
|
|
3339
|
+
h('div', { class: 'dv-bar' }, [h('i', { style: 'width:' + c.score + '%;background:' + BAND_COLOR[band] }, [])])
|
|
3340
|
+
]);
|
|
3341
|
+
});
|
|
3342
|
+
var chips = renderFilterChips(s.report.categories);
|
|
3343
|
+
var totalCount = s.report.routes.reduce(function (n, r) { return n + r.issues.length; }, 0) + s.report.siteIssues.length;
|
|
3344
|
+
var entries = [];
|
|
3345
|
+
s.report.routes.forEach(function (r) {
|
|
3346
|
+
r.issues.forEach(function (issue) { entries.push({ issue: issue, route: r.route }); });
|
|
3347
|
+
});
|
|
3348
|
+
s.report.siteIssues.forEach(function (issue) { entries.push({ issue: issue, route: null }); });
|
|
3349
|
+
entries = entries.filter(function (e) { return passesFilter(e.issue); });
|
|
3350
|
+
entries.sort(function (a, b) {
|
|
3351
|
+
var sd = SEVERITY_ORDER[a.issue.severity] - SEVERITY_ORDER[b.issue.severity];
|
|
3352
|
+
if (sd !== 0) return sd;
|
|
3353
|
+
return (a.route || '').localeCompare(b.route || '');
|
|
3354
|
+
});
|
|
3355
|
+
var body = entries.length
|
|
3356
|
+
? entries.map(function (e) { return renderFinding(e.issue, e.route); })
|
|
3357
|
+
: [h('p', { class: 'dv-empty', text: totalCount ? 'No issues match the current filter.' : 'No issues found \u2014 nice work!' }, [])];
|
|
3358
|
+
var findings = h('section', { class: 'dv-section' }, [h('h2', { text: 'Findings' }, [])].concat(body));
|
|
3359
|
+
return h('div', { class: 'dv-overview' }, [gauge, h('div', { class: 'dv-cats' }, cats), chips, findings].filter(Boolean));
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
function renderRouteDetail(route) {
|
|
3363
|
+
var badge = state.snapshot.badges[route.route];
|
|
3364
|
+
var band = scoreBand(route.score);
|
|
3365
|
+
var header = h('div', { class: 'dv-detail-header' }, [
|
|
3366
|
+
h('span', { class: 'dv-route-path', text: route.route }, []),
|
|
3367
|
+
badge ? h('span', { class: 'dv-badge dv-badge-' + badge, text: badge }, []) : null,
|
|
3368
|
+
h('span', { class: 'dv-score-chip', style: 'color:' + BAND_COLOR[band], text: String(route.score) }, [])
|
|
3369
|
+
].filter(Boolean));
|
|
3370
|
+
var chips = renderFilterChips(state.snapshot.report.categories);
|
|
3371
|
+
var findings = route.issues.filter(passesFilter);
|
|
3372
|
+
var body = findings.length
|
|
3373
|
+
? findings.map(function (issue) { return renderFinding(issue, undefined, route.route); })
|
|
3374
|
+
: [h('p', { class: 'dv-empty', text: 'No issues match the current filter.' }, [])];
|
|
3375
|
+
return h('div', { class: 'dv-route-detail' }, [header, chips].concat(body));
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
function renderDetail() {
|
|
3379
|
+
var s = state.snapshot;
|
|
3380
|
+
if (state.selected === 'overview') {
|
|
3381
|
+
mount('dv-detail', renderOverview(s));
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
var route = s.report.routes.filter(function (r) { return r.route === state.selected; })[0];
|
|
3385
|
+
if (!route) {
|
|
3386
|
+
state.selected = 'overview';
|
|
3387
|
+
mount('dv-detail', renderOverview(s));
|
|
3388
|
+
return;
|
|
3389
|
+
}
|
|
3390
|
+
mount('dv-detail', renderRouteDetail(route));
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
function renderAll() {
|
|
3394
|
+
renderTopbar();
|
|
3395
|
+
renderSidebar();
|
|
3396
|
+
renderDetail();
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
function restoreSelectionFromHash() {
|
|
3400
|
+
var raw = location.hash.replace(/^#/, '');
|
|
3401
|
+
if (!raw || raw === 'overview') { state.selected = 'overview'; return; }
|
|
3402
|
+
var m = /^route\\/(.+)$/.exec(raw);
|
|
3403
|
+
if (m && state.routeBySlug[m[1]]) state.selected = state.routeBySlug[m[1]];
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
function fetchSnapshot() {
|
|
3407
|
+
fetch('/__svelte-vitals/data.json').then(function (r) { return r.json(); }).then(function (data) {
|
|
3408
|
+
if (state.snapshot && data.sequence <= state.snapshot.sequence) return;
|
|
3409
|
+
state.snapshot = data;
|
|
3410
|
+
renderAll();
|
|
3411
|
+
}).catch(function () {});
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
function boot() {
|
|
3415
|
+
var raw = document.getElementById('svelte-vitals-data');
|
|
3416
|
+
state.snapshot = JSON.parse(raw.textContent);
|
|
3417
|
+
applyTheme();
|
|
3418
|
+
renderSidebar(); // populates routeBySlug before the hash can be trusted
|
|
3419
|
+
restoreSelectionFromHash();
|
|
3420
|
+
renderAll();
|
|
3421
|
+
|
|
3422
|
+
window.addEventListener('hashchange', function () {
|
|
3423
|
+
restoreSelectionFromHash();
|
|
3424
|
+
renderSidebar();
|
|
3425
|
+
renderDetail();
|
|
3426
|
+
});
|
|
3427
|
+
|
|
3428
|
+
// Static export (the CLI's --reporter html): no dev server behind the page, so no
|
|
3429
|
+
// SSE connection, no /data.json refetch, no connection indicator.
|
|
3430
|
+
if (state.snapshot.live && typeof EventSource !== 'undefined') {
|
|
3431
|
+
var es = new EventSource('/__svelte-vitals/events');
|
|
3432
|
+
es.addEventListener('open', function () { state.connection = 'connected'; renderTopbar(); fetchSnapshot(); });
|
|
3433
|
+
es.addEventListener('update', fetchSnapshot);
|
|
3434
|
+
es.addEventListener('error', function () { state.connection = 'reconnecting'; renderTopbar(); });
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
boot();
|
|
2972
3439
|
})();
|
|
2973
3440
|
`;
|
|
3441
|
+
function renderAppShell(snapshot) {
|
|
3442
|
+
const safe = { ...snapshot, report: sanitizeReport(snapshot.report) };
|
|
3443
|
+
const title = snapshot.live ? "svelte-vitals dashboard" : "svelte-vitals report";
|
|
3444
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title><style>${APP_STYLE}</style></head><body><div class="dv-app" id="dv-app"><header class="dv-topbar" id="dv-topbar"></header><nav class="dv-sidebar" id="dv-sidebar"></nav><main class="dv-detail" id="dv-detail"></main></div><script type="application/json" id="svelte-vitals-data">${embedJson(safe)}</script><script>${APP_SCRIPT}</script></body></html>`;
|
|
3445
|
+
}
|
|
2974
3446
|
function buildHtmlDocument(report, meta, opts) {
|
|
2975
|
-
|
|
3447
|
+
const badges = Object.fromEntries(
|
|
3448
|
+
Object.entries(opts?.routeBadges ?? {}).filter(([, b]) => b === "measured" || b === "static")
|
|
3449
|
+
);
|
|
3450
|
+
return renderAppShell({
|
|
3451
|
+
report,
|
|
3452
|
+
badges,
|
|
3453
|
+
analyzing: false,
|
|
3454
|
+
sequence: 0,
|
|
3455
|
+
live: false,
|
|
3456
|
+
meta
|
|
3457
|
+
});
|
|
2976
3458
|
}
|
|
2977
3459
|
function formatHtmlReport(results, config, meta) {
|
|
2978
3460
|
return buildHtmlDocument(buildJsonReport(results, config, meta), meta);
|
|
2979
3461
|
}
|
|
2980
3462
|
|
|
3463
|
+
// src/reporter/html.ts
|
|
3464
|
+
var BAND_COLOR = {
|
|
3465
|
+
good: "#2FA968",
|
|
3466
|
+
warn: "#E8A317",
|
|
3467
|
+
poor: "#E5484D"
|
|
3468
|
+
};
|
|
3469
|
+
function scoreBand(score) {
|
|
3470
|
+
return score >= 90 ? "good" : score >= 50 ? "warn" : "poor";
|
|
3471
|
+
}
|
|
3472
|
+
function escapeHtml(s) {
|
|
3473
|
+
return s.replace(
|
|
3474
|
+
/[&<>"']/g,
|
|
3475
|
+
(c) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : c === '"' ? """ : "'"
|
|
3476
|
+
);
|
|
3477
|
+
}
|
|
3478
|
+
function safeHref(url) {
|
|
3479
|
+
const normalized = url.replace(/\s/g, "").toLowerCase();
|
|
3480
|
+
return /^https?:\/\//.test(normalized) ? url : null;
|
|
3481
|
+
}
|
|
3482
|
+
|
|
2981
3483
|
// src/config-apply.ts
|
|
2982
3484
|
function selectRules(rules, config) {
|
|
2983
3485
|
return rules.filter((rule) => config.rules[rule.id] !== "off");
|
|
@@ -2989,6 +3491,8 @@ function applyRuleSeverities(results, config) {
|
|
|
2989
3491
|
});
|
|
2990
3492
|
}
|
|
2991
3493
|
export {
|
|
3494
|
+
APP_SCRIPT,
|
|
3495
|
+
APP_STYLE,
|
|
2992
3496
|
BAND_COLOR,
|
|
2993
3497
|
CHILD_NODE_KEYS,
|
|
2994
3498
|
ROBOTS_SOURCE_PATHS,
|
|
@@ -3046,6 +3550,7 @@ export {
|
|
|
3046
3550
|
perf008Preconnect,
|
|
3047
3551
|
perf009HeavyImport,
|
|
3048
3552
|
perf010NamespaceImport,
|
|
3553
|
+
renderAppShell,
|
|
3049
3554
|
runRules,
|
|
3050
3555
|
safeHref,
|
|
3051
3556
|
scoreBand,
|