Skip to content

feat(toolchain): adopt Biome, oxlint, attw; keep eslint-config-next + knip#98

Merged
cevheri merged 7 commits into
mainfrom
feat/toolchain
Jun 27, 2026
Merged

feat(toolchain): adopt Biome, oxlint, attw; keep eslint-config-next + knip#98
cevheri merged 7 commits into
mainfrom
feat/toolchain

Conversation

@cevheri

@cevheri cevheri commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Toolchain adoption (single PR, phased)

Adopts five tools, ported from the libredb-database toolchain decision record and adapted to Studio
(Next.js + React + TSX, dual-format npm package). Full rationale and as-implemented deviations in
docs/TOOLCHAIN.md.

Tool Decision
@biomejs/biome (format-only) ADOPT - fills the no-formatter gap (lineWidth 120; CSS/JSON excluded)
oxlint ADOPT - fast syntactic linter in front of ESLint
typescript-eslint + eslint KEEP (Strategy A: eslint-config-next stays; oxlint layered on top; narrow type-aware layer for api+db)
knip KEEP - already wired; verified clean, no config change needed
@arethetypeswrong/cli (attw) ADOPT - validates dual-format / 5 subpath exports (--profile node16)

Phases (each committed separately, CI green before advancing)

  • Phase 0 - plan + .editorconfig
  • Phase 1 - Biome formatter + one-shot reformat (461 files)
  • Phase 2 - oxlint (rule tuning + 3 genuine bug fixes)
  • Phase 3 - narrow type-aware ESLint layer (5 floating-promise bugs fixed)
  • Phase 4 - attw + CI integration (format / build:lib / attw steps)
  • Phase 5 - knip verification + docs finalized

CI status

Functional gates all green: Lint/Typecheck/Build (now incl. format + oxlint + build:lib + attw), Unit &
Integration Tests, E2E, Helm Chart Lint, CodeQL Analyze jobs, Platform Integration Rules.

Two advisory analyzers (CodeQL "new alerts", SonarCloud quality gate) report failures that are artifacts of
the 461-file one-shot reformat: it makes pre-existing lines count as "new code", so both re-attribute
PRE-EXISTING findings to this PR (their own caveat: "code changes were too large"). Verified the flagged
files' diffs are formatting-only; the findings are pre-existing and largely by-design (Math.random in a
test-data generator; js/sql-injection in a tool whose purpose is running user SQL). No new vulnerability or
bug is introduced by this PR. Neither check is a required status check on main.

Studio-specific deviations from database: attw uses --profile node16 (subpath exports fail only the legacy
node10 algorithm; the package targets Node >=24 + modern bundlers); ESLint is not reduced to type-aware-only
(eslint-config-next remains the Next/React owner); CSS is kept out of the Biome formatter (platform-integration).

Add the 5-tool toolchain adoption plan (Biome formatter, oxlint,
typescript-eslint+eslint, knip, attw) ported from libredb-database, and
the .editorconfig that matches the Biome formatter settings.
@cevheri

cevheri commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

@copilot review the PR

Copilot AI commented Jun 26, 2026

Copy link
Copy Markdown

@copilot review the PR

Reviewed the current PR changes (phase 0). No blocking issues found. The .editorconfig settings are safe, and docs/TOOLCHAIN.md is consistent with the current repo state and tooling setup.

@cevheri cevheri requested a review from Copilot June 26, 2026 21:20
@cevheri cevheri added the enhancement New feature or request label Jun 26, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Studio-specific 2026 toolchain adoption plan (Biome formatter, oxlint, attw; keep eslint-config-next + knip) and introduces an .editorconfig baseline so editors converge on formatting ahead of the formatter rollout.

Changes:

  • Added docs/TOOLCHAIN.md describing phased adoption, rationale, and proposed configs/scripts for the five tools.
  • Added .editorconfig to standardize basic whitespace/newline/indent behavior (with a Markdown trailing-space exception).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
docs/TOOLCHAIN.md New adoption plan doc covering scope, phases, and proposed configs/scripts for Biome/oxlint/ESLint/attw/knip.
.editorconfig Establishes editor defaults (2-space indent, LF, UTF-8, final newline; preserve trailing spaces in Markdown).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/TOOLCHAIN.md Outdated
Comment on lines +3 to +7
> Status: PLANNED (no code changes yet). This is a per-tool adoption plan for five tools, ported from the
> researched-then-adversarially-verified decision record in `libredb-database/docs/TOOLCHAIN.md` and adapted to
> Studio's reality: a Next.js 16 + React 19 + TSX application that ALSO ships as the dual-format npm package
> `@libredb/studio` (consumed by `libredb-platform`). The database record is the rationale source of truth;
> this document records only what changes for Studio and why.
Comment thread docs/TOOLCHAIN.md
Comment on lines +84 to +85
"format": "biome format src tests *.ts *.mjs",
"format:fix": "biome format --write src tests *.ts *.mjs"
Add @biomejs/biome (formatter only; linter and assist disabled), biome.json
(lineWidth 120, 2-space, double quotes, semicolons; CSS and JSON formatting
disabled), and format/format:fix scripts. Apply a one-shot reformat across
src, tests, e2e, scripts (461 files). CSS left untouched to avoid
platform-integration style breakage. lint/typecheck/build/build:lib/test all
green.
Comment thread src/lib/logger.ts
const safeStack = errInfo.stack.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
console.error(full, '\n', safeStack);
const safeStack = errInfo.stack.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "");
console.error(full, "\n", safeStack);
`[DB:${sanitize(this.type)}] ${sanitize(operation)} failed: ${sanitize(errorMessage)}`
);
const sanitize = (v: string) => v.replace(/[\r\n]/g, " ").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "");
console.error(`[DB:${sanitize(this.type)}] ${sanitize(operation)} failed: ${sanitize(errorMessage)}`);
Comment thread src/lib/db/factory.ts
const sanitize = (v: string) => v.replace(/[\r\n]/g, ' ').replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
console.log(`[DB] Creating ${sanitize(connection.type)} provider for "${sanitize(connection.name || '')}"`);
const sanitize = (v: string) => v.replace(/[\r\n]/g, " ").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "");
console.log(`[DB] Creating ${sanitize(connection.type)} provider for "${sanitize(connection.name || "")}"`);
Comment thread src/lib/logger.ts
const safeStack = errInfo.stack.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
console.error(full, '\n', safeStack);
const safeStack = errInfo.stack.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "");
console.error(full, "\n", safeStack);
cevheri added 4 commits June 27, 2026 00:46
Add oxlint (correctness/suspicious=error, perf=warn) with React/Next/hooks/
jsx-a11y/import/typescript plugins, run before ESLint via the lint script.
Disable false-positives and eslint-config-next-owned duplicates
(react-in-jsx-scope under the automatic JSX runtime, no-unassigned-import,
no-underscore-dangle, no-shadow, no-unused-vars, exhaustive-deps,
no-control-regex for log sanitization, no-unstable-nested-components for the
shadcn/TanStack idiom). Scope test-only idioms (no-new, no-extraneous-class,
no-useless-constructor, no-constant-binary-expression) to tests. Keep
jsx-a11y findings as warnings pending a dedicated accessibility pass.

Fix three genuine issues oxlint surfaced:
- profile/route.ts: typeof is always a truthy string, so the || "unknown"
  fallback was dead code; use a length check for the empty-sample case.
- seed/config-loader.ts: preserve the caught error via the cause option.
- merge-lcov.mjs: drop a useless escape inside a regex character class.
Strategy A: eslint-config-next keeps owning all React/Next/hooks linting and
oxlint is the syntactic layer in front of it (phase 2). This adds a scoped
type-aware safety net via typescript-eslint with projectService, limited to
the async-heavy paths (src/app/api, src/lib/db) to keep lint fast:
no-floating-promises, no-misused-promises, await-thenable.

Fix the five genuine fire-and-forget bugs it surfaced (async functions invoked
in setInterval/setTimeout/process signal handlers without handling the
promise) using the void operator to make the intent explicit:
factory.ts (idle sweep + SIGTERM/SIGINT shutdown), mysql.ts and postgres.ts
(transaction auto-rollback timeout).
Add @arethetypeswrong/cli to validate the published type-resolution across
module modes. Run against the packed tarball (bun pm pack) so it tests exactly
what npm ships; rm runs first so a trailing rm cannot mask the exit code. Use
--profile node16: the package requires Node >=24 and is consumed by modern
bundlers, so node16 (CJS+ESM) + bundler are validated and the legacy node10
algorithm (which cannot resolve subpath exports without redirect stubs) is
ignored. Wire prepublishOnly to build then run attw, and git-ignore the
.attw/ + *.tgz packaging scratch.

CI: add a Biome format check and build:lib + attw steps to the lint-and-build
job, and rename the lint step (oxlint now runs before ESLint).
knip is green with NO knip.json change needed: every new tool is seen via its
package.json script (biome, oxlint, attw) or a config import (typescript-eslint
in eslint.config.mjs) - the database finding held. Update docs/TOOLCHAIN.md
status to IMPLEMENTED and record the as-implemented deviations (oxlint rule
tuning, the type-aware layer landing in phase 3, attw --profile node16). Sync
CLAUDE.md dev commands and the pre-commit verification list (now five with
format).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
D Reliability Rating on New Code (required ≥ A)
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@cevheri

cevheri commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

Summary

Adopts five tools in five phased commits, each green through CI before the next: Biome (formatter-only),
oxlint (fast syntactic linter), a narrow type-aware ESLint layer (kept on top of eslint-config-next,
Strategy A), knip (verified), and @arethetypeswrong/cli (package types). Full rationale and
as-implemented deviations: docs/TOOLCHAIN.md.

Genuine bugs fixed along the way (not just config):

  • 5 floating/misused promises caught by the type-aware layer (fire-and-forget async in timer/signal
    callbacks): factory.ts (idle sweep + SIGTERM/SIGINT shutdown), mysql.ts, postgres.ts (tx
    auto-rollback timeout) - fixed with the void operator.
  • profile/route.ts: dead typeof x || "unknown" fallback (a typeof result is always a truthy string).
  • seed/config-loader.ts: dropped error cause, now preserved via { cause }.
  • merge-lcov.mjs: useless regex escape.

Local verification (beyond CI)

  • bun dev - healthy on :3000.
  • docker run (image built from this branch, 495 MB) - healthy on :3001, "Ready in 36ms".
  • Playwright against a seeded local PostgreSQL (running the reformatted branch source): login -> admin
    dashboard -> create + Test ("Connected successfully, 10ms") + establish a Postgres connection -> schema
    introspection (both tables) -> a LEFT JOIN + aggregate query returning correct rows, with column masking
    working. This exercises the actual DB provider path through the reformatted code, which is the strongest
    evidence the Phase 1 reformat is behaviour-safe.

CI status and open items

Functional gates are all green: Lint/Typecheck/Build (now incl. Biome format + oxlint + build:lib +
attw), Unit & Integration Tests, E2E, Helm Chart Lint, CodeQL Analyze jobs, Platform Integration Rules.

Two analyzers report failures that are advisory (neither is a required status check on main) and are
artifacts of the 461-file one-shot Biome reformat, not regressions introduced here:

  • CodeQL "new alerts" (11: 4 js/sql-injection high, 6 js/log-injection medium, 1
    js/tainted-format-string high).
    The reformat changed nearly every line, so CodeQL's diff-based
    attribution flags PRE-EXISTING findings as "new" (its own caveat: "code changes were too large"). The
    flagged files' diffs are formatting-only (single->double quotes, import reflow); the sinks are logically
    identical to main. The findings are largely by-design (js/sql-injection in a tool whose purpose is
    running user-supplied SQL; js/log-injection in logger.ts, which already sanitises control chars).
  • SonarCloud quality gate. new_coverage 92.9%, new_maintainability_rating 1, duplications 1.3% are
    all OK. Two conditions fail, both pre-existing and re-attributed by the same reformat effect:
    new_security_rating = 3 (25x Math.random in TestDataGenerator.tsx - fake-data generation, not
    security-sensitive) and new_reliability_rating = 4 (a few real minor bugs: PivotTable sort compare,
    SchemaDiagram constant conditional, DataCharts non-interactive click handler; the route.ts
    constant-|| bug in this set was already fixed in Phase 2).

After this PR squash-merges to main, the CodeQL/Sonar "new code" baseline resets, so these stop blocking
unrelated PRs. The underlying findings persist in the codebase and are tracked for real resolution in #100,
together with the deferred accessibility work (see below).

Deliberate, philosophy-aligned decisions

  • Accessibility (jsx-a11y). 60 findings were kept as oxlint warn (not disabled) rather than rewriting
    vendored shadcn/ui and many components inside a tooling PR. Tracked in Follow-ups from toolchain adoption (#98): accessibility pass + triage pre-existing CodeQL/Sonar findings #100; the 8 rules get promoted to
    error once the dedicated a11y pass lands.
  • attw --profile node16. The 5 subpath exports fail only the legacy node10 resolution algorithm
    (node16 CJS+ESM and bundler are green). The package targets Node >=24 and modern bundlers, so node10 is
    moot; node16 is more precise and honest than a broad --ignore-rules no-resolution.
  • ESLint not reduced to type-aware-only (unlike libredb-database): eslint-config-next remains the
    owner of React/Next/hooks rules for a shipping Next app; oxlint is layered in front.
  • CSS kept out of the Biome formatter to avoid platform-integration style breakage on globals.css.

Follow-ups tracked in #100.

@cevheri cevheri merged commit 15ca045 into main Jun 27, 2026
9 of 11 checks passed
@cevheri cevheri deleted the feat/toolchain branch June 27, 2026 11:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants