Skip to content

[eslint-config] Separate type-aware rules, add without-type-information helper, and convert to TypeScript - #6026

Merged
Ian Clanton-Thuon (iclanton) merged 16 commits into
microsoft:mainfrom
iclanton:feature/eslint-config-typescript
Sep 14, 2026
Merged

Ian Clanton-Thuon (iclanton) merged 16 commits into
microsoft:mainfrom
iclanton:feature/eslint-config-typescript

Conversation

@iclanton

Copy link
Copy Markdown
Member

Summary

Precursor to #6006. This PR does two related things to @rushstack/eslint-config (plus small type-only changes to the @rushstack ESLint plugins), so that the follow-up work in #6006 can consume a published version:

  1. Separate the type-aware rules in the flat profile and add a flat/without-type-information helper, so TypeScript files that are not part of a project's TypeScript program (e.g. config files or tests excluded from tsconfig.json) can be linted with only the non-type-aware rules instead of failing with a fatal typescript-eslint parse error.
  2. Convert the package to TypeScript. The configuration modules are now authored in src/**/*.ts and built with Heft to lib-commonjs + lib-dts.

Details

Type-aware rule separation

  • The flat profile's type-aware rules (naming-convention, no-floating-promises, no-for-in-array) are grouped into an exported typeAwareRules object.
  • New helper @rushstack/eslint-config/flat/without-type-information returns flat-config objects that disable type-aware parsing (project: false) and turn those rules off for a given set of files.
  • disabledTypeAwareRules in the helper is typed as Record<keyof typeof typeAwareRules, 'off'> (with typeAwareRules using as const satisfies Linter.RulesRecord), so adding a type-aware rule to the profile becomes a compile error until it is also disabled in the helper.

TypeScript conversion

  • All config modules moved to src/** and are built with decoupled-local-node-rig to CommonJS (export =), preserving the existing subpath entry points (e.g. @rushstack/eslint-config/flat/profile/node-trusted-tool) via a package.json exports map. CommonJS output is retained because these modules are consumed via require().
  • The compiled configuration was verified to be runtime-identical to the previous JavaScript sources (deep-compared object-by-object).

ESLint.Plugin typing (small plugin changes)

typescript-eslint's RuleModule type is intentionally not structurally assignable to ESLint's RuleDefinition, so a plugin object authored with typescript-eslint's types cannot be placed in a flat-config plugins map without a widening cast. To keep that cast out of the config, the @rushstack/eslint-plugin, @rushstack/eslint-plugin-security, and @rushstack/eslint-plugin-packlets default exports are now typed as ESLint.Plugin (a type-only change; runtime exports are unchanged). Only the genuinely third-party @typescript-eslint plugin retains a single localized cast, which widens through object (never unknown).

How it was tested

  • heft build --clean passes (TypeScript compile + self-lint) for @rushstack/eslint-config and the three @rushstack ESLint plugins.
  • The compiled flat/legacy profiles and mixins were deep-compared against the pre-conversion JavaScript and are runtime-identical.
  • rush build --to @rushstack/heft-lint-plugin builds cleanly through the full rig chain (local-node-rig -> local-eslint-config -> @rushstack/eslint-config + plugins), confirming consumers still resolve the new exports map and lint identically.
  • rush change files added for all affected published packages.

…ation helper

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Author the ESLint configuration modules in TypeScript under src/ and build them
with Heft (decoupled-local-node-rig) to lib-commonjs. Preserve the existing
subpath entry points (e.g. @rushstack/eslint-config/flat/profile/node-trusted-tool)
via a package.json "exports" map, and keep CommonJS output using `export =` so
that existing require()-based consumers are unaffected. The compiled configuration
output is verified to be runtime-identical to the previous JavaScript sources.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… type system

Use `satisfies Linter.RulesRecord` on typeAwareRules so its literal rule names are
preserved in the inferred type, and type disabledTypeAwareRules as
`Record<keyof typeof typeAwareRules, 'off'>`. Adding a type-aware rule now becomes a
compile error until it is also disabled in without-type-information. Also add the
change file for the TypeScript conversion.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… rule group

Replace the remaining `import x = require(...)` forms with ES `import` statements
(esModuleInterop is enabled), and use `as const satisfies Linter.RulesRecord` for
the typeAwareRules group. The compiled configuration remains runtime-identical.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…p the coercion helper

typescript-eslint's `RuleModule` type is intentionally not structurally assignable
to ESLint's `RuleDefinition`, so a plugin object authored with typescript-eslint's
types cannot be assigned to an `ESLint.Plugin` without a widening cast. Move that
cast to each @rushstack plugin's export (`@rushstack/eslint-plugin`,
`-security`, `-packlets`) so their public type is `ESLint.Plugin`, and drop the
`toEslintPlugin` helper from the config. Only the genuinely third-party
@typescript-eslint plugin now needs a localized cast. The casts widen through
`object` (never `unknown`), and the compiled configuration remains runtime-identical.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…json

Explain that the override is needed so that TypeScript uses Node16 module
resolution (which honors dependencies' "exports" maps) instead of the legacy
"node" resolution implied by the rig's "module": "commonjs", while still
emitting CommonJS output.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eScript

`@typescript-eslint/parser` marks its CommonJS export with `__esModule` but
exposes the parser API on `module.exports` with no `default` export, so a default
`import` resolved to `undefined` at runtime. ESLint then silently fell back to its
built-in parser and failed to parse TypeScript ("Unexpected token ...") across every
consumer of the shared config. Use a namespace import to bind the module export.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@iclanton
Ian Clanton-Thuon (iclanton) merged commit 2554250 into microsoft:main Sep 14, 2026
7 checks passed
@github-project-automation github-project-automation Bot moved this from Needs triage to Closed in Bug Triage Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Closed

Development

Successfully merging this pull request may close these issues.

2 participants