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>
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>
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>