ENG-2118 Investigate whether to upgrade repo ESLint from version 8 to 9 - #1293
Draft
trangdoan982 wants to merge 1 commit into
Draft
ENG-2118 Investigate whether to upgrade repo ESLint from version 8 to 9#1293trangdoan982 wants to merge 1 commit into
trangdoan982 wants to merge 1 commit into
Conversation
Working proof-of-concept for the ENG-2118 investigation. Not intended to merge as-is: ENG-2118 scopes the actual upgrade out and the upgrade should land under its own ticket. Pushed so the diff and CI results are reviewable alongside the findings. - eslint 8.57.1 -> ^9.39.5, @types/eslint 8.56.12 -> ^9.6.1 (catalog) - typescript-eslint ^7.18.0 -> ^8.67.0 - drop @vercel/style-guide and eslint-config-turbo: both were declared but never imported, and @vercel/style-guide was the only remaining hard pin on eslint <9 - drop the now-unneeded eslint ^8.57 allowedDeprecatedVersions entry Three breaking changes had to be handled for the repo to lint at all: - tsconfigRootDir must be an absolute path under typescript-eslint v8; "." is rejected and every file fails to parse. Uses import.meta.dirname (safe: the repo already requires Node >=22). - Type-aware linting must be scoped to TS files. With project: true applied to every file, v8 errors on .mjs files absent from any tsconfig where v7 skipped them. The glob must include .mts/.cts or ESLint crashes outright on packages/database/scripts. - Plain JS needs disableTypeChecked for the same reason. Result: 0 errors across all 7 linted workspaces, turbo check-types 8/8. Repo-wide warnings go 573 -> 754; see the PR body for the rule-level breakdown and why `eslint --fix` must not be run over this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
Collaborator
|
Really great work! Re |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Scope check
$scope-checkagainst the ENG ticket and final diff.Done When:Yes — deliberately, and this PR is not meant to merge as-is.
ENG-2118 lists "upgrade directly here" as out of scope, and its
Done Whenis a written recommendation. This PR carries the working upgrade anyway, as a reference implementation, for two reasons:docs/or RFC convention, so the findings have no natural home as a committed file — they live in this PR body instead.If the recommendation is accepted, the upgrade should land under its own ticket (blocking ENG-1832), and this PR should be closed rather than merged. Keeping it draft to make that explicit.
Recommendation: upgrade
Measured empirically by performing the upgrade in a worktree off
main@5f4d9ea0, not inferred from release notes.Why now
eslint-plugin-obsidianmd@0.4.1, needed by ENG-1832, requires:maintodayeslint>=9.19.0@eslint/js^9.30.1typescript-eslint^8.35.1Verified: with this branch applied,
pnpm add -D eslint-plugin-obsidianmdinstalls with zero ESLint-related peer warnings. (That probe was removed from the diff — it belongs to ENG-1832.)Why it's cheaper than it looks
The expensive part of an 8→9 migration is the flat-config rewrite, and that is already done. All 7 linted workspaces use
eslint.config.mjs, there are zero.eslintrc*files, and ESLint is pinned in exactly onecatalog:entry.Only one dependency genuinely blocks v9:
typescript-eslint@7.18.0(peerseslint ^8.56.0). Every other plugin already declares v9 support. The one other blocker,@vercel/style-guide@6.0.0(peerseslint <9), turns out to be dead — declared but never imported. Deleting it removes every remaining peer conflict.Also worth noting:
@eslint/jsis already at 9.34.0 whileeslintis 8.57.1. That mismatch is latent today; this resolves it. And 8.57.1 is end-of-life, currently parked inallowedDeprecatedVersions.Will anything break? No — for normal linting
CI is structurally protected.
lint-changed-filesdoesn't lint the repo — it lints changed files and pipes them through reviewdog with-filter-mode=added -fail-level=warning. Only warnings on lines a PR adds can fail. The new warnings land on untouched lines and are invisible to CI. No existing PR starts failing.This answers the ticket's own question — "current CI only fails for newly added files? is this good enough of a stop gap?" — with yes, and stronger: newly added lines, not just files.
Everything else checks out:
turbo check-types— 8/8 pass on this branch.eslintor@types/eslint. (That catalog entry is dead and could be dropped outright.)lint-stagedruns prettier only, not ESLint — no new pre-commit friction.What actually changes: 573 → 754 warnings
Full-repo lint before and after. Errors stay at 0 in both (the
onlyWarnplugin downgrades everything), every workspace exits 0, and both runs linted the same 515 files — apples to apples.Where +181 comes from
@typescript-eslint/no-unnecessary-type-assertioneslint-disabledirectives@typescript-eslint/no-unused-vars@typescript-eslint/no-base-to-string@typescript-eslint/no-require-imports@typescript-eslint/prefer-promise-reject-errors@typescript-eslint/no-unused-expressions@typescript-eslint/await-thenable@typescript-eslint/no-empty-object-type@typescript-eslint/restrict-template-expressions@typescript-eslint/ban-types@typescript-eslint/no-var-requiresThree are just renames, not new findings:
ban-types→no-empty-object-type,no-var-requires→no-require-imports.Two of these are explained directly by the release notes:
linterOptions.reportUnusedDisableDirectivesnow defaults to"warn"; it was off in v8). Pure cleanup — they flageslint-disablecomments that no longer suppress anything.no-unused-varscaughtErrorsdefault flipping"none"→"all"in ESLint 9, which typescript-eslint v8 realigned to match. Settable back withcaughtErrors: 'none'if we want v7 behavior.eslint --fixsilently breaks the roam buildThis is the one genuine hazard, and it is not visible in lint output.
eslint .eslint --fixturbo check-typesafter--fixUnder ESLint 8,
--fixis benign — it modestly improves things and the tree still compiles.Under ESLint 9,
--fixtouches 83 files and makes lint look better (754 → 612, still 0 errors, every workspace exits 0) while breakingtsc:Cause:
no-unnecessary-type-assertionautofix strips assertions that were narrowinganyout of untyped host APIs — gray-matter'sdata, Obsidian'sfrontmatter, Roam query results:The rule is locally correct — the assertion is redundant to ESLint. But the assertion is load-bearing: it's how we pin down an
anyat the boundary. Removing it propagatesunknown/anyacross module boundaries and the consumers stop compiling.Mitigations, in order of preference:
eslint --fixrepo-wide during the upgrade. Fix the 62 stale directives by hand or with a targeted--rulefilter.turbo check-typesalready runs in CI (ci.yaml:35), so this cannot reachmainsilently — but it will waste someone's afternoon if they run--fixlocally and don't connect the two.no-unnecessary-type-assertionis not sufficient on its own — I tested it, and other autofixes still degrade types. Treat--fixas unsafe wholesale rather than trying to allowlist around it.packages/databaseships a committedlint:fixscript (eslint --fix . && …). I checked it specifically: it is safe — 15 → 10 warnings, no type errors in that package. The hazard is scoped to roam.Pros and cons
Pros
allowedDeprecatedVersions.@eslint/js@9.34.0/eslint@8.57.1mismatch.no-base-to-string,prefer-promise-reject-errors, 32 more unused vars).Cons
eslint-disablecomments want a mechanical cleanup pass.eslint --fixbecomes a foot-gun that breaks the roam build without failing lint. Needs documenting.Why 9 and not 10
ESLint 10.8.1 exists, but
eslint-plugin-react@7.37.5declareseslint: "^3 || … || ^9.7"— it does not permit v10. Nine is the right target today.Useful de-risking option:
typescript-eslint@8.67.0peerseslint ^8.57.0 || ^9.0.0 || ^10.0.0. The 7→8 bump can ship on its own first, while still on ESLint 8, making the v9 bump a near-trivial follow-up.Appendix A — breaking changes hit, and their fixes
All three were encountered for real, in this order:
tsconfigRootDirmust be absolute. typescript-eslint v8 rejects".", which v7 accepted and resolved againstprocess.cwd(). Symptom: 515 fatal parse errors — the entire repo fails to lint. Fix:import.meta.dirname(safe; repo requires Node ≥22). This is also strictly more correct, since the old value only worked becauseturbo linthappens to runeslint .from inside each workspace.project: trueon every file, v8 errors on.mjsabsent from any tsconfig (packages/database/src/dbDotEnv.mjs) where v7 skipped it. Fix:files: ["**/*.{ts,tsx,mts,cts}"]plusdisableTypeCheckedfor plain JS. The glob must include.mts—packages/database/scripts/*.mtsexists and a**/*.ts-only glob crashes ESLint outright.reportUnusedDisableDirectivesnow defaults to"warn"— source of the 62 new unused-directive warnings.Appendix B — release-note items checked and cleared
Audited every breaking change in the ESLint v9.0.0 and typescript-eslint v8 announcements against this repo. Non-issues, with the reason:
.eslintignoreno longer supported.eslintignorefiles exist--ext,--no-eslintrc,--rulesdirremovedcontext/SourceCodeAPI removals, function-style rules,CodePath#currentSegmentscheckstyle,compact,junit,tap,unix, …)--format stylish, which is retainedvalid-jsdoc/require-jsdoc/no-new-symbolremovedno-throw-literal→only-throw-error,prefer-ts-expect-error→ban-ts-comment^18.18 || ^20.9 || >=21.1node >=22; CI pins 22>=4.8.4typescript-eslint@8.67supports<6.1.0meta.schema)/* eslint */comments now errorOne item worth a second look:
prefer-nullish-coalescing'signoreConditionalTestsdefault flipped totruein v8, which makes the rule quieter. It didn't appear in the diff, so it is likely not enabled in our preset — but it's the only change here that removes coverage rather than adding noise.projectServiceis now the recommended replacement forparserOptions.projectand is claimed to be faster. This PR deliberately keepsproject: trueto hold the diff minimal; switching is a reasonable follow-up but should be measured separately.Appendix C — plugin compatibility audit
typescript-eslint^8.56.0@vercel/style-guide>=8.48.0 <9eslint-plugin-react^3 … ^9.7eslint-plugin-react-hooks… ^9.0.0eslint-config-prettier>=7.0.0eslint-plugin-prefer-arrow-functions>=8.0.0eslint-plugin-turbo>6.6.0eslint-plugin-only-warn@next/eslint-plugin-next@eslint/jsAppendix D — method
Two worktrees off
main@5f4d9ea0: one at the pre-upgrade commit (verifiedeslint --version→ v8.57.1) and one with the upgrade applied (v9.39.5). For each of the 7 workspaces with aneslint.config.mjs,npx eslint . --format jsonwas run and tallied by rule and severity.--fixbehavior was measured by actually applying fixes and re-running both lint andturbo check-types, then reverting.Intermediate states of the upgrade, for the record:
tsconfigRootDir.mts.mts/.ctsin globMethodology note / correction. An earlier revision of this PR body claimed
--fixinflated the repo to ~22,700 warnings. That number came from--fix-dry-runand was an artifact: with type-aware linting,--fix-dry-runchecks modified in-memory source against a TypeScript program still built from the on-disk files, so types desync and it emits a flood of spuriousno-unsafe-*reports. A real--fixwrites to disk and stays coherent — the true figure is 754 → 612 warnings. Do not use--fix-dry-runto estimate impact on a type-checked config. The conclusion that--fixis unsafe still holds, but the actual damage is the 25tscerrors documented above, not a warning explosion.🤖 Generated with Claude Code