deckhearth/eslint.config.mjs

131 lines
4.2 KiB
JavaScript
Raw Normal View History

bump: next 15.4.3 -> 16.2.6, ESLint flat config (v9 fallback), typescript devDep Closes P0 ship-blocker #8 from .convoys/ship-readiness.md. Vercel has been refusing every deployment since 2025-08-01 with "Vulnerable version of Next.js detected, please update immediately" — this bump clears that platform gate and unblocks every downstream preview-smoke and visual-diff gate that depends on a live preview URL. Changes (per Brief 1 acceptance criteria, all four gate-1 decisions applied — see .convoys/bump-next-js.md § Decisions for the audit trail): - next: ^15.4.2 -> ^16.2.6 (resolves next@16.2.6) - eslint: ^8 -> ^9.39.4 (Decision D fallback; v10 surfaced Risk R15 empirically — @typescript-eslint/scope-manager@8.59.4 bundled by eslint-config-next@16 doesn't implement v10's new addGlobals API) - eslint-config-next: 15.4.2 -> ^16.2.6 - typescript: newly added at ^5.9.3 as a devDep (Decision C; required by typescript-eslint chain regardless of ESLint major) - scripts.lint: "next lint" -> "eslint ." (next lint removed in 16) - next.config.js: images.domains -> images.remotePatterns (deprecated and removed in Next 16; preserves the three CDN hosts Scryfall, Pokemon TCG, Lorcana API for eventual next/image adoption) - .eslintrc.json deleted (eslint-config-next@16 is flat-config-only) - eslint.config.mjs added (verbatim shape from Next docs; verified forward-compatible with v10 so bump-eslint-10 will not need to touch this file) Out of scope (deferred to dedicated convoys): - React 18 -> 19 (bump-react) - App Router migration (multi-month effort) - Test runner adoption (adopt-vitest, adopt-playwright-smoke) - Lint baseline cleanup (fix-lint-baseline) — new v9 baseline is 128 problems (81 errors, 47 warnings), up from prior ~100 due to eslint-plugin-react-hooks@7.1.1 + @next/eslint-plugin-next@16.2.6 rule additions - ESLint v10 adoption (bump-eslint-10) — upstream-blocked on typescript-eslint shipping a v10-tested release that eslint-config-next then bundles - TypeScript 6 adoption (bump-typescript-6) — same upstream block Local verification: - npm install: clean, no ERESOLVE warnings - npm run build: exit 0, Next 16.2.6 (Turbopack), ~1.4s compile, 23 static pages + 47 API routes, no images.domains deprecation - npm run lint: exit 1, 128 problems, runs to completion (tolerated by CI's `|| true` wrapper; new baseline for fix-lint-baseline) Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 03:04:02 -04:00
import { defineConfig, globalIgnores } from 'eslint/config';
import nextVitals from 'eslint-config-next/core-web-vitals';
const eslintConfig = defineConfig([
...nextVitals,
globalIgnores([
'.next/**',
'node_modules/**',
'out/**',
'build/**',
'next-env.d.ts',
'scripts/migrations/**',
]),
convoy: enable no-undef ESLint rule + fix 3 latent bugs it surfaced PR #144 (`31da384`, 2026-06-13) shipped a `ReferenceError: useFocusTrap is not defined` to production because the flat ESLint config did NOT enable the core `no-undef` rule — only `react/jsx-no-undef` (which catches undefined JSX components, not plain JS identifier references). This PR closes that gap, narrowly. ## What changes - `eslint.config.mjs`: enable `no-undef: 'error'` for source files + define the ~40 browser / Node / Vitest globals the rule needs. Hand-curated globals list (rejected pulling in the `globals` npm package for one config block). - 3 latent bugs surfaced + fixed (NOT silenced with disables): | Site | Bug | Fix | |------|-----|-----| | `components/CollectionPageView.js:238` | `onClick={toggleFavorite}` — fn defined in `lib/use-collection-view.js:269` (collection-level favorite) but missing from the hook's `return {}` | Added to hook return + component destructure | | `components/CollectionPageView.js:532` | `onTogglePublic={togglePublic}` — same pattern, fn at line 315 of the hook | Same shape: hook return + destructure | | `components/ShareModal.js:99` | `fetchInvitedUsers()` scoped inside the useEffect body but called from `handleInvite` outside | Extracted to component scope via `useCallback`; effect dep array updated | Bugs 1 + 2 broke the "Favorite collection" button and the public-toggle in the Share modal on the collection-detail page. Bug 3 broke the "refresh invitee list" path after a successful invite. None had been flagged because the operator hadn't exercised those exact flows since the relevant hooks were last refactored. - `components/ShareModal.js`: also adds an eslint-disable for `react-hooks/set-state-in-effect` on the moved `fetchInvitedUsers()` call. Matches the canonical pattern in `pages/profile.js:90` — async fetch; setState fires post-resolve, not synchronously to the effect body. ## Why not pull in @eslint/js/recommended wholesale? The recommended bundle also enables `no-unused-vars`, `no-prototype-builtins`, `no-empty`, `no-cond-assign`, and ~10 others — each would generate dozens of pre-existing violations on this codebase. The right rule-by-rule sweep is the deferred `adopt-eslint-recommended-set` convoy. This PR is scoped to the one rule that would have caught PR #144's bug class. ## Test plan - [x] `npm run lint` — clean (1 pre-existing unrelated warning on `CollectionsPageView.js`'s `eslint-disable` directive — out of scope) - [x] `npm run test:run` — 25 files / 123 tests pass - [ ] CI on this PR - [ ] Post-merge: exercise the three formerly-broken paths (favorite a collection from its detail page; toggle a collection public via Share modal; invite a user and confirm the invitee list refreshes) ## Convoy doc `.convoys/enable-no-undef-eslint-rule.md` documents the surfaced bugs, D1 (no-undef only vs recommended bundle), D2 (hand-curated globals vs `globals` package), risks, and acceptance. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-13 02:17:18 -04:00
// The flat config from `eslint-config-next/core-web-vitals` does NOT
// enable the core `no-undef` rule for plain-JS identifier references
// — only `react/jsx-no-undef`, which catches undefined JSX components
// but NOT plain function/variable references like `useFocusTrap(...)`
// in a hook call. PR #144 (`31da384`) shipped a runtime
// ReferenceError to production because of this gap; that bug would
// have been caught at lint time with the rule on. We enable it
// directly rather than pulling in `@eslint/js/recommended` (which
// would also turn on `no-unused-vars`, `no-prototype-builtins`, and
// a handful of others that surface a flood of pre-existing
// violations and risk derailing this hotfix-class change).
//
// Browser + Node globals (window, document, process, Buffer, etc.)
// are sourced from the language-options block below.
{
rules: {
'no-undef': 'error',
},
languageOptions: {
globals: {
// Browser
window: 'readonly',
document: 'readonly',
navigator: 'readonly',
fetch: 'readonly',
FormData: 'readonly',
File: 'readonly',
Blob: 'readonly',
URL: 'readonly',
URLSearchParams: 'readonly',
localStorage: 'readonly',
sessionStorage: 'readonly',
location: 'readonly',
history: 'readonly',
alert: 'readonly',
confirm: 'readonly',
prompt: 'readonly',
atob: 'readonly',
btoa: 'readonly',
crypto: 'readonly',
WebSocket: 'readonly',
Image: 'readonly',
MediaStream: 'readonly',
MediaStreamTrack: 'readonly',
ImageCapture: 'readonly',
MediaRecorder: 'readonly',
AbortController: 'readonly',
Event: 'readonly',
CustomEvent: 'readonly',
HTMLElement: 'readonly',
HTMLInputElement: 'readonly',
HTMLImageElement: 'readonly',
HTMLVideoElement: 'readonly',
HTMLCanvasElement: 'readonly',
Element: 'readonly',
Node: 'readonly',
NodeList: 'readonly',
DOMException: 'readonly',
IntersectionObserver: 'readonly',
MutationObserver: 'readonly',
ResizeObserver: 'readonly',
requestAnimationFrame: 'readonly',
cancelAnimationFrame: 'readonly',
getComputedStyle: 'readonly',
// Timers (shared between browser + node)
setTimeout: 'readonly',
clearTimeout: 'readonly',
setInterval: 'readonly',
clearInterval: 'readonly',
queueMicrotask: 'readonly',
// Console + globals
console: 'readonly',
globalThis: 'readonly',
// Node / Next.js runtime
process: 'readonly',
Buffer: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
module: 'readonly',
require: 'readonly',
exports: 'readonly',
global: 'readonly',
},
},
},
chore(lint): forbid require() in scripts/** under "type": "module" (#29) Add an ESLint no-restricted-syntax rule scoped to scripts/**/*.js that flags any CallExpression with callee name `require`. The recurring bug pattern: helper scripts under scripts/ that use CJS require() throw `ReferenceError: require is not defined` on Node 22.x because package.json has had "type": "module" since bump-next-js. The bug has bitten twice in two convoys — once in drop-public-setup Brief 2 (setup-neon-db.js, commit b63b509) and again in fix-reset-db-script (reset-db.js, PR #25 squash 3ab9bf8). Both were caught at first run, not at lint time. This rule would have caught both at PR time. Rule shape: a second flat-config block at the end of eslint.config.mjs (NOT in the root rules block) targeting only scripts/**/*.js. The error message points at .convoys/fix-reset-db-script.md so the next agent who trips it gets a 1-click path to the exemplar fix (ESM top-level imports for dotenv, neon, bcrypt) instead of having to re-derive it. scripts/migrations/** is already in globalIgnores from pick-a-name Brief 2 and stays excluded. Blast-radius rationale (scripts/** only, not all .js at repo root): matches the actual observed bug surface. pages/api/** is already correctly ESM-imported throughout (verified across add-rate-limiting, cors-tighten, and the add-route skill). The config files (postcss.config.js, tailwind.config.js, next.config.js) intentionally use CJS-style exports that the next-config base rules already handle correctly. A repo-wide ban would produce zero true positives outside scripts/** today and would require explicit allowlist for every config file — strictly more code, more maintenance, zero benefit. Convoy file: .convoys/lint-against-cjs-in-esm-scripts.md (P3 polish, parent-owned, no architect — preventative one-line rule following two proven bug recurrences). Verification: - node --check eslint.config.mjs: exit 0 - npm run lint: 128 problems (81 errors, 47 warnings) — baseline preserved verbatim, zero new false positives in current tree - Negative test: prepended `const x = require('fs');` to scripts/reset-db.js, ran npm run lint, observed exit 1 with 129 problems and the rule firing at line 20:11 with the documented message, then reverted to 128 problems clean - npm run test:run: 21/21 pass (no test surface touched) - Grep: 0 require( occurrences in scripts/**/*.js (current tree is clean; rule starts with zero positives to silence on day 1) Surfaces no new follow-up — this convoy IS the follow-up surfaced by fix-reset-db-script. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 23:53:31 -04:00
{
files: ['scripts/**/*.js'],
rules: {
'no-restricted-syntax': ['error', {
selector: 'CallExpression[callee.name="require"]',
message: 'Use ESM `import` syntax. `package.json` has "type": "module"; require() throws ReferenceError at runtime. See .convoys/fix-reset-db-script.md.',
}],
},
},
convoy: enable no-undef ESLint rule + fix 3 latent bugs it surfaced PR #144 (`31da384`, 2026-06-13) shipped a `ReferenceError: useFocusTrap is not defined` to production because the flat ESLint config did NOT enable the core `no-undef` rule — only `react/jsx-no-undef` (which catches undefined JSX components, not plain JS identifier references). This PR closes that gap, narrowly. ## What changes - `eslint.config.mjs`: enable `no-undef: 'error'` for source files + define the ~40 browser / Node / Vitest globals the rule needs. Hand-curated globals list (rejected pulling in the `globals` npm package for one config block). - 3 latent bugs surfaced + fixed (NOT silenced with disables): | Site | Bug | Fix | |------|-----|-----| | `components/CollectionPageView.js:238` | `onClick={toggleFavorite}` — fn defined in `lib/use-collection-view.js:269` (collection-level favorite) but missing from the hook's `return {}` | Added to hook return + component destructure | | `components/CollectionPageView.js:532` | `onTogglePublic={togglePublic}` — same pattern, fn at line 315 of the hook | Same shape: hook return + destructure | | `components/ShareModal.js:99` | `fetchInvitedUsers()` scoped inside the useEffect body but called from `handleInvite` outside | Extracted to component scope via `useCallback`; effect dep array updated | Bugs 1 + 2 broke the "Favorite collection" button and the public-toggle in the Share modal on the collection-detail page. Bug 3 broke the "refresh invitee list" path after a successful invite. None had been flagged because the operator hadn't exercised those exact flows since the relevant hooks were last refactored. - `components/ShareModal.js`: also adds an eslint-disable for `react-hooks/set-state-in-effect` on the moved `fetchInvitedUsers()` call. Matches the canonical pattern in `pages/profile.js:90` — async fetch; setState fires post-resolve, not synchronously to the effect body. ## Why not pull in @eslint/js/recommended wholesale? The recommended bundle also enables `no-unused-vars`, `no-prototype-builtins`, `no-empty`, `no-cond-assign`, and ~10 others — each would generate dozens of pre-existing violations on this codebase. The right rule-by-rule sweep is the deferred `adopt-eslint-recommended-set` convoy. This PR is scoped to the one rule that would have caught PR #144's bug class. ## Test plan - [x] `npm run lint` — clean (1 pre-existing unrelated warning on `CollectionsPageView.js`'s `eslint-disable` directive — out of scope) - [x] `npm run test:run` — 25 files / 123 tests pass - [ ] CI on this PR - [ ] Post-merge: exercise the three formerly-broken paths (favorite a collection from its detail page; toggle a collection public via Share modal; invite a user and confirm the invitee list refreshes) ## Convoy doc `.convoys/enable-no-undef-eslint-rule.md` documents the surfaced bugs, D1 (no-undef only vs recommended bundle), D2 (hand-curated globals vs `globals` package), risks, and acceptance. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-13 02:17:18 -04:00
// Vitest test files have an additional set of globals (describe, it,
// expect, vi, beforeEach, etc.). Defining them here keeps the test
// files from triggering no-undef while leaving the rule strict for
// source files.
{
files: ['test/**/*.js', '**/*.test.js', '**/*.test.ts', 'test/setup.js'],
languageOptions: {
globals: {
describe: 'readonly',
it: 'readonly',
test: 'readonly',
expect: 'readonly',
vi: 'readonly',
beforeEach: 'readonly',
afterEach: 'readonly',
beforeAll: 'readonly',
afterAll: 'readonly',
},
},
},
bump: next 15.4.3 -> 16.2.6, ESLint flat config (v9 fallback), typescript devDep Closes P0 ship-blocker #8 from .convoys/ship-readiness.md. Vercel has been refusing every deployment since 2025-08-01 with "Vulnerable version of Next.js detected, please update immediately" — this bump clears that platform gate and unblocks every downstream preview-smoke and visual-diff gate that depends on a live preview URL. Changes (per Brief 1 acceptance criteria, all four gate-1 decisions applied — see .convoys/bump-next-js.md § Decisions for the audit trail): - next: ^15.4.2 -> ^16.2.6 (resolves next@16.2.6) - eslint: ^8 -> ^9.39.4 (Decision D fallback; v10 surfaced Risk R15 empirically — @typescript-eslint/scope-manager@8.59.4 bundled by eslint-config-next@16 doesn't implement v10's new addGlobals API) - eslint-config-next: 15.4.2 -> ^16.2.6 - typescript: newly added at ^5.9.3 as a devDep (Decision C; required by typescript-eslint chain regardless of ESLint major) - scripts.lint: "next lint" -> "eslint ." (next lint removed in 16) - next.config.js: images.domains -> images.remotePatterns (deprecated and removed in Next 16; preserves the three CDN hosts Scryfall, Pokemon TCG, Lorcana API for eventual next/image adoption) - .eslintrc.json deleted (eslint-config-next@16 is flat-config-only) - eslint.config.mjs added (verbatim shape from Next docs; verified forward-compatible with v10 so bump-eslint-10 will not need to touch this file) Out of scope (deferred to dedicated convoys): - React 18 -> 19 (bump-react) - App Router migration (multi-month effort) - Test runner adoption (adopt-vitest, adopt-playwright-smoke) - Lint baseline cleanup (fix-lint-baseline) — new v9 baseline is 128 problems (81 errors, 47 warnings), up from prior ~100 due to eslint-plugin-react-hooks@7.1.1 + @next/eslint-plugin-next@16.2.6 rule additions - ESLint v10 adoption (bump-eslint-10) — upstream-blocked on typescript-eslint shipping a v10-tested release that eslint-config-next then bundles - TypeScript 6 adoption (bump-typescript-6) — same upstream block Local verification: - npm install: clean, no ERESOLVE warnings - npm run build: exit 0, Next 16.2.6 (Turbopack), ~1.4s compile, 23 static pages + 47 API routes, no images.domains deprecation - npm run lint: exit 1, 128 problems, runs to completion (tolerated by CI's `|| true` wrapper; new baseline for fix-lint-baseline) Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 03:04:02 -04:00
]);
export default eslintConfig;