Skip to content

Check referenced project sources before probing their declaration outputs - #64847

Open
Guru 🦄 (guru-irl) wants to merge 2 commits into
microsoft:mainfrom
guru-irl:fix/project-reference-output-probes
Open

Guru 🦄 (guru-irl) wants to merge 2 commits into
microsoft:mainfrom
guru-irl:fix/project-reference-output-probes

Conversation

@guru-irl

@guru-irl Guru 🦄 (guru-irl) commented Oct 16, 2026 •

Copy link
Copy Markdown

There is no associated issue.

Problem

  • When a program uses the sources of referenced projects instead of their built declarations (useSourceOfProjectReference, used by the language service), module resolution still checks the disk for referenced .d.ts outputs and output folders before it maps them to the source.
  • In a workspace with many referenced projects, this adds many checks of other projects' output folders: failed ones when outputs are not built, unneeded ones when they are.
  • Tools that track file access, such as sandboxed or incremental build systems, treat those checks as dependencies on other projects' build outputs.

Change

  • .d.ts files and folders in a referenced project's output are looked up in the reference mapping before the disk.
  • A .d.ts file still falls back to the disk when its source is missing. Output folders were already reported as existing; they are now answered without the disk.
  • For files reached through a linked package in node_modules, the mapping is cached, so realpath does not touch the output folder.
  • A lookup checks only its own parent folders for package links and maps through the nearest one, so its cost does not grow with the number of known links.
  • Command-line tsc and tsc -b do not use referenced sources and are unaffected.

Behavior changes

  • If a referenced project's .d.ts output, or its output folder, is a symlink to another location and the source exists, the program now uses the source, as it already does for unbuilt references. Before, it loaded the linked .d.ts. Reading the JavaScript implementation, it also checks the disk first and loads the linked file, so this differs from it.
  • In programs that use referenced sources, other .d.ts and folder lookups now consult the reference mapping before the disk. Each uncached lookup does more work, and the folder check grows with the number of referenced projects.
  • Only declaration files and folders are mapped. With a folder types entry, resolution still checks lib.ts, lib.tsx, lib.d.ts, lib/index.ts and lib/index.tsx on disk, as before.

Tests

  • New TestProjectReferenceSourceRedirectOutputAccesses in tsc/internal/compiler records checks of the dependency's lib/index.d.ts and lib folder. Ten cases: four output states (built, unbuilt, source missing, both missing), each with types pointing at a file and at a folder, plus two built cases where the output file or folder is a symlink.
  • Each case checks module resolution and the program's file list. With the source present, it checks that lib/index.d.ts and lib are not touched. With the source missing, it checks that lib/index.d.ts is still checked on disk.
  • On the base commit, the six cases with a source present fail and the other four pass.
  • go test for ./internal/compiler, ./internal/module, ./internal/symlinks/..., ./internal/project/... and ./internal/ls/... in tsc/ passes, and gofmt and go vet are clean.

AI disclosure

  • This change, its tests and this description were written with AI coding agents. I will discuss and revise it in review myself.

Use project-reference mappings before probing declaration files and
directories on disk. Cache symlinked file mappings even when
declarations already exist, so realpath does not access redirected
outputs. Retain filesystem fallback when a referenced source is missing.

When a referenced project's declaration output is itself a symlink to
another location and the source exists, the program now uses the
source, as it already does for unbuilt references. Previously the
symlink target's declaration file was loaded.

Check ancestor links before scanning the symlink cache, and test
emptiness without counting every link. This keeps external declaration
lookups from scaling with unrelated workspace links.

Cover built, unbuilt, source-missing and missing cases, outputs that
are symlinks, and packages whose types entry names a declaration
directory, using a recording filesystem and the program file lists.
Copilot AI balanced review requested due to automatic review settings October 16, 2026 19:59
@github-project-automation github-project-automation Bot moved this to Not started in PR Backlog Oct 16, 2026
@typescript-automation typescript-automation Bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Oct 16, 2026
@typescript-automation

Copy link
Copy Markdown
Contributor

This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Changed symlink-resolution behavior needs broader validation, and redundant link scanning remains unresolved.

1 open finding
What changed in this PR

This PR prioritizes referenced project sources over declaration-output disk checks during language-service module resolution.

Changes:

  • Checks source mappings before probing declaration files and directories.
  • Adds ancestor-link checks before scanning cached symlinks.
  • Adds ten cases covering output availability and symlink behavior.
File Description
tsc/​internal/​compiler/​projectreferencedtsfakinghost.go Prioritizes source mappings and checks ancestor links.
tsc/​internal/​compiler/​projectreferencedtsfakinghost_test.go Tests resolution results and declaration-output access.

🧠 Review effort: Balanced


💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +224 to +226
if ok && existsUsingSource(tspath.RootedPath(realFile)).IsTrue() {
canMap = true
break
The ancestor walk already finds a link whose target exists in a
referenced project's sources. Use that result instead of scanning every
known link again, so each lookup no longer grows with the number of
links. When several ancestors are links, the nearest one is used; the
scan picked any of them.

This branch has not been deployed

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

Labels

For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

Status: Not started

Development

Successfully merging this pull request may close these issues.

2 participants