Skip to content

fix(eslint-plugin): use project root as path-normalization anchor in monorepos#365

Merged
chadjw merged 3 commits into
mainfrom
fix/eslint-plugin-monorepo-path-anchor
May 20, 2026
Merged

fix(eslint-plugin): use project root as path-normalization anchor in monorepos#365
chadjw merged 3 commits into
mainfrom
fix/eslint-plugin-monorepo-path-anchor

Conversation

@chadjw
Copy link
Copy Markdown
Contributor

@chadjw chadjw commented May 20, 2026

Summary

  • In monorepos, <package>/src/** files normalized to src/<...>, destroying the package prefix and making layer-based rules with from: "packages/<x>/**" patterns unable to match files inside <package>/src/**.
  • Add an optional projectRoot parameter to normalizePath / resolveImportPath in packages/eslint-plugin/src/utils/path-utils.ts. Rules resolve the anchor via a new getConfigRoot(filePath) helper that walks up to harness.config.json (wraps the existing findConfigFile).
  • When projectRoot is provided and the file lives under it, return the project-root-relative path (preserves package identity). Otherwise fall back to the legacy /src/ heuristic — byte-identical to current behavior.

Backward compatibility

  • Every existing test passes unmodified. They don't thread a projectRoot and hit the legacy fallback path.
  • No schema changes. projectRoot is implicit (derived from config file location), not a config field.

Repro & consumer impact

Before this fix, in a monorepo:

normalizePath('/abs/repo/packages/types/src/foo.ts') === 'src/foo.ts'

so a forbiddenImports rule with from: "packages/types/**" never matched. After this fix, the same call with a projectRoot of /abs/repo returns 'packages/types/src/foo.ts', and layer-internal rules for packages/<x>/src/** start firing as intended.

Out of scope

  • The importMatchesDisallowed matcher is unchanged — workspace-package specifier matching is a separate config-side concern.
  • No schema additions.

Test plan

  • New unit tests in tests/utils/path-utils.test.ts for with projectRoot (monorepo) covering: anchoring, files without /src/, outside-root fallback, trailing-slash equivalence, legacy preservation.
  • New unit tests in tests/utils/config-loader.test.ts for getConfigRoot (resolved directory + null branch).
  • New integration test tests/integration/monorepo-path-anchor.test.ts with a tests/fixtures/monorepo/harness.config.json fixture: forbiddenImports rule with from: "packages/a/**" fires on a file at packages/a/src/index.ts (it does not on main).
  • Revert-verified: stashing the four source files produces 7 test failures; restoring the fix → 180/180 pass.
  • pnpm test, pnpm typecheck, pnpm lint, pnpm build all clean in packages/eslint-plugin.
  • Repo-wide pnpm -w lint and pnpm test:ci clean.

chadjw added 3 commits May 20, 2026 11:36
…monorepos

The `/src/` heuristic in `normalizePath` and `resolveImportPath` collapses
`<monorepo>/packages/<x>/src/foo.ts` to `src/foo.ts`, destroying the package
prefix and making `no-forbidden-imports` / `no-layer-violation` patterns
that reference `packages/<x>/**` unable to match files inside
`<package>/src/**`.

Add an optional `projectRoot` parameter to both utilities. When provided
and the file lives under it, the project-root-relative path is returned;
otherwise the existing `/src/` fallback is used. Rules resolve the anchor
via a new `getConfigRoot` helper that walks up to `harness.config.json`.

Existing tests pass unmodified — they don't thread a `projectRoot` and
hit the fallback path.
…tPath fallback

Add tests for two branches introduced by the projectRoot widening that
weren't exercised by the existing suite: getConfigRoot returning null
when no harness.config.json ancestor exists, and resolveImportPath
returning the import path unchanged when the resolved location has no
/src/ boundary and no projectRoot anchor.
`path.resolve` on Windows prepends a drive letter to Unix-style absolute
paths (e.g. '/abs/repo/...' becomes 'D:/abs/repo/...'), which makes the
synthetic Unix paths in three monorepo resolveImportPath cases fall
through the `startsWith(root + '/')` check. This is a test-fixture
limitation, not a production bug — real Windows ESLint usage receives
filenames and projectRoot values that share a drive prefix.

The integration test (`monorepo-path-anchor.test.ts`) uses real
`path.join` to construct fixture paths, so it exercises the actual
cross-platform code path on all OSes.
@chadjw chadjw merged commit 44c4612 into main May 20, 2026
7 checks passed
@chadjw chadjw deleted the fix/eslint-plugin-monorepo-path-anchor branch May 20, 2026 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant