deckhearth/eslint.config.mjs
Randall Stillwell 2e68574393 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 01:17:18 -05:00

130 lines
4.2 KiB
JavaScript

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/**',
]),
// 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',
},
},
},
{
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.',
}],
},
},
// 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',
},
},
},
]);
export default eslintConfig;