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

[eslint-config] Separate type-aware rules, add without-type-information helper, and convert to TypeScript#6026
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
@iclanton
Ian Clanton-Thuon (iclanton) deleted the feature/eslint-config-typescript branch September 14, 2026 21:23
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