A purely client-side site that renders RefactorFirst reports by fetching
.refactorfirst/refactor-first.json directly from GitHub/GitLab/Bitbucket. Built
as a Next.js static export (output: 'export'): Server Components render the
static shells, client components (components/) own all interactivity. The export
in out/ is served by any static host (GitHub Pages, GitLab Pages, Bitbucket).
Key Features:
- Search over curated repository listing (
repositories.txt) - Dark mode, 100% CSS switching (
components/theme-toggle.jsx+plans/css-only-dark-mode.md): three radios (light/dark/system) on the right side of the.theme-barrow below the header, rendered byapp/layout.jsx— the breadcrumb trail shares the row on report routes (crumbs left, toggle right, vertically centered; the right-edge mirror of the breadcrumbs' left-edge alignment) — palettes selected inapp/globals.cssvia:root:has(#rf-theme-…:checked)+prefers-color-scheme; an inline pre-paint script inapp/layout.jsxonly restores/persists the choice (CSP-hashed by the build). Chart legends are canvas-drawn, so they can't follow the CSS switch:resolveLegendTextColor/bindThemeChartRedrawinlib/report-view.jsresolve a per-theme legend color (>= 4.5:1 on each--bg-color) and redraw live charts on radio/prefers-color-schemechanges. Guards:tests/unit/theme-css.test.js(the two dark blocks must stay byte-identical; all hex colors live in the palette blocks; dark palette passes the same WCAG AA math astests/unit/css-a11y.test.js) - Breadcrumb trail on the theme bar row under the menu
(
components/breadcrumbs.jsx+lib/breadcrumbs.js): mirrors/user/repo/branchwith linked ancestors; the repo crumb never self-links, and ReportView announces the resolved branch (rf:branch-resolvedevent) so the label matches the loaded report (main404 →masterfallback) - Reports rendered with Mustache.js (bundled template is authoritative)
- Enhanced report tables: sticky headers, pagination (>20 rows), sortable columns, in-table search, CSV export, click/keyboard cell copy with toasts
- Repository submission via pre-filled platform issues — no login, apps or tokens
- Fully static deploy; deep links handled via generateStaticParams +
404.html
bun install # install dependencies
bun run dev # next dev at http://localhost:3000
bun run build # static export to out/ (sync repositories + next build + CSP hashes)
python3 scripts/serve-out.py # serve out/ at :8003 with GitHub Pages 404 semanticsUnit + Integration Tests (Bun):
bun test tests/unit tests/integration # run all
bun test --watch tests/unit # watch mode
bun test --coverage tests/unit tests/integration # coverage reportE2E Tests (Playwright) — always against the built export:
npx playwright install # one-time: download browsers
npx playwright test # full E2E (builds out/, all 3 browsers)
bun run test:e2e:basepath # NEXT_PUBLIC_BASE_PATH=/preview leg (chromium)Linting:
npx eslint "lib/**/*.js" "app/**/*.{js,jsx}" "components/**/*" "tests/**/*"app/ # Next.js App Router (static export)
layout.jsx # CSP meta, header/footer, submission-target meta
not-found.jsx # 404 page + branch deep-link redirect (§6)
error.jsx # global error boundary (+ per-route boundaries)
page.jsx # landing (/)
add-repo|about|api|.../page.jsx # static content pages
[username]/page.jsx # user listing (pagination client-side)
[username]/[repository]/page.jsx # report shell
[username]/[repository]/[branch]/page.jsx # main|master pre-generated
globals.css # css/main.css + components.css
components/ # client components: report-view, repo-list,
# repo-submission-form, search-combobox,
# hero-search, menu-search, menu-toggle, breadcrumbs
# (route trail under the menu), workflow-sample,
# platform-config, sentry-provider,
# toast-notification (copy feedback live region)
lib/ # shared logic (client + RSC): routes, fetcher,
# renderer, search, utils, host, rate-limiter,
# cache-manager, error-handler, repo-submission,
# report-view, static-params, widget-loader,
# breadcrumbs,
# table-operations (filter/sort/paginate/CSV/copy +
# TABLE_CONFIG + REPORT_TABLES descriptors),
# table-enhancer (binds toolbar/sort/pagination/
# copy onto the rendered report DOM); the Node-side
# listing loader is lib/repositories.js
public/ # static files copied verbatim into out/:
repositories.txt # synced from the repo root (sync-repositories.mjs)
assets/ # mustache template, logo
templates/ # workflow-sample-*.html fragments
widgets/ # module bridges: vizdom WASM, three-spritetext,
# sentry (runtime CDN imports cannot be bundled)
templates/ # user CI samples (user-refactorfirst-*.yml) for the docs
ci/process-submissions.sh # shared submission validator for GH/Gl/BB CI
.github/workflows/ # test.yml, static.yml, redeploy.yml,
# add-repository.yml, deploy-repositories-fast.yml
.gitlab-ci.yml # GitLab Pages: build out/ → public/
bitbucket-pipelines.yml # Bitbucket build producing out/
scripts/ # sync-repositories.mjs, fix-csp-hashes.mjs, serve-out.py
tests/ # unit/ (Bun), integration/ (Bun + RTL/jsdom), e2e/ (Playwright)
TDD is mandatory — write failing tests before production code:
- Write a failing test in
tests/unit/(pure module logic) ortests/integration/(RTL/jsdom) - Run
bun test tests/unit tests/integrationand watch it fail - Write the minimal implementation in
lib//components//app/ - Refactor while keeping tests green
Accessibility is mandatory — every feature MUST comply with WCAG 2.2 AA
and all markup MUST use HTML5-valid elements/attributes (no obsolete
presentational attributes; presentation lives in CSS). New pages, components
and template changes must keep the a11y guards green:
tests/unit/html5-attributes.test.js, tests/unit/report-template-wcag.test.js,
tests/unit/css-a11y.test.js, tests/unit/page-titles.test.js — and add
coverage there when introducing new markup patterns.
- CSP lives in
app/layout.jsx(script-srcincludes the CDN widget hosts andwasm-unsafe-eval); afternext build,scripts/fix-csp-hashes.mjsinjects sha256 hashes of the inline bootstrap scripts into the exported HTML's CSP meta — keep it in the build pipeline. - Static export constraints:
dynamicParams = falseon dynamic routes;generateStaticParamsenumerates(username),(username, repository)and(username, repository, main|master)fromrepositories.txt; new repos render client-side immediately thanks to the client-side listing refresh and the?branch=deep-link redirect inapp/not-found.jsx. - Widget loading: CDN scripts load via
next/scriptlazyOnloadinsidecomponents/report-view.jsx, registered throughlib/widget-loader.js); wasm/ESM bridges inpublic/widgets/run as native module scripts. - Environment config: meta tags in
app/layout.jsx(submission-target,sentry-dsn,platform-base-url) plusNEXT_PUBLIC_HOSTING_ENVIRONMENT/NEXT_PUBLIC_BASE_PATHat build time. - Theming: see the dark-mode bullet above — the radios in
components/theme-toggle.jsxare the single source of truth; never add JS-driven styling. All colors must be custom properties declared only in the four palette blocks (:root,html:rootmvp overrides, and the two identical dark blocks) inapp/globals.css; mvp.css light overrides must stay onhtml:root(mvp loads after the bundle). Report-template widgets that hardcode light colors are re-driven frommain#app …rules there (the mustache stays byte-identical); the vizdom graph SVGs' embedded black strokes/fills are likewise re-themed to--graph-line(#9fb0c0dark =CHART_LEGEND_TEXT.dark) viapath[stroke]/polygonselectors on.fullscreen-svg— red cycle edges stay red. Edge labels are glyph paths baked by vizdom, so CSS can't reach them:withEdgeFontColorinlib/report-view.jsrewrites the DOT withfontcolor = "#9fb0c0"per edge when the dark palette is active (vizdom uppercases the hex in the SVG), and theme switches re-parse the graphs viaredrawGraphsForThemealongside the chart legend redraw inbindThemeChartRedraw.
- Every data table in the report (class/package relationships, disharmony
findings, cycle summary, cycle breakdown) is enhanced: sticky
thead th, toolbar (match live region + copy hint left; search + CSV export right — the.rf-table-blockwrapper shrink-wraps the table and the toolbar usescontain: inline-sizeso controls align with the table's right edge), sortable th buttons witharia-sort, pagination below 20+ row tables, and click/Enter/Space cell copy with toast feedback. The filter prompt lives in the input's placeholder ("Filter table...", with a screen-reader-only label); the clear control is an × button overlaid on the input's right edge (accessible name "Clear the … table filter"), revealed only while the box holds a term and invoked by click or Escape while the box has focus. - Tables open sorted by their Priority column ascending (▲, priority 1
first), signalling both the report's priority ordering and that headers are
sortable (
TABLE_CONFIG.sortingdefaults in lib/table-operations.js);resolveSortKeymaps the semanticprioritykey onto disharmony tables' dynamiccol<n>keys via a label match, tables without a Priority column keep the original report ordering, and clicking the default-sorted column flips it to descending (sort toggling starts from the effective default). - Horizontal scrollbar: tables wider than the viewport get
overflow-x: autovia therf-scroll-x-enabledclass, toggled bylib/table-enhancer.jsafter measuringwrapper.scrollWidth > clientWidth(re-measured on each re-render and on window resize). It MUST stay conditional — any overflow ancestor becomes the sticky constraint container and breaks the viewport-stickythead th. The unwrapped problem/solution tables (nodata-rf-table, no.rf-table-scroll, no sticky header) instead get an unconditionaloverflow-x: autofrom amain#apprule in globals.css so their nowrap rows scroll inside the 5px border on narrow screens (guarded by tests/unit/report-template-wcag.test.js and tests/e2e/report-overflow.spec.js). Scrolling tables keep their header pinned anyway:refreshStickyHeadersin lib/table-enhancer.js compensates by translating everythead thdown by the viewport scroll offset (clamped to the table's bottom edge) on window scroll/resize; narrow tables keep pure CSS stickiness and stale transforms are cleared when overflow goes away. - Pipeline:
prepareReportData(data, tableStates, TABLE_CONFIG)in lib/renderer.js applies filter → sort → paginate per table and injectstableUiblocks the mustache template renders;enhanceTablesin lib/table-enhancer.js binds the controls and reports state changes back to components/report-view.jsx, which re-renders (widgets only gate the first render; the search input's focus/caret is restored after each re-render). The expensiveenhanceReportpipeline (Chart.js charts, WASM DOT layout) runs only when the payload changes — table-state re-renders stash the live chart canvases / graph containers before the innerHTML swap and graft them back into the fresh DOM (statefulElementIds/stashStatefulDom/graftStatefulDomin lib/report-view.js), re-binding only the cheap popup handlers. - Search
<input>s are injected by table-enhancer —<input>is FORBID in the renderer's sanitization allow-list, so it must never appear in the mustache template. - CSV export honors the current filter + sort but ignores pagination;
filenames are
refactorfirst-<table>-<ISO timestamp>.csv.
~584 unit/integration + ~229 E2E (~202 across three browsers + 4 basePath leg + 27 dark mode).
WCAG 2.2 AA / HTML5 guards live in tests/unit/html5-attributes.test.js,
tests/unit/report-template-wcag.test.js, tests/unit/css-a11y.test.js and
tests/unit/page-titles.test.js — the report mustache keeps a single h1,
scoped table headers, captions, labelled canvases and a named nav; obsolete
presentational attributes are FORBID_ATTR-stripped in lib/renderer.js. The
report-template-wcag guard also asserts sticky-header CSS, per-table
toolbars/aria-labelled export buttons and pagination navs, valid aria-sort
on every enhanced th, sortable keyboard-operable header buttons and live
match-count regions.