Skip to content

fix(resolver): resolve tsconfig extends through the real path - #15718

Open
orgads wants to merge 1 commit into
web-infra-dev:mainfrom
orgads:fix/tsconfig-extends-symlink
Open

orgads wants to merge 1 commit into
web-infra-dev:mainfrom
orgads:fix/tsconfig-extends-symlink

Conversation

@orgads

@orgads orgads commented Sep 16, 2026

Copy link
Copy Markdown

Motivation

Closes #15717.

When resolve.tsConfig points at a config whose extends chain passes through
a package in node_modules, and that package is a symlink (the normal pnpm
layout), the build fails:

ERROR in ./entry.ts 1:1-31
  × Tsconfig not found @tsconfig/node22/tsconfig.json

The package extends specifier is resolved through node_modules, but the
resulting path was never canonicalized. The nested extends inside that
package was therefore looked up from the symlink location
(<root>/node_modules/shared-config/), which walks up to the project's root
node_modules, where the package's own dependencies do not exist — under pnpm
they live next to the real package directory in
node_modules/.pnpm/shared-config@…/node_modules/.

tsc resolves the same chain without complaint, since it looks up
module-resolved configs through their real path. The same tree materialized
without a symlink (npm-style) also builds fine today, confirming the problem is
symlink-specific.

This is easy to hit unintentionally: any monorepo shipping a shared base
tsconfig as a package that itself extends @tsconfig/* breaks under pnpm. It
also surfaces very indirectly through Rstest, which sets resolve.tsConfig
automatically and swallows the resolver error.

Changes

Apply the existing load_realpath to the config resolved for a bare extends
specifier, so nested extends (and relative extends within that package)
resolve from its real directory. options.symlinks is honored, so
symlinks: false keeps the previous behavior. Relative and absolute extends
specifiers are untouched — tsc only resolves module-resolved configs through
the real path.

Regression test: a pnpm-style fixture where node_modules/shared-config is a
symlink into a store directory holding its @tsconfig/base dependency next to
it, absent from the project's own node_modules. Without the fix the test
fails with exactly TsconfigNotFound("@tsconfig/base/tsconfig.json"). The
symlink is created at test runtime (skipped when unprivileged on Windows),
matching the existing symlink test.

🤖 Generated with Claude Code

A package `extends` specifier was resolved through node_modules but the
resulting path was never canonicalized, so a nested `extends` inside that
package was looked up from the symlink location. Under pnpm,
`node_modules/pkg` points into `node_modules/.pnpm/pkg@x/node_modules/pkg`,
so the lookup walked up to the project's root `node_modules`, where the
package's own dependencies do not exist:

    × Tsconfig not found @tsconfig/node22/tsconfig.json

`tsc` resolves the same chain because it looks up module-resolved configs
through their real path. Apply `load_realpath` to the resolved config,
honoring `options.symlinks`, so nested `extends` resolve from the real
directory. Relative and absolute specifiers are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Issue: web-infra-dev#15717
@LingyuCoder

Copy link
Copy Markdown
Contributor

@stormslowly PTAL

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.

[Bug]: tsconfig extends through a symlinked node_modules package fails to resolve (pnpm)

2 participants