Skip to content

Bump to eslint 10 + typescript 6#41

Merged
plx merged 10 commits intomainfrom
plx/lint-ts-upgrade
Apr 24, 2026
Merged

Bump to eslint 10 + typescript 6#41
plx merged 10 commits intomainfrom
plx/lint-ts-upgrade

Conversation

@plx
Copy link
Copy Markdown
Owner

@plx plx commented Apr 19, 2026

Summary

Step 1 of the major-version migration plan (planning/major-migration-plan.md): bump eslint 9.39 → 10.2 and typescript 5.9 → 6.0, shipped ahead of Astro 6 so astro check/tsc are already on the new compiler when we hit Astro-6 API breaks.

The only blocker was eslint-plugin-jsx-a11y@6.10.2 (peer caps at eslint ^9, and upstream PRs #1081/#1079 for ESLint-10 support remain unmerged), so the plugin is dropped per the migration plan's guidance. The astro.configs["jsx-a11y-strict"].rules spread in eslint.config.js was already a silent no-op (the getter returns Linter.Config[], so .rules was undefined), so removing it costs zero actual lint coverage — a correct reintroduction rides in a later pass.

@typescript-eslint/*@8.58.2 already peers on eslint ^10 + typescript <6.1 so stays put. @astrojs/check@0.9.8's typescript: ^5.0.0 peer warning is cosmetic (it's a CLI wrapper around @astrojs/language-server, no runtime gate) and astro check runs clean against TS 6.

Test plan

  • npm run lint passes
  • npm run build (= astro check && astro build) passes — TS 6 smoke test; 24 pages generated
  • npm run validate:all passes (lint + spellcheck + build + HTML spellcheck + link validation)
  • git grep jsx-a11y outside planning/ / package-lock.json returns zero hits
  • CI (build.yml on ubuntu-latest + Node 20) reproduces the local green

🤖 Generated with Claude Code

Step 1 of the major-version migration plan. Upgrades `eslint` 9.39 → 10.2
and `typescript` 5.9 → 6.0; shipping these before Astro 6 means
`astro check`/`tsc` are already running on the new compiler when we chase
down Astro-6 API breaks — one signal at a time instead of two.

The only blocker was `eslint-plugin-jsx-a11y@6.10.2` (latest), whose peer
range caps at `eslint ^9`; the two open ESLint-10 PRs upstream (#1081,
#1079) are still unmerged. Drop the plugin per the migration plan's
explicit guidance. The `astro.configs["jsx-a11y-strict"].rules` spread
in eslint.config.js was already a silent no-op (the getter returns a
`Linter.Config[]` array, and `...(array).rules` is `...undefined`), so
removing the plugin costs zero actual lint coverage. Reintroducing real
a11y linting for .astro files needs a separate pass that consumes the
array-shaped preset correctly.

`@typescript-eslint/*@8.58.2` already peers on `eslint ^10` and
`typescript <6.1`, so no bump needed there. `@astrojs/check@0.9.8` still
peers on `typescript ^5.0.0`; the override warning is cosmetic since it's
a CLI wrapper around `@astrojs/language-server` and doesn't gate on the
peer version — `astro check` passes clean against TS 6 locally.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 636144e81e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread package.json
"astro-seo": "^0.8.4",
"clsx": "^2.1.1",
"eslint": "^9.39.4",
"eslint": "^10.2.1",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add @eslint/js to dependencies

After bumping to eslint@10.2.1, @eslint/js is no longer installed transitively (it does not appear anywhere in the new lockfile), but eslint.config.js still imports it on line 1. On a clean npm ci environment this causes npm run lint to fail at startup with a module resolution error for @eslint/js, so CI and fresh local setups cannot run lint until @eslint/js is declared directly.

Useful? React with 👍 / 👎.

Keep cspell v10 from main. Skip re-adding eslint-plugin-jsx-a11y —
its latest release (6.10.2) caps the eslint peer at ^9, incompatible
with the eslint 10 bump on this branch. The previous jsx-a11y rules
spread in eslint.config.js was already a silent no-op, so lint
coverage is unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

PR Validation ❌

Some checks failed. Please review the errors above.

Checks Performed:

Please check the workflow logs for details on which checks failed.

This is a complete dry-run of the deployment process, ensuring your changes will deploy successfully when merged.

@claude
Copy link
Copy Markdown

claude Bot commented Apr 19, 2026

Code Review: Bump to eslint 10 + typescript 6

Overview

Clean Step 1 execution of the migration plan — surgically correct scope (lint/TS track only, Astro/Tailwind untouched). The bulk of the diff is lock file churn from ESLint's own dependency tree modernizing; the actual source change is a single-line deletion in eslint.config.js.


Strengths

  • Correct no-op analysis. The observation that astro.configs["jsx-a11y-strict"].rules was already undefined (the getter returns Linter.Config[], not Linter.Config) is accurate and important — removing the spread costs zero real lint coverage. Good catch.
  • Well-sequenced. Shipping TS 6 before Astro 6 means astro check/tsc will already be on the new compiler when Astro-6 API breaks need chasing down. This is the right call per the migration plan rationale.
  • @astrojs/check peer warning is cosmetic. Correctly dismissed; it's a CLI wrapper with no runtime gate on the TS version.
  • Thorough test plan. The git grep jsx-a11y verification step is a solid sanity check.

Issues / Risks

1. Node 20 pinning in CI (moderate risk — unchecked test plan item)

ESLint 10 declares engines: { node: "^20.19.0 || ^22.13.0 || >=24" }. The test plan notes CI uses Node 20 on ubuntu-latest, but that checkbox is still unchecked. If the workflow pins to a Node 20 version older than 20.19.0 (e.g. node-version: '20' resolves to an older LTS point release), the build will fail in CI even though it passes locally on Node 25.

Recommendation: Verify the build.yml Node 20 pin resolves to ≥ 20.19.0, or bump the CI matrix to 20.19 / 22 explicitly before merging.

2. Accessibility linting gap needs a tracking issue

The site's CLAUDE.md calls out "reasonable accessibility support" as a goal. Even though the jsx-a11y-strict spread was a no-op, removing both the plugin and the spread means there is now no stated intent to restore that coverage. The PR description says "a correct reintroduction rides in a later pass" — that pass should be tracked (e.g. a GitHub issue or a note in planning/major-migration-plan.md) so it doesn't get lost across migration steps.

Recommendation: File a follow-up issue or add a bullet to the migration plan noting that eslint-plugin-jsx-a11y re-introduction depends on upstream PRs #1081/#1079.

3. Minor: eslint / @typescript-eslint/* in dependencies vs devDependencies

Pre-existing, not introduced here, but eslint, eslint-plugin-astro, @typescript-eslint/eslint-plugin, and @typescript-eslint/parser currently live in dependencies. These are build-time tools with no runtime role in the static site output. Worth cleaning up in a follow-on (or as part of the Step 0 "quick wins" pass), but it's not a blocker for this PR.


Summary

The code change itself is correct and well-motivated. The one concrete action before merging is confirming the CI Node 20 version satisfies ESLint 10's ^20.19.0 engine floor — everything else is low-risk or a follow-up. LGTM once that CI checkbox is green.

CI `npm ci` fails on the @astrojs/check@0.9.8 peer dep pinning to
typescript ^5.0.0. The TS 6 bump commit (636144e) flagged this as a
cosmetic override warning, but strict `npm ci` treats it as an error.
`legacy-peer-deps=true` matches the lockfile's resolution and unblocks
CI until @astrojs/check ships a TS 6-compatible release.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

PR Validation ❌

Some checks failed. Please review the errors above.

Checks Performed:

Please check the workflow logs for details on which checks failed.

This is a complete dry-run of the deployment process, ensuring your changes will deploy successfully when merged.

After the eslint 10 bump, @eslint/js was no longer pulled in
transitively, but eslint.config.js still imports it. Declare it
directly so lint runs on clean npm ci environments.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

PR Validation ✅

All checks passed! Ready for review.

Checks Performed:

✓ Linting
✓ Spell check (source)
✓ Type checking & Build
✓ Spell check (HTML)
✓ Internal link validation
✓ Artifact upload

This is a complete dry-run of the deployment process, ensuring your changes will deploy successfully when merged.

@claude
Copy link
Copy Markdown

claude Bot commented Apr 19, 2026

Code Review — PR #41: Bump to eslint 10 + typescript 6

Overall: Clean, well-scoped migration step with good rationale documented in planning/major-migration-plan.md. A few points worth addressing before merge.


Issues

1. .npmrc addition is unexplained (medium)

The PR adds .npmrc with legacy-peer-deps=true but the PR description doesn't mention it. This is a meaningful project-wide change: it silences npm's peer-dep conflict detection for all future installs, not just this migration.

The most likely trigger is @astrojs/check@0.9.8's typescript: ^5.0.0 peer decl conflicting with the newly installed typescript@6. The PR description calls this "cosmetic" — that's probably true at runtime — but legacy-peer-deps is a blunt instrument. Consider:

  • Adding a comment in .npmrc naming the offending package (e.g., # needed until @astrojs/check bumps its typescript peer to ^6)
  • Or scoping it to the install step in CI rather than committing it project-wide

Either way, the PR description should document why this file was added.

2. CI node version is misreported in the PR description (low)

The description's unchecked CI item says "ubuntu-latest + Node 20", but build.yml actually specifies node-version: '>=22.18.0' (Node 22+). This doesn't affect correctness — 22.18 satisfies ESLint 10's ^22.13 engine requirement — but the discrepancy in the PR description is confusing.

3. No tracking for jsx-a11y re-introduction (low)

CLAUDE.md calls out "reasonable accessibility support" as a site goal. The PR correctly notes that the jsx-a11y-strict spread was a silent no-op (array .rulesundefined), so this is zero net regression in enforced rules. But there's no issue or TODO capturing the planned "correct reintroduction in a later pass." Given the site only has one React file, this is easy to forget.


What looks good

  • The eslint.config.js change is exactly right: one line removed, no collateral changes.
  • @typescript-eslint/*@8.58.2 peer analysis is correct (eslint: ^8.57 || ^9 || ^10, typescript: <6.1).
  • The claim that astro.configs["jsx-a11y-strict"].rules was undefined is accurate for flat config — the getter returns Linter.Config[], not an object with a .rules key.
  • Lock file diff is credible: net −1973 lines after dropping eslint-plugin-jsx-a11y and its transitive deps.
  • Migration plan ordering (lint/TS before Astro 6) is sound.

Minor note (not a blocker)

accessible-astro-components@4.1.3 is still in package.json — the migration plan lists its removal as a Step 0 quick win. Not this PR's problem, but worth keeping on the radar.

@github-actions
Copy link
Copy Markdown
Contributor

PR Validation ✅

All checks passed! Ready for review.

Checks Performed:

✓ Linting
✓ Spell check (source)
✓ Type checking & Build
✓ Spell check (HTML)
✓ Internal link validation
✓ Artifact upload

This is a complete dry-run of the deployment process, ensuring your changes will deploy successfully when merged.

@claude
Copy link
Copy Markdown

claude Bot commented Apr 21, 2026

Code Review

This is a clean, well-scoped dependency bump. The PR description is unusually thorough (the explanation of why the jsx-a11y-strict spread was already a no-op is genuinely useful context to have on record). A few things worth flagging:


Node version constraint — potential CI break

ESLint 10's engine requirement jumped from ^18.18.0 || ^20.9.0 || >=21.1.0 to ^20.19.0 || ^22.13.0 || >=24. If build.yml specifies node-version: '20' without a patch floor, GitHub Actions will resolve that to the latest 20.x available on the runner, which is currently fine — but worth double-checking that the workflow doesn't pin a specific 20.x that predates 20.19.0. A comment in the workflow or a node-version: '>=20.19' constraint would make this explicit and prevent a surprise regression if the runner image changes.


legacy-peer-deps=true in .npmrc — broad scope

Adding this globally via .npmrc means every npm install (including CI, contributor setups, and future dependency additions) will silently skip peer dependency validation. This hides a real problem rather than solving it — it's the diff-minimizing move for now, but it can let genuinely broken peer graphs through later.

A narrower alternative: use package.json overrides to force the specific version that satisfies the conflicting peer cap, or accept the npm warning output and track it rather than suppressing it. If the plan is to remove legacy-peer-deps once upstream plugins update, it's worth a TODO comment in .npmrc referencing the upstream issues.


Accessibility linting gap — no concrete follow-up PR

The PR correctly notes that astro.configs["jsx-a11y-strict"].rules was a no-op spread (returning Linter.Config[], so .rules was undefined). That's a good catch. However, the net effect is still that no a11y lint rules are active until a follow-up restores them — and the repo's CLAUDE.md calls out accessibility as an explicit goal. It would be good to see a linked follow-up issue or PR tracking restoration of a11y coverage (perhaps via eslint-plugin-jsx-a11y@7 when it lands ESLint-10 support, or via an alternative like eslint-plugin-react's a11y rules or astro-accessibility).


@eslint/js in dependencies vs devDependencies

@eslint/js was added to dependencies, consistent with how eslint itself is classified in this project. For a static site build that never ships node_modules to end users, this doesn't affect runtime behavior — but both eslint and @eslint/js are dev tools and belong in devDependencies. Worth a follow-up cleanup, though this is a pre-existing pattern rather than a regression introduced here.


tsconfig.json — explicit types: ["astro/client"]

Replacing the implicit type inclusion with an explicit ["astro/client"] is cleaner, but it also means Node.js built-in types are no longer included in the main tsconfig. The new tsconfig.playwright.json correctly adds "node" for test files, but any utility scripts outside tests/ that reference Node globals would stop getting type-checked. Worth confirming nothing in the project falls into that gap.


Minor: planning doc included in the PR

planning/typescript6-item4-subpath-imports-migration.md is a forward-looking spec for the @* → #/* alias migration. Including planning docs in feature PRs is fine, but it's worth noting the current tsconfig.json still maps @* — so this doc describes future work, not work done in this PR. No action needed, just noting for clarity.


Overall: the mechanical work (version bumps, lockfile, config wiring) looks correct. The main asks are: (1) verify the Node version floor in CI is explicit, (2) consider narrowing legacy-peer-deps or adding a tracking comment, and (3) open a follow-up for restoring a11y linting coverage.

…ride

The .npmrc `legacy-peer-deps=true` was a blunt mitigation for a single
known conflict: @astrojs/check@0.9.8 pins its typescript peer to ^5.0.0
but we're on TS 6. Applying it globally suppresses peer validation for
every future install — hiding genuine conflicts we'd want to see.

Narrow the scope via package.json `overrides` so only @astrojs/check's
typescript peer is rewritten to the root version. Every other peer-dep
conflict (current or future) still fails fast under `npm ci`. When
@astrojs/check ships a TS 6-compatible release, deleting this entry is
a one-liner.

Verified: `rm -rf node_modules package-lock.json && npm install`,
`rm -rf node_modules && npm ci`, `npm run build`, `npm run lint` all
pass. `npm ls @astrojs/check typescript` confirms @astrojs/check@0.9.8
resolves typescript@6.0.3 (deduped from root).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

PR Validation ✅

All checks passed! Ready for review.

Checks Performed:

✓ Linting
✓ Spell check (source)
✓ Type checking & Build
✓ Spell check (HTML)
✓ Internal link validation
✓ Artifact upload

This is a complete dry-run of the deployment process, ensuring your changes will deploy successfully when merged.

@claude
Copy link
Copy Markdown

claude Bot commented Apr 23, 2026

Code Review — PR #41: Bump to eslint 10 + typescript 6

Status: CI is green (all checks passing as of the latest validation run), so this is a merge-readiness review.


What's in this PR (beyond the obvious version bumps)

The diff contains more than the title suggests — worth being explicit:

File Change
eslint.config.js Drop ...astro.configs["jsx-a11y-strict"].rules spread (was already undefined)
package.json Add typecheck, typecheck:app, typecheck:playwright scripts; bump eslint + typescript; add @eslint/js + @types/node; drop eslint-plugin-jsx-a11y
tsconfig.json Remove baseUrl: ".", add types: ["astro/client"], add trailing newline
tsconfig.playwright.json New file — extends main tsconfig, adds node + @playwright/test types, scoped to tests/
planning/typescript6-item4-subpath-imports-migration.md Forward-looking spec for @*#/* alias migration (future work)

Things that look correct

baseUrl removal is valid. TypeScript 5.0+ no longer requires baseUrl to use paths. Since "@*": ["./src/*"] uses a relative path, it resolves correctly relative to tsconfig.json's directory with or without baseUrl. This is the right direction for TS 6.

Type isolation is well-structured. Pinning types: ["astro/client"] in the main tsconfig prevents test types (node, @playwright/test) from polluting source-file globals. The dedicated tsconfig.playwright.json then explicitly opts into those types for tests/ and playwright.config.ts. Clean separation.

Astro's nested typescript@5.9.3 in the lockfile is expected. Astro peers on typescript: ^5.0.0, so npm correctly installs a compatible 5.x under node_modules/astro/. This is normal semver resolution, not a regression.

typecheck scripts are a solid addition. Having explicit tsc -p tsconfig.json --noEmit and tsc -p tsconfig.playwright.json --noEmit invocations lets you catch type errors outside of astro check, and correctly uses the scoped playwright tsconfig for test files.

The jsx-a11y-strict drop is accurately analyzed. astro.configs["jsx-a11y-strict"] returns Linter.Config[], so .rules was undefined, making the spread a no-op. The PR description earns points for catching and documenting this precisely.


One open item before merging

Accessibility lint gap needs a tracking issue. Prior reviews noted this, and it remains unaddressed in the PR. CLAUDE.md lists "reasonable accessibility support" as an explicit goal. Even though zero a11y rules were enforced before (no-op spread), removing the plugin without a concrete follow-up issue creates drift risk across the remaining migration steps. This is especially salient because the blocking upstream PRs (eslint-plugin-jsx-a11y #1081/#1079) are noted but not linked from any tracking artifact in this repo.

Suggested action: Add a bullet to planning/major-migration-plan.md or open a GitHub issue referencing upstream #1081/#1079, before or alongside merge.


Minor (non-blocking)

  • @eslint/js and eslint both land in dependencies rather than devDependencies. Pre-existing pattern; worth a follow-on cleanup pass since neither has a runtime role in the static site output.
  • The planning doc for @*#/* describes future work, but the current tsconfig.json still maps @*. No action needed — just noting this is a forward-spec, not a completed migration.

Overall: Mechanically correct, well-tested (CI green, thorough test plan). The one ask is capturing the a11y lint restoration as a tracked item so it survives across the remaining migration steps.

Establish an explicit Node floor that clears the ESLint 10 compatibility
gap in the 20.x series. Adds engines to package.json, .nvmrc for local
version managers, engine-strict=true in .npmrc for hard failures on
mismatched Node, and bumps the workflow's setup-node range to match.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

PR Validation ✅

All checks passed! Ready for review.

Checks Performed:

✓ Linting
✓ Spell check (source)
✓ Type checking & Build
✓ Spell check (HTML)
✓ Internal link validation
✓ Artifact upload

This is a complete dry-run of the deployment process, ensuring your changes will deploy successfully when merged.

@claude
Copy link
Copy Markdown

claude Bot commented Apr 23, 2026

Code Review

Overview

This PR bumps ESLint 9→10 and TypeScript 5→6 as part of a staged major-version migration. The changes are clean and the PR description is detailed. A few items worth flagging below.


Correctness

tsconfig.json: removing baseUrl
Removing baseUrl: "." while keeping paths: { "@*": ["./src/*"] } is safe under moduleResolution: bundler (which Astro strict config sets), but it's worth documenting why the removal is safe. TypeScript 4.x required baseUrl whenever paths was used; TypeScript 5.5+ dropped that requirement under bundler/node16/nodenext. Someone who hits a compile error in a different project and copies this config may be confused. A brief inline comment would help.

tsconfig.playwright.json type merging
When you specify types in a child tsconfig, it replaces (not merges) the parent types array. So tsconfig.playwright.json effectively has types: ["node", "@playwright/test"] and does not inherit astro/client. For Playwright tests this is probably fine — they don't need astro/client ambient types. But it's worth verifying tsc -p tsconfig.playwright.json --noEmit passes clean (the test plan checks this via the new typecheck:playwright script, so presumably it does).


Minor Issues

@types/node version vs. runtime version
devDependencies pins @types/node: "^22.19.17" (Node 22 types), but engines and CI now require Node >=24.0.0. For a static-site build tool chain this is rarely a problem in practice, but the mismatch can surface if any build scripts use Node 24-specific APIs. Consider ^24 to stay aligned with the engine constraint — or at minimum call it out in the PR description.

.nvmrc uses a major-only pin (24)
.nvmrc: 24 will resolve to whatever nvm considers the latest Node 24.x at install time, which could drift differently than the lockfile-installed environment. A specific version like 24.0.0 (or the LTS alias lts/iron when it ships) gives more reproducible local environments. Low priority, but worth tightening.


Accessibility

The PR correctly identifies that ...astro.configs["jsx-a11y-strict"].rules was a silent no-op (spreading .rules off an array yields undefined), so removing it costs zero actual enforcement. Still, the jsx-a11y plugin provided potential lint coverage that the project intends to have (per CLAUDE.md: "reasonable accessibility support"). The planning doc acknowledges a later pass will restore it. It would be helpful to either:

  • open a tracking issue, or
  • add a // TODO: re-add jsx-a11y once upstream adds ESLint 10 support (#1081/#1079) comment in eslint.config.js

so this doesn't silently fall through the cracks.


Positive Notes

  • The npm overrides entry forcing @astrojs/check to use the workspace $typescript is the correct solution to the peer-version cosmetic warning.
  • Explicitly adding @eslint/js to dependencies is correct for ESLint 10's restructured package surface.
  • engine-strict=true in .npmrc + engines in package.json is a good enforcement pair.
  • The new typecheck:app / typecheck:playwright scripts are a useful addition.
  • The planning doc for the upcoming #/* subpath import migration is clear and well-structured.

Summary

The logic is sound and the test plan is thorough. The main actionable items are: align @types/node to ^24, tighten .nvmrc to a patch version, and leave a breadcrumb for the jsx-a11y reintroduction. None of these are blockers.

@plx plx merged commit c31c89a into main Apr 24, 2026
3 checks passed
@plx plx deleted the plx/lint-ts-upgrade branch April 25, 2026 15:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant