Skip to content

UI Quality Tooling ​

Objective ​

Standardize formatting and linting for the React + TypeScript UI using the OXC toolchain.

The goal is:

  • fast feedback
  • minimal configuration
  • no formatter/linter conflicts
  • predictable behavior across contributors and CI

This setup intentionally mirrors the philosophy of modern single-tool stacks (similar to Ruff on the backend).

Target path: apps/ui


Tooling Model (Important) ​

This setup intentionally splits responsibility:

Formatting ​

  • Oxfmt Handles all formatting concerns:

    • whitespace
    • quotes
    • wrapping
    • commas

There are no stylistic lint rules.

Linting ​

  • Oxlint Focuses on:

    • correctness
    • suspicious patterns
    • performance pitfalls

Oxlint is not a plugin-based linter like ESLint. It does not attempt to replicate framework-specific rule ecosystems.

In addition to Oxlint, the enforced UI gate also includes:

  • stylelint via npm run lint:css
  • eslint-plugin-tailwindcss via npm run lint:tailwind

Type Safety ​

  • TypeScript (tsc --noEmit) Enforces type correctness.

Oxlint does not replace TypeScript type checking.


What This Toolchain Does Not Do ​

This is intentional and by design:

  • No React-hooks exhaustive dependency enforcement
  • No JSX a11y plugin rules
  • No broad framework plugin ecosystem beyond focused checks (for example Tailwind class linting)
  • No arbitrary plugin ecosystem

These concerns are handled via:

  • TypeScript
  • code review
  • shared patterns
  • tests

1) Install dependencies ​

Using npm (works reliably on Windows, macOS, Linux):

bash
npm --prefix apps/ui i -D oxlint @oxc/oxfmt typescript

Recommendation ​

  • Pin exact versions of oxlint and @oxc/oxfmt
  • Do not use caret (^) ranges for these tools
  • The toolchain evolves quickly

2) Formatter configuration ​

Create apps/ui/.oxfmtrc.json:

json
{
  "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/crates/oxfmt/schema.json",
  "printWidth": 100,
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all"
}

Keep formatting rules minimal to reduce churn.

Generated modules under src/generated/ are emitted byte-exact by scripts/generate-docs.py and verified by task docs:verify-sync, so they are excluded from formatting via apps/ui/.prettierignore (oxfmt reads it by default) - formatters must never rewrite verified generated output.


3) Linter configuration ​

Create apps/ui/.oxlintrc.json:

json
{
  "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/crates/oxlint/schema.json",
  "rules": {
    "correctness": "warn",
    "suspicious": "warn",
    "perf": "warn"
  }
}

Category rules (correctness, suspicious, perf) run as warnings; individual rules are promoted to error where the codebase satisfies them (see no-restricted-imports in .oxlintrc.json).


4) Ignore files ​

OXC tools (oxfmt and oxlint) ignore node_modules, dist, build, and coverage by default, so those need no configuration.

The two tools take additional exclusions from different files - there is no .oxfmtignore or .oxlintignore:

ToolExclusion mechanismCurrent entries
oxfmtapps/ui/.prettierignore (and .gitignore), read by default - confirmed by npx oxfmt --help under "Ignore Options"src/generated/, e2e/fixtures/step-handler-types.json
oxlintthe ignorePatterns array in apps/ui/.oxlintrc.jsonsee that file

Both current oxfmt entries are generated artifacts whose bytes a generator owns and a gate verifies (task docs:verify-sync, task docs:verify-fixtures). That is the bar for adding an entry: a formatter and a byte-exact generator cannot both own one file. Do not exclude hand-written source to avoid fixing it.


5) UI package.json scripts ​

Update apps/ui/package.json:

json
{
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",

    "format": "oxfmt .",
    "format:check": "oxfmt . --check",

    "lint": "oxlint .",
    "lint:fix": "oxlint . --fix",
    "lint:css": "stylelint src/**/*.css",
    "lint:css:fix": "stylelint src/**/*.css --fix",
    "lint:tailwind": "eslint --config eslint.tailwind.config.mjs 'src/**/*.{tsx,jsx}'",

    "typecheck": "tsc --noEmit",

    "check:api-headers": "node scripts/check-api-headers.mjs",
    "generate:types": "node scripts/generate-types.mjs",

    "test:e2e": "playwright test",
    "test:e2e:smoke": "playwright test -g @smoke --project=chromium",
    "test:e2e:coverage": "COVERAGE=true playwright test",

    "coverage:report": "node scripts/generate-v8-coverage.mjs --html",
    "coverage:summary": "node scripts/generate-v8-coverage.mjs --summary",

    "preview": "vite preview"
  }
}

Notes on format:check ​

  • oxfmt --check is supported

  • If a future version regresses, temporarily replace with:

    • run format
    • then git diff --exit-code in CI

Notes on generate:types ​

Canonical local workflow: use task api:regenerate instead of running npm run generate:types directly. The Taskfile task also exports the OpenAPI spec and lints it with Spectral before regenerating UI TypeScript types.


6) Single formatter/linter stack ​

The UI has a single formatter/linter stack (Oxfmt + Oxlint). The only ESLint artifact is eslint.tailwind.config.mjs, required by npm run lint:tailwind, task ui:lint:tailwind, and CI; .prettierignore remains because oxfmt reads it for exclusions.


7) Pre-commit integration (repo root) ​

The canonical pre-commit hooks are defined in .pre-commit-config.yaml at the repo root — treat that file as the source of truth.

The current UI hooks use Taskfile tasks as entry points and cover:

yaml
- repo: local
  hooks:
    - id: ui-format-check
      name: UI format check (oxfmt)
      entry: task ui:format:check
      language: system
      pass_filenames: false
      files: ^apps/ui/

    - id: ui-lint
      name: UI lint (oxlint)
      entry: task ui:lint
      language: system
      pass_filenames: false
      files: ^apps/ui/

    - id: ui-lint-css
      name: UI CSS lint (stylelint)
      entry: task ui:lint:css
      language: system
      pass_filenames: false
      files: ^apps/ui/src/.*\.css$

    - id: ui-lint-tailwind
      name: UI Tailwind lint (eslint-plugin-tailwindcss)
      entry: task ui:lint:tailwind
      language: system
      pass_filenames: false
      files: ^apps/ui/src/.*\.(tsx|jsx)$

    - id: ui-typecheck
      name: UI typecheck (tsc)
      entry: task ui:typecheck
      language: system
      pass_filenames: false
      files: ^apps/ui/
      stages: [pre-push]

    - id: ui-api-headers
      name: UI API auth headers check
      entry: task ui:check:api-headers
      language: system
      pass_filenames: false
      files: ^apps/ui/src/api/client\.ts$

Run task precommit to execute all default-stage (pre-commit) hooks locally. To also run hooks configured for the pre-push stage (for example ui-typecheck), run pre-commit run --all-files --hook-stage pre-push manually.


8) CI enforcement ​

CI must gate on these checks:

bash
# All checks in task ui:all (format, lint, CSS, Tailwind, typecheck, api-headers):
task ui:all

# Additional CI gates — also required before pushing:
task api:verify-types-sync           # verifies generated TS types are up-to-date
npm --prefix apps/ui run build
task ui:audit                        # npm audit --audit-level=high

Run task ui:verify for lint/typecheck/build/types-sync parity with CI, plus task ui:audit for npm audit parity.

E2E dev server and workers ​

playwright.config.ts starts the Vite dev server itself. Two details matter:

  • Port. Locally it is a fixed 5173, so reuseExistingServer can attach to a server you already have running — the biggest saving in the edit-test loop. Under CI it asks the kernel for a free ephemeral port instead. The self-hosted runners are separate users on one host and therefore share a loopback interface, so a fixed port is a single global resource: two E2E jobs anywhere in the repository used to collide on it, and the second to start failed before running a test. Set E2E_PORT to pin a port yourself.
  • Workers. Four by default, overridable with E2E_WORKERS. Every test mocks its own API surface through page.route and gets its own browser context, and workers are separate processes, so nothing is shared but the dev server — which only serves modules.

9) VS Code notes (pragmatic expectations) ​

  • OXC provides a VS Code extension
  • Binary auto-detection may not work in all environments
  • Formatting-on-save is optional

If formatting on save is unreliable:

  • run npm run format manually
  • rely on pre-commit and CI for enforcement

Guarantees ​

  • formatting is deterministic
  • linting catches correctness/suspicious/perf issues
  • TypeScript errors are blocked via tsc --noEmit
  • pre-commit and CI enforce the same rules
  • no ESLint/Prettier configuration exists except eslint.tailwind.config.mjs (required for Tailwind class linting)
  • contributors do not need to think about style decisions

Final note (important for Copilot) ​

When generating UI code:

  • follow TypeScript strictness
  • do not rely on ESLint-specific patterns
  • assume formatting will be handled automatically by Oxfmt
  • treat lint warnings as signals, not noise

Released as open source under the AGPL-3.0-or-later license. Development is sponsored by Rexonix s.r.o.. Contact — [email protected].