diff --git a/.eslintrc.json b/.eslintrc.json
index 31b7203..b6d26fb 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -6,12 +6,25 @@
},
"parserOptions": {
"ecmaVersion": 2022,
- "sourceType": "module"
+ "sourceType": "module",
+ "ecmaFeatures": { "jsx": true }
+ },
+ "extends": [
+ "eslint:recommended",
+ "plugin:react/recommended",
+ "plugin:react/jsx-runtime",
+ "plugin:react-hooks/recommended"
+ ],
+ "settings": {
+ "react": { "version": "detect" }
+ },
+ "globals": {
+ "process": "readonly"
},
- "extends": "eslint:recommended",
"rules": {
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
- "no-console": ["warn", { "allow": ["warn", "error"] }]
+ "no-console": ["warn", { "allow": ["warn", "error"] }],
+ "react/prop-types": "off"
},
- "ignorePatterns": ["node_modules/", "playwright-report/", "test-results/"]
+ "ignorePatterns": ["node_modules/", "playwright-report/", "test-results/", ".next/", "out/", "public/widgets/"]
}
diff --git a/.github/workflows/deploy-repositories-fast.yml b/.github/workflows/deploy-repositories-fast.yml
new file mode 100644
index 0000000..2ab2b4b
--- /dev/null
+++ b/.github/workflows/deploy-repositories-fast.yml
@@ -0,0 +1,40 @@
+name: Deploy Repositories Fast Path
+
+# Fast feedback loop for submissions (Technical Appendix §5 Option 3):
+# runs right after the Add Repository workflow, uploads repositories.txt
+# as an artifact and comments the expected availability time on the issue.
+
+on:
+ workflow_run:
+ workflows: ["Add Repository"]
+ types: [completed]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ issues: write
+
+concurrency:
+ group: "repositories-fast-path"
+ cancel-in-progress: false
+
+jobs:
+ notify-fast-path:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Upload repositories.txt snapshot
+ uses: actions/upload-artifact@v4
+ with:
+ name: current-listing
+ path: repositories.txt
+ retention-days: 5
+
+ - name: Log availability message
+ run: |
+ echo "repositories.txt snapshot uploaded."
+ echo "Report pages are generated by the cached Pages deployment; the newly"
+ echo "submitted repository renders client-side from raw content immediately,"
+ echo "and its static shell appears once the Pages build completes."
diff --git a/.github/workflows/redeploy.yml b/.github/workflows/redeploy.yml
index ae3ea51..2d871bb 100644
--- a/.github/workflows/redeploy.yml
+++ b/.github/workflows/redeploy.yml
@@ -9,9 +9,13 @@ permissions:
pages: write
id-token: write
+# Redeployment when repositories.txt changed recently. The build is cached
+# (actions/cache on .next/cache) to hit the <6 minute target.
jobs:
- check-and-deploy:
+ check:
runs-on: ubuntu-latest
+ outputs:
+ changed: ${{ steps.check-changes.outputs.changed }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -36,17 +40,41 @@ jobs:
echo "No recent changes, skipping deployment"
fi
- - name: Setup Pages
- if: steps.check-changes.outputs.changed == 'true'
- uses: actions/configure-pages@v4
-
+ build:
+ needs: check
+ if: needs.check.outputs.changed == 'true'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: oven-sh/setup-bun@v1
+ with:
+ bun-version: latest
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ - name: Cache .next build cache
+ uses: actions/cache@v4
+ with:
+ path: .next/cache
+ key: ${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lock', '**/package.json') }}-${{ github.run_id }}
+ restore-keys: ${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lock', '**/package.json') }}-
+ - run: bun install
+ - name: Build static export
+ run: bun run build
+ env:
+ NEXT_PUBLIC_HOSTING_ENVIRONMENT: github
- name: Upload artifact
- if: steps.check-changes.outputs.changed == 'true'
uses: actions/upload-pages-artifact@v3
with:
- path: '.'
+ path: 'out'
+ deploy:
+ needs: build
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ steps:
- name: Deploy to GitHub Pages
- if: steps.check-changes.outputs.changed == 'true'
id: deployment
- uses: actions/deploy-pages@v4
+ uses: actions/deploy-pages@v5
diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml
index 460f782..9fdc0dd 100644
--- a/.github/workflows/static.yml
+++ b/.github/workflows/static.yml
@@ -1,5 +1,5 @@
-# Simple workflow for deploying static content to GitHub Pages
-name: Deploy static content to Pages
+# Build the Next.js static export and deploy out/ to GitHub Pages
+name: Build and deploy Next.js export to Pages
on:
# Runs on pushes targeting the default branch
@@ -22,22 +22,43 @@ concurrency:
cancel-in-progress: false
jobs:
- # Single deploy job since we're just deploying
- deploy:
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
+ build:
runs-on: ubuntu-latest
+ outputs:
+ artifact-path: out
steps:
- name: Checkout
uses: actions/checkout@v4
- - name: Setup Pages
- uses: actions/configure-pages@v5
+ - uses: oven-sh/setup-bun@v1
+ with:
+ bun-version: latest
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ - name: Cache .next build cache
+ uses: actions/cache@v4
+ with:
+ path: .next/cache
+ key: ${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lock', '**/package.json') }}-${{ github.run_id }}
+ restore-keys: ${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lock', '**/package.json') }}-
+ - name: Install dependencies
+ run: bun install
+ - name: Build static export
+ run: bun run build
+ env:
+ NEXT_PUBLIC_HOSTING_ENVIRONMENT: github
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
- # Upload entire repository
- path: '.'
+ path: 'out'
+
+ deploy:
+ needs: build
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 3006cef..bbeb21f 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -11,11 +11,13 @@ jobs:
bun-version: latest
- run: bun install
- run: bun test tests/unit tests/integration
- - run: bunx eslint "js/**/*.js" "tests/**/*.js"
+ - run: bunx eslint "lib/**/*.js" "app/**/*.{js,jsx}" "components/**/*.{js,jsx}" "tests/**/*.{js,jsx}"
- run: shellcheck ci/process-submissions.sh
e2e-tests:
# Matrix execution: one runner per browser so the suites run in parallel.
+ # The Playwright webServer builds the static export (bun run build) and
+ # serves ./out with GitHub Pages semantics (scripts/serve-out.py).
strategy:
fail-fast: false
matrix:
@@ -35,3 +37,21 @@ jobs:
name: playwright-report-${{ matrix.browser }}
path: playwright-report/
retention-days: 7
+
+ e2e-basepath:
+ # Sub-path deployment leg (GitHub Pages project sites): NEXT_PUBLIC_BASE_PATH
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: oven-sh/setup-bun@v1
+ with:
+ bun-version: latest
+ - run: bun install
+ - run: bunx playwright install --with-deps chromium
+ - run: bunx playwright test --config playwright.basepath.config.js
+ - uses: actions/upload-artifact@v4
+ if: failure()
+ with:
+ name: playwright-report-basepath
+ path: playwright-report/
+ retention-days: 7
diff --git a/.gitignore b/.gitignore
index 54faf00..de50e8b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@ pnpm-lock.yaml
dist/
build/
out/
+.next/
*.tsbuildinfo
# Test coverage and reports
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a978c2b..47c69e8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,49 +1,40 @@
-# GitLab CI configuration for this RefactorFirst Pages site.
-#
-# Deploys the static files to GitLab Pages. A copy of index.html is published as
-# 404.html so client-side routes like /user/repo serve the application instead of
-# GitLab's generic 404 page.
+# GitLab Pages deployment for the RefactorFirst Pages app.
+# Deploys the Next.js static export (out/) as public/ for GitLab Pages.
stages:
- - test
- - process
- - deploy
-
-validate-site:
- stage: test
- image: alpine:3.20
- script:
- - test -f index.html
- - test -f repositories.txt
- - sort -c repositories.txt
- - '! grep -Ev "^[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9][A-Za-z0-9_.-]*$" repositories.txt'
- rules:
- - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
-
-# Processes open "Add repository: owner/repo" issues: validates the issue
-# author has write access to the project, checks the report file exists,
-# commits repositories.txt and closes the issue with the outcome.
-# GitLab has no issue-triggered pipelines, so create a pipeline schedule
-# (e.g. every 10 minutes) in Settings -> CI/CD -> Schedules.
-process-submissions:
- stage: process
- image: alpine:3.20
- before_script:
- - apk add --no-cache curl jq
- script:
- - sh ci/process-submissions.sh gitlab
- rules:
- - if: $CI_PIPELINE_SOURCE == "schedule"
+ - build
pages:
- stage: deploy
- image: alpine:3.20
+ stage: build
+ image: node:22
+ before_script:
+ - npm install -g bun
script:
- - mkdir -p public
- - cp -r index.html repositories.txt css js templates assets public/
- - cp index.html public/404.html # client-side routing for deep links
+ - bun install
+ - NEXT_PUBLIC_HOSTING_ENVIRONMENT=gitlab bun run build
+ - rm -rf public
+ - mkdir public
+ - cp -r out/. public/
artifacts:
paths:
- public
+ cache:
+ key:
+ files:
+ - bun.lock
+ paths:
+ - .next/cache
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+
+test:
+ stage: build
+ image: node:22
+ before_script:
+ - npm install -g bun
+ script:
+ - bun install
+ - bun test tests/unit tests/integration
+ rules:
+ - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
+ - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
diff --git a/AGENTS.md b/AGENTS.md
index 7865d32..b6e39c1 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,136 +1,114 @@
-# RefactorFirst GitHub Pages Application - Agent Guide
+# RefactorFirst Pages — Agent Guide
## Project Overview
-A purely client-side static web application that renders RefactorFirst reports by fetching `.refactorfirst/refactor-first.json` data directly from GitHub repositories. No server-side code, no database, no build step — HTML, CSS and ES6 JavaScript modules served as static files.
+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`)
-- Reports rendered with Mustache.js from raw platform content
-- Repository submission via pre-filled platform issues — no login, apps or
- tokens; identity is captured as the issue author and validated in CI
-- Works with plain static file server
+- Reports rendered with Mustache.js (bundled template is authoritative)
+- Repository submission via pre-filled platform issues — no login, apps or tokens
+- Fully static deploy; deep links handled via generateStaticParams + `404.html`
## Development Setup
```bash
-bun install # install devDependencies
-python3 -m http.server 8000 # run locally at http://localhost:8000
+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 semantics
```
## Testing Commands
**Unit + Integration Tests (Bun):**
```bash
-bun test tests/unit tests/integration # run all unit/integration tests
+bun test tests/unit tests/integration # run all
bun test --watch tests/unit # watch mode
bun test --coverage tests/unit tests/integration # coverage report
```
-**E2E Tests (Playwright):**
+**E2E Tests (Playwright) — always against the built export:**
```bash
npx playwright install # one-time: download browsers
-npx playwright test # full E2E suite (chromium, firefox, webkit)
-npx playwright test --ui # interactive mode
+npx playwright test # full E2E (builds out/, all 3 browsers)
+bun run test:e2e:basepath # NEXT_PUBLIC_BASE_PATH=/preview leg (chromium)
```
**Linting:**
```bash
-npx eslint js/**/*.js tests/**/*.js # lint
-npx eslint js/**/*.js tests/**/*.js --fix # auto-fix
+npx eslint "lib/**/*.js" "app/**/*.{js,jsx}" "components/**/*" "tests/**/*"
```
## Project Structure
```
-index.html # Single-page app shell (top menu + #app container)
-repositories.txt # Listed repositories, one "user/repo" per line
-js/ # ES6 modules: router, fetcher, renderer, search,
- # repo-submission, error-handler,
- # rate-limiter, cache-manager, utils, main
-ci/process-submissions.sh # Shared submission validator for GitHub Actions,
- # GitLab CI and Bitbucket Pipelines
-css/ # main.css + components.css
-templates/ # Static page templates (about, faq, errors, ...)
- # + user CI templates for GitHub/GitLab/Bitbucket
-assets/ # Fallback Mustache template, logo, Sentry config
-tests/ # unit/ (Bun), integration/ (Bun), e2e/ (Playwright)
-.github/workflows/ # add-repository.yml, redeploy.yml, test.yml
+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,
+ # workflow-sample, platform-config, sentry-provider
+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; 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)
```
## Development Workflow
**TDD is mandatory** — write failing tests before production code:
-1. Write a failing test in `tests/unit/` (pure module logic) or `tests/integration/` (DOM + routing flows)
+1. Write a failing test in `tests/unit/` (pure module logic) or `tests/integration/` (RTL/jsdom)
2. Run `bun test tests/unit tests/integration` and watch it fail
-3. Write the minimal implementation in `js/` to make it pass
+3. Write the minimal implementation in `lib/` / `components/` / `app/`
4. Refactor while keeping tests green
-## Key Module Responsibilities
-
-| Module | Responsibility |
-|--------|---------------|
-| `js/router.js` | URL routes and routing logic |
-| `js/fetcher.js` | Platform-aware raw fetching / branch fallback |
-| `js/renderer.js` | Mustache rendering |
-| `js/search.js` | Search / type-ahead functionality |
-| `js/repo-submission.js` | Submission flow: validation, report check, per-platform issue URLs |
-| `js/report-view.js` | Interactive report widgets: DOT popups (Sigma/3D), Chart.js bubbles, vizdom WASM graphs |
-| `js/error-handler.js` | Error page rendering |
-| `js/utils.js` | Utility functions, environment detection |
-| `js/main.js` | Application entry point |
-| `ci/process-submissions.sh` | CI-side submission validation + write-back for all platforms |
-
-## Testing Requirements
-
-- **Unit tests**: Pure module logic (router, fetcher, renderer, search, etc.)
-- **Integration tests**: DOM + routing flows (search flow, submission flow incl. per-platform issue redirect)
-- **E2E tests**: User journeys (incl. submission → pre-filled issue hand-off), cross-browser smoke tests, mobile responsiveness
-- **Coverage target**: 80%+ on core modules
-- **Current suite**: 165 tests
-
-## CI/CD
-
-- `.github/workflows/test.yml` runs Bun unit/integration tests and Playwright E2E suite on every push and PR
-- Keep tests green before merging
-- GitHub Actions used for scheduled redeployment and repository submission validation
-
-## Environment-Aware Documentation
-
-The Getting Started page shows only the CI sample matching the hosting environment, detected from hostname:
-- `*.github.io` → GitHub Actions
-- `*.gitlab.io` → GitLab CI
-- `*.bitbucket.io` → Bitbucket Pipelines
-- Anything else → defaults to GitHub
-
-Detection logic in `js/utils.js` → `detectHostingEnvironment()`
-
-## Deployment Targets
-
-This project supports deployment to:
-- GitHub Pages (organization or personal account)
-- GitHub Enterprise Server
-- GitLab Pages
-- Bitbucket static hosting
-
-See README.md for detailed deployment instructions for each platform.
-
-## Code Conventions
-
-- ES6 modules throughout
-- No build step required
-- Client-side routing from single `index.html`
-- Mustache.js for templating
-- No client-side authentication — submission identity comes from the platform issue author
-- Static file serving (no server-side code)
-
-## Important Notes
-
-- The `` tag in `index.html` points submissions at the listing project; self-managed GitLab deployments add ``
-- Deployments must extend the CSP `connect-src` with the platform endpoints they use (`api.gitlab.com`/custom base, `api.bitbucket.org`, ...)
-- Report rendering loads CDN libs (Chart.js, sigma/graphology, graphlib-dot, svg-pan-zoom, 3d-force-graph, vizdom WASM) — keep CSP `script-src`/`connect-src` entries (`cdn.jsdelivr.net`, `cdnjs.cloudflare.com`, `esm.sh`, `buttons.github.io`, `wasm-unsafe-eval`) when tightening the policy
-- `assets/refactor-first-report.mustache` is a port of the RefactorFirst viewer template — keep it in sync with upstream
-- For GitHub Enterprise Server, update API/raw endpoints in `js/repo-submission.js`, `js/fetcher.js`, and `ci/process-submissions.sh`
-- Deep links require `404.html` copy of `index.html` for proper client-side routing on some platforms
-- Reports are fetched client-side — end users' browsers must reach GitHub/raw endpoints
+## Platform-Aware Sections in This Repo
+
+- **CSP** lives in `app/layout.jsx` (`script-src` includes the CDN widget hosts and
+ `wasm-unsafe-eval`); after `next build`, `scripts/fix-csp-hashes.mjs` injects
+ sha256 hashes of the inline bootstrap scripts into the exported HTML's CSP meta —
+ keep it in the build pipeline.
+- **Static export constraints:** `dynamicParams = false` on dynamic routes;
+ `generateStaticParams` enumerates `(username)`, `(username, repository)` and
+ `(username, repository, main|master)` from `repositories.txt`; new repos render
+ client-side immediately thanks to the client-side listing refresh and the
+ `?branch=` deep-link redirect in `app/not-found.jsx`.
+- **Widget loading:** CDN scripts load via `next/script` `lazyOnload` inside
+ `components/report-view.jsx`, registered through `lib/widget-loader.js`);
+ wasm/ESM bridges in `public/widgets/` run as native module scripts.
+- **Environment config:** meta tags in `app/layout.jsx` (`submission-target`,
+ `sentry-dsn`, `platform-base-url`) plus `NEXT_PUBLIC_HOSTING_ENVIRONMENT`
+ / `NEXT_PUBLIC_BASE_PATH` at build time.
+
+## Current Test Count
+
+~271 unit/integration + 81 E2E (three browsers + basePath leg).
diff --git a/README.md b/README.md
index 30feff5..7ff6609 100644
--- a/README.md
+++ b/README.md
@@ -2,22 +2,19 @@
A purely client-side static web application that renders
[RefactorFirst](https://github.com/refactorfirst/refactorfirst) reports by fetching
-`.refactorfirst/refactor-first.json` data directly from repositories. No server-side
-code, no database, no build step — HTML, CSS and ES6 JavaScript modules served as
-static files.
+`.refactorfirst/refactor-first.json` data directly from repositories. No server-side code and no database — the site
+is a **Next.js static export** (`bun run build` produces `out/`, which any static host can serve).
- **Search** over a curated listing of repositories (`repositories.txt`)
- **Reports** rendered with Mustache.js from raw platform content, with `main` → `master`
branch fallback — the same report the
[RefactorFirst report viewer](https://github.com/RefactorFirst/RefactorFirst) produces:
- class/package maps (vizdom WASM SVGs with pan/zoom, plus Sigma 2D and 3D force-graph
- popups), relationship-removal priority tables, Chart.js disharmony bubble charts and
- class cycle summaries
-- **Repository submission** via a pre-filled issue on the hosting platform
- (no login, apps or tokens on this site): your platform account is captured as
- the issue author and validated server-side by the platform's CI
-- Reports and submissions work for repositories hosted on the same platform as
- the deployment (GitHub, GitLab or Bitbucket)
+ class/package maps (vizdom WASM SVGs with pan/zoom, plus Sigma 2D and 3D force-graph popups), relationship-removal
+ priority tables, Chart.js disharmony bubble charts and class cycle summaries
+- **Repository submission** via a pre-filled issue on the hosting platform (no login, apps or tokens on this site): your
+ platform account is captured as the issue author and validated server-side by the platform's CI
+- Reports and submissions work for repositories hosted on the same platform as the deployment (GitHub, GitLab or
+ Bitbucket)
- Works with a plain static file server: `python3 -m http.server 8000`
---
@@ -38,24 +35,36 @@ static files.
## Project Layout
```
-index.html # Single-page app shell (top menu + #app container)
+app/ # Next.js App Router (static export): layout
+ # (CSP, header/footer), static pages, 404 +
+ # pipeline error surfaces, and the dynamic
+ # routes /{user}, /{user}/{repo},
+ # /{user}/{repo}/{branch}
+components/ # Client components (search combobox, hero/menu
+ # search, menu toggle, workflow sample, user
+ # listing, report view, submission form,
+ # platform-config context, sentry provider)
+lib/ # Shared logic consumed by client components and
+ # RSC alike (routes, fetcher, renderer, search,
+ # repo-submission, error-handler, rate-limiter,
+ # cache-manager, utils, host, report-view,
+ # static-params, widget-loader)
+public/ # Published verbatim: repositories.txt, assets/
+ # (mustache template, logo), templates/
+ # (workflow samples), widgets/ (WASM + ESM
+ # bridges for vizdom, three-spritetext, Sentry)
repositories.txt # Listed repositories, one "user/repo" per line
-js/ # ES6 modules: router, fetcher, renderer, search,
- # repo-submission, error-handler,
- # rate-limiter, cache-manager, utils, main
ci/process-submissions.sh # Shared submission validator used by GitHub
# Actions, GitLab CI and Bitbucket Pipelines
-css/ # main.css + components.css
-templates/ # Static page templates (about, faq, errors, ...)
- # + user CI templates: user-refactorfirst-workflow.yml (GitHub),
- # user-refactorfirst-gitlab-ci.yml, user-refactorfirst-bitbucket-pipeline.yml
- # + workflow-sample-{github,gitlab,bitbucket}.html shown on the
- # Getting Started page based on the detected hosting environment
-.gitlab-ci.yml # Deploys this site to GitLab Pages
-bitbucket-pipelines.yml # Validates this site's files on Bitbucket
-assets/ # Fallback Mustache template, logo, Sentry config
-tests/ # unit/ (Bun), integration/ (Bun), e2e/ (Playwright)
-.github/workflows/ # add-repository.yml, redeploy.yml, test.yml
+templates/ # User CI samples: user-refactorfirst-workflow.yml,
+ # user-refactorfirst-gitlab-ci.yml,
+ # user-refactorfirst-bitbucket-pipeline.yml
+.gitlab-ci.yml # Deploys the static export (out/) to GitLab Pages
+bitbucket-pipelines.yml # Builds out/ on Bitbucket Pipelines
+tests/ # unit/ (Bun), integration/ (Bun + RTL), e2e/ (Playwright
+ # against the built out/ via scripts/serve-out.py)
+.github/workflows/ # test.yml, static.yml, redeploy.yml,
+ # add-repository.yml, deploy-repositories-fast.yml
```
---
@@ -65,13 +74,15 @@ tests/ # unit/ (Bun), integration/ (Bun), e2e/ (Playwrigh
### 1. Fork or create the repository
- **Personal account**: create a repository named `.github.io`.
-- **Organization**: create a repository named `.github.io` in the org,
- or any project repository if you want a project page
- (`https://.github.io//`).
+- **Organization**: create a repository named `.github.io` in the org, or any project repository if you want a
+ project page (`https://.github.io//`).
### 2. Push this project's files
-Everything in this directory is the site — push it to the default branch:
+This project is a Next.js app that builds a static export (`out/`). Push this directory to the default branch — the
+included
+`.github/workflows/static.yml` workflow builds the export (`bun run build`)
+and deploys `out/` to GitHub Pages:
```bash
git init
@@ -81,37 +92,37 @@ git remote add origin https://github.com//.git
git push -u origin main
```
+Set `NEXT_PUBLIC_BASE_PATH=/` for a project page (`https://.github.io//`).
+
### 3. Enable GitHub Pages
Go to **Settings → Pages**:
-- **Source**: *GitHub Actions* (required for the scheduled redeployment workflow in
- `.github/workflows/redeploy.yml`).
-- Alternatively choose *Deploy from a branch* (main, `/ (root)`) if you don't need
- scheduled redeploys — the site is fully static.
+- **Source**: *GitHub Actions* (required — `static.yml` and the scheduled redeployment in
+ `.github/workflows/redeploy.yml` both build the Next.js static export and upload `out/`).
The included `redeploy.yml` workflow redeploys every 10 minutes, but only when
-`repositories.txt` changed in the last 15 minutes. The `add-repository.yml` workflow
-reacts to newly opened submission issues, validates the submitter and commits new
-entries to `repositories.txt`.
+`repositories.txt` changed in the last 15 minutes. The `add-repository.yml` workflow reacts to newly opened submission
+issues, validates the submitter and commits new entries to `repositories.txt`.
### 4. Configure the submission target
-"Add Your Repo" submissions are pre-filled issues created in the listing
-repository. Point the site at your repository via the meta tag in `index.html`:
+"Add Your Repo" submissions are pre-filled issues created in the listing repository. Point the site at your repository
+via the meta tag in
+`app/layout.jsx`:
```html
+
```
-No GitHub Apps, OAuth apps, client IDs or secrets are needed — identity is
-captured by GitHub as the issue author. See
+No GitHub Apps, OAuth apps, client IDs or secrets are needed — identity is captured by GitHub as the issue author. See
[How repository submission works](#how-repository-submission-works).
### 5. (Optional) Custom domain
-Add a `CNAME` file containing your domain (e.g. `reports.example.com`), configure
-your DNS (CNAME record pointing to `.github.io`), and enable **Enforce HTTPS**
+Add a `CNAME` file containing your domain (e.g. `reports.example.com`), configure your DNS (CNAME record pointing to
+`.github.io`), and enable **Enforce HTTPS**
in Settings → Pages.
---
@@ -123,24 +134,22 @@ instance with Pages enabled.
### 1. Enable Pages on the appliance
-A site admin must enable GitHub Pages for the instance
-(**Management Console → Pages → Enable**), then create the repository
-(`.` or a project repo) and push this project as described above.
+A site admin must enable GitHub Pages for the instance (**Management Console → Pages → Enable**), then create the
+repository (`.` or a project repo) and push this project as described above.
### 2. Point the app at your enterprise endpoints
Raw content and API calls default to `github.com` / `raw.githubusercontent.com`
/ `api.github.com`. For a self-hosted instance, update the URL builders:
-- `js/fetcher.js` — the `github` entry of `PLATFORM_BUILDERS` should build
- URLs like
+- `lib/fetcher.js` — the `github` entry of `PLATFORM_BUILDERS` should build URLs like
`https://github.example.com/raw////.refactorfirst/refactor-first.json`.
-- `js/repo-submission.js` — `repositoryInfoUrl()` and `buildSubmissionIssueUrl()`
+- `lib/repo-submission.js` — `repositoryInfoUrl()` and `buildSubmissionIssueUrl()`
github branches must target your instance (`https://github.example.com/...`).
-- `ci/process-submissions.sh` — set `GH_API` (and raw URL handling) to your
- instance endpoints (`GH_HOST` is respected by `gh`-style tooling).
-- `index.html` — extend the CSP `connect-src` directive with your instance
- host and set `submission-target` to your listing repository.
+- `ci/process-submissions.sh` — set `GH_API` (and raw URL handling) to your instance endpoints (`GH_HOST` is respected
+ by `gh`-style tooling).
+- `app/layout.jsx` — extend the CSP `connect-src` directive with your instance host and set the `submission-target` meta
+ to your listing repository.
(Tip: keep these behind a single `config` module such as `enterprise-config.json`
if you need to support multiple deployments from one codebase.)
@@ -148,18 +157,16 @@ if you need to support multiple deployments from one codebase.)
### 3. Workflows
`add-repository.yml` and `redeploy.yml` use the built-in `GITHUB_TOKEN`;
-`ci/process-submissions.sh` needs only `curl` and `jq` (preinstalled on
-Actions runners). If your instance lacks internet access, ensure raw/API
-endpoints are reachable from the browser — reports and submission pre-checks
-are **client-side**, so *end users'* browsers (not the server) must be able to
-reach your GHES host.
+`ci/process-submissions.sh` needs only `curl` and `jq` (preinstalled on Actions runners). If your instance lacks
+internet access, ensure raw/API endpoints are reachable from the browser — reports and submission pre-checks are
+**client-side**, so *end users'* browsers (not the server) must be able to reach your GHES host.
---
## Deploying to Bitbucket
-A Bitbucket deployment lists Bitbucket-hosted repositories: report fetching and
-submission use `bitbucket.org/.../raw/...` and the Bitbucket REST API.
+A Bitbucket deployment lists Bitbucket-hosted repositories: report fetching and submission use
+`bitbucket.org/.../raw/...` and the Bitbucket REST API.
### 1. Create the site repository
@@ -176,23 +183,23 @@ git remote add origin git@bitbucket.org:/.bitbucket.io.git
git push -u origin main
```
-The site goes live at `https://.bitbucket.io`. Note that Bitbucket static
-sites serve all paths from one `index.html`-style tree — since this app routes
-client-side from a single `index.html`, request every path as `/index.html`-relative
-links, or accept that deep links (e.g. `/user/repo`) return 404 unless Bitbucket
-serves `index.html` for unknown paths (it does not by default — consider using the
-query-style links or hosting deep routes via a redirect service).
+Run `NEXT_PUBLIC_HOSTING_ENVIRONMENT=bitbucket bun run build` locally or let the included `bitbucket-pipelines.yml` run
+it in CI, then publish the generated
+`out/` directory to `https://.bitbucket.io` (Bitbucket serves the uploaded static tree; deep links to
+`/{user}/{repo}` paths rely on the exported `_404`/`404.html` fallback semantics — where unavailable, share the
+two-segment URLs from search results which are pre-generated).
### 3. Enable submission processing
The site is detected as `bitbucket` from the `.bitbucket.io`
-hostname; set `submission-target` in `index.html` to
-`/.bitbucket.io`, enable the issue tracker on that
-repository and extend the CSP `connect-src` with `https://api.bitbucket.org`
+hostname (or via `NEXT_PUBLIC_HOSTING_ENVIRONMENT=bitbucket` at build time); set `submission-target` in `app/layout.jsx`
+to
+`/.bitbucket.io`, enable the issue tracker on that repository and extend the CSP `connect-src`
+with `https://api.bitbucket.org`
and `https://bitbucket.org`.
-Bitbucket has no issue-triggered pipelines, so submissions are processed by the
-custom `process-submissions` pipeline in `bitbucket-pipelines.yml`:
+Bitbucket has no issue-triggered pipelines, so submissions are processed by the custom `process-submissions` pipeline in
+`bitbucket-pipelines.yml`:
1. In the repository go to **Pipelines → Schedules** and schedule
`custom: process-submissions` (e.g. every 10 minutes).
@@ -201,10 +208,9 @@ custom `process-submissions` pipeline in `bitbucket-pipelines.yml`:
repository variables `BITBUCKET_CLIENT_ID` / `BITBUCKET_CLIENT_SECRET`
(server-side CI secrets only — the site itself never sees them).
-The pipeline polls open issues titled `Add repository: owner/repo`, checks the
-author has `write`/`admin` permission on the repository, verifies the report
-file exists, commits `repositories.txt` and closes the issue with the outcome.
-The manual `sort-repos` pipeline from the shipped `bitbucket-pipelines.yml`
+The pipeline polls open issues titled `Add repository: owner/repo`, checks the author has `write`/`admin` permission on
+the repository, verifies the report file exists, commits `repositories.txt` and closes the issue with the outcome. The
+manual `sort-repos` pipeline from the shipped `bitbucket-pipelines.yml`
also normalizes the listing on demand.
> **Users generating reports on Bitbucket**: point them at
@@ -219,25 +225,22 @@ also normalizes the listing on demand.
### 1. Create the project
- **Personal account**: create a project named `.gitlab.io`.
-- **Group**: create a project named `.gitlab.io`, or any project for a
- project page at `https://.gitlab.io//`.
+- **Group**: create a project named `.gitlab.io`, or any project for a project page at
+ `https://.gitlab.io//`.
### 2. Add a Pages pipeline
-This repository already ships a ready-to-use `.gitlab-ci.yml` (validates the site and
-deploys `public/` via a `pages` job, including a `404.html` copy of `index.html` for
-client-side routing). It looks like this:
+This repository ships a ready-to-use `.gitlab-ci.yml`: the `pages` job runs
+`bun run build` (the Next.js static export) with
+`NEXT_PUBLIC_HOSTING_ENVIRONMENT=gitlab` and publishes `out/` as the Pages `public/` directory:
```yaml
pages:
- stage: deploy
+ stage: build
script:
- - mkdir -p public
- # Publish everything except VCS metadata, tests and tooling
- - |
- for f in index.html repositories.txt css js templates assets; do
- cp -r "$f" public/
- done
+ - bun install
+ - NEXT_PUBLIC_HOSTING_ENVIRONMENT=gitlab bun run build
+ - mkdir -p public && cp -r out/. public/
artifacts:
paths:
- public
@@ -260,25 +263,22 @@ GitLab Pages deploys from the `pages` job and serves
### 4. GitLab-specific considerations
-- **Client-side routing**: GitLab Pages serves `404.html` for unknown paths; keep a
- copy of `index.html` as `public/404.html` in the pipeline (`cp index.html public/404.html`)
- so deep links like `/user/repo` load the app.
-- **Submission processing**: set `submission-target` in `index.html` to your
- `/`, extend the CSP `connect-src` with your GitLab base
- (`https://gitlab.com` or your self-managed host), and create a pipeline
- schedule (**CI/CD → Schedules**, e.g. every 10 minutes) — GitLab has no
- issue-triggered pipelines, so the `process-submissions` job in
+- **Client-side routing**: GitLab Pages serves `404.html` for unknown paths; the Next.js export already produces
+ `out/404.html` (from `app/not-found.jsx`), which handles branch deep-link recovery — no extra copy step needed.
+- **Submission processing**: set `submission-target` in `app/layout.jsx` to your
+ `/`, extend the CSP `connect-src` with your GitLab base (`https://gitlab.com` or your self-managed
+ host), and create a pipeline schedule (**CI/CD → Schedules**, e.g. every 10 minutes) — GitLab has no issue-triggered
+ pipelines, so the `process-submissions` job in
`.gitlab-ci.yml` polls open submission issues. For **self-managed GitLab**
- also add ``.
- The job uses `CI_JOB_TOKEN` by default; if your GitLab version/instance
- restricts its API scope, set a masked `GITLAB_TOKEN` CI variable with a
+ also add ``. The job uses `CI_JOB_TOKEN` by
+ default; if your GitLab version/instance restricts its API scope, set a masked `GITLAB_TOKEN` CI variable with a
project access token (`api` scope) instead.
-- **Listing redeploys**: schedule another pipeline (or extend the same one) to
- re-run `pages` when `repositories.txt` changed.
-- **Custom domains**: set up under **Settings → Pages** with automatic Let's Encrypt
- certificates. Note: the hostname-based environment detection only recognises
- `*.gitlab.io`; on a custom domain pass `hostEnvironment: 'gitlab'` to
- `createApp()` in `js/main.js`.
+- **Listing redeploys**: schedule another pipeline (or extend the same one) to re-run `pages` when `repositories.txt`
+ changed.
+- **Custom domains**: set up under **Settings → Pages** with automatic Let's Encrypt certificates. Note: the
+ hostname-based environment detection only recognises
+ `*.gitlab.io`; on a custom domain set `NEXT_PUBLIC_HOSTING_ENVIRONMENT=gitlab`
+ at build time so the app identifies as GitLab.
> **Users generating reports on GitLab**: point them at
> `templates/user-refactorfirst-gitlab-ci.yml` — a copy-paste pipeline that runs
@@ -289,39 +289,36 @@ GitLab Pages deploys from the `pages` job and serves
## How repository submission works
-No OAuth app, client ID, token or secret is involved on the client side — forks
-need **zero auth setup**. The flow on every supported platform:
+No OAuth app, client ID, token or secret is involved on the client side — forks need **zero auth setup**. The flow on
+every supported platform:
-1. The user fills in *owner* and *repository* on `/add-repo` (no login on this
- site — identity is captured later, by the platform itself).
-2. The app verifies client-side (unauthenticated) that the repository exists
- and publishes `.refactorfirst/refactor-first.json` on its `main`, default or
+1. The user fills in *owner* and *repository* on `/add-repo` (no login on this site — identity is captured later, by the
+ platform itself).
+2. The app verifies client-side (unauthenticated) that the repository exists and publishes
+ `.refactorfirst/refactor-first.json` on its `main`, default or
`master` branch, then opens a **pre-filled issue**
(`Add repository: owner/repo`) in the listing project in a new tab.
-3. The user — now on GitHub/GitLab/Bitbucket, logged in there — creates the
- issue. The platform-verified **issue author** is the captured submitter
- identity; it cannot be spoofed.
+3. The user — now on GitHub/GitLab/Bitbucket, logged in there — creates the issue. The platform-verified **issue
+ author** is the captured submitter identity; it cannot be spoofed.
4. The platform's CI (GitHub Actions `add-repository.yml`, GitLab scheduled
`process-submissions` pipeline, Bitbucket scheduled `process-submissions`
pipeline; all driving `ci/process-submissions.sh`) validates:
- - the issue title matches the exact submission format,
- - the issue author has write access to the submitted repository
- (GitHub collaborator permission, GitLab Developer+ membership, Bitbucket
- `write`/`admin` permission),
- - the report file exists and the repository is not already listed.
-5. Valid submissions are committed to `repositories.txt` and the issue receives
- a comment with the outcome and is closed; rejected submissions are commented
- with the reason and closed.
-
-| | GitHub | GitLab | Bitbucket |
-|---|---|---|---|
-| Trigger | instant (`issues: opened` event) | scheduled pipeline (10 min) | scheduled pipeline (10 min) |
-| CI credentials | built-in `GITHUB_TOKEN` | `CI_JOB_TOKEN` (or `GITLAB_TOKEN` project token) | workspace OAuth consumer (secured variables) |
-| Access check | collaborator `permission` | member `access_level >= 30` (Developer) | permissions `write`/`admin` |
-
-Limitations: only **public** repositories can be submitted (the report checks
-are unauthenticated), and each deployment serves exactly one platform — the
-one it is hosted on.
+ - the issue title matches the exact submission format,
+ - the issue author has write access to the submitted repository (GitHub collaborator permission, GitLab Developer+
+ membership, Bitbucket
+ `write`/`admin` permission),
+ - the report file exists and the repository is not already listed.
+5. Valid submissions are committed to `repositories.txt` and the issue receives a comment with the outcome and is
+ closed; rejected submissions are commented with the reason and closed.
+
+| | GitHub | GitLab | Bitbucket |
+|----------------|----------------------------------|--------------------------------------------------|----------------------------------------------|
+| Trigger | instant (`issues: opened` event) | scheduled pipeline (10 min) | scheduled pipeline (10 min) |
+| CI credentials | built-in `GITHUB_TOKEN` | `CI_JOB_TOKEN` (or `GITLAB_TOKEN` project token) | workspace OAuth consumer (secured variables) |
+| Access check | collaborator `permission` | member `access_level >= 30` (Developer) | permissions `write`/`admin` |
+
+Limitations: only **public** repositories can be submitted (the report checks are unauthenticated), and each deployment
+serves exactly one platform — the one it is hosted on.
---
@@ -329,9 +326,9 @@ one it is hosted on.
### Prerequisites
-- [Bun](https://bun.sh) ≥ 1.0 (unit/integration tests) — or run it via `npx bun`
-- Node.js ≥ 18 (Playwright E2E tests only)
-- Python 3 (local static server)
+- [Bun](https://bun.sh) ≥ 1.0 (install + unit/integration tests)
+- Node.js 22 (Next.js build + Playwright E2E)
+- Python 3 (serves the static export locally, scripts/serve-out.py)
### Setup
@@ -342,9 +339,10 @@ bun install # install devDependencies (mustache, jsdom, playwright, e
### Run locally
```bash
-python3 -m http.server 8000 # then open http://localhost:8000
+bun run dev # Next.js dev server at http://localhost:3000
+# or the production-shaped static export:
+bun run build && python3 scripts/serve-out.py # then open http://localhost:8003
```
-When making changes to the site locally, you may need to clear the cache to see your changes. Alternatively, you can disable caching in your browser.
### Test-driven development (mandatory)
@@ -353,7 +351,7 @@ This project follows strict TDD — write the failing test **before** production
1. Write a failing test in `tests/unit/` (pure module logic) or
`tests/integration/` (DOM + routing flows).
2. Run `bun test tests/unit tests/integration` and watch it fail.
-3. Write the minimal implementation in `js/` to make it pass.
+3. Write the minimal implementation in `lib/`, `components/` or `app/` to make it pass.
4. Refactor while keeping tests green.
```bash
@@ -362,65 +360,64 @@ bun test --watch tests/unit # watch mode
bun test --coverage tests/unit tests/integration
```
-E2E tests run under Node.js/Playwright with a live local server:
+E2E tests run under Node.js/Playwright against the built static export (the Playwright webServer runs `bun run build`
+then serves `out/`):
```bash
npx playwright install # one-time: download browsers
npx playwright test # full E2E suite (chromium, firefox, webkit)
npx playwright test --ui # interactive mode
+bun run test:e2e:basepath # NEXT_PUBLIC_BASE_PATH=/preview leg (chromium)
```
### Lint
```bash
-npx eslint js/**/*.js tests/unit/**/*.js tests/integration/**/*.js
+npx eslint "lib/**/*.js" "app/**/*.{js,jsx}" "components/**/*" "tests/**/*"
```
### Environment-aware documentation
-The **Getting Started** page shows only the CI sample matching the hosting
-environment, detected from the hostname (`*.github.io` → GitHub Actions,
-`*.gitlab.io` → GitLab CI, `*.bitbucket.io` → Bitbucket Pipelines; anything else
-defaults to GitHub). The samples live in
-`templates/workflow-sample-{github,gitlab,bitbucket}.html`, and detection lives in
-`detectHostingEnvironment()` in `js/utils.js`. To override detection (e.g. a custom
-domain hosting the GitLab variant), pass `hostEnvironment: 'gitlab'` to
-`createApp()` in `js/main.js`.
+The **Getting Started** page shows only the CI sample matching the hosting environment, detected from the hostname
+(`*.github.io` → GitHub Actions,
+`*.gitlab.io` → GitLab CI, `*.bitbucket.io` → Bitbucket Pipelines; anything else defaults to GitHub). The samples live
+in
+`public/templates/workflow-sample-{github,gitlab,bitbucket}.html`, and detection lives in
+`lib/host.js` (or `NEXT_PUBLIC_HOSTING_ENVIRONMENT` at build time, plus the
+`platform` meta tag).
### Where things live
-| Change | Files |
-|---|---|
-| URL routes | `js/router.js` (+ `tests/unit/router-ext.test.js`) |
-| Raw fetching / branch fallback (platform-aware) | `js/fetcher.js` |
-| Mustache rendering | `js/renderer.js`, `assets/refactor-first-report.mustache` (port of the RefactorFirst viewer template) |
-| Interactive report widgets | `js/report-view.js` (+ CDN libs declared in `index.html`: Chart.js, sigma/graphology, graphlib-dot, svg-pan-zoom, 3d-force-graph, vizdom WASM) |
-| Search / type-ahead | `js/search.js` |
-| Submission flow | `js/repo-submission.js`, `js/main.js` (`renderAddRepo`) |
-| Submission validation (CI) | `ci/process-submissions.sh`, `.github/workflows/add-repository.yml`, `.gitlab-ci.yml`, `bitbucket-pipelines.yml` |
-| Error pages | `js/error-handler.js`, `templates/error-*.html` |
-| Page content | `templates/*.html` |
-| Styling | `css/main.css`, `css/components.css` |
-| Listing data | `repositories.txt` (one `user/repo` per line) |
-| Scheduled redeploy | `.github/workflows/redeploy.yml` |
+| Change | Files |
+|-------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| URL routes | `lib/routes.js` (+ `tests/unit/routes.test.js`) |
+| Raw fetching / branch fallback (platform-aware) | `lib/fetcher.js` |
+| Mustache rendering | `lib/renderer.js`, `public/assets/refactor-first-report.mustache` (port of the RefactorFirst viewer template) |
+| Interactive report widgets | `lib/report-view.js` + CDN widgets loaded via `next/script` in `components/report-view.jsx` (Chart.js, sigma/graphology, graphlib-dot, svg-pan-zoom, 3d-force-graph) and the ES-module bridges in `public/widgets/` (vizdom WASM, three-spritetext) |
+| Search / type-ahead | `lib/search.js` + `components/{search-combobox,hero-search,menu-search}.jsx` |
+| Submission flow | `lib/repo-submission.js`, `components/repo-submission-form.jsx` |
+| Submission validation (CI) | `ci/process-submissions.sh`, `.github/workflows/add-repository.yml`, `.gitlab-ci.yml`, `bitbucket-pipelines.yml` |
+| Error pages | `lib/error-handler.js`, `components/error-boundary-view.jsx`, `app/error.jsx`, `app/not-found.jsx` |
+| Page content | `app/*/page.jsx` |
+| Styling | `css/main.css`, `css/components.css` |
+| Listing data | `repositories.txt` (one `user/repo` per line) |
+| Scheduled redeploy | `.github/workflows/redeploy.yml` |
### CI/CD
-`.github/workflows/test.yml` runs Bun unit/integration tests and the Playwright suite
-on every push and pull request. Keep it green before merging.
+`.github/workflows/test.yml` runs Bun unit/integration tests and the Playwright suite on every push and pull request.
+Keep it green before merging.
---
## Testing
-- **Unit** (`tests/unit/`): router, fetcher (incl. branch fallback, retry and
- per-platform URL construction), renderer, report-view (charts/graphs/popups),
- search, repo-submission (incl. report-file existence check and per-platform
- issue URLs), error-handler, rate-limiter, cache-manager, utils.
-- **Integration** (`tests/integration/`): search flow, submission flow (missing
- report, unknown repo, per-platform issue redirect), report rendering.
-- **E2E** (`tests/e2e/`): user journeys (incl. the submission → pre-filled
- issue hand-off), cross-browser smoke tests, mobile responsiveness
- (hamburger menu, single-column grid).
+- **Unit** (`tests/unit/`): router, fetcher (incl. branch fallback, retry and per-platform URL construction), renderer,
+ report-view (charts/graphs/popups), search, repo-submission (incl. report-file existence check and per-platform issue
+ URLs), error-handler, rate-limiter, cache-manager, utils.
+- **Integration** (`tests/integration/`): search flow, submission flow (missing report, unknown repo, per-platform issue
+ redirect), report rendering.
+- **E2E** (`tests/e2e/`): user journeys (incl. the submission → pre-filled issue hand-off), cross-browser smoke tests,
+ mobile responsiveness (hamburger menu, single-column grid).
Coverage target: 80%+ on core modules. Current suite: 165 tests.
diff --git a/app/[username]/[repository]/[branch]/page.jsx b/app/[username]/[repository]/[branch]/page.jsx
new file mode 100644
index 0000000..5253a70
--- /dev/null
+++ b/app/[username]/[repository]/[branch]/page.jsx
@@ -0,0 +1,34 @@
+// /{username}/{repository}/{branch} — report shell pinned to a branch.
+// Pre-generated for main/master; other branches reach this route via the
+// not-found client redirect which conveys the branch through the ?branch=
+// query parameter handled by ReportView.
+
+import { Suspense } from 'react';
+import { notFound } from 'next/navigation';
+import { loadListedRepositories } from '../../../../lib/repositories';
+import { branchStaticParams } from '../../../../lib/static-params';
+import { isValidGitHubName } from '../../../../lib/routes';
+import ReportView from '../../../../components/report-view';
+
+export const dynamicParams = false;
+
+export function generateStaticParams() {
+ return branchStaticParams(loadListedRepositories());
+}
+
+export async function generateMetadata({ params }) {
+ const { username, repository, branch } = await params;
+ return { title: `${username}/${repository}@${branch} - RefactorFirst` };
+}
+
+export default async function BranchPage({ params }) {
+ const { username, repository, branch } = await params;
+ if (!isValidGitHubName(username) || !isValidGitHubName(repository)) {
+ notFound();
+ }
+ return (
+ Loading report…
}>
+
+
+ );
+}
diff --git a/app/[username]/[repository]/error.jsx b/app/[username]/[repository]/error.jsx
new file mode 100644
index 0000000..3aaf41b
--- /dev/null
+++ b/app/[username]/[repository]/error.jsx
@@ -0,0 +1,7 @@
+'use client';
+
+import ErrorBoundaryView from '../../../components/error-boundary-view';
+
+export default function RepositoryError({ error, reset }) {
+ return ;
+}
diff --git a/app/[username]/[repository]/page.jsx b/app/[username]/[repository]/page.jsx
new file mode 100644
index 0000000..447ba73
--- /dev/null
+++ b/app/[username]/[repository]/page.jsx
@@ -0,0 +1,32 @@
+// /{username}/{repository} — report shell. Pre-generated per listed
+// repository; ReportView fetches the report client-side.
+
+import { Suspense } from 'react';
+import { notFound } from 'next/navigation';
+import { loadListedRepositories } from '../../../lib/repositories';
+import { reportStaticParams } from '../../../lib/static-params';
+import { isValidGitHubName } from '../../../lib/routes';
+import ReportView from '../../../components/report-view';
+
+export const dynamicParams = false;
+
+export function generateStaticParams() {
+ return reportStaticParams(loadListedRepositories());
+}
+
+export async function generateMetadata({ params }) {
+ const { username, repository } = await params;
+ return { title: `${username}/${repository} - RefactorFirst` };
+}
+
+export default async function RepositoryPage({ params }) {
+ const { username, repository } = await params;
+ if (!isValidGitHubName(username) || !isValidGitHubName(repository)) {
+ notFound();
+ }
+ return (
+ Loading report…}>
+
+
+ );
+}
diff --git a/app/[username]/error.jsx b/app/[username]/error.jsx
new file mode 100644
index 0000000..c3fb1e9
--- /dev/null
+++ b/app/[username]/error.jsx
@@ -0,0 +1,7 @@
+'use client';
+
+import ErrorBoundaryView from '../../components/error-boundary-view';
+
+export default function UsernameError({ error, reset }) {
+ return ;
+}
diff --git a/app/[username]/page.jsx b/app/[username]/page.jsx
new file mode 100644
index 0000000..3d6615e
--- /dev/null
+++ b/app/[username]/page.jsx
@@ -0,0 +1,35 @@
+// /{username} — repository listing page for a GitHub user/org. Statically
+// generated per username; pagination and the post-deploy listing refresh
+// happen client-side in RepoList.
+
+import { Suspense } from 'react';
+import { notFound } from 'next/navigation';
+import { loadListedRepositories } from '../../lib/repositories';
+import { userStaticParams } from '../../lib/static-params';
+import { reposForUser } from '../../lib/utils';
+import { isValidGitHubName } from '../../lib/routes';
+import RepoList from '../../components/repo-list';
+
+export const dynamicParams = false;
+
+export function generateStaticParams() {
+ return userStaticParams(loadListedRepositories());
+}
+
+export async function generateMetadata({ params }) {
+ const { username } = await params;
+ return { title: `${username} - RefactorFirst` };
+}
+
+export default async function UserPage({ params }) {
+ const { username } = await params;
+ if (!isValidGitHubName(username)) {
+ notFound();
+ }
+ const repositories = reposForUser(loadListedRepositories(), username);
+ return (
+ Loading…}>
+
+
+ );
+}
diff --git a/app/about/page.jsx b/app/about/page.jsx
new file mode 100644
index 0000000..fd54ede
--- /dev/null
+++ b/app/about/page.jsx
@@ -0,0 +1,20 @@
+export default function AboutPage() {
+ return (
+
+
About RefactorFirst
+
RefactorFirst is a static analysis tool that identifies which classes in your Java
+ codebase you should refactor first, ranked by cost-benefit. It is based on the paper
+ Prioritized Technical Debt Identification and uses the XRank metric to surface
+ the classes with the highest impact on maintainability.
+
How it works
+
Run the RefactorFirst Maven plugin on your project. It generates
+ .refactorfirst/refactor-first.json — a ranked list of classes to
+ refactor with effort estimates and recommendations. This site renders those reports
+ directly from your GitHub repository.
+
Open Source
+
RefactorFirst is open source. Visit the
+ GitHub repository
+ to contribute, report issues or learn more.
+
+ );
+}
diff --git a/app/add-repo/page.jsx b/app/add-repo/page.jsx
new file mode 100644
index 0000000..b15e582
--- /dev/null
+++ b/app/add-repo/page.jsx
@@ -0,0 +1,11 @@
+// /add-repo — repository submission form. Config comes from the same
+// tags (submission-target, platform-base-url) so self-managed GitLab
+// deployments keep working by editing the layout.
+
+import RepoSubmissionForm from '../../components/repo-submission-form';
+
+export const metadata = { title: 'Add Your Repository - RefactorFirst' };
+
+export default function AddRepoPage() {
+ return ;
+}
diff --git a/app/api/page.jsx b/app/api/page.jsx
new file mode 100644
index 0000000..6e56e83
--- /dev/null
+++ b/app/api/page.jsx
@@ -0,0 +1,36 @@
+export default function ApiPage() {
+ return (
+
+
API for Tool Integrations
+
+
Fetching reports programmatically
+
Reports are plain JSON stored in each repository. Fetch them directly from
+ GitHub raw content:
+
GET https://raw.githubusercontent.com/<user>/<repo>/refs/heads/<branch>/.refactorfirst/refactor-first.json
+
+
URL patterns
+
+
/<user>/<repo> — rendered report (default branch)
+
/<user>/<repo>/<branch> — rendered report for a branch
+
/<user> — all listed repositories of a user
+
+
+
JSON schema
+
The report contains projectName, version,
+ totalClasses, classesToRefactor and a
+ priorities array with per-class rank,
+ className, priority, effort,
+ disharmonies and recommendation fields.
+
+
Webhooks & third-party tools
+
Trigger report regeneration from any CI system by calling
+ mvn refactorfirst:jsonReport and committing the
+ .refactorfirst directory.
+
+
Rate limiting & caching
+
GitHub raw and API endpoints are rate-limited (5000 requests/hour authenticated,
+ 60/hour unauthenticated). Cache responses client-side and respect the
+ X-RateLimit-* headers.
+
+ );
+}
diff --git a/app/documentation/page.jsx b/app/documentation/page.jsx
new file mode 100644
index 0000000..e0321e9
--- /dev/null
+++ b/app/documentation/page.jsx
@@ -0,0 +1,33 @@
+import Link from 'next/link';
+
+export default function DocumentationPage() {
+ return (
+
+
Documentation
+
+
Generating Reports
+
Run the Maven plugin:
+
mvn refactorfirst:jsonReport
+
This writes .refactorfirst/refactor-first.json. A graphical HTML report
+ is also generated into the targets/site directory by
+ mvn refactorfirst:report.
+
+
Understanding the Report
+
+
Priority — XRank-based ordering: refactor these classes first.
+
Effort — estimated relative effort to refactor the class.
Recommendation — suggested next step for each class.
+
+
+
CI/CD Integration
+
Add the RefactorFirst workflow to your repository so the report regenerates on every
+ push to the default branch. See Getting Started
+ for a copy-paste workflow.
+
+ );
+}
diff --git a/app/error.jsx b/app/error.jsx
new file mode 100644
index 0000000..42c6e51
--- /dev/null
+++ b/app/error.jsx
@@ -0,0 +1,7 @@
+'use client';
+
+import ErrorBoundaryView from '../components/error-boundary-view';
+
+export default function GlobalError({ error, reset }) {
+ return ;
+}
diff --git a/app/examples/page.jsx b/app/examples/page.jsx
new file mode 100644
index 0000000..638e48f
--- /dev/null
+++ b/app/examples/page.jsx
@@ -0,0 +1,32 @@
+import Link from 'next/link';
+
+export default function ExamplesPage() {
+ return (
+
+
Example Reports
+
See RefactorFirst reports for real projects. Each link opens the live report
+ rendered from that repository.
+
+
Featured projects
+
+
+ refactorfirst/refactorfirst
+
The RefactorFirst project itself — a medium-sized Maven codebase.
+
+
+
+
What to look for
+
+
Small codebases: short priority lists, quick wins.
+
Medium codebases: a handful of high-priority God Classes.
+
Large codebases: long tails — focus on the top of the ranking.
+
+
+
Before / after refactoring
+
Compare a report on a release branch (/<user>/<repo>/<branch>)
+ before and after addressing the top-ranked class to see the impact of your work.
+
+
Add your project via Add Your Repo to appear in this gallery.
+
+ );
+}
diff --git a/app/faq/page.jsx b/app/faq/page.jsx
new file mode 100644
index 0000000..fba2ac7
--- /dev/null
+++ b/app/faq/page.jsx
@@ -0,0 +1,54 @@
+import Link from 'next/link';
+
+export default function FaqPage() {
+ return (
+
+
Frequently Asked Questions
+
+
What do the different priority colors mean?
+
Priority is ranked by XRank: higher priority classes give the most benefit per unit
+ of refactoring effort. Colors range from red (highest priority) to green.
+
+
How often should I run RefactorFirst?
+
On every merge to your default branch. The provided GitHub Actions workflow does
+ this automatically.
+
+
What if my repository doesn't have a report?
+
Generate one with mvn refactorfirst:jsonReport and commit the
+ .refactorfirst/refactor-first.json file. See
+ Getting Started.
+
+
How do I add my repository to the listing?
+
Use the Add Your Repo page. It opens a
+ pre-filled issue on this site's platform (GitHub, GitLab or Bitbucket);
+ submitting that issue is all you need to do. You must have write access
+ to the repository.
+
+
How long until my repository appears after submission?
+
The CI validation picks issues up shortly after they are created (the
+ GitHub deployment reacts immediately; GitLab and Bitbucket deployments
+ poll on a schedule), and the site redeploys on a 10-minute schedule —
+ so in the worst case about 20 minutes.
+
+
Why is there no sign-in on this site?
+
Your identity is captured by your platform when you create the submission
+ issue: the issue author is a verified account, and the CI job
+ independently confirms that account has write access to the submitted
+ repository. That proves ownership without any tokens or apps.
+
+
Is my platform data safe?
+
Yes. This site never receives credentials, tokens or permissions from you.
+ The validation runs entirely in this project's own CI.
+
+
What are disharmonies like God Class and Brain Method?
+
Disharmonies are design flaws detected from metrics: a God Class does too
+ much, a Brain Method is an overly complex method, Feature Envy
+ means a method relies on another class's data more than its own.
+
+
Can I compare branches?
+
You can view the report on any branch via the URL
+ /<user>/<repo>/<branch>, but side-by-side comparison is
+ not currently supported.
Once a report exists on your default branch, go to
+ Add Your Repo and enter your repository name.
+ A pre-filled issue opens on this site's platform (GitHub, GitLab or
+ Bitbucket) — submit it there and a CI job validates and adds your
+ repository automatically.
+
+
How is my identity verified?
+
This site has no login and stores no credentials: your identity is captured
+ by your platform (GitHub, GitLab or Bitbucket) as the author of the issue
+ you create. The CI job then verifies — independently — that you actually
+ have write access to the repository you submitted. This prevents abuse and
+ keeps the listing trustworthy.
+
Any permissions or apps needed?
+
None. You never grant this site access to anything, and no OAuth apps or
+ tokens are involved. You just need an account on the platform hosting the
+ site you are using.
+
+
Troubleshooting
+
+
Report doesn't render? Ensure .refactorfirst/refactor-first.json exists on your default branch.
+
Changed default branch? Reports are looked up on the branch in the URL. If no branch is specified, the request falls back to main, then master.
+
Submission fails? You need write access to the repository, and it must not already be listed.
+
Submission is slow to appear? The site redeploys on a 10-minute schedule.
+
+
+ );
+}
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000..3d821fe
--- /dev/null
+++ b/app/globals.css
@@ -0,0 +1,410 @@
+/* Main stylesheet: CSS custom properties, base layout, footer. */
+
+:root {
+ --brand-color: #2a6f97;
+ --brand-accent: #61c0bf;
+ --text-color: #1f2d3d;
+ --muted-color: #5c6b7a;
+ --bg-color: #ffffff;
+ --surface-color: #f5f8fa;
+ --error-color: #c0392b;
+ --success-color: #1e8449;
+ --menu-max-height: 140px;
+ --menu-height: 48px;
+ --radius: 6px;
+ --shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
+ /* mvp.css caps header/footer/main at --width-content (1080px by default).
+ Report pages and listings should fill the view: content uses 95% of the
+ viewport width instead. */
+ --width-content: min(3840px, 95vw);
+}
+
+body {
+ margin: 0;
+ color: var(--text-color);
+ background: var(--bg-color);
+}
+
+.skip-link {
+ position: absolute;
+ left: -9999px;
+ top: 0;
+ background: var(--brand-color);
+ color: #fff;
+ padding: 0.5rem 1rem;
+ z-index: 100;
+}
+
+.skip-link:focus {
+ left: 0;
+}
+
+/* mvp.css targets `footer, header, main` with the same specificity and is
+ loaded after the bundle CSS, so the id-qualified selector is the stable
+ way to win the cascade. Report pages should fill 95% of the view. */
+main#app {
+ max-width: min(3840px, 95vw);
+ margin: 0 auto;
+ padding: 1rem 1.5rem 3rem;
+ min-height: 60vh;
+}
+
+main:focus {
+ outline: none;
+}
+
+.hero {
+ text-align: center;
+ padding: 2.5rem 1rem;
+}
+
+.hero h1 {
+ color: var(--brand-color);
+ margin-bottom: 0.25rem;
+}
+
+.loading {
+ text-align: center;
+ color: var(--muted-color);
+ padding: 3rem 0;
+}
+
+.site-footer {
+ text-align: center;
+ padding: 1.5rem 1rem;
+ color: var(--muted-color);
+ font-size: 0.85rem;
+ border-top: 1px solid var(--surface-color);
+}
+
+a {
+ color: var(--brand-color);
+}
+
+code {
+ background: var(--surface-color);
+ padding: 0.1em 0.35em;
+ border-radius: 4px;
+}
+
+pre {
+ background: var(--surface-color);
+ padding: 1rem;
+ border-radius: var(--radius);
+ overflow-x: auto;
+}
+
+:focus-visible {
+ outline: 2px solid var(--brand-accent);
+ outline-offset: 2px;
+}
+/* Component styles: top menu, search, repo grid, pagination, forms, errors. */
+
+/* ---- Top navigation (max 140px, mobile hamburger) ---- */
+#top-menu {
+ background: var(--surface-color);
+ border-bottom: 1px solid #dde4ea;
+ /* mvp.css adds generous header padding; reset it so max-height bounds the
+ whole rendered box (padding contributes outside max-height). */
+ padding: 0;
+ max-height: var(--menu-max-height);
+}
+
+.menu-bar {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ flex-wrap: nowrap;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0.3rem 0.8rem;
+ min-height: 48px;
+ overflow: visible;
+}
+
+.brand {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ font-weight: bold;
+ color: var(--brand-color);
+ text-decoration: none;
+ white-space: nowrap;
+ font-size: 0.9rem;
+}
+
+.menu-search {
+ position: relative;
+ /* Grow into the space freed by the tighter link spacing. */
+ flex: 1 0 240px;
+ /* Space between the last menu link and the search bar matches the
+ left-to-right spacing measured between menu links: the .menu-bar flex
+ gap plus this small margin reproduce it (verified at 1280px via E2E). */
+ margin-left: 0.1rem;
+}
+
+.menu-search input {
+ width: 100%;
+ padding: 0.35rem 0.6rem;
+ border: 1px solid #c6d2dc;
+ border-radius: var(--radius);
+}
+
+.menu-toggle {
+ display: none;
+ background: none;
+ border: 1px solid #c6d2dc;
+ border-radius: var(--radius);
+ font-size: 1.1rem;
+ padding: 0.25rem 0.6rem;
+ cursor: pointer;
+}
+
+.menu-links {
+ display: flex;
+ align-items: center;
+ gap: 0.1rem;
+ flex-wrap: nowrap;
+ flex-shrink: 0; /* never squeeze the links — the bar scrolls/wraps instead */
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+}
+
+/* mvp.css gives every li a horizontal margin — zero it so the menu items
+ sit at the same tight rhythm as the search bar to their right. */
+.menu-links li {
+ margin-left: 0;
+ margin-right: 0;
+}
+
+.menu-links a {
+ display: inline-block;
+ padding: 0.15rem 0.2rem;
+ border-radius: var(--radius);
+ text-decoration: none;
+ color: var(--text-color);
+ font-size: 0.8rem;
+}
+
+.menu-links a:hover,
+.menu-links a:focus-visible {
+ background: var(--brand-color);
+ color: #fff;
+}
+
+/* ---- Search results ---- */
+.search-results {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 100%;
+ z-index: 50;
+ margin: 0.15rem 0 0;
+ padding: 0;
+ list-style: none;
+ background: #fff;
+ border: 1px solid #c6d2dc;
+ border-radius: var(--radius);
+ box-shadow: var(--shadow);
+ max-height: 16rem;
+ overflow-y: auto;
+}
+
+.search-results li {
+ padding: 0.5rem 0.75rem;
+ cursor: pointer;
+}
+
+.search-results li.active,
+.search-results li:hover {
+ background: var(--brand-color);
+ color: #fff;
+}
+
+.hero-search {
+ position: relative;
+ max-width: 480px;
+ margin: 1rem auto;
+ text-align: left;
+}
+
+/* ---- Repository grid ---- */
+.repo-grid,
+.featured-repos ul {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.75rem;
+ list-style: none;
+ padding: 0;
+ max-width: 720px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.repo-card {
+ display: block;
+ padding: 0.85rem 1rem;
+ border: 1px solid #dde4ea;
+ border-radius: var(--radius);
+ text-decoration: none;
+ color: var(--text-color);
+ background: #fff;
+ box-shadow: var(--shadow);
+}
+
+.repo-card:hover,
+.repo-card:focus-visible {
+ border-color: var(--brand-color);
+ color: var(--brand-color);
+}
+
+/* ---- Pagination ---- */
+.pagination {
+ display: flex;
+ gap: 0.4rem;
+ justify-content: center;
+ margin-top: 1.5rem;
+}
+
+.pagination .page {
+ padding: 0.35rem 0.65rem;
+ border: 1px solid #c6d2dc;
+ border-radius: var(--radius);
+ text-decoration: none;
+}
+
+.pagination .page.current {
+ background: var(--brand-color);
+ border-color: var(--brand-color);
+ color: #fff;
+}
+
+/* ---- Forms / add-repo ---- */
+.user-info {
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+ margin-bottom: 1rem;
+}
+
+.user-avatar {
+ border-radius: 50%;
+}
+
+#repo-form {
+ display: grid;
+ gap: 0.5rem;
+ max-width: 420px;
+}
+
+.form-status.error {
+ color: var(--error-color);
+}
+
+.form-status.success {
+ color: var(--success-color);
+}
+
+.cta {
+ display: inline-block;
+ background: var(--brand-color);
+ color: #fff;
+ padding: 0.6rem 1.2rem;
+ border-radius: var(--radius);
+ border: none;
+ font-size: 1rem;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+.cta:hover {
+ background: var(--brand-accent);
+ color: #fff;
+}
+
+/* ---- Error pages ---- */
+.error-page {
+ max-width: 640px;
+ margin: 3rem auto;
+ text-align: center;
+ padding: 2rem;
+ border: 1px solid #f0c6c0;
+ border-radius: var(--radius);
+ background: #fdf6f5;
+}
+
+.error-page h1 {
+ color: var(--error-color);
+}
+
+.error-code {
+ color: var(--muted-color);
+ font-size: 0.85rem;
+}
+
+.error-actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ margin-top: 1rem;
+}
+
+.error-actions .retry {
+ background: var(--brand-color);
+ color: #fff;
+ border: none;
+ border-radius: var(--radius);
+ padding: 0.5rem 1rem;
+ cursor: pointer;
+}
+
+/* ---- Responsive ---- */
+/* The header (brand + search + 9 menu links at 0.8rem) needs roughly 900px.
+ * Below that widths links would clip because .menu-links uses nowrap +
+ * overflow:hidden without a visible toggle, so switch to the collapsible
+ * menu early — before any navigation item becomes unreachable. */
+@media (max-width: 900px) {
+ #top-menu {
+ max-height: none;
+ }
+
+ .menu-bar {
+ flex-wrap: wrap;
+ overflow: visible;
+ }
+
+ .menu-toggle {
+ display: block;
+ margin-left: auto;
+ order: 2;
+ }
+
+ .menu-search {
+ /* On mobile the search sits on its own row between toggle and links. */
+ order: 3;
+ flex-basis: 100%;
+ max-width: none;
+ margin-left: 0;
+ margin-top: 0.5rem;
+ }
+
+ .menu-links {
+ display: none;
+ flex-basis: 100%;
+ flex-direction: column;
+ align-items: stretch;
+ padding-bottom: 0.5rem;
+ order: 4;
+ }
+
+ .menu-links.open {
+ display: flex;
+ }
+
+ .repo-grid,
+ .featured-repos ul {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/app/layout.jsx b/app/layout.jsx
new file mode 100644
index 0000000..6eac8d5
--- /dev/null
+++ b/app/layout.jsx
@@ -0,0 +1,44 @@
+import SiteHeader from '../components/site-header';
+import SiteFooter from '../components/site-footer';
+import SentryProvider from '../components/sentry-provider';
+import { loadListedRepositories } from '../lib/repositories.js';
+import './globals.css';
+
+// frame-ancestors is intentionally served via HTTP header (ignored in meta);
+// platform deployment docs list the header. The `submission-target` meta names
+// the "/" project whose issue tracker receives submissions;
+// self-managed GitLab deployments also set a platform-base-url meta.
+// NOTE: after `next build`, scripts/fix-csp-hashes.mjs injects sha256 hashes
+// of the inline bootstrap scripts into this policy so the strict CSP survives
+// the static export (see plans/nextjs-conversion.md Spike Results C).
+const CSP =
+ "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://esm.sh https://buttons.github.io 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; img-src 'self' https://avatars.githubusercontent.com https://buttons.github.io https://github.githubassets.com data:; connect-src 'self' https://api.github.com https://raw.githubusercontent.com https://cdn.jsdelivr.net https://esm.sh https://gitlab.com https://api.gitlab.com https://bitbucket.org https://api.bitbucket.org; base-uri 'self';";
+
+export const metadata = {
+ title: 'RefactorFirst - Know Where to Refactor First',
+ icons: { icon: '/assets/logo.png' },
+};
+
+export default function RootLayout({ children }) {
+ const repositories = loadListedRepositories();
+ return (
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/app/not-found.jsx b/app/not-found.jsx
new file mode 100644
index 0000000..fc44662
--- /dev/null
+++ b/app/not-found.jsx
@@ -0,0 +1,71 @@
+'use client';
+
+import { useEffect } from 'react';
+import Link from 'next/link';
+import { usePathname } from 'next/navigation';
+import { isValidGitHubName, STATIC_PAGES } from '../lib/routes.js';
+import { getBasePath } from '../lib/base-path';
+
+// Exported for the 404.html produced by the static export. GitHub Pages (and
+// similar static hosts) serve this page for any path that has no exported
+// HTML file. Two recovery behaviors:
+// - Branch deep links (/user/repo/) that are not pre-generated are
+// redirected to the repo shell (/user/repo/) which loads the requested
+// branch client-side (Technical Appendix §6).
+// - Repos added after the last deploy get the "recently added" note
+// (Technical Appendix §5).
+//
+// `performRedirect` is injectable for tests (window.location is not
+// configurable under jsdom).
+export default function NotFound({ performRedirect }) {
+ const pathname = usePathname() || '';
+ const basePath = getBasePath();
+ const path = basePath && pathname.startsWith(basePath)
+ ? pathname.slice(basePath.length) || '/'
+ : pathname;
+ const segments = path.split('/').filter(Boolean);
+ const [username, repository] = segments;
+
+ const isRepoLike =
+ segments.length >= 2 &&
+ username !== 'add-repo' &&
+ !STATIC_PAGES.has(username) &&
+ isValidGitHubName(username) &&
+ isValidGitHubName(repository);
+
+ // Branch deep links are redirected to the statically generated repo shell;
+ // ReportView picks the requested branch back up from the query parameter
+ // (Technical Appendix §6). Branch names may contain slashes, so any
+ // segments beyond user/repo belong to the branch name.
+ const redirectTarget =
+ segments.length >= 3 && isRepoLike
+ ? `/${username}/${repository}/?branch=${encodeURIComponent(segments.slice(2).join('/'))}`
+ : null;
+
+ useEffect(() => {
+ const redirect = performRedirect || (url => window.location.replace(url));
+ if (redirectTarget) {
+ redirect(`${basePath}${redirectTarget}`);
+ }
+ }, [redirectTarget, basePath, performRedirect]);
+
+ return (
+
+
Page Not Found
+
+ The page or repository you requested does not exist. If you were looking for a
+ report, check that the repository contains
+ .refactorfirst/refactor-first.json on its default branch.
+
+ {segments.length === 2 && isRepoLike && (
+
+ This repository may have been recently added. The listing is updated during deployment.
+ If you just submitted this repository, please wait a few minutes and try again.
+
+ )}
+
+ Back to home · Getting Started
+
+
+ );
+}
diff --git a/app/page.jsx b/app/page.jsx
new file mode 100644
index 0000000..905e99b
--- /dev/null
+++ b/app/page.jsx
@@ -0,0 +1,6 @@
+import Landing from '../components/landing';
+import { loadListedRepositories } from '../lib/repositories.js';
+
+export default function HomePage() {
+ return ;
+}
diff --git a/app/privacy-policy/page.jsx b/app/privacy-policy/page.jsx
new file mode 100644
index 0000000..36fe04a
--- /dev/null
+++ b/app/privacy-policy/page.jsx
@@ -0,0 +1,27 @@
+export default function PrivacyPolicyPage() {
+ return (
+
+
Privacy Policy
+
+
Data Collected
+
RefactorFirst does not run analytics, tracking or logins. This site does not ask for
+ or store your platform credentials or access tokens.
+
+
Submissions
+
When you add a repository, you create an issue on this site's platform
+ (GitHub, GitLab or Bitbucket). Your platform username (the issue author)
+ and the repository name are recorded in the public
+ repositories.txt file, the public issue and CI logs for
+ auditing. The validation job uses platform credentials to verify that
+ you have write access to the submitted repository by checking membership
+ and permission status via the platform API. This check is performed by the
+ CI system and the results are retained in CI logs and issue comments.
+
+
Cookies
+
This site does not use cookies.
+
+
Your rights
+
To have a submission removed, open an issue in the RefactorFirst repository.
Only submit repositories you own or have write access to, and only repositories with
+ a valid RefactorFirst report. Do not attempt to abuse, flood or disrupt the service.
+
+
Content moderation
+
Repository listings may be removed at the maintainers' discretion, including for
+ inappropriate content or abuse.
+
+
No warranty
+
The service is provided “as is”, without warranty of any kind. Reports are
+ generated by the RefactorFirst tool and rendered as-is from each repository.
+
+
Limitation of liability
+
To the maximum extent permitted by law, the maintainers are not liable for damages
+ arising from use of this service.
+
+
DMCA
+
To report content that infringes your rights, open an issue in the RefactorFirst
+ repository with details of the claim.
+
+ );
+}
diff --git a/assets/refactor-first-report.mustache b/assets/refactor-first-report.mustache
index 799f595..9aa29bc 100644
--- a/assets/refactor-first-report.mustache
+++ b/assets/refactor-first-report.mustache
@@ -57,10 +57,16 @@
cursor: pointer;
font-size: 20px;
font-weight: bold;
+ background: none;
+ border: none;
+ padding: 2px 6px;
+ /* Keep the close control clickable above graph canvases injected
+ into the popup by sigma/3d-force-graph. */
+ z-index: 10;
}
.chart-container {
- max-width: 1100px;
+ width: 95%;
margin: 20px auto;
}
@@ -153,7 +159,7 @@
+ {totalPages > 1 && (
+
+ )}
+
+ );
+}
diff --git a/components/repo-submission-form.jsx b/components/repo-submission-form.jsx
new file mode 100644
index 0000000..e5c1201
--- /dev/null
+++ b/components/repo-submission-form.jsx
@@ -0,0 +1,98 @@
+'use client';
+
+// Repository submission form (/add-repo). Validates input inline, checks the
+// target repository for a published report, then hands off to a pre-filled
+// platform issue in a new tab — no login or tokens on this site; the issue
+// author is the submitter identity (checked in CI).
+
+import { useState } from 'react';
+import { validateRepositoryInput, submitRepository, platformLabel } from '../lib/repo-submission';
+import { logError } from '../lib/error-handler';
+import { usePlatformConfig } from './platform-config';
+
+export default function RepoSubmissionForm({ onExternalRedirect }) {
+ const { environment, platformBaseUrl, submissionTarget } = usePlatformConfig();
+ const [pending, setPending] = useState(false);
+ const [status, setStatus] = useState({ kind: 'idle', html: '', text: '' });
+
+ // Successful submissions open the pre-filled issue in a new tab.
+ const externalRedirect =
+ onExternalRedirect || (url => window.open(url, '_blank', 'noopener,noreferrer'));
+
+ async function handleSubmit(event) {
+ event.preventDefault();
+ const form = event.target;
+ const owner = form.querySelector('#repo-owner').value;
+ const repo = form.querySelector('#repo-name').value;
+
+ const validation = validateRepositoryInput(owner, repo);
+ if (!validation.valid) {
+ setStatus({ kind: 'error', text: validation.errors.join('. ') });
+ return;
+ }
+
+ setPending(true);
+ setStatus({ kind: 'pending', text: 'Validating repository...' });
+ try {
+ const result = await submitRepository({ owner, repo }, {
+ environment,
+ baseUrl: platformBaseUrl,
+ target: submissionTarget
+ });
+ if (result.success && result.issueUrl) {
+ // Popup blockers may swallow window.open after async work, so the
+ // status message always carries the clickable issue link.
+ setStatus({
+ kind: 'success',
+ issueUrl: result.issueUrl,
+ text: result.message
+ });
+ externalRedirect(result.issueUrl);
+ } else {
+ setStatus({ kind: 'error', text: result.message });
+ }
+ } catch (error) {
+ logError(error, { route: 'add-repo' });
+ setStatus({ kind: 'error', text: error.message });
+ } finally {
+ setPending(false);
+ }
+ }
+
+ const label = platformLabel(environment);
+
+ return (
+
+
Add Your Repository
+
+ Only repositories with a .refactorfirst/refactor-first.json file
+ can be added. After the check, a pre-filled {label} issue opens in a new tab
+ — submit it there and your {label} account will be recorded as the
+ submitter. No login or tokens are needed on this site.
+