fix(eslint): trace flat-config plugin imports through workspace-internal config packages#266
Merged
BartWaardenburg merged 1 commit intofallow-rs:mainfrom May 3, 2026
Conversation
…nal config packages
In Turborepo/Nx monorepos that centralize ESLint config in a workspace
package, every workspace gets false `unused-devdep` flags for the
plugins that the shared config imports.
Pattern:
// apps/foo/eslint.config.mjs
import next from '@scope/eslint-config/next';
export default [...next];
// packages/eslint-config/next.js
import reactPlugin from 'eslint-plugin-react';
export default [{ plugins: { react: reactPlugin } }];
// apps/foo/package.json
{
"devDependencies": {
"eslint": "^9",
"eslint-plugin-react": "^7",
"@scope/eslint-config": "*"
}
}
`eslint-plugin-react` is flagged as unused-devdep in
`apps/foo/package.json` even though the workspace package transitively
imports it.
Two changes work together:
1. `crates/core/src/plugins/eslint.rs` — `extract_eslint_config` walks
up the directory tree to find hoisted `node_modules` (Turborepo/pnpm
do not co-locate workspace deps under the workspace's own
`node_modules`), and `read_package_entry_for_specifier` resolves
subpath imports like `@scope/eslint-config/next` via the package's
`exports` map or by probing `.js`/`.mjs`/`.cjs` extensions on the
bare subpath name.
2. `crates/core/src/plugins/registry/mod.rs` — adds `"eslint"` to the
`must_parse_workspace_config_when_root_active` allowlist. Without
this, when ESLint is already active at the monorepo root, every
workspace's `eslint.config.*` is silently skipped by
`run_workspace_fast`, and the (1) resolver never gets called.
Discovered via integration testing: the resolver was correct in
isolation but never invoked at runtime.
Tests:
- Existing `eslint.rs` unit tests cover the resolver (38 passing).
- New `run_workspace_fast_eslint_config_parsed_when_eslint_active_at_root`
registry test reproduces the real-world Turborepo layout (hoisted
`@scope/eslint-config` package + workspace eslint.config.mjs +
skip_config_plugins containing 'eslint') and asserts
`eslint-plugin-react` appears in `referenced_dependencies`.
Collaborator
|
Released in v2.63.0. Thanks @fmguerreiro. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
In Turborepo/Nx monorepos that centralize ESLint config in a workspace package, every workspace gets false
unused-devdepflags for the plugins that the shared config imports. Pattern:```js
// apps/foo/eslint.config.mjs
import next from '@scope/eslint-config/next';
export default [...next];
// packages/eslint-config/next.js
import reactPlugin from 'eslint-plugin-react';
export default [{ plugins: { react: reactPlugin } }];
```
```json
// apps/foo/package.json
{
"devDependencies": {
"eslint": "^9",
"eslint-plugin-react": "^7",
"@scope/eslint-config": "*"
}
}
```
eslint-plugin-reactis flagged as unused-devdep inapps/foo/package.json, even though the workspace package transitively imports it.Fix
Two cooperating changes:
1.
crates/core/src/plugins/eslint.rsfind_package_dirwalks upstart.ancestors()checkingnode_modules/<pkg>/package.json. Bounded byMAX_NODE_MODULES_WALK_DEPTH = 8so the walk cannot escape into the host filesystem. Handles Turborepo/pnpm hoisting where deps live in the monorepo root rather than per-workspace.read_package_entry_for_specifierresolves@scope/pkg/subpathimports via the package'sexportsmap, falling back to.js/.mjs/.cjsextension probing on the bare subpath name.resolve_package_entryreturnsOption<String>— when no exports-map entry and no extension probe matches, returnsNoneso the caller skips the subpath cleanly. Replaces an earlier silent-fallthrough that would synthesize a guessed path and rely onread_to_stringto fail.2.
crates/core/src/plugins/registry/mod.rsAdds
"eslint"to themust_parse_workspace_config_when_root_activeallowlist (already containsdocusaurus,jest,tanstack-router,vitest). Without this, when ESLint is already active at the monorepo root, every workspace'seslint.config.*is silently skipped byrun_workspace_fast, and the resolver from (1) never gets called. Discovered via integration testing: the resolver was correct in isolation but never invoked at runtime.Tests
find_package_dir_finds_local_install_at_depth_zero: walk does not skip a package co-located with the workspace.find_package_dir_returns_none_when_walk_finds_nothing: walk-up failure returns None cleanly.run_workspace_fast_eslint_config_parsed_when_eslint_active_at_root: full integration test reproducing the real-world Turborepo layout (hoisted@scope/eslint-configpackage + workspaceeslint.config.mjs+skip_config_pluginscontaining"eslint") and assertingeslint-plugin-reactappears inreferenced_dependencies.40 ESLint tests pass total. Other plugins not regressed.
Known follow-ups (out of scope)
resolve_exports_subpathdoes not handle wildcard patterns ("./features/*": "./src/features/*.js"). Increasingly common in design-system packages. Filed as separate concern.referenced_dependenciesaccumulates duplicates from both passes. Final consumption already deduplicates viaFxHashSet<&str>, so this is a memory bloat issue rather than correctness. If the maintainers prefer pre-dedup at aggregation time, happy to follow up.Verified on
cargo test --workspace,cargo clippy --workspace --all-targets -- -D warnings,cargo fmt --all -- --check. Real-world: this fork-pinned binary running in a Turborepo monorepo CI cleared the false-positiveunused-devdepflags foreslint-config-nextandeslint-plugin-prettierwhile still correctly flagging a genuinely-unusedeslint-plugin-unused-imports.