Skip to content

feat!: migrate to ESLint 10 native flat config - #95

Open
uPaymeiFixit wants to merge 22 commits into
mainfrom
feat/eslint-10-flat-config
Open

uPaymeiFixit wants to merge 22 commits into
mainfrom
feat/eslint-10-flat-config

Conversation

@uPaymeiFixit

@uPaymeiFixit uPaymeiFixit commented Aug 11, 2026 •

Copy link
Copy Markdown
Collaborator

Migrates every @code-style/eslint-config* package, eslint-plugin-nest, and create-configs from ESLint 8 .eslintrc configs to ESLint 10 flat configs. Rule content is unchanged except where the plugin majors forced it.

Context: Paciolan's Maps team wants to standardize on code-style, but it needs a current ESLint first (their spike rejected it over ESLint 8 / eslintrc). Frontend rules come in a follow-up.

Tip: review with whitespace ignored. Wrapping each config in a Linter.Config[] re-indents most files; the rule bodies are otherwise unchanged.

What changes for consumers

// eslint.config.mjs
import { defineConfig } from 'eslint/config';

import { config as base } from '@code-style/eslint-config';
import { config as node } from '@code-style/eslint-config-node';

export default defineConfig(base, node);
  • Every package exports a named config array (like jest-configs); layering still works the same way, with later configs winning. -typescript and -jest also export their files, for scoping your own changes to their rules; see docs/customizing-eslint.md.
  • Requires ESLint 10 and Node ^20.19.0 || ^22.13.0 || >=24. The packages are ESM-only, which also works for CommonJS projects since config files are loaded via eslint.config.mjs.
  • create-configs now writes eslint.config.mjs, installs eslint directly, and drops --ext. When overwriting an old .eslintrc, it carries over the project's own rules, ignore patterns, overrides' rules, and .eslintignore, and warns about anything it can't migrate.
  • Migration guide: docs/migrating-v2-to-v3.md.

Notable implementation details

  • eslint-plugin-import (no ESLint 10 support) is replaced with eslint-plugin-import-x, registered under the import namespace so every import/* rule ID and disable comment keeps working.
  • eslint-plugin-react, eslint-plugin-jsx-a11y, and eslint-plugin-json-files still call rule-context methods ESLint 10 removed; they're wrapped with @eslint/compat's fixupPluginRules. React & jsx-a11y also don't declare ESLint 10 in their peer ranges yet (see the migration guide).
  • react-hooks is pinned to rules-of-hooks + exhaustive-deps, since v6+'s recommended adds the React Compiler rules.
  • .mjs files are linted as ES modules, so CommonJS projects can lint their own eslint.config.mjs.
  • The YAML packages (browser, cli, nest) are now built TS packages like the others.

Fixes to v2 behavior

  • Test & lenient relaxations now apply. Base's testing and lenient configs turned off n/* and @typescript-eslint/* rules, but the node and typescript layers come after base and re-enabled them, so they never took effect. Each relaxation now lives in the package that owns the rule.
  • no-barreling's suggestion deleted the whole file; it now deletes only the export * declarations.
  • create-configs config replacement. Overlapping name checks could race to delete an existing config twice, leaving the project with no config, and with overwrite off an existing config was ignored, so setup wrote a second one next to it.
  • Prereleases publish under next. The publish action runs a bare npm publish, so every prerelease took latest; the workflow now sets npm's tag from the version.

Behavior changes forced by the upgrade

  • ESLint 10's recommended rules add no-useless-assignment, no-unassigned-vars, preserve-caught-error, and others.
  • typescript-eslint 8's strict/stylistic configs rename and add rules (eg: no-throw-literal → only-throw-error). switch-exhaustiveness-check keeps v7's behavior of accepting a default case.
  • Plugin majors: eslint-plugin-n 18, eslint-plugin-jest 29, eslint-plugin-promise 7, @next/eslint-plugin-next 16.

Not in this PR

No version bump. With the workflow change, bumping to 3.0.0-N publishes under next, and existing ^2.0.0-35 ranges can't resolve to 3.x.

🤖 Generated with Claude Code

uPaymeiFixit and others added 9 commits August 10, 2026 22:11
…^10, engines node >=20.19, lockfile regen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… fix to ExportAllDeclaration

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- YAML packages (browser/cli/nest) converted to built TS packages
- eslint-plugin-import → eslint-plugin-import-x registered under the import namespace
- typescript-eslint 8, eslint-plugin-n 18, jest 29, promise 7, react-hooks 7 (classic pair pinned)
- foreign-rule relaxations relocated to their owning packages
- cli gains an empty ./lenient export
- json-files rules consolidated into one **/*.json config with explicit processor

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… delete all .eslintrc.yaml

Also: ESLint 10 context-method shims for json-files/react/jsx-a11y plugins,
import-x resolver-next wiring, type-aware linting disabled for config files,
eslint.config.* exempted from import/no-default-export.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- initESLint uses overrideConfigFile:true + flat overrideConfig; tmp-dir tests get cwd-scoped linters
- utils workspace goes ESM with a typed exports map (fixes Node 24 interop + phantom any types)
- test configs import the flat arrays instead of eslintrc extends strings
- base covers the full JS/TS file family (parity with the old --ext ts,js,json behavior)
- json-files processor gains the meta object ESLint 10's --cache requires
- switch-exhaustiveness-check pinned to v7 semantics (considerDefaultExhaustiveForUnions)
- test snippets updated for eslint 10 / typescript-eslint 8 forced rule deltas

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v7's eslintrc version only applied to *.ts; v8's flat version is universal,
which silently turned off no-undef & friends for plain JS. Also pin
switch-exhaustiveness-check to v7 semantics. Found via pre/post
--print-config diff audit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- flat-config imports + defineConfig in today's exact layering order
- eslint installed as a direct devDependency (it's a peer of the config packages now)
- lint:js drops --ext (files globs govern extensions); uninstall list gains
  import-x/@types/eslint/@rushstack-eslint-patch; removal set covers legacy
  eslintrc files and .eslintignore
- specs assert the import list and defineConfig call; new cli+lenient case

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… release/dist-tag policy

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread utils/package.json
},
"engines": {
"node": ">=18"
"node": "^20.19.0 || ^22.13.0 || >=24.0.0"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Should 23 be supported?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No. This range is ESLint 10's own engines field. ESLint 10 doesn't support Node 23 (an odd-numbered release that's now end-of-life), and utils depends on ESLint 10 directly, so it couldn't run on 23 anyway.

That said, this question made me look at where else I'd changed engines. I've kept this range only on packages that depend on ESLint 10, and reverted it on the ones that don't (code-style, jest-configs, and the rubocop, stylelint, and typescript configs) in 9c1e95c.

Comment thread utils/eslint.config.mjs Outdated
Comment on lines +17 to +25
files: [
'**/test/**',
'**/testing/**',
'**/__test__/**',
'**/*.test.*',
'**/*.spec.*',
'**/*.unit.*',
'**/*.e2e.*',
],

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Should this be included in the base config?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I'd keep it out of base. A testing/ folder of test helpers is this repo's convention, and putting it in base would relax rules in any consumer's testing/ folder.

Also, this block isn't redundant with base, though it looks it. Base's testing override comes before the typescript layer, so typescript-eslint's strict configs re-enable the @typescript-eslint/* rules it turns off. Re-applying it here, last, is what makes those relaxations work for utils. v2's .eslintrc.yaml did the same thing for the same reason. The real fix is for eslint-config-typescript to own its test-file relaxations, but that changes what consumers get, so I'd rather do it as a follow-up than fold it into this migration.

I did switch this to defineConfig's extends, which reads like the original eslintrc override (8bbfb05, 6668a91).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Update: I fixed the underlying problem instead of working around it. The testing and lenient relaxations for n/* and @typescript-eslint/* rules now live in eslint-config-node and eslint-config-typescript, so they come after the rules they relax. With that, utils doesn't need this block at all, so it's gone (b711e47).

Comment thread utils/eslint.config.mjs Outdated
Comment on lines +3 to +7
import base from '@code-style/eslint-config';
import node from '@code-style/eslint-config-node';
import esmodule from '@code-style/eslint-config-esmodule';
import typescript from '@code-style/eslint-config-typescript';
import testing from '@code-style/eslint-config/overrides/testing';

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

do we need these to be default exports?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No. Every config module now has a named config export, matching jest-configs:

import { config as base } from '@code-style/eslint-config';

That also let me delete the three import/no-default-export exemptions the default exports needed. The remaining default exports are ones that have to be: the eslint.config.* files themselves, eslint-plugin-nest's plugin object, and type stubs for CJS plugins (8bbfb05).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Maybe we should add a migration script to automate this

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed. create-configs already does most of it: re-running it with overwrite writes eslint.config.mjs and removes the old .eslintrc* and .eslintignore. The gap is rules you've added to your own .eslintrc.yaml, which it drops. I'd rather teach create-configs to carry those over than add a separate script, but I'll do that in a follow-up so this PR stays focused. I've also cut this doc down to just the steps (8bbfb05).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Update: done in create-configs instead of a separate script. When overwrite replaces an old .eslintrc, it now carries over the project's rules, ignore patterns, overrides' rules, and .eslintignore into eslint.config.mjs, and warns about anything it can't migrate (eg: env) (d1a1df0).

Comment thread packages/code-style/package.json Outdated
"scripts": {
"build": "concurrently --group --prefix none \"npm:build:*\"",
"build:jsonschema": "typescript-json-schema src/config-types.ts CodeStyleRCFile > codestylerc.schema.json",
"build:jsonschema": "typescript-json-schema tsconfig.jsonschema.json CodeStyleRCFile > codestylerc.schema.json",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This looks sus. Why did we drop config-types.ts from the command? I get that we need to override the tsconfig for typescript-json-schema

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, it was a workaround. The command only broke because I'd regenerated the whole lockfile, which changed which version of @types/glob got hoisted. With the lockfile now rebuilt incrementally from main's (e434c1a), the original command works again, so I've restored it and deleted tsconfig.jsonschema.json (9c1e95c).

Comment thread CONTRIBUTING.md Outdated
Comment on lines +19 to +21
**Only run this on `main` when you intend to release** — CI publishes any
pushed branch whose version is new (gated by the `production` environment
approval).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

out of scope. look for more changes that are OOS

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reverted. Looking for others turned up these, which I've also reverted:

  • the .nvmrc bump, the CI test matrix, and the e2e smoke test script
  • engines bumps on packages that don't depend on ESLint
  • a ts-jest pin in the published jest-configs, which would've downgraded its consumers
  • the build:jsonschema workaround
  • a behavior change to eslint-plugin-nest's no-barreling (its suggestion deletes the whole file instead of just the export; that's a real bug, but it belongs in its own PR)

Several of these traced back to regenerating the lockfile from scratch, which upgraded every transitive dependency. It's now rebuilt incrementally from main's (e434c1a, 9c1e95c, 8bbfb05).

uPaymeiFixit and others added 7 commits October 21, 2026 20:39
eslint-config-nest imports eslint-plugin-nest's types, and the concurrent
workspace build raced it (every CI job failed with TS2307).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- CONTRIBUTING.md, .nvmrc, the CI matrix, and the e2e smoke script
- engines of packages that don't depend on ESLint
- the ts-jest pin in jest-configs and the build:jsonschema workaround,
  both only needed because of the regenerated lockfile
- committed *.tsbuildinfo build artifacts (now gitignored)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lockfile was regenerated from scratch, upgrading every transitive
dependency and forcing ts-jest/type-fest workaround pins. Rebuild it from
main's lockfile instead, updating only what the migration needs
(the ESLint 10 plugin stack, plus TypeScript and @types/node, which
eslint-plugin-n 18's types require).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- export `config` by name (matching jest-configs), dropping the
  import/no-default-export exemptions default exports needed
- flat config tolerates `off` for unregistered plugins' rules, so move
  lenient/testing rules back to where they were in v2; json-files rules go
  back into footguns/readability
- replace the hand-written ESLint 10 context shims with @eslint/compat's
  fixupPluginRules
- fix: lint `.mjs` files (eg: eslint.config.mjs) as ES modules; CommonJS
  projects couldn't lint their own eslint.config.mjs
- restore the original exports map shapes; ship d.ts files next to dist
- drop dead code: unused deps, dead settings and ignores, the cli/lenient
  export, eslint-plugin-nest's recommended_flat alias
- revert eslint-plugin-nest's no-barreling behavior change (follow-up)
- examples and READMEs mirror the originals; trim the migration docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Like v2's .eslintrc.yaml: base's testing override is shadowed by the
typescript layer, so utils re-applies it last.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fresh checkouts inherited this incremental build state, so `tsc` skipped
emitting eslint-config's declaration files and CI's type checks failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
typescript-eslint switches to single-run mode when CI=true, building its
programs from files on disk. The tests lint in-memory code, so in CI it fell
back to programs without the fixture's compiler options.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
uPaymeiFixit and others added 4 commits October 21, 2026 21:45
The suggestion removed the whole Program, deleting the entire file instead
of the export declaration it described.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base's testing and lenient configs turned off `n/*` and
`@typescript-eslint/*` rules, but the node and typescript layers come
after base and re-enabled them, so those relaxations never applied (in v2
either). Move them into eslint-config-node and eslint-config-typescript,
which also makes utils' re-applied testing override unnecessary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Overwriting an old .eslintrc used to drop a project's own rules. Carry over
its rules, ignore patterns, and overrides' rules, plus .eslintignore, and
warn about anything else.

Also fix chalk tagged templates mangling interpolated values in warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The publish action runs a bare `npm publish`, so every prerelease took
`latest`. Set npm's tag from the version instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Changing a `@typescript-eslint/*` or `jest/*` rule without scoping it to
those configs' files fails with `Could not find plugin`, so export the
globs to scope overrides with, and document how to customize the configs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- overlapping patterns (eg: `.prettierrc.mjs` and a regex matching it)
  could race to delete the same file; the second failure was swallowed, so
  setup deleted the old config without writing the new one
- with remove off, an existing match was reported but ignored, so setup
  wrote a second config next to it (eg: `.prettierrc.mjs` next to
  `.prettierrc.json`)
- `reject` never threw; drop the two editor checks that relied on that

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
production — 97e4722b Deployed Oct 23, 2026 by uPaymeiFixit via publish #566
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant