Skip to content

eslint-plugin-tsdoc: published typings import from 'eslint' without declaring it as a peer dependency #479

Description

Summary

eslint-plugin-tsdoc's published lib/index.d.ts imports from 'eslint', but the package declares no dependency or peerDependency on eslint, only a devDependency. Under non-hoisted layouts (pnpm, Yarn PnP), that import never reaches the consumer's ESLint; it binds to whatever @types/eslint is reachable in the tree. When that's the v8-era @types/eslint, // @ts-checked flat configs fail to compile.

Repro

https://github.com/michaeldev5/bug-reproduction/tree/eslint-plugin-tsdoc-undeclared-eslint-types

git clone -b eslint-plugin-tsdoc-undeclared-eslint-types \
  https://github.com/michaeldev5/bug-reproduction.git
cd bug-reproduction && pnpm install
./node_modules/.bin/tsc --noEmit

A // @ts-checked flat config doing plugins: { tsdoc } fails:

eslint.config.js(6,14): error TS2322: Type 'IPlugin' is not assignable to type 'Plugin'.
  Types of property 'rules' are incompatible.
    Type '{ [x: string]: RuleModule; }' is not assignable to
    type 'Record<string, RuleDefinition<RuleDefinitionTypeOptions>>'.

eslint-plugin-tsdoc@0.5.2, eslint@10.8.0, typescript@6.0.3, pnpm 11.

Cause

tsc --traceResolution shows the plugin's own import type * as eslint from 'eslint' landing on the wrong package:

======== Resolving module 'eslint' from '.../eslint-plugin-tsdoc/lib/index.d.ts'. ========
======== Module name 'eslint' was successfully resolved to
         '.../.pnpm/@types+eslint@9.6.1/node_modules/@types/eslint/index.d.ts' ========

Because eslint is undeclared, pnpm never links it into the plugin's private node_modules, so TypeScript's upward walk continues past it. In the repro, @types/eslint arrives purely transitively via @types/eslint-scope; in real projects it usually comes through webpack. Nothing depends on it directly.

The declared shape isn't itself wrong. Against ESLint's own bundled typings, { [k: string]: Rule.RuleModule } is assignable to ESLint.Plugin["rules"]. The bug is only which 'eslint' the declaration file binds to, which is also why a flat npm install doesn't reproduce: the real eslint is hoisted and its bundled types win.

Fix

"peerDependencies": { "eslint": ">=7" }

Verified sufficient: declaring the peer via pnpm packageExtensions links the consumer's ESLint into the plugin's node_modules, resolution moves to the real package, and tsc exits 0.

More durably, the published typings could drop the ambient 'eslint' import and use @typescript-eslint/utils (already a runtime dependency) or @eslint/core's Plugin / RuleDefinition. That removes the undeclared-module dependency entirely, and composes with #425.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions