Skip to content

feat!: upgrade to vitest 5, eslint 10, and eslint-config-oclif 7 - #100

Open
eablack wants to merge 9 commits into
mainfrom
feat/vitest-5-eslint-10-oclif-7
Open

eablack wants to merge 9 commits into
mainfrom
feat/vitest-5-eslint-10-oclif-7

Conversation

@eablack

@eablack eablack commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Upgrades the dev/test toolchain to current majors:

  • vitest 4 → 5 (vitest + @vitest/coverage-v8)
  • eslint 9 → 10
  • eslint-config-oclif 6 → 7

vitest 5 requires Node ^22.12.0 || ^24 || >=26, so this drops Node 20:

  • engines.node: >=20 → >=22.12.0
  • CI matrix: [20.x, 22.x] → [22.x, 24.x] (lint job → 24.x)
  • .tool-versions: nodejs 24.21.0

vitest is only a devDependency (not part of the runtime or peer API), so the breaking part is the narrowing of the supported-runtime contract — hence the major bump.

Notable follow-on changes

  • oclif 7 renamed the import plugin (eslint-plugin-import → eslint-plugin-import-x); updated our custom import/namespace → import-x/namespace, which also fixed a real runtime break in the config test.
  • oclif 7 enables type-aware linting (projectService). Added test/vitest.config.ts to tsconfig.json's include, plus a new tsconfig.build.json (src-only) so build still emits only src. Documented the new consumer requirement in the README.
  • TS target ES2022 → ES2024 so the regex v flag (now required by oclif 7's require-unicode-regexp) compiles; safe given the Node 22.12+ floor.
  • Assorted behavior-preserving fixes for oclif 7's stricter rules: nullish coalescing, catch (error: unknown), named default exports, typed globalThis, default node:path imports, and typing the config test against Linter.Config[].
  • README: Node/ESLint versions, install command, @vitest/eslint-plugin name fix, typed-linting note.

The version bump to 2.0.0 is left to release-please, driven by this feat! / BREAKING CHANGE commit.

Type of Change

Breaking Changes (major semver update)

  • Add a ! after your change type to denote a change that breaks current behavior

Feature Additions (minor semver update)

  • feat: Introduces a new feature to the codebase

Patch Updates (patch semver update)

  • fix: Bug fix
  • deps: Dependency upgrade
  • revert: Revert a previous commit
  • chore: Change that does not affect production code
  • refactor: Refactoring existing code without changing behavior
  • test: Add/update/remove tests

Testing

Notes: Validated locally on both Node 22.22.3 and Node 24.21.0.

Steps:

  1. Passing CI suffices.
  2. npm install, npm run build, npm run lint (0 problems), npm test (33/33 passing), and npm run test:coverage (thresholds met) — all green on Node 22 and Node 24.

Screenshots (if applicable)

N/A

@eablack
eablack requested a review from a team as a code owner September 22, 2026 20:32

@michaelmalave michaelmalave left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@eablack
eablack force-pushed the feat/vitest-5-eslint-10-oclif-7 branch from 1ac366e to 652f5b6 Compare September 23, 2026 16:29
@eablack
eablack force-pushed the feat/vitest-5-eslint-10-oclif-7 branch from 164a8d7 to 0afbe7f Compare October 1, 2026 19:42
Bumps the dev/test toolchain: vitest 4 -> 5, eslint 9 -> 10, and
eslint-config-oclif 6 -> 7. vitest 5 requires Node >=22.12, so the
supported runtime floor moves up and Node 20 is dropped from the
engines field and CI matrix. Adds tsconfig.build.json so builds keep
emitting only src now that test/config files are in the lint tsconfig
for oclif 7's type-aware linting.

BREAKING CHANGE: drops Node 20 support; the package now requires Node >=22.12.0
eslint-config-oclif 7 (via eslint-config-xo) declares a typescript >=6
optional peer. With our root typescript at 5.x, npm installed a second,
nested TypeScript 6 for the lint stack; that root-vs-nested version skew
crashed type-aware rules with 'tsutils.unionConstituents is not a
function', and the nested copy also drifted between npm 10 (CI) and
npm 11 (local), breaking npm ci.

Pinning root typescript to ~6.0.3 (in [6.0.0,6.1.0), satisfying both xo's
>=6 and typescript-eslint's <6.1.0) hoists a single TypeScript, removing
the skew and the drift. Add overrides.typescript=$typescript as a guard,
set explicit rootDir for TS6 build, and mirror node's write() callback
signature (Error|null) now that TS6 checks it strictly.
Under ESLint 10, 'dir/**/*' matches files but doesn't prune the directory
from traversal, so `eslint .` still descends into ignored dirs. For
workflows-repo (the heroku/npm-release-workflows checkout the release workflow
injects) that means ESLint imports its eslint.config.js — whose @eslint/js dep
isn't installed in the consumer — and the release validate lint step fails with
ERR_MODULE_NOT_FOUND. Switching to 'dir/**' prunes the directory. Verified
against this repo's own lint with a simulated workflows-repo present (was exit
2, now exit 0).
Commit 5db183c changed the ignore patterns from 'dir/**/*' to 'dir/**'
so ESLint prunes the directory from traversal. Update the test to match:
key the lookup on the distinctive 'workflows-repo/**' entry (oclif's own
global ignores block also contains 'dist/**'), and assert all three
patterns.
The typescript:$typescript override was added in 8a30d1c alongside the
~6.0.3 pin as a defensive guard. The pin alone resolves a single hoisted
typescript@6.0.3: every consumer in the tree accepts it (typescript-eslint
>=4.8.4 <6.1.0, eslint-config-xo >=6, @vitest/eslint-plugin/eslint-plugin-n
>=5.0.0, ts-api-utils >=4.8.4). Removing the override leaves the lockfile
byte-identical (no resolution change), confirming it was inert. Build,
33 tests, and lint stay green.
Rebased onto main; merge main's standardized scripts (rimraf clean,
`eslint .`, test:watch, deduped prepare) with the eslint-10/vitest-5/
oclif-7 toolchain and node>=22.12 floor. Take @types/node ^26 from main
and add the rimraf devDep. Regenerate package-lock.json.
@eablack
eablack force-pushed the feat/vitest-5-eslint-10-oclif-7 branch from 0afbe7f to a678eb9 Compare October 1, 2026 22:03
It conflicts with perfectionist/sort-classes (both enabled transitively by
eslint-config-oclif@7): the two enforce opposite public/private method
orderings and a class with both can satisfy neither. Perfectionist is the
more specifically configured of the pair and matches idiomatic oclif layout,
so unicorn's rule gives way. Latent until the oclif@7 install lands.
@eablack
eablack force-pushed the feat/vitest-5-eslint-10-oclif-7 branch from 7bb1233 to ea41b63 Compare October 3, 2026 00:03
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.

2 participants