@svelte-vitals/core 0.20.0 → 0.22.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 CHANGED
@@ -717,6 +717,16 @@ declare function formatSarifReport(results: Result[], config: Config, meta: {
717
717
  */
718
718
  declare function formatGithubReport(results: Result[], config: Config): string;
719
719
 
720
+ /**
721
+ * Render a compact Markdown summary — Health score, per-category table, severity counts, and
722
+ * a findings table — suitable for a GitHub Actions job summary or a sticky PR comment
723
+ * (`svelte-vitals ci install`). Delegates all aggregation to `buildJsonReport` so the numbers
724
+ * never drift from the JSON/console reporters.
725
+ */
726
+ declare function formatMarkdownReport(results: Result[], config: Config, meta: {
727
+ version: string;
728
+ }): string;
729
+
720
730
  type Band = 'good' | 'warn' | 'poor';
721
731
  declare const BAND_COLOR: Record<Band, string>;
722
732
  declare function scoreBand(score: number): Band;
@@ -733,9 +743,13 @@ declare function escapeHtml(s: string): string;
733
743
  declare function safeHref(url: string): string | null;
734
744
  declare function buildHtmlDocument(report: JsonReport, meta: {
735
745
  version: string;
746
+ coreVersion?: string;
747
+ }, opts?: {
748
+ routeBadges?: Record<string, 'measured' | 'static'>;
736
749
  }): string;
737
750
  declare function formatHtmlReport(results: Result[], config: Config, meta: {
738
751
  version: string;
752
+ coreVersion?: string;
739
753
  }): string;
740
754
 
741
755
  /** Drop rules disabled via config (design §6). */
@@ -743,4 +757,4 @@ declare function selectRules(rules: Rule[], config: Config): Rule[];
743
757
  /** Apply per-rule severity overrides to results (design §6). */
744
758
  declare function applyRuleSeverities(results: Result[], config: Config): Result[];
745
759
 
746
- 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, 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 };
760
+ 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 };
package/dist/index.js CHANGED
@@ -2654,6 +2654,82 @@ function formatGithubReport(results, config) {
2654
2654
  return lines.join("\n");
2655
2655
  }
2656
2656
 
2657
+ // src/reporter/markdown.ts
2658
+ var MAX_FINDINGS = 50;
2659
+ var SEVERITY_EMOJI = { critical: "\u{1F534}", warning: "\u{1F7E1}", info: "\u{1F535}" };
2660
+ var SEVERITY_RANK2 = { critical: 0, warning: 1, info: 2 };
2661
+ function escapeCell(s) {
2662
+ return s.replace(/\|/g, "\\|").replace(/\r\n|\r|\n/g, " ");
2663
+ }
2664
+ function locationOf(issue, route) {
2665
+ if (issue.location) return issue.line !== void 0 ? `${issue.location}:${issue.line}` : issue.location;
2666
+ return route ?? "-";
2667
+ }
2668
+ function flattenFindings(report) {
2669
+ const findings = [];
2670
+ for (const r of report.routes) {
2671
+ for (const issue of r.issues) {
2672
+ findings.push({
2673
+ severity: issue.severity,
2674
+ id: issue.id,
2675
+ location: locationOf(issue, r.route),
2676
+ message: issue.title
2677
+ });
2678
+ }
2679
+ }
2680
+ for (const issue of report.siteIssues) {
2681
+ findings.push({
2682
+ severity: issue.severity,
2683
+ id: issue.id,
2684
+ location: locationOf(issue, void 0),
2685
+ message: issue.title
2686
+ });
2687
+ }
2688
+ return findings.map((f, index) => ({ f, index })).sort((a, b) => SEVERITY_RANK2[a.f.severity] - SEVERITY_RANK2[b.f.severity] || a.index - b.index).map(({ f }) => f);
2689
+ }
2690
+ function categoryRows(categories) {
2691
+ const names = Object.keys(categories).sort();
2692
+ return names.map((cat) => `| ${cat} | ${categories[cat].score} |`);
2693
+ }
2694
+ function formatMarkdownReport(results, config, meta) {
2695
+ const report = buildJsonReport(results, config, meta);
2696
+ const lines = [];
2697
+ lines.push(`<!-- svelte-vitals v${meta.version} -->`);
2698
+ lines.push(`## svelte-vitals \u2014 Health ${report.score}/100`);
2699
+ lines.push("");
2700
+ const catRows = categoryRows(report.categories);
2701
+ if (catRows.length > 0) {
2702
+ lines.push("| Category | Score |");
2703
+ lines.push("| --- | --- |");
2704
+ lines.push(...catRows);
2705
+ lines.push("");
2706
+ }
2707
+ const { critical, warning, info, passed } = report.summary;
2708
+ lines.push(`**${critical} critical \xB7 ${warning} warning \xB7 ${info} info** (${passed} checks passed)`);
2709
+ lines.push("");
2710
+ const findings = flattenFindings(report);
2711
+ if (findings.length === 0) {
2712
+ lines.push("\u2705 No issues found.");
2713
+ } else {
2714
+ lines.push("### Findings");
2715
+ lines.push("");
2716
+ lines.push("| Severity | Rule | Location | Message |");
2717
+ lines.push("| --- | --- | --- | --- |");
2718
+ const shown = findings.slice(0, MAX_FINDINGS);
2719
+ for (const f of shown) {
2720
+ const rule = `[${f.id}](${docsUrlFor(f.id)})`;
2721
+ lines.push(
2722
+ `| ${SEVERITY_EMOJI[f.severity]} ${f.severity} | ${rule} | ${escapeCell(f.location)} | ${escapeCell(f.message)} |`
2723
+ );
2724
+ }
2725
+ if (findings.length > MAX_FINDINGS) {
2726
+ lines.push("");
2727
+ lines.push(`\u2026and ${findings.length - MAX_FINDINGS} more (run \`npx svelte-vitals\` locally for the full report)`);
2728
+ }
2729
+ }
2730
+ return lines.join("\n");
2731
+ }
2732
+
2657
2733
  // src/reporter/html.ts
2658
2734
  var BAND_COLOR = {
2659
2735
  good: "#2FA968",
@@ -2687,7 +2763,8 @@ function renderFinding(issue) {
2687
2763
  }
2688
2764
  function renderTopbar(report, meta) {
2689
2765
  const findings = report.routes.reduce((n, r) => n + r.issues.length, 0) + report.siteIssues.length;
2690
- 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>`;
2766
+ const core = meta.coreVersion ? `<span title="@svelte-vitals/core version">core v${escapeHtml(meta.coreVersion)}</span>` : "";
2767
+ 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>`;
2691
2768
  }
2692
2769
  function renderHero(report) {
2693
2770
  const C = 2 * Math.PI * 58;
@@ -2704,7 +2781,8 @@ function renderHero(report) {
2704
2781
  }).join("");
2705
2782
  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 &amp; 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>`;
2706
2783
  }
2707
- function renderRoutes(report) {
2784
+ var ROUTE_BADGES = ["measured", "static"];
2785
+ function renderRoutes(report, routeBadges) {
2708
2786
  if (report.routes.length === 0) return "";
2709
2787
  const rows = report.routes.map((r) => {
2710
2788
  const b = scoreBand(r.score);
@@ -2717,7 +2795,10 @@ function renderRoutes(report) {
2717
2795
  if (info) parts.push(`${info} info`);
2718
2796
  const sum = parts.length ? parts.join(" \xB7 ") : '<span class="none">no issues</span>';
2719
2797
  const body = r.issues.length ? r.issues.map(renderFinding).join("") : '<p class="empty">No issues found on this route.</p>';
2720
- 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>`;
2798
+ const rawBadge = routeBadges?.[r.route];
2799
+ const badge = rawBadge && ROUTE_BADGES.includes(rawBadge) ? rawBadge : void 0;
2800
+ const badgeHtml = badge ? ` <span class="badge badge-${badge}">${escapeHtml(badge)}</span>` : "";
2801
+ 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>`;
2721
2802
  }).join("");
2722
2803
  return `<section class="section"><h2>Routes</h2><div class="routes">${rows}</div></section>`;
2723
2804
  }
@@ -2770,8 +2851,11 @@ body{margin:0;background:var(--ground);color:var(--ink);font-family:var(--sans);
2770
2851
  .route>summary{display:grid;grid-template-columns:1fr auto auto auto;gap:16px;align-items:center;padding:13px 16px;cursor:pointer;list-style:none}
2771
2852
  .route>summary::-webkit-details-marker{display:none}
2772
2853
  .route>summary:hover{background:#fbfcfd}
2773
- .route-name{font-family:var(--mono);font-size:13.5px;min-width:0}
2854
+ .route-name{font-family:var(--mono);font-size:13.5px;min-width:0;display:flex;align-items:center;gap:8px}
2774
2855
  .route-name .path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2856
+ .badge{font-family:var(--sans);font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:2px 7px;border-radius:999px;flex-shrink:0}
2857
+ .badge-measured{background:rgba(47,169,104,.14);color:var(--good)}
2858
+ .badge-static{background:rgba(140,149,163,.18);color:var(--muted)}
2775
2859
  .issue-sum{font-size:12.5px;color:var(--muted);font-family:var(--mono);white-space:nowrap}.issue-sum .none{color:var(--good)}
2776
2860
  .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}
2777
2861
  .score-chip .ring{width:10px;height:10px;border-radius:50%}
@@ -2833,8 +2917,8 @@ var SCRIPT = `
2833
2917
  });
2834
2918
  })();
2835
2919
  `;
2836
- function buildHtmlDocument(report, meta) {
2837
- 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>`;
2920
+ function buildHtmlDocument(report, meta, opts) {
2921
+ 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, opts?.routeBadges) + 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>`;
2838
2922
  }
2839
2923
  function formatHtmlReport(results, config, meta) {
2840
2924
  return buildHtmlDocument(buildJsonReport(results, config, meta), meta);
@@ -2888,6 +2972,7 @@ export {
2888
2972
  formatGithubReport,
2889
2973
  formatHtmlReport,
2890
2974
  formatJsonReport,
2975
+ formatMarkdownReport,
2891
2976
  formatSarifReport,
2892
2977
  hasFailureAtOrAbove,
2893
2978
  headTagRule,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/core",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Shared, runtime-agnostic core for svelte-vitals (types, rule engine, scorer, reporter).",
5
5
  "type": "module",
6
6
  "license": "MIT",