From 464d6cac98de054a4446b335e6c2148657c5a3e2 Mon Sep 17 00:00:00 2001 From: sirjmann92 Date: Fri, 18 Sep 2026 08:14:38 -0500 Subject: [PATCH] refactor(lint): make frontend/package.json the only Biome version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Biome ran through the remote biomejs/pre-commit hook, which fetches its own hermetic copy and never looks at frontend/node_modules. That copy needed its own version, so one fact — which Biome this repo uses — was declared five times: the npm pin, the lockfile, the hook rev, the hook's additional_dependencies, and biome.json's $schema URL. Drift between them failed silently, the gate quietly enforcing the older ruleset; that is how the repo once ran 2.2.4 against a declared 2.5.11 (issue #81). scripts/check_biome_pins.py turned that silence into a loud failure, but it made every Biome bump a two-PR affair: Dependabot can only reach two of the five pins, so its PR failed CI by construction. Biome now runs as a repo: local hook invoking `npm run lint` — the same command a developer runs, and the pattern svelte-check already used. The version comes from node_modules, so package.json is the single source of truth and the guard has nothing left to check. Also moves file scoping out of the hook's regex into biome.json's files.includes, where it belongs. It had never been in biome.json at all, so `npm run lint` was unscoped: it swept frontend/.svelte-kit/ and emitted ~5600 diagnostics, making the documented lint command unusable. Both the hook and the npm script now check the same 26 files. Coverage is unchanged — verified the same file set before and after, and verified the hook still fails on an injected formatting violation. --- .pre-commit-config.yaml | 44 +++++-------- AGENTS.md | 65 ++++++++++--------- biome.json | 30 +++++++-- frontend/package.json | 4 +- scripts/check_biome_pins.py | 126 ------------------------------------ 5 files changed, 78 insertions(+), 191 deletions(-) delete mode 100644 scripts/check_biome_pins.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aef397e..5cf90be 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,37 +48,25 @@ repos: types_or: [ts, svelte] pass_filenames: false - - id: check-biome-pins - name: check biome version pins agree - entry: python3 scripts/check_biome_pins.py + # Runs the Biome that npm installed, via the same `npm run lint` a + # developer runs by hand, so there is exactly one Biome version in the + # repo: frontend/package.json. It used to be declared here as well (a + # `rev` plus an `additional_dependencies` pin) because pre-commit + # fetches its own hermetic copy and never looks at frontend/ + # node_modules -- which is how this repo once enforced 2.2.4 against a + # declared 2.5.11, nine releases apart and silently (issue #81). + # + # What Biome looks at is now biome.json's files.includes, not a regex + # here. Keeping that scoping in this file meant `npm run lint` had no + # scoping at all: it swept frontend/.svelte-kit/ and reported ~5600 + # diagnostics, so nobody could use it. + - id: biome-check + name: biome check + entry: bash -c "cd frontend && npm run lint" language: system - files: '^(frontend/package(-lock)?\.json|\.pre-commit-config\.yaml|biome\.json)$' + types_or: [ts, tsx, javascript, jsx, json, svelte] pass_filenames: false - # Both pins below must match "@biomejs/biome" in frontend/package.json. - # The rev is what actually gates CI; the npm pin only governs `npm run - # lint`, which CI never runs. When they drift, the gate silently enforces - # the older ruleset. The check-biome-pins hook above fails the run if so. - - repo: https://github.com/biomejs/pre-commit - rev: 'v2.5.13' - hooks: - - id: biome-check - # Scoped to source files rather than a bare ^frontend/. Biome gained - # HTML/SVG support after 2.2.4, so a bare prefix now sweeps in files - # the old pin silently skipped: - # - app.html / favicon.svg: attributes get reordered, a trailing - # space is emitted before '>', the pre-paint theme script's - # function expression is rewritten, and noSvgWithoutTitle fails - # on the favicon. Linting markup is a separate decision from - # this version bump. - # - app.css: the only CSS file, and biome.json already excludes it - # twice. Passing it explicitly bypasses experimentalScannerIgnores - # and the parser rejects Tailwind's @plugin before the override's - # disabled linter/formatter applies. - files: '^frontend/.*\.(jsx?|tsx?|c(js|ts)|m(js|ts)|jsonc?|svelte)$' - additional_dependencies: - - '@biomejs/biome@2.5.13' - ci: autofix_commit_msg: | [pre-commit.ci] auto fixes from pre-commit hooks diff --git a/AGENTS.md b/AGENTS.md index ffa8c8e..5b694da 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,33 +37,34 @@ pre-commit run --all-files argument, not a pytest suite — it errors under plain collection. Run `pytest tests/ --ignore=tests/test_workers.py`. -### Biome's version is pinned in five places - -They must all agree, and `scripts/check_biome_pins.py` (wired into pre-commit) -fails the run when they don't: - -| Location | -| --- | -| `frontend/package.json` → `@biomejs/biome` | -| `frontend/package-lock.json` → resolved entry | -| `.pre-commit-config.yaml` → `rev` | -| `.pre-commit-config.yaml` → `additional_dependencies` | -| `biome.json` → `$schema` URL | - -The pre-commit `rev`/`additional_dependencies` pair is what actually gates CI: -the hook runs its own pinned copy of Biome in an isolated environment and never -touches `frontend/node_modules`. `npm run lint` uses the npm pin but CI never -invokes it. When these drift, **nothing fails** — the gate just silently -enforces an older ruleset. That is how the repo once ended up enforcing 2.2.4 -against a declared 2.5.11, nine minor releases apart. - -Dependabot bumps the first two and **cannot** touch the other three, so expect -`check-biome-pins` to fail on the first run after a Biome bump. Update all five -together. - -`frontend/node_modules` can also be stale relative to the lockfile — run -`npm ci` before trusting any local lint result. A stale tree is what masked the -nine-version discrepancy above. +### Biome's version lives in exactly one place + +`frontend/package.json` → `@biomejs/biome` (with the lockfile npm maintains +alongside it). Nothing else declares a Biome version, so Dependabot can bump it +unaided and there is nothing to keep in sync. + +It was not always so. Biome used to run through the remote +`biomejs/pre-commit` hook, which fetches its own hermetic copy and never looks +at `frontend/node_modules`. That copy needed its own version — a `rev` plus an +`additional_dependencies` pin — which, with the npm pin, the lockfile and +`biome.json`'s `$schema` URL, made five declarations of one fact. When they +drifted **nothing failed**: the gate just quietly enforced the older ruleset. +That is how this repo once enforced 2.2.4 against a declared 2.5.11, nine +releases apart (issue #81). A guard script was added to catch the drift, then +removed along with the drift itself. + +Biome now runs as a `repo: local` hook invoking `npm run lint`, the same +command a developer runs by hand — the pattern `svelte-check` already used. +Two consequences worth knowing: + +- **The hook needs `frontend/node_modules` to exist**, so run `npm ci` before + trusting any local lint result. A stale tree is what masked the nine-version + discrepancy above. CI installs it before running pre-commit. +- **What Biome looks at is `biome.json`'s `files.includes`**, not a regex in + `.pre-commit-config.yaml`. That scoping used to live in the hook, which meant + `npm run lint` had none: it swept `frontend/.svelte-kit/` and reported ~5600 + diagnostics, so the command was effectively unusable. Change the scope in + `biome.json` and both the hook and the npm script follow. --- @@ -155,9 +156,12 @@ for a webhook to come back around. several releases, review the diagnostics — a bump once began reformatting `app.html`/`favicon.svg` (reordered attributes, a trailing space before `>`, a rewritten pre-paint script) and erroring on the favicon, none of it wanted. -- **Prefer scoped lint globs over broad prefixes.** The Biome hook is scoped to - source extensions rather than a bare `^frontend/`, because tools grow support - for new file types and a broad prefix silently widens what they touch. +- **Keep lint scoping in the tool's own config, not the hook's.** `biome.json` + names the source extensions it covers rather than a bare `frontend/`, because + tools grow support for new file types and a broad prefix silently widens what + they touch — Biome gained HTML/SVG support after 2.2.4, which would sweep in + `app.html` and `favicon.svg`. Scoping there rather than in the hook also + keeps `npm run lint` and CI enforcing the same set. - **Ship user-visible changes with their docs.** See below — a change a user can see is not finished until the page describing it says so. @@ -222,7 +226,6 @@ frontend/ src/routes/config/+page.svelte # Settings UI src/lib/types.ts # Types matching backend API responses scripts/ - check_biome_pins.py # Lint-gate version-drift guard fix_container_mismatches.py # One-off library sweep, dry-run by default docs/ # User-facing reference ``` diff --git a/biome.json b/biome.json index c501541..d622511 100644 --- a/biome.json +++ b/biome.json @@ -1,9 +1,27 @@ { - "$schema": "https://biomejs.dev/schemas/2.5.13/schema.json", + "$schema": "./frontend/node_modules/@biomejs/biome/configuration_schema.json", "files": { "ignoreUnknown": true, "maxSize": 4194304, - "experimentalScannerIgnores": ["frontend/src/app.css"] + "includes": [ + "frontend/**/*.js", + "frontend/**/*.jsx", + "frontend/**/*.cjs", + "frontend/**/*.mjs", + "frontend/**/*.ts", + "frontend/**/*.tsx", + "frontend/**/*.cts", + "frontend/**/*.mts", + "frontend/**/*.json", + "frontend/**/*.jsonc", + "frontend/**/*.svelte", + "!frontend/node_modules/**", + "!frontend/.svelte-kit/**", + "!frontend/build/**" + ], + "experimentalScannerIgnores": [ + "frontend/src/app.css" + ] }, "formatter": { "enabled": true, @@ -32,7 +50,9 @@ }, "overrides": [ { - "includes": ["frontend/src/app.css"], + "includes": [ + "frontend/src/app.css" + ], "linter": { "enabled": false }, @@ -41,7 +61,9 @@ } }, { - "includes": ["**/*.svelte"], + "includes": [ + "**/*.svelte" + ], "linter": { "rules": { "correctness": { diff --git a/frontend/package.json b/frontend/package.json index d2c99de..cc77231 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,8 +8,8 @@ "build": "vite build", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "lint": "biome check . --config-path=../biome.json", - "lint:fix": "biome check . --write --config-path=../biome.json" + "lint": "cd .. && biome check frontend", + "lint:fix": "cd .. && biome check frontend --write" }, "dependencies": { "daisyui": "^5.7.37" diff --git a/scripts/check_biome_pins.py b/scripts/check_biome_pins.py deleted file mode 100644 index 76779f5..0000000 --- a/scripts/check_biome_pins.py +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env python3 -"""Fail if the Biome version is pinned inconsistently across the repo. - -Biome's version is declared in four places that must agree: - - 1. frontend/package.json "@biomejs/biome" (devDependencies) - 2. frontend/package-lock.json the resolved lock entry - 3. .pre-commit-config.yaml the biomejs/pre-commit `rev` (vX.Y.Z) - 4. .pre-commit-config.yaml `additional_dependencies` npm pin - 5. biome.json the "$schema" URL - -Only 3 and 4 gate CI: the lint hook runs its own pinned copy of Biome in an -isolated environment and never touches frontend/node_modules. `npm run lint` -uses 1/2 but CI never invokes it. So when these drift apart nothing fails -- -the gate just silently enforces an older ruleset than the repo advertises. -That is exactly how this repo ended up enforcing 2.2.4 against a declared -2.5.11, nine minor releases apart (issue #81). - -Dependabot bumps 1 and 2 on its own and cannot touch 3, 4 or 5, so this -check is what turns that silent divergence into a loud, actionable failure -on the very first run after a bump. -""" - -from __future__ import annotations - -import json -from pathlib import Path -import re -import sys - -REPO_ROOT = Path(__file__).resolve().parent.parent - -PACKAGE_JSON = REPO_ROOT / "frontend" / "package.json" -PACKAGE_LOCK = REPO_ROOT / "frontend" / "package-lock.json" -PRE_COMMIT_CONFIG = REPO_ROOT / ".pre-commit-config.yaml" -BIOME_CONFIG = REPO_ROOT / "biome.json" - -PACKAGE = "@biomejs/biome" - - -def _declared_version() -> str | None: - """The version in frontend/package.json — the source of truth.""" - data = json.loads(PACKAGE_JSON.read_text()) - for section in ("devDependencies", "dependencies"): - version = data.get(section, {}).get(PACKAGE) - if version: - # Tolerate (but do not require) a range prefix such as ^ or ~. - return version.lstrip("^~") - return None - - -def _locked_version() -> str | None: - """The resolved version in frontend/package-lock.json.""" - if not PACKAGE_LOCK.is_file(): - return None - data = json.loads(PACKAGE_LOCK.read_text()) - entry = data.get("packages", {}).get(f"node_modules/{PACKAGE}", {}) - return entry.get("version") - - -def _hook_versions() -> tuple[str | None, str | None]: - """The biomejs/pre-commit `rev` and its additional_dependencies pin.""" - text = PRE_COMMIT_CONFIG.read_text() - block = re.search( - r"- repo:\s*https://github\.com/biomejs/pre-commit\s*\n(.*?)(?=\n\s*- repo:|\nci:|\Z)", - text, - re.DOTALL, - ) - if block is None: - return None, None - body = block.group(1) - rev = re.search(r"^\s*rev:\s*['\"]?v?([0-9][^'\"\s]*)['\"]?", body, re.MULTILINE) - dep = re.search(rf"{re.escape(PACKAGE)}@([0-9][^'\"\s]*)", body) - return (rev.group(1) if rev else None, dep.group(1) if dep else None) - - -def _schema_version() -> str | None: - """The version embedded in biome.json's "$schema" URL.""" - if not BIOME_CONFIG.is_file(): - return None - data = json.loads(BIOME_CONFIG.read_text()) - match = re.search(r"/schemas/([0-9][^/]*)/schema\.json", data.get("$schema", "")) - return match.group(1) if match else None - - -def main() -> int: - """Compare every pin against package.json and report any mismatch.""" - declared = _declared_version() - if declared is None: - print(f"ERROR: could not find {PACKAGE} in {PACKAGE_JSON}", file=sys.stderr) - return 1 - - hook_rev, hook_dep = _hook_versions() - found = { - "frontend/package-lock.json (resolved)": _locked_version(), - ".pre-commit-config.yaml (rev)": hook_rev, - ".pre-commit-config.yaml (additional_dependencies)": hook_dep, - "biome.json ($schema)": _schema_version(), - } - - mismatched = {where: v for where, v in found.items() if v is not None and v != declared} - missing = [where for where, v in found.items() if v is None] - - if not mismatched and not missing: - return 0 - - print( - f"Biome version pins disagree with frontend/package.json ({declared}):\n", - file=sys.stderr, - ) - for where, version in mismatched.items(): - print(f" {where}: {version}", file=sys.stderr) - for where in missing: - print(f" {where}: could not be read", file=sys.stderr) - print( - "\nThe pre-commit rev/additional_dependencies pair is what actually gates CI," - "\nso leaving these out of step means the lint gate silently enforces a" - "\ndifferent Biome ruleset than this repo declares. Update every pin above" - f"\nto {declared} (rev takes a leading 'v'), then re-run.", - file=sys.stderr, - ) - return 1 - - -if __name__ == "__main__": - raise SystemExit(main())