From cam-fleet-ops
Upgrade a single repo from ESLint 8 + legacy `.eslintrc.*` config to ESLint 9 + flat config (`eslint.config.js` / `.mjs`). Handles all the v9 breaking changes: dropped `.flat` namespace in plugins, `@eslint/js` export shape, `eslint-config-next` incompatibility, `react/display-name` rule broken in v7.x. Cites real fixes from a 3-repo migration (fleetflow-mvp, family-planner, Shift6).
How this skill is triggered — by the user, by Claude, or both
Slash command
/cam-fleet-ops:eslint-flat-config-upgradeThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
ESLint 9 was released in early 2024 and is the new default. The flat-config format (`eslint.config.js` / `.mjs`) replaces the legacy `.eslintrc.*` family. Most repos on ESLint 8 with legacy configs need a real migration, not a `npm i eslint@9` — the v9 changes break things silently.
ESLint 9 was released in early 2024 and is the new default. The flat-config format (eslint.config.js / .mjs) replaces the legacy .eslintrc.* family. Most repos on ESLint 8 with legacy configs need a real migration, not a npm i eslint@9 — the v9 changes break things silently.
This skill covers the upgrade for a single repo. For fleet-wide upgrades, run this skill 3 times (once per repo) or use fleet-ci-audit first to identify the queue.
.eslintrc.json / .eslintrc.cjs / .eslintrc.js fileTypeError: Cannot read properties of undefined (reading 'recommended') after a naive npm i eslint@9 upgradeBefore starting, verify the repo is actually on ESLint 8:
gh api repos/<org>/<repo>/contents/package.json | jq -r '.content' | base64 -d | jq '.devDependencies.eslint'
If the version is ^9.x or higher already, the user might just need help debugging the config — not migrating. Run a quick gh run list --workflow lint.yml to see if the lint is actually broken.
If the version is ^8.x or lower, proceed with the migration.
These are the issues that actually break when you npm i eslint@9 on a legacy config. Each has a real fix.
eslint-plugin-react-hooks@^4 peer-dep conflict with ESLint 9The error:
npm error ERESOLVE could not resolve
npm error dev eslint@"^9.15.0" from the root project
npm error Conflicting peer dependency: [email protected]
npm error dev eslint-plugin-react-hooks@"^4.6.0" from the root project
The fix:
"eslint-plugin-react-hooks": "^5.2.0"
v5 of eslint-plugin-react-hooks is the first version that supports ESLint 9.
eslint-plugin-react-hooks@^5 dropped the .flat namespaceThe error:
TypeError: Cannot read properties of undefined (reading 'recommended')
This is because in v4, the recommended config was at reactHooks.configs.flat.recommended. In v5, it's at reactHooks.configs['recommended-latest'] or reactHooks.configs.recommended.
The fix (in eslint.config.js):
// v4 (broken on ESLint 9):
extends: [reactHooks.configs.flat.recommended],
// v5 (works on ESLint 9):
extends: [reactHooks.configs['recommended-latest']],
Same issue for eslint-plugin-react-refresh — use reactRefresh.configs.vite (no .flat).
@eslint/js default export shape changedIn v9, @eslint/js is the default export of an object with .configs.recommended. The js variable in import js from '@eslint/js' is the recommended config. The error Cannot read properties of undefined (reading 'recommended') is sometimes a sign the import shape is wrong (e.g., trying to do js.configs.recommended when js itself is the config, or when the import returned undefined due to a missing dep).
The fix (in eslint.config.js):
// Confirm the dep is in package.json devDependencies:
"@eslint/js": "^9.15.0"
"globals": "^15.14.0"
// And the import:
import js from '@eslint/js'
import globals from 'globals'
// If you want the recommended config, just use `js` directly:
extends: [js, ...] // js IS the config object
eslint-config-next v15 incompatibilityeslint-config-next@^15.5.19 and some v14 builds break with ESLint 9 because:
eslint-config-next changed between versions@rushstack/eslint-patch which fails on Node 20+The error:
TypeError: nextVitals is not iterable
TypeError: Expected an object but received /path/to/eslint-config-next/index.js
The fix (for repos that aren't strictly using Next.js ESLint rules):
// Replace eslint-config-next with a minimal tseslint config:
import tseslint from 'typescript-eslint'
export default tseslint.config(
...tseslint.configs.recommended,
{
rules: {
'no-undef': 'off',
'no-unused-vars': 'off',
'react-hooks/exhaustive-deps': 'warn',
},
}
)
[email protected]'s react/display-name rule is broken on ESLint 9The error:
TypeError: Error while loading rule 'react/display-name': contextOrFilename.getFilename is not a function
The fix:
rules: {
'react/display-name': 'off',
}
git clone https://github.com/<org>/<repo>.git /tmp/<repo>
cd /tmp/<repo>
# Confirm ESLint version
cat package.json | jq '.devDependencies.eslint'
In package.json devDependencies:
{
"eslint": "^9.15.0",
"@eslint/js": "^9.15.0",
"globals": "^15.14.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"typescript-eslint": "^8.46.0"
}
If the repo has eslint-plugin-react (not the hooks plugin), keep it but make sure it's ^7.33.2 or later. The react/display-name rule is broken in ^7.0.0 through some 7.34 versions.
If the repo had .eslintrc.json:
# Back up the old config (don't delete — empty it)
echo "// Replaced by eslint.config.js (ESLint 9 flat config)" > .eslintrc.json
If the repo had .eslintrc.cjs:
module.exports = {};
Then create eslint.config.js:
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist', 'dev-dist', 'ios', 'android', 'node_modules', 'coverage', '**/*.ts', '**/*.tsx']),
{
files: ['**/*.{js,jsx}'],
ignores: ['**/*.test.{js,jsx}', '**/test/**'],
extends: [
js.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
globals: globals.browser,
parserOptions: {
ecmaFeatures: { jsx: true },
},
},
rules: {
'no-undef': 'off',
'no-unused-vars': 'off',
'no-empty': 'off',
'no-unused-expressions': 'off',
'no-cond-assign': 'off',
'no-prototype-builtins': 'off',
'no-useless-escape': 'off',
'prefer-const': 'off',
'react-hooks/exhaustive-deps': 'warn',
},
},
])
Common patterns to fix:
{
"scripts": {
"lint": "eslint . --ext js,jsx" // for pure JS/JSX repos
"lint": "eslint . --ext js,jsx,ts,tsx" // for TS repos
"lint": "next lint" // DEPRECATED in Next 16 — replace with eslint directly
"lint": "eslint ." // already fine
}
}
If lint.yml uses npm ci, change to npm install so the new lockfile is regenerated:
- name: Install dependencies
run: npm install --no-audit --no-fund
git add -A
git commit -m "chore(deps): upgrade ESLint to v9 (flat config)"
git push origin main
Wait 3-5 min for the next push-triggered run. If the new run fails:
TypeError: Cannot read properties of undefined — usually a missing depnpm error ERESOLVE — usually a peer-dep conflictPrisma CLI Version 5.22.0 — Prisma is on a different path (see prisma-fleet-migration)If it succeeds: commit and move to the next repo.
After the migration, the new config will surface old eslint-disable comments that no longer match disabled rules. Run:
npx eslint . --fix
This removes unused eslint-disable-next-line directives and applies safe fixes. Commit and push.
.eslintrc.cjs and eslint.config.js — ESLint will load the legacy config as a fallback. Empty it out: echo "" > .eslintrc.cjs (or replace with module.exports = {}; for CJS).js.configs.recommended if js is undefined — first verify the import. import js from '@eslint/js' should give an object. If it gives undefined, the dep isn't installed.tseslint.configs.recommended doesn't include JSX — for repos with .jsx files, either add JSX parser config or drop --ext jsx from the lint script.eslint-config-next@^14 works with ESLint 8 but breaks on ESLint 9. If the repo absolutely must keep it, downgrade the repo to ESLint 8.reactHooks.configs.flat.recommended legacy path is removed in v5 — always use the new path configs['recommended-latest'] or configs.recommended.eslint.config.js parsesnpx eslint . runs without "Cannot read properties" or other v9 errorscompleted/success--ext ts,tsx warning if the repo has no TypeScriptfleet-ci-audit — Use this skill first to identify which repos in the fleet need the upgrade. The audit's "billing cap" vs "real source error" categorization is the triage step.prisma-fleet-migration — Often the same repos that need ESLint 9 also need Prisma 7. Migrate ESLint first (lower-risk), then Prisma.lockfile-regen — When npm ci fails because the lockfile doesn't have the new ESLint plugins, this is the fix.github-fleet-lint-cleanup — The single-skill format of this content. Useful if you only need the lint cleanup portion, not the v9 migration.npx claudepluginhub camster91/cam-fleet-bundle --plugin cam-fleet-opsProvides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.