From 222f283e07db0b684ff2e2d65450a285e15dfa78 Mon Sep 17 00:00:00 2001 From: Ryan Davis Date: Mon, 13 Jul 2026 22:27:55 -0700 Subject: [PATCH] build: #119 add Biome formatter and improve ESLint Add Biome for consistent code formatting, decoupled from ESLint so correctness and style concerns don't overlap. Add lint:fix, format, and format:check npm scripts. Rework eslint.config.js to layer: - js.recommended for base JS correctness (previously missing, so bugs like no-undef/no-unused-vars in plain JS went unchecked) - eslint-config-next for React/hooks/a11y coverage - eslint-plugin-import-x for import correctness, using a node resolver instead of Next's bundled import plugin, which crashes under ESLint 9 flat config due to a broken TypeScript resolver - eslint-config-prettier last, to disable stylistic rules now owned by Biome Bump CI Node.js version to 26 to match engines requirement. --- .github/workflows/code-analysis.yml | 2 +- biome.json | 47 +++++ eslint.config.js | 94 ++++++++- package-lock.json | 291 +++++++++++++++++++++++++++- package.json | 8 +- 5 files changed, 437 insertions(+), 5 deletions(-) create mode 100644 biome.json diff --git a/.github/workflows/code-analysis.yml b/.github/workflows/code-analysis.yml index 6745f62..c386bed 100644 --- a/.github/workflows/code-analysis.yml +++ b/.github/workflows/code-analysis.yml @@ -114,7 +114,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 26 cache: 'npm' - name: Install dependencies diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..b46aad3 --- /dev/null +++ b/biome.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.3/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "includes": [ + "**", + "!public/**", + "!package-lock.json", + "!.next/**", + "!out/**", + "!build/**" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100, + "lineEnding": "lf" + }, + "linter": { + "enabled": false, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "semicolons": "always", + "trailingCommas": "all" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/eslint.config.js b/eslint.config.js index f0d67c0..d80fa83 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,3 +1,93 @@ -const nextConfig = require("eslint-config-next/core-web-vitals"); +// ESLint flat config (ESLint 9+). CommonJS to match the repo (no "type":"module"). +// +// Layering, applied in order — later entries win on conflicts: +// 1. ignores — vendored + build output we must NOT lint +// 2. js.recommended — the base-JavaScript correctness floor (no-undef, no-unused-vars, +// no-dupe-keys, no-unreachable, use-isnan, ...). eslint-config-next +// does NOT include this, so without it the whole plain-JS bug class +// goes unchecked. This is the coverage that was missing. +// 3. next/core-web-vitals — React + React Hooks (incl. React-Compiler rules) + jsx-a11y +// + @next/next, and 1000+ browser/node globals. +// 4. import-x — import-correctness (catches importing a name that doesn't exist). +// See the long note below for why we use import-x, not Next's bundled +// import plugin. +// 5. project tuning — severity choices for THIS repo. +// 6. eslint-config-prettier — MUST be last: turns off any stylistic rules so ESLint and +// the Biome formatter never fight. Biome owns formatting; ESLint owns +// correctness. No overlap. +// +// Blocking: the CI ESLint job is still report-only (continue-on-error) — findings surface in +// GitHub code-scanning but do not fail PRs yet. Promoting a rule to a hard gate later is a +// one-line change here (flip a "warn" to "error") plus removing continue-on-error in CI. +// +// WHY import-x INSTEAD OF Next's bundled eslint-plugin-import: +// Next ships a vendored eslint-plugin-import + eslint-module-utils whose default resolver is a +// TypeScript resolver. Under ESLint 9 flat config that resolver loads with an "invalid +// interface" and CRASHES the entire run the moment a resolver-dependent rule (import/named, +// import/no-unresolved) hits a file that imports a package. Overriding the resolver setting +// doesn't help because ESLint deep-merges `settings`, so Next's broken `typescript` resolver +// can't be removed by omission. eslint-plugin-import-x is the maintained, flat-config-native +// fork; we register it under its own `import-x/*` namespace with a node-only resolver (correct +// for this pure-JS repo) and leave Next's import rules untouched. This is dev-time only — +// Next 16's `next build` does not run ESLint, so none of this can affect the build or deploy. -module.exports = nextConfig; +const js = require("@eslint/js"); +const nextCoreWebVitals = require("eslint-config-next/core-web-vitals"); +const importX = require("eslint-plugin-import-x"); +const prettier = require("eslint-config-prettier"); + +// eslint-config-next exports an array of flat-config objects; spread it in. +const nextConfigs = Array.isArray(nextCoreWebVitals) + ? nextCoreWebVitals + : [nextCoreWebVitals]; + +module.exports = [ + // 1. Ignore what isn't ours. A config object with ONLY `ignores` is global. + // public/ holds the vendored Q.js library + static assets — linting it produced 38 + // spurious no-undef errors, so it's excluded here. + { + ignores: [ + "node_modules/**", + ".next/**", + "out/**", + "build/**", + "public/**", + "next-env.d.ts", + ], + }, + + // 2. Base JavaScript correctness — the previously-missing floor. + js.configs.recommended, + + // 3. Next.js / React / hooks / a11y / @next (existing coverage + globals). + ...nextConfigs, + + // 4. Import correctness via import-x (see header note). Node resolver = correct for pure JS. + { + plugins: { "import-x": importX }, + settings: { + "import-x/resolver": { node: { extensions: [".js", ".jsx", ".json"] } }, + }, + rules: { + // Imported a name the target module doesn't export (e.g. requestIsCertificateValid, + // Typograph). no-unused-vars can't catch these — they're imported AND used. + "import-x/named": "warn", + // Import path that doesn't resolve to a real module. + "import-x/no-unresolved": "warn", + }, + }, + + // 5. Project-specific severity tuning. + { + rules: { + // Dead vars/imports are worth surfacing but there are ~100 today; keep them visible as + // warnings rather than a wall of errors while we're still non-blocking. Ratchet to + // "error" once the backlog is cleared. + "no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], + }, + }, + + // 6. Disable formatting-related rules so Biome (the formatter) is the single source of truth. + // Keep LAST so it overrides anything stylistic enabled above. + prettier, +]; diff --git a/package-lock.json b/package-lock.json index fd99ff2..1b83b00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,8 +28,11 @@ "react-split": "^2.0.14" }, "devDependencies": { + "@biomejs/biome": "^2.5.3", "eslint": "^9.0.0", - "eslint-config-next": "^16.2.6" + "eslint-config-next": "^16.2.6", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-import-x": "^4.17.1" }, "engines": { "node": ">=26" @@ -311,6 +314,181 @@ "node": ">=6.9.0" } }, + "node_modules/@biomejs/biome": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.3.tgz", + "integrity": "sha512-MrJswFdei9EfDwwUy2tQrPDpK0AO+RmMFvBoaaJ6ayBc3sUbHdCE+XG5N8vp+5So41ZupZJQm0roHFFhMGVD7A==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.5.3", + "@biomejs/cli-darwin-x64": "2.5.3", + "@biomejs/cli-linux-arm64": "2.5.3", + "@biomejs/cli-linux-arm64-musl": "2.5.3", + "@biomejs/cli-linux-x64": "2.5.3", + "@biomejs/cli-linux-x64-musl": "2.5.3", + "@biomejs/cli-win32-arm64": "2.5.3", + "@biomejs/cli-win32-x64": "2.5.3" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.3.tgz", + "integrity": "sha512-QhYP9muVQ0nUO5zztFuPbEwi4+94sJWVjaZds9aMi1l/KNZBiUjdiSUrGHsTaMGDXrYl+r4AS2sUKfgH3w+V3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.3.tgz", + "integrity": "sha512-NC1Ss13UaW7QZX+y8j44bF7AP0jSJdBl6iRhe0MAkvaSqZy+mWg3GaXsrb+eSoHoGDBtaXWEbMVV0iVN2cZ7cQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.3.tgz", + "integrity": "sha512-ksx1KWeyYW18ILL04msF/J4ZBtBDN33znYK8Z/aNv/vlBVxL9/g3mGP+omgHJKy4+KWbK87vcmmpmurfNjSgiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.3.tgz", + "integrity": "sha512-fccix0w6xp6csCXgxeC0dU/3ecgRQal0y+cv2SP9ajNlhe7Yrk2Ug7UDe2j9AT9ZDYitkXpvUKgZjjuoYeP4Vg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.3.tgz", + "integrity": "sha512-yMkJtilsgvILDcVkh187aVLTb64xYsrxYajx5kym+r1ULkO5HUOfu9AYKLGQbOVLwJtT2utNw7hhFNg+17mUYA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.3.tgz", + "integrity": "sha512-O/yU9YKRUiHhmcjF2f38PSjseVk3G4VLWYc0G2HWpzdBVREV6G8IGWIVEFf7MFPfWIzNUIvPsEjeAZQIOgnLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.3.tgz", + "integrity": "sha512-cX5z+GYwRcqEok0AH3KSfQGgqYd0Nomfp6Fbe1uiTtELE38hdH2k842wQ9wLNaF/JJ7r4rjJQ4VR+ce+fRmQbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.3.tgz", + "integrity": "sha512-ExSaJWi4/u6+GXCszlSKpWSjKNbDseAYqqkCznsCsZ/4uidZ/BEqsCc5/3ctlq6dfIubdIIRSVLC/PG9xPl70Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -3824,6 +4002,16 @@ "node": ">= 10" } }, + "node_modules/comment-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.7.tgz", + "integrity": "sha512-0h+uSNtQGW3D98eQt3jJ8L06Fves8hncB4V/PKdw/Qb8Hnk19VaKuTr55UNRYiSoVa7WwrFls+rh3ux9agmkeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5218,6 +5406,47 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-context": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", + "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-tsconfig": "^4.10.1", + "stable-hash-x": "^0.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-context" + }, + "peerDependencies": { + "unrs-resolver": "^1.0.0" + }, + "peerDependenciesMeta": { + "unrs-resolver": { + "optional": true + } + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.10", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", @@ -5292,6 +5521,56 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-plugin-import-x": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.17.1.tgz", + "integrity": "sha512-4cdstYkKCyjumM2Q9NSI03K8D2a9F4Ssz33K2lv2hQa4KmR9jPLwk3uWGtNvclfqBrPGfGuMBwsGMbe6dMRbfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "^8.56.0", + "comment-parser": "^1.4.1", + "debug": "^4.4.1", + "eslint-import-context": "^0.1.9", + "is-glob": "^4.0.3", + "minimatch": "^9.0.3 || ^10.1.2", + "semver": "^7.7.2", + "stable-hash-x": "^0.2.0", + "unrs-resolver": "^1.9.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-import-x" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "eslint-import-resolver-node": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/utils": { + "optional": true + }, + "eslint-import-resolver-node": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-import-x/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-plugin-react-hooks": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", @@ -8174,6 +8453,16 @@ "dev": true, "license": "MIT" }, + "node_modules/stable-hash-x": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", + "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", diff --git a/package.json b/package.json index 51f294e..4e7a9d2 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,9 @@ "build": "next build", "start": "next start", "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "biome check --write .", + "format:check": "biome check .", "postinstall": "next telemetry disable" }, "dependencies": { @@ -32,7 +35,10 @@ "react-split": "^2.0.14" }, "devDependencies": { + "@biomejs/biome": "^2.5.3", "eslint": "^9.0.0", - "eslint-config-next": "^16.2.6" + "eslint-config-next": "^16.2.6", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-import-x": "^4.17.1" } }