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>
130 lines
4.2 KiB
JavaScript
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;
|