feat: visual HTML report (--reporter html) - #47
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds escapeHtml, scoreBand, BAND_COLOR, buildHtmlDocument, and formatHtmlReport to packages/core. The renderer is pure string building — no node: imports, no I/O, no external resources. STYLE/SCRIPT placeholders are empty strings pending Tasks 2–3. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fills the STYLE constant with the approved instrument-report design CSS, targeting all classes emitted in Task 1. Adds styling test to verify CSS presence and self-containment (no external resource references). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fill SCRIPT constant with gauge animation (respecting prefers-reduced-motion) and filter logic - Gauge animates on page load with cubic-bezier easing over 1.1s - Finding cards filter by severity/category via chip interaction - Wire test assertions for interactivity behavior Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the HTML reporter into the CLI: adds 'html' to ReporterName/isReporterName, parses --out-file in resolve-args, adds outFile/writeFile to RunOptions, dispatches to formatHtmlReport in run(), and updates HELP + mri string options in bin.ts. Also fixes a TypeScript strict-null error in packages/core/src/reporter/html.ts (location/recommendation are optional). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…mport Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 50 minutes and 9 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate. For paid Pro and Pro+ PR reviews, CodeRabbit uses rolling per-developer review limits. Reviews become available again as older review attempts age out of the rolling limit window. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds an ChangesHTML Reporter Feature
Sequence Diagram(s)sequenceDiagram
participant User as User (CLI)
participant bin as bin.ts (mri)
participant resolveArgs as resolve-args.ts
participant run as index.ts run()
participant formatHtmlReport as formatHtmlReport (core)
participant fs as writeFileSync / stdout
User->>bin: svelte-vitals --reporter html --out-file report.html
bin->>resolveArgs: argv with reporter, out-file
resolveArgs-->>bin: RunOptions { reporter: 'html', outFile: 'report.html' }
bin->>run: RunOptions
run->>formatHtmlReport: results, config, { version }
formatHtmlReport-->>run: HTML string
alt outFile === '-'
run->>fs: log(html) to stdout
else outFile provided or default
run->>fs: writeFileSync(outFile, html)
run->>fs: errorLog("Report written to outFile")
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/core/test/html-report.test.ts (1)
108-120: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReplace
as neverwith a properly typed Config object.The
as nevercast at line 114 completely bypasses TypeScript's type checking. If theConfigtype evolves, this test won't catch breaking changes. Consider creating a minimal valid Config fixture or using a builder function instead.♻️ Suggested approach
describe('formatHtmlReport', () => { it('matches buildHtmlDocument over the built JsonReport (smoke)', () => { // formatHtmlReport builds the JsonReport internally; here we only assert it returns a full doc. // A fuller integration check lives in the CLI tests. + const minimalConfig: Config = { + treatDynamicAs: 'pass', + metaComponents: [], + rules: {}, + failOn: 'critical' + } as Config; const out = formatHtmlReport( [], - { treatDynamicAs: 'pass', metaComponents: [], rules: {}, failOn: 'critical' } as never, + minimalConfig, { version: '9.9.9' } );Alternatively, if
Configrequires many fields, create abuildTestConfig()helper in a test utilities module.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/test/html-report.test.ts` around lines 108 - 120, The `as never` type cast in the call to formatHtmlReport bypasses TypeScript's type checking, which means changes to the Config interface won't be caught by this test. Replace the `as never` cast with a properly typed Config object by either providing all required Config fields in the object literal passed to formatHtmlReport, or create a test utility helper function (such as buildTestConfig) that constructs a minimal valid Config fixture and use that instead of the cast.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/src/reporter/html.ts`:
- Around line 84-85: The `name` variable at line 84 in the buildHtmlDocument
function is being inserted directly into HTML without escaping, creating an XSS
vulnerability if untrusted JsonReport data is provided. Escape the `name`
variable using an appropriate HTML escaping function (such as a utility that
converts special HTML characters like <, >, &, ", ' to their entity equivalents)
before inserting it into the HTML string template where it appears in the span
with class "name".
- Line 44: The code on line 44 in the `docs` variable assignment uses escapeHtml
to escape HTML metacharacters in the `docsUrl`, but this does not prevent
malicious URLs with `javascript:` scheme from executing. Add URL scheme
validation before rendering the link to ensure only safe schemes like `https:`
and `http:` are allowed. Modify the condition that checks `issue.docsUrl` to
also validate that the URL has a safe protocol scheme before constructing the
anchor element with the docsUrl in the href attribute.
---
Nitpick comments:
In `@packages/core/test/html-report.test.ts`:
- Around line 108-120: The `as never` type cast in the call to formatHtmlReport
bypasses TypeScript's type checking, which means changes to the Config interface
won't be caught by this test. Replace the `as never` cast with a properly typed
Config object by either providing all required Config fields in the object
literal passed to formatHtmlReport, or create a test utility helper function
(such as buildTestConfig) that constructs a minimal valid Config fixture and use
that instead of the cast.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4509dd8b-b91e-4e77-bd3c-1437c763e7c5
📒 Files selected for processing (16)
.changeset/visual-html-report.md.gitignoredocs/src/content/docs/guides/cli.mddocs/src/content/docs/guides/reporters.mddocs/src/content/docs/ja/guides/cli.mddocs/src/content/docs/ja/guides/reporters.mddocs/superpowers/plans/2026-06-23-visual-html-report.mddocs/superpowers/specs/2026-06-23-visual-html-report-design.mdpackages/cli/src/bin.tspackages/cli/src/index.tspackages/cli/src/reporter-resolve.tspackages/cli/src/resolve-args.tspackages/cli/test/html-reporter.test.tspackages/core/src/index.tspackages/core/src/reporter/html.tspackages/core/test/html-report.test.ts
…gory XSS Address CodeRabbit review on PR #47 — buildHtmlDocument is a public API and JsonReport is loosely typed (docsUrl?: string, categories: Record<string,...>): - safeHref(): render a finding's docsUrl link only when it cleanly resolves to http(s) after stripping whitespace (browsers strip tab/newline before scheme resolution), so javascript:/data: hrefs are dropped. escapeHtml guards attribute breakout but not the scheme. - Escape the category label before inserting it into the hero markup. - Tests for both, plus safeHref unit cases; replace test's 'as never' scoreModel casts with a properly typed ScoreModel helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new self-contained HTML reporter to svelte-vitals, backed by a runtime-agnostic HTML renderer in @svelte-vitals/core, plus tests, docs, and a release changeset.
Changes:
- Implement
@svelte-vitals/coreHTML renderer (buildHtmlDocument/formatHtmlReport) with inline CSS/JS. - Wire
svelte-vitals --reporter htmlwith--out-file <path>(including-for stdout) and add CLI tests. - Document the new reporter/flags (EN/JA) and add a changeset.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core/src/reporter/html.ts | New pure string renderer that builds a full HTML report (markup + inline CSS/JS). |
| packages/core/src/index.ts | Exports the new HTML report APIs from @svelte-vitals/core. |
| packages/core/test/html-report.test.ts | Adds core-level tests for HTML rendering, self-contained guard, and safety helpers. |
| packages/cli/src/index.ts | Adds html reporter branch that writes the report (or prints to stdout when --out-file -). |
| packages/cli/src/reporter-resolve.ts | Extends reporter name union + validation to include html. |
| packages/cli/src/resolve-args.ts | Parses --out-file and updates reporter validation error message. |
| packages/cli/src/bin.ts | Updates CLI help and mri option parsing to include --out-file and html. |
| packages/cli/test/html-reporter.test.ts | Adds CLI tests covering default output file, custom output path, and stdout mode. |
| docs/src/content/docs/guides/reporters.md | Documents the new HTML reporter (EN). |
| docs/src/content/docs/guides/cli.md | Documents html reporter and --out-file flag (EN). |
| docs/src/content/docs/ja/guides/reporters.md | Documents the new HTML reporter (JA). |
| docs/src/content/docs/ja/guides/cli.md | Documents html reporter and --out-file flag (JA). |
| docs/superpowers/specs/2026-06-23-visual-html-report-design.md | Adds the design spec for the visual HTML report. |
| docs/superpowers/plans/2026-06-23-visual-html-report.md | Adds the implementation plan and verification checklist. |
| .changeset/visual-html-report.md | Declares minor releases for @svelte-vitals/core and svelte-vitals. |
| .gitignore | Adjusts ignore entry for .superpowers/ scratch directory. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…uard Address Copilot review on PR #47: - Clamp issue.severity to the known set (critical/warning/info) before it flows into class/data attributes — buildHtmlDocument is public, input may be malformed. - Make the footer text non-clickable so the report's only external links are the per-finding docsUrl anchors (matches the stated self-contained constraint). - Narrow the self-contained guard regex to the per-finding docsUrl rules pattern so any other external href still trips the assertion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Derive category filter chips from report.categories (was hardcoded SEO/Performance) so they stay in sync with the hero bars; filter by an explicit data-filter value instead of chip label text. - CLI: mkdir -p the parent dir before writing the HTML report, and fall back to the default path when --out-file is an empty string. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
packages/core/test/html-report.test.ts (2)
152-178: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueConsider verifying selective filtering of unsafe docsUrl.
The test verifies that an unsafe
javascript:docsUrl is dropped, but doesn't verify that a safe docsUrl in another finding would still be rendered. Adding a second finding with a validhttps://docsUrl would confirm the filtering is selective rather than blanket, catching potential regressions where all docsUrl values are accidentally dropped.📋 Example mixed safe/unsafe test structure
issues: [ { id: 'SEO001', category: 'seo', title: 't', detection: { presence: 'none', value: 'absent' }, location: 'f.svelte', recommendation: 'r', docsUrl: 'javascript:alert(1)', severity: 'critical' - } + }, + { + id: 'SEO002', + category: 'seo', + title: 'safe finding', + detection: { presence: 'present', value: 'static' }, + location: 'g.svelte', + recommendation: 'check docs', + docsUrl: 'https://example.com/docs', + severity: 'warning' + } ] } ], siteIssues: [] }; const html = buildHtmlDocument(evil, { version: '0' }); expect(html).not.toContain('javascript:alert(1)'); expect(html).not.toContain('href="javascript:'); +expect(html).toContain('href="https://example.com/docs"'); // safe link still rendered🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/test/html-report.test.ts` around lines 152 - 178, The test for unsafe docsUrl filtering only verifies that the dangerous javascript: URL is removed, but it does not verify that safe docsUrl values are still rendered, making it impossible to detect if the filtering is selective or if all docsUrl values are being blanket dropped. Add a second finding with a valid https:// docsUrl to the issues array in the evil JsonReport object, then add a positive assertion to verify that this safe URL is present in the generated HTML, ensuring the filtering is selective rather than blanket.
92-93: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider making the docsUrl domain assumption more robust.
The regex hardcodes
oekazuma\.github\.io\/svelte-vitals\/rules/to strip docsUrl anchors. While this matches the current implementation, it creates test brittleness: if a future docsUrl points to a different domain (e.g., a CDN or documentation host migration), the test will fail even though the behavior is correct.Consider either:
- Documenting the assumption that all docsUrl values must start with this specific base URL, or
- Deriving the pattern from the actual docsUrl values in the test fixture to make the coupling explicit.
Also applies to: 133-134, 145-146
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/test/html-report.test.ts` around lines 92 - 93, The test uses a hardcoded regex pattern to strip docsUrl anchors at lines 92-93 (and similarly at 133-134 and 145-146), which assumes the domain will always be oekazuma.github.io/svelte-vitals/rules/. To make this more robust, extract the actual docsUrl base from the test fixture data and dynamically construct the regex pattern from that value instead of hardcoding the domain. This way, if the docsUrl domain changes in the future, the test will automatically adapt to the new domain without requiring manual regex updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/test/html-report.test.ts`:
- Around line 193-199: The test for safeHref function is missing coverage for
important security behaviors: whitespace-stripping and case-insensitive scheme
validation. Add three additional expect statements to the safeHref test case to
verify these security-critical features: test that safeHref rejects schemes with
embedded whitespace like 'java\tscript:alert(1)' returning null, rejects
uppercase dangerous schemes like 'JavaScript:alert(1)' returning null, and
accepts valid https schemes even when uppercase like 'HTTPS://example.com'.
These test cases ensure the documented attack-surface reduction behaviors are
verified and protected against regression.
---
Nitpick comments:
In `@packages/core/test/html-report.test.ts`:
- Around line 152-178: The test for unsafe docsUrl filtering only verifies that
the dangerous javascript: URL is removed, but it does not verify that safe
docsUrl values are still rendered, making it impossible to detect if the
filtering is selective or if all docsUrl values are being blanket dropped. Add a
second finding with a valid https:// docsUrl to the issues array in the evil
JsonReport object, then add a positive assertion to verify that this safe URL is
present in the generated HTML, ensuring the filtering is selective rather than
blanket.
- Around line 92-93: The test uses a hardcoded regex pattern to strip docsUrl
anchors at lines 92-93 (and similarly at 133-134 and 145-146), which assumes the
domain will always be oekazuma.github.io/svelte-vitals/rules/. To make this more
robust, extract the actual docsUrl base from the test fixture data and
dynamically construct the regex pattern from that value instead of hardcoding
the domain. This way, if the docsUrl domain changes in the future, the test will
automatically adapt to the new domain without requiring manual regex updates.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 671cda5d-165d-4fa6-9fb8-4e53e31b4255
📒 Files selected for processing (5)
packages/cli/src/index.tspackages/cli/test/html-reporter.test.tspackages/core/src/index.tspackages/core/src/reporter/html.tspackages/core/test/html-report.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/core/src/index.ts
- packages/cli/test/html-reporter.test.ts
- packages/cli/src/index.ts
- packages/core/src/reporter/html.ts
Per CodeRabbit review on PR #47: assert the security-critical behaviors (java\tscript:/java\nscript: and JavaScript: rejected; uppercase HTTPS:// accepted) so the documented attack-surface reduction can't regress.
Add a visual HTML report — the "Lighthouse-like visualization" pillar of the 1.0 thesis.
svelte-vitals --reporter htmlwrites a single, self-contained, styled HTML page (Health gauge, per-category and per-route scores, findings with fixes) you open in a browser.This is sub-project A: the shared renderer + the CLI reporter. A future vite live-UI mode (sub-project B,
vitest --ui-style) will reuse the same renderer — designed in, not built here.What's in it
@svelte-vitals/core,packages/core/src/reporter/html.ts) —buildHtmlDocument(report, meta)/formatHtmlReport(results, config, meta)turn the existingJsonReportinto a full self-contained HTML string. Server-side templating: data is rendered into the markup, with inline<style>and inline<script>for styling and light interactivity. Pure string function — nonode:imports, no I/O, deterministic output.svelte-vitals --reporter html) — writessvelte-vitals-report.htmlby default;--out-file <path>to change it;--out-file -to stream to stdout. Printswrote report to <path>to stderr. The file write is the CLI's job (node:fs); core never touches the filesystem. Exit codes unchanged (HTML is output, not a gate).prefers-reduced-motion), severity tallies, per-category score bars with weights, a route list (native<details>, expandable), and finding cards with the rule id, severity, location, recommendation, fix snippet, and a docs link. Filter chips (severity/category) work client-side.Design choices
docsUrlanchors. A guard test enforces this.#FF3E00is brand chrome only (wordmark,↯, links); the red/amber/green score bands are a separate functional scale.Out of scope (follow-ups)
The vite live-UI mode (sub-project B), syntax highlighting, dark mode, and a per-route sort toggle (routes already arrive sorted by path) — all deferred.
Release
@svelte-vitals/core+svelte-vitalsminor changeset.@svelte-vitals/vite/@svelte-vitals/mcpcascade a patch viaworkspace:*.Validation
pnpm -r typecheck,pnpm -r test(core 11/11 new + full suite),pnpm build,pnpm --filter docs build(39 pages),pnpm lint, publint — all green.attwfails locally only (sandboxnpm pack) — known pre-existing, CI-unaffected.Process
Built subagent-driven: 5 tasks (each spec + quality reviewed) + a whole-branch review on Opus (verdict: ready to merge). Docs guides (Reporters + CLI, en + ja) updated.
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
--reporter htmlto generate a self-contained styled HTML report with health score, per-category/route breakdowns, and findings (including suggested fixes).--out-file <path>to choose where the HTML is written (use-to print to stdout).Documentation
Tests