Add Yarn PnP module resolution support - #63919

Open
Guyllian Gomez (GGomez99) wants to merge 7 commits into
microsoft:mainfrom
GGomez99:guyllian.gomez/yarn-pnp
Open

Add Yarn PnP module resolution support#63919
Guyllian Gomez (GGomez99) wants to merge 7 commits into
microsoft:mainfrom
GGomez99:guyllian.gomez/yarn-pnp

Conversation

@GGomez99

@GGomez99Guyllian Gomez (GGomez99) commented Aug 20, 2026

Copy link
Copy Markdown

Closes#63769

See original PR: microsoft/typescript-go#1966
Git history has been lost as replaying all commits would have generated a huge amount of unnecessary conflicts

Co-authored with gun-yu (@gun-yu), EJ (@ejc3) and Valentin MELUSSON (@valentinmelusson), original commits were lost during the move from the old repo

Motivation

This PR adds Plug'n'Play support natively to Typescript Go, following this issue: #63769
It has been reviewed and supported by Maël Nison (@arcanis), the lead maintainer of Yarn, and the original author of Yarn PnP.

Datadog has a frontend monorepo using yarn with over 6k packages, and seeing how TS Strada struggles with our current scaling, we decided to invest time in adding a native Yarn PnP support for Typescript Go.
This PnP implementation is being actively used by all engineers at Datadog, including in IDEs, by agents, and it's currently being beta-tested in CI with a custom lage setup.

Changes

It's based on the main changes from the original yarn patch (99f3e13) that the community has been maintaining for years throughout Typescript Strada updates, except that we implemented the official PnP specification so it doesn't depend on third-party code.

Implemented features:

  • PnP VFS that handles virtual folders and zip files seamlessly, with caching and fallback to the original vfs if pnp is not available
  • Add PnP API and manifest handling, following the yarn PnP specification
  • Initialize the PnP API every time a Host is initialized for both build and LSP modes
  • Add PnP support when resolving modules in internal/module/resolver.go
  • Add PnP support for auto-imports and completion at internal/modulespecifiers/specifiers.go
  • Add PnP support for root types at internal/core/compileroptions.go
  • Handle zip paths when going to implementation with the LSP
  • Update the baseline testing framework to handle PnP when needed
  • PnP manifest auto-refresh by watching .pnp.cjs changes

Tests

  • Basic PnP setup
  • Types from transitive dependencies
  • Root types loading from PnP dependencies
  • Completion and autoimports

Ports the Yarn Plug'n'Play (PnP) support developed on
guyllian.gomez/yarn-pnp in typescript-go into this repo's layout,
adding a pnp resolver and vfs overlay so tsc/lsp can resolve modules
through .pnp.cjs/.pnp.data.json manifests without node_modules.
- internal/pnp: parses PnP manifests and resolves package/dependency
locations
- internal/vfs/pnpvfs: read-through vfs.FS overlay that exposes
zip-packed dependencies (zip:/path/to.zip/inner/path) as regular
files
- module resolver, autoimport, and project/session wiring to plug the
PnP resolver and vfs into the existing module resolution and LSP
pipelines
- new compiler/fourslash test fixtures and baselines under
tsc/testdata covering PnP resolution scenarios
- packages/vscode-typescript/src/client.ts: register the "zip" URI
scheme in the LSP client's documentSelector so the extension
attaches to files served from zip-packed PnP dependencies
Ported and squashed from the original branch history to reconcile
with divergent upstream refactors (module rename to
github.com/microsoft/TypeScript/tsc, auto-import infrastructure
rewrite) that landed in this repo's main before this port. Original
work also includes contributions from valentinmelusson (fix package
IDs for Yarn PnP submodule imports) and gun-yu (add pnp error
handling and test).
Co-Authored-By: valentinmelusson <valentin.melusson@datadoghq.com>
Co-Authored-By: gun-yu <163322737+gun-yu@users.noreply.github.com>
@typescript-automationtypescript-automationBot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Aug 20, 2026
@GGomez99
Guyllian Gomez (GGomez99) marked this pull request as ready for review August 20, 2026 15:59
CopilotAI balanced review requested due to automatic review settings August 20, 2026 15:59

CopilotAI 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.

Pull request overview

Adds native Yarn Plug’n’Play support across the compiler, language server, module resolution, and virtual filesystem.

Changes:

  • Implements PnP manifest parsing, dependency resolution, type roots, and auto-imports.
  • Adds zip/virtual-path filesystem support and manifest refresh handling.
  • Adds compiler, resolver, VFS, and fourslash coverage with baselines.

Reviewed changes

Copilot reviewed 84 out of 88 changed files in this pull request and generated 10 comments.

Show a summary per file
FileDescription
tsc/testdata/tests/cases/compiler/pnpTypeRootsResolution.tsTests PnP type resolution.
tsc/testdata/tests/cases/compiler/pnpTransitiveDependencies.tsTests transitive dependency isolation.
tsc/testdata/tests/cases/compiler/pnpSimpleTest.tsTests basic PnP imports.
tsc/testdata/tests/cases/compiler/pnpDeclarationEmitWorkspace.tsTests workspace declaration emit.
tsc/testdata/fixtures/pnp/test-expectations.jsonDefines PnP resolver scenarios.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.errors.txtDiagnostic baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.jsEmit baseline.
tsc/internal/vfs/pnpvfs/pnpvfs.goImplements PnP zip and virtual-path VFS.
tsc/internal/vfs/pnpvfs/pnpvfs_test.goTests the PnP VFS.
tsc/internal/vfs/cachedvfs/cachedvfs.goPropagates cache clearing.
tsc/internal/tspath/path.goDetects zip paths.
tsc/internal/tsoptions/tsoptionstest/vfsparseconfighost.goEnables PnP in config tests.
tsc/internal/tsoptions/tsconfigparsing.goExposes PnP through config hosts.
tsc/internal/tsoptions/contentmappers_test.goUpdates test host contracts.
tsc/internal/transpile/transpile.goUpdates compiler-host construction.
tsc/internal/transformers/tstransforms/importelision_test.goUpdates fake program contract.
tsc/internal/testutil/tsbaseline/js_emit_baseline.goPreserves PnP loader fixtures.
tsc/internal/testutil/harnessutil/harnessutil.goInitializes PnP in compiler tests.
tsc/internal/project/snapshot.goCarries and refreshes PnP state.
tsc/internal/project/session.goWires PnP into sessions and watches.
tsc/internal/project/projectcollectionbuilder.goBuilds PnP manifest watchers.
tsc/internal/project/project.goStores project PnP watchers.
tsc/internal/project/overlayfs.goInvalidates snapshots after installs.
tsc/internal/project/filechange.goAdds a PnP install change kind.
tsc/internal/project/extendedconfigcache_test.goUpdates PnP-aware config tests.
tsc/internal/project/configfileregistrybuilder.goExposes PnP during config parsing.
tsc/internal/project/compilerhost.goExposes PnP from project hosts.
tsc/internal/project/autoimport.goCarries PnP into auto-import cloning.
tsc/internal/pnp/pnpapi.goImplements the PnP resolution API.
tsc/internal/pnp/pnpapi_test.goTests manifest loading and resolution.
tsc/internal/pnp/pnp.goInitializes and identifies PnP loaders.
tsc/internal/pnp/manifestparser.goParses serialized PnP manifests.
tsc/internal/modulespecifiers/types.goExtends specifier host contracts.
tsc/internal/modulespecifiers/specifiers.goGenerates PnP package specifiers.
tsc/internal/modulespecifiers/specifiers_test.goUpdates specifier test hosts.
tsc/internal/module/types.goExtends module resolution hosts.
tsc/internal/module/resolver.goAdds PnP module and type resolution.
tsc/internal/module/resolver_test.goTests PnP package IDs.
tsc/internal/lsp/server.goInitializes PnP for LSP sessions.
tsc/internal/lsp/lsproto/lsp.goConverts zip document URIs.
tsc/internal/ls/lsconv/converters.goEmits zip document URIs.
tsc/internal/ls/lsconv/converters_test.goTests zip URI conversion.
tsc/internal/ls/host.goAdds PnP to language-service hosts.
tsc/internal/ls/findallreferences_test.goUpdates compiler-host construction.
tsc/internal/ls/file_rename_test.goUpdates test host contracts.
tsc/internal/ls/autoimport/util.goUses PnP for auto-import resolution.
tsc/internal/ls/autoimport/registry.goIndexes importable PnP packages.
tsc/internal/ls/autoimport/aliasresolver.goUpdates checker host contracts.
tsc/internal/ls/autoimport/aliasresolver_crash_test.goUpdates fake clone hosts.
tsc/internal/fourslash/tests/pnpAutoImportCompletion_test.goTests PnP auto-import completions.
tsc/internal/execute/watcher.goPasses PnP into watch builds.
tsc/internal/execute/tsctests/sys.goAdds PnP-aware test systems.
tsc/internal/execute/tsc/extendedconfigcache_test.goUpdates config test hosts.
tsc/internal/execute/tsc/emit_test.goUpdates test systems and hosts.
tsc/internal/execute/tsc/compile.goAdds PnP to system contracts.
tsc/internal/execute/tsc.goPasses PnP into compilation.
tsc/internal/execute/incremental/buildinfo_contentmapper_test.goUpdates incremental test hosts.
tsc/internal/execute/build/orchestrator.goPasses PnP into build orchestration.
tsc/internal/execute/build/host.goExposes PnP from build hosts.
tsc/internal/execute/build/compilerHost.goExposes PnP from compiler hosts.
tsc/internal/diagnostics/loc_generated.goUpdates generated import ordering.
tsc/internal/diagnostics/extraDiagnosticMessages.jsonAdds PnP diagnostics.
tsc/internal/diagnostics/diagnostics_generated.goGenerates PnP diagnostic definitions.
tsc/internal/core/compileroptions.goRefactors type-root discovery.
tsc/internal/compiler/projectreferencedtsfakinghost.goForwards PnP through reference hosts.
tsc/internal/compiler/program.goExposes PnP from programs.
tsc/internal/compiler/program_test.goUpdates compiler-host calls.
tsc/internal/compiler/host.goStores PnP on compiler hosts.
tsc/internal/compiler/emitHost.goExposes PnP during emit.
tsc/internal/compiler/emit_test.goUpdates compiler-host calls.
tsc/internal/compiler/contentmapper_test.goUpdates compiler-host calls.
tsc/internal/checker/checker_test.goUpdates compiler-host calls.
tsc/internal/api/server.goInitializes PnP for API sessions.
tsc/cmd/tsc/sys.goInitializes PnP for the CLI.
packages/vscode-typescript/src/client.tsRegisters the zip URI scheme.
Files not reviewed (2)
  • tsc/internal/diagnostics/diagnostics_generated.go: Generated file
  • tsc/internal/diagnostics/loc_generated.go: Generated file

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

Comment threadtsc/internal/module/resolver.go Outdated
Comment on lines +382 to +395
func (p *PnpApi) AppendPnpTypeRoots(nmTypes []string, currentDirectory string, compilerOptions *core.CompilerOptions, nmFromConfig bool) ([]string, bool) {
baseDir := compilerOptions.GetBaseDirFromOptions(currentDirectory)
pnpTypes := p.GetPnpTypeRoots(baseDir)

if len(nmTypes) > 0 {
return append(nmTypes, pnpTypes...), nmFromConfig
}

if len(pnpTypes) > 0 {
return pnpTypes, false
}

return nil, false
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

resolveFromTypeRoot which reads from compilerOptions.TypeRoots as a fallback when node_modules resolution fails (resolver.go@L478), is never denying getEffectiveTypeRoots calls. And the latter uses the same resolution logic no matter the value of compilerOptions.TypeRoots

So I think this suggested change doesn't make sense here

Comment threadtsc/internal/modulespecifiers/specifiers.go Outdated
Comment threadtsc/internal/pnp/manifestparser.go
Comment threadtsc/internal/pnp/pnpapi.go
Comment on lines +1732 to +1735
pnpApi := pnp.InitPnpApi(s.fs, cwd)
if pnpApi != nil {
s.fs = pnpvfs.From(s.fs)
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

As discussed in the old PR in typescript-go, it would be complex to initialize the PnP API at the project level, as we use the PnP resolver for global scope logic, like parsing the project config. Implementing it would also stray us away from the original TS PnP patch which could potentially introduce unknown regressions

However, I still generated a quick POC that changes how the PnP API is initialized, and it seems to properly work on multi-root projects. Let me know if you want me to refine this change and integrate it in this PR 🙇

Comment on lines +161 to +171
cachedReader, ok := pnpFS.cachedZipReadersMap[zipPath]
if ok {
usedReader = cachedReader
} else {
zipReader, err := zip.OpenReader(zipPath)
if err != nil {
return pnpFS.fs, path, ""
}

usedReader = zipReader
pnpFS.cachedZipReadersMap[zipPath] = usedReader

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I had memory issues with bounded cache, as I couldn't find a way to make Go GC properly clear closed zipReaders, since some parts of the Typescript process seem to keep references of these readers.

It's been a while since I looked into it, and we never had memory issues related to zip readers in our monorepo. I can have a look at it again if you really want to improve this part of the pnpvfs

Comment on lines +356 to +361
var prefix string
if tspath.IsZipPath(fileName) {
prefix = "zip:"
} else {
prefix = "file:"
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I don't think I can fix this stateless helper without adding an additional argument and heavily refactor usages of FileNameToDocumentURI

I could look into it if needed, but I don't know if this edge case should really be handled

(Note that neovim/coc-nvim use zipfile: and we could also take the time to handle these if we do refactor FileNameToDocumentURI)

Comment on lines +99 to +102
func (pnpFS *pnpFS) UseCaseSensitiveFileNames() bool {
// pnp fs is always case sensitive
return true
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The yarn pnp patch for Typescript <7.0 always used to set the case sensitiveness to true (source), so I think this change would still be valid in TS7

Comment threadtsc/testdata/fixtures/pnp/test-expectations.json Outdated
…ests
Co-Authored-By: ejc3 <ejc3@users.noreply.github.com>
@typescript-automation

Copy link
Copy Markdown

The TypeScript team hasn't accepted the linked issue #63769. If you can get it accepted, this PR will have a better chance of being reviewed.

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

Labels

For Uncommitted BugPR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Yarn PnP

2 participants

@GGomez99
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add Yarn PnP module resolution support - #63919

Open
Guyllian Gomez (GGomez99) wants to merge 7 commits into
microsoft:mainfrom
GGomez99:guyllian.gomez/yarn-pnp
Open

Add Yarn PnP module resolution support#63919
Guyllian Gomez (GGomez99) wants to merge 7 commits into
microsoft:mainfrom
GGomez99:guyllian.gomez/yarn-pnp

Conversation

@GGomez99

@GGomez99Guyllian Gomez (GGomez99) commented Aug 20, 2026

Copy link
Copy Markdown

Closes#63769

See original PR: microsoft/typescript-go#1966
Git history has been lost as replaying all commits would have generated a huge amount of unnecessary conflicts

Co-authored with gun-yu (@gun-yu), EJ (@ejc3) and Valentin MELUSSON (@valentinmelusson), original commits were lost during the move from the old repo

Motivation

This PR adds Plug'n'Play support natively to Typescript Go, following this issue: #63769
It has been reviewed and supported by Maël Nison (@arcanis), the lead maintainer of Yarn, and the original author of Yarn PnP.

Datadog has a frontend monorepo using yarn with over 6k packages, and seeing how TS Strada struggles with our current scaling, we decided to invest time in adding a native Yarn PnP support for Typescript Go.
This PnP implementation is being actively used by all engineers at Datadog, including in IDEs, by agents, and it's currently being beta-tested in CI with a custom lage setup.

Changes

It's based on the main changes from the original yarn patch (99f3e13) that the community has been maintaining for years throughout Typescript Strada updates, except that we implemented the official PnP specification so it doesn't depend on third-party code.

Implemented features:

  • PnP VFS that handles virtual folders and zip files seamlessly, with caching and fallback to the original vfs if pnp is not available
  • Add PnP API and manifest handling, following the yarn PnP specification
  • Initialize the PnP API every time a Host is initialized for both build and LSP modes
  • Add PnP support when resolving modules in internal/module/resolver.go
  • Add PnP support for auto-imports and completion at internal/modulespecifiers/specifiers.go
  • Add PnP support for root types at internal/core/compileroptions.go
  • Handle zip paths when going to implementation with the LSP
  • Update the baseline testing framework to handle PnP when needed
  • PnP manifest auto-refresh by watching .pnp.cjs changes

Tests

  • Basic PnP setup
  • Types from transitive dependencies
  • Root types loading from PnP dependencies
  • Completion and autoimports

Ports the Yarn Plug'n'Play (PnP) support developed on
guyllian.gomez/yarn-pnp in typescript-go into this repo's layout,
adding a pnp resolver and vfs overlay so tsc/lsp can resolve modules
through .pnp.cjs/.pnp.data.json manifests without node_modules.
- internal/pnp: parses PnP manifests and resolves package/dependency
locations
- internal/vfs/pnpvfs: read-through vfs.FS overlay that exposes
zip-packed dependencies (zip:/path/to.zip/inner/path) as regular
files
- module resolver, autoimport, and project/session wiring to plug the
PnP resolver and vfs into the existing module resolution and LSP
pipelines
- new compiler/fourslash test fixtures and baselines under
tsc/testdata covering PnP resolution scenarios
- packages/vscode-typescript/src/client.ts: register the "zip" URI
scheme in the LSP client's documentSelector so the extension
attaches to files served from zip-packed PnP dependencies
Ported and squashed from the original branch history to reconcile
with divergent upstream refactors (module rename to
github.com/microsoft/TypeScript/tsc, auto-import infrastructure
rewrite) that landed in this repo's main before this port. Original
work also includes contributions from valentinmelusson (fix package
IDs for Yarn PnP submodule imports) and gun-yu (add pnp error
handling and test).
Co-Authored-By: valentinmelusson <valentin.melusson@datadoghq.com>
Co-Authored-By: gun-yu <163322737+gun-yu@users.noreply.github.com>
@typescript-automationtypescript-automationBot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Aug 20, 2026
@GGomez99
Guyllian Gomez (GGomez99) marked this pull request as ready for review August 20, 2026 15:59
CopilotAI balanced review requested due to automatic review settings August 20, 2026 15:59

CopilotAI 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.

Pull request overview

Adds native Yarn Plug’n’Play support across the compiler, language server, module resolution, and virtual filesystem.

Changes:

  • Implements PnP manifest parsing, dependency resolution, type roots, and auto-imports.
  • Adds zip/virtual-path filesystem support and manifest refresh handling.
  • Adds compiler, resolver, VFS, and fourslash coverage with baselines.

Reviewed changes

Copilot reviewed 84 out of 88 changed files in this pull request and generated 10 comments.

Show a summary per file
FileDescription
tsc/testdata/tests/cases/compiler/pnpTypeRootsResolution.tsTests PnP type resolution.
tsc/testdata/tests/cases/compiler/pnpTransitiveDependencies.tsTests transitive dependency isolation.
tsc/testdata/tests/cases/compiler/pnpSimpleTest.tsTests basic PnP imports.
tsc/testdata/tests/cases/compiler/pnpDeclarationEmitWorkspace.tsTests workspace declaration emit.
tsc/testdata/fixtures/pnp/test-expectations.jsonDefines PnP resolver scenarios.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.errors.txtDiagnostic baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.jsEmit baseline.
tsc/internal/vfs/pnpvfs/pnpvfs.goImplements PnP zip and virtual-path VFS.
tsc/internal/vfs/pnpvfs/pnpvfs_test.goTests the PnP VFS.
tsc/internal/vfs/cachedvfs/cachedvfs.goPropagates cache clearing.
tsc/internal/tspath/path.goDetects zip paths.
tsc/internal/tsoptions/tsoptionstest/vfsparseconfighost.goEnables PnP in config tests.
tsc/internal/tsoptions/tsconfigparsing.goExposes PnP through config hosts.
tsc/internal/tsoptions/contentmappers_test.goUpdates test host contracts.
tsc/internal/transpile/transpile.goUpdates compiler-host construction.
tsc/internal/transformers/tstransforms/importelision_test.goUpdates fake program contract.
tsc/internal/testutil/tsbaseline/js_emit_baseline.goPreserves PnP loader fixtures.
tsc/internal/testutil/harnessutil/harnessutil.goInitializes PnP in compiler tests.
tsc/internal/project/snapshot.goCarries and refreshes PnP state.
tsc/internal/project/session.goWires PnP into sessions and watches.
tsc/internal/project/projectcollectionbuilder.goBuilds PnP manifest watchers.
tsc/internal/project/project.goStores project PnP watchers.
tsc/internal/project/overlayfs.goInvalidates snapshots after installs.
tsc/internal/project/filechange.goAdds a PnP install change kind.
tsc/internal/project/extendedconfigcache_test.goUpdates PnP-aware config tests.
tsc/internal/project/configfileregistrybuilder.goExposes PnP during config parsing.
tsc/internal/project/compilerhost.goExposes PnP from project hosts.
tsc/internal/project/autoimport.goCarries PnP into auto-import cloning.
tsc/internal/pnp/pnpapi.goImplements the PnP resolution API.
tsc/internal/pnp/pnpapi_test.goTests manifest loading and resolution.
tsc/internal/pnp/pnp.goInitializes and identifies PnP loaders.
tsc/internal/pnp/manifestparser.goParses serialized PnP manifests.
tsc/internal/modulespecifiers/types.goExtends specifier host contracts.
tsc/internal/modulespecifiers/specifiers.goGenerates PnP package specifiers.
tsc/internal/modulespecifiers/specifiers_test.goUpdates specifier test hosts.
tsc/internal/module/types.goExtends module resolution hosts.
tsc/internal/module/resolver.goAdds PnP module and type resolution.
tsc/internal/module/resolver_test.goTests PnP package IDs.
tsc/internal/lsp/server.goInitializes PnP for LSP sessions.
tsc/internal/lsp/lsproto/lsp.goConverts zip document URIs.
tsc/internal/ls/lsconv/converters.goEmits zip document URIs.
tsc/internal/ls/lsconv/converters_test.goTests zip URI conversion.
tsc/internal/ls/host.goAdds PnP to language-service hosts.
tsc/internal/ls/findallreferences_test.goUpdates compiler-host construction.
tsc/internal/ls/file_rename_test.goUpdates test host contracts.
tsc/internal/ls/autoimport/util.goUses PnP for auto-import resolution.
tsc/internal/ls/autoimport/registry.goIndexes importable PnP packages.
tsc/internal/ls/autoimport/aliasresolver.goUpdates checker host contracts.
tsc/internal/ls/autoimport/aliasresolver_crash_test.goUpdates fake clone hosts.
tsc/internal/fourslash/tests/pnpAutoImportCompletion_test.goTests PnP auto-import completions.
tsc/internal/execute/watcher.goPasses PnP into watch builds.
tsc/internal/execute/tsctests/sys.goAdds PnP-aware test systems.
tsc/internal/execute/tsc/extendedconfigcache_test.goUpdates config test hosts.
tsc/internal/execute/tsc/emit_test.goUpdates test systems and hosts.
tsc/internal/execute/tsc/compile.goAdds PnP to system contracts.
tsc/internal/execute/tsc.goPasses PnP into compilation.
tsc/internal/execute/incremental/buildinfo_contentmapper_test.goUpdates incremental test hosts.
tsc/internal/execute/build/orchestrator.goPasses PnP into build orchestration.
tsc/internal/execute/build/host.goExposes PnP from build hosts.
tsc/internal/execute/build/compilerHost.goExposes PnP from compiler hosts.
tsc/internal/diagnostics/loc_generated.goUpdates generated import ordering.
tsc/internal/diagnostics/extraDiagnosticMessages.jsonAdds PnP diagnostics.
tsc/internal/diagnostics/diagnostics_generated.goGenerates PnP diagnostic definitions.
tsc/internal/core/compileroptions.goRefactors type-root discovery.
tsc/internal/compiler/projectreferencedtsfakinghost.goForwards PnP through reference hosts.
tsc/internal/compiler/program.goExposes PnP from programs.
tsc/internal/compiler/program_test.goUpdates compiler-host calls.
tsc/internal/compiler/host.goStores PnP on compiler hosts.
tsc/internal/compiler/emitHost.goExposes PnP during emit.
tsc/internal/compiler/emit_test.goUpdates compiler-host calls.
tsc/internal/compiler/contentmapper_test.goUpdates compiler-host calls.
tsc/internal/checker/checker_test.goUpdates compiler-host calls.
tsc/internal/api/server.goInitializes PnP for API sessions.
tsc/cmd/tsc/sys.goInitializes PnP for the CLI.
packages/vscode-typescript/src/client.tsRegisters the zip URI scheme.
Files not reviewed (2)
  • tsc/internal/diagnostics/diagnostics_generated.go: Generated file
  • tsc/internal/diagnostics/loc_generated.go: Generated file

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

Comment threadtsc/internal/module/resolver.go Outdated
Comment on lines +382 to +395
func (p *PnpApi) AppendPnpTypeRoots(nmTypes []string, currentDirectory string, compilerOptions *core.CompilerOptions, nmFromConfig bool) ([]string, bool) {
baseDir := compilerOptions.GetBaseDirFromOptions(currentDirectory)
pnpTypes := p.GetPnpTypeRoots(baseDir)

if len(nmTypes) > 0 {
return append(nmTypes, pnpTypes...), nmFromConfig
}

if len(pnpTypes) > 0 {
return pnpTypes, false
}

return nil, false
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

resolveFromTypeRoot which reads from compilerOptions.TypeRoots as a fallback when node_modules resolution fails (resolver.go@L478), is never denying getEffectiveTypeRoots calls. And the latter uses the same resolution logic no matter the value of compilerOptions.TypeRoots

So I think this suggested change doesn't make sense here

Comment threadtsc/internal/modulespecifiers/specifiers.go Outdated
Comment threadtsc/internal/pnp/manifestparser.go
Comment threadtsc/internal/pnp/pnpapi.go
Comment on lines +1732 to +1735
pnpApi := pnp.InitPnpApi(s.fs, cwd)
if pnpApi != nil {
s.fs = pnpvfs.From(s.fs)
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

As discussed in the old PR in typescript-go, it would be complex to initialize the PnP API at the project level, as we use the PnP resolver for global scope logic, like parsing the project config. Implementing it would also stray us away from the original TS PnP patch which could potentially introduce unknown regressions

However, I still generated a quick POC that changes how the PnP API is initialized, and it seems to properly work on multi-root projects. Let me know if you want me to refine this change and integrate it in this PR 🙇

Comment on lines +161 to +171
cachedReader, ok := pnpFS.cachedZipReadersMap[zipPath]
if ok {
usedReader = cachedReader
} else {
zipReader, err := zip.OpenReader(zipPath)
if err != nil {
return pnpFS.fs, path, ""
}

usedReader = zipReader
pnpFS.cachedZipReadersMap[zipPath] = usedReader

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I had memory issues with bounded cache, as I couldn't find a way to make Go GC properly clear closed zipReaders, since some parts of the Typescript process seem to keep references of these readers.

It's been a while since I looked into it, and we never had memory issues related to zip readers in our monorepo. I can have a look at it again if you really want to improve this part of the pnpvfs

Comment on lines +356 to +361
var prefix string
if tspath.IsZipPath(fileName) {
prefix = "zip:"
} else {
prefix = "file:"
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I don't think I can fix this stateless helper without adding an additional argument and heavily refactor usages of FileNameToDocumentURI

I could look into it if needed, but I don't know if this edge case should really be handled

(Note that neovim/coc-nvim use zipfile: and we could also take the time to handle these if we do refactor FileNameToDocumentURI)

Comment on lines +99 to +102
func (pnpFS *pnpFS) UseCaseSensitiveFileNames() bool {
// pnp fs is always case sensitive
return true
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The yarn pnp patch for Typescript <7.0 always used to set the case sensitiveness to true (source), so I think this change would still be valid in TS7

Comment threadtsc/testdata/fixtures/pnp/test-expectations.json Outdated
…ests
Co-Authored-By: ejc3 <ejc3@users.noreply.github.com>
@typescript-automation

Copy link
Copy Markdown

The TypeScript team hasn't accepted the linked issue #63769. If you can get it accepted, this PR will have a better chance of being reviewed.

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

Labels

For Uncommitted BugPR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Yarn PnP

2 participants

@GGomez99
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Yarn PnP module resolution support - #63919

Open
Guyllian Gomez (GGomez99) wants to merge 7 commits into
microsoft:mainfrom
GGomez99:guyllian.gomez/yarn-pnp
Open

Add Yarn PnP module resolution support#63919
Guyllian Gomez (GGomez99) wants to merge 7 commits into
microsoft:mainfrom
GGomez99:guyllian.gomez/yarn-pnp

Conversation

@GGomez99

@GGomez99Guyllian Gomez (GGomez99) commented Aug 20, 2026

Copy link
Copy Markdown

Closes#63769

See original PR: microsoft/typescript-go#1966
Git history has been lost as replaying all commits would have generated a huge amount of unnecessary conflicts

Co-authored with gun-yu (@gun-yu), EJ (@ejc3) and Valentin MELUSSON (@valentinmelusson), original commits were lost during the move from the old repo

Motivation

This PR adds Plug'n'Play support natively to Typescript Go, following this issue: #63769
It has been reviewed and supported by Maël Nison (@arcanis), the lead maintainer of Yarn, and the original author of Yarn PnP.

Datadog has a frontend monorepo using yarn with over 6k packages, and seeing how TS Strada struggles with our current scaling, we decided to invest time in adding a native Yarn PnP support for Typescript Go.
This PnP implementation is being actively used by all engineers at Datadog, including in IDEs, by agents, and it's currently being beta-tested in CI with a custom lage setup.

Changes

It's based on the main changes from the original yarn patch (99f3e13) that the community has been maintaining for years throughout Typescript Strada updates, except that we implemented the official PnP specification so it doesn't depend on third-party code.

Implemented features:

  • PnP VFS that handles virtual folders and zip files seamlessly, with caching and fallback to the original vfs if pnp is not available
  • Add PnP API and manifest handling, following the yarn PnP specification
  • Initialize the PnP API every time a Host is initialized for both build and LSP modes
  • Add PnP support when resolving modules in internal/module/resolver.go
  • Add PnP support for auto-imports and completion at internal/modulespecifiers/specifiers.go
  • Add PnP support for root types at internal/core/compileroptions.go
  • Handle zip paths when going to implementation with the LSP
  • Update the baseline testing framework to handle PnP when needed
  • PnP manifest auto-refresh by watching .pnp.cjs changes

Tests

  • Basic PnP setup
  • Types from transitive dependencies
  • Root types loading from PnP dependencies
  • Completion and autoimports

Ports the Yarn Plug'n'Play (PnP) support developed on
guyllian.gomez/yarn-pnp in typescript-go into this repo's layout,
adding a pnp resolver and vfs overlay so tsc/lsp can resolve modules
through .pnp.cjs/.pnp.data.json manifests without node_modules.
- internal/pnp: parses PnP manifests and resolves package/dependency
locations
- internal/vfs/pnpvfs: read-through vfs.FS overlay that exposes
zip-packed dependencies (zip:/path/to.zip/inner/path) as regular
files
- module resolver, autoimport, and project/session wiring to plug the
PnP resolver and vfs into the existing module resolution and LSP
pipelines
- new compiler/fourslash test fixtures and baselines under
tsc/testdata covering PnP resolution scenarios
- packages/vscode-typescript/src/client.ts: register the "zip" URI
scheme in the LSP client's documentSelector so the extension
attaches to files served from zip-packed PnP dependencies
Ported and squashed from the original branch history to reconcile
with divergent upstream refactors (module rename to
github.com/microsoft/TypeScript/tsc, auto-import infrastructure
rewrite) that landed in this repo's main before this port. Original
work also includes contributions from valentinmelusson (fix package
IDs for Yarn PnP submodule imports) and gun-yu (add pnp error
handling and test).
Co-Authored-By: valentinmelusson <valentin.melusson@datadoghq.com>
Co-Authored-By: gun-yu <163322737+gun-yu@users.noreply.github.com>
@typescript-automationtypescript-automationBot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Aug 20, 2026
@GGomez99
Guyllian Gomez (GGomez99) marked this pull request as ready for review August 20, 2026 15:59
CopilotAI balanced review requested due to automatic review settings August 20, 2026 15:59

CopilotAI 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.

Pull request overview

Adds native Yarn Plug’n’Play support across the compiler, language server, module resolution, and virtual filesystem.

Changes:

  • Implements PnP manifest parsing, dependency resolution, type roots, and auto-imports.
  • Adds zip/virtual-path filesystem support and manifest refresh handling.
  • Adds compiler, resolver, VFS, and fourslash coverage with baselines.

Reviewed changes

Copilot reviewed 84 out of 88 changed files in this pull request and generated 10 comments.

Show a summary per file
FileDescription
tsc/testdata/tests/cases/compiler/pnpTypeRootsResolution.tsTests PnP type resolution.
tsc/testdata/tests/cases/compiler/pnpTransitiveDependencies.tsTests transitive dependency isolation.
tsc/testdata/tests/cases/compiler/pnpSimpleTest.tsTests basic PnP imports.
tsc/testdata/tests/cases/compiler/pnpDeclarationEmitWorkspace.tsTests workspace declaration emit.
tsc/testdata/fixtures/pnp/test-expectations.jsonDefines PnP resolver scenarios.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.errors.txtDiagnostic baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.jsEmit baseline.
tsc/internal/vfs/pnpvfs/pnpvfs.goImplements PnP zip and virtual-path VFS.
tsc/internal/vfs/pnpvfs/pnpvfs_test.goTests the PnP VFS.
tsc/internal/vfs/cachedvfs/cachedvfs.goPropagates cache clearing.
tsc/internal/tspath/path.goDetects zip paths.
tsc/internal/tsoptions/tsoptionstest/vfsparseconfighost.goEnables PnP in config tests.
tsc/internal/tsoptions/tsconfigparsing.goExposes PnP through config hosts.
tsc/internal/tsoptions/contentmappers_test.goUpdates test host contracts.
tsc/internal/transpile/transpile.goUpdates compiler-host construction.
tsc/internal/transformers/tstransforms/importelision_test.goUpdates fake program contract.
tsc/internal/testutil/tsbaseline/js_emit_baseline.goPreserves PnP loader fixtures.
tsc/internal/testutil/harnessutil/harnessutil.goInitializes PnP in compiler tests.
tsc/internal/project/snapshot.goCarries and refreshes PnP state.
tsc/internal/project/session.goWires PnP into sessions and watches.
tsc/internal/project/projectcollectionbuilder.goBuilds PnP manifest watchers.
tsc/internal/project/project.goStores project PnP watchers.
tsc/internal/project/overlayfs.goInvalidates snapshots after installs.
tsc/internal/project/filechange.goAdds a PnP install change kind.
tsc/internal/project/extendedconfigcache_test.goUpdates PnP-aware config tests.
tsc/internal/project/configfileregistrybuilder.goExposes PnP during config parsing.
tsc/internal/project/compilerhost.goExposes PnP from project hosts.
tsc/internal/project/autoimport.goCarries PnP into auto-import cloning.
tsc/internal/pnp/pnpapi.goImplements the PnP resolution API.
tsc/internal/pnp/pnpapi_test.goTests manifest loading and resolution.
tsc/internal/pnp/pnp.goInitializes and identifies PnP loaders.
tsc/internal/pnp/manifestparser.goParses serialized PnP manifests.
tsc/internal/modulespecifiers/types.goExtends specifier host contracts.
tsc/internal/modulespecifiers/specifiers.goGenerates PnP package specifiers.
tsc/internal/modulespecifiers/specifiers_test.goUpdates specifier test hosts.
tsc/internal/module/types.goExtends module resolution hosts.
tsc/internal/module/resolver.goAdds PnP module and type resolution.
tsc/internal/module/resolver_test.goTests PnP package IDs.
tsc/internal/lsp/server.goInitializes PnP for LSP sessions.
tsc/internal/lsp/lsproto/lsp.goConverts zip document URIs.
tsc/internal/ls/lsconv/converters.goEmits zip document URIs.
tsc/internal/ls/lsconv/converters_test.goTests zip URI conversion.
tsc/internal/ls/host.goAdds PnP to language-service hosts.
tsc/internal/ls/findallreferences_test.goUpdates compiler-host construction.
tsc/internal/ls/file_rename_test.goUpdates test host contracts.
tsc/internal/ls/autoimport/util.goUses PnP for auto-import resolution.
tsc/internal/ls/autoimport/registry.goIndexes importable PnP packages.
tsc/internal/ls/autoimport/aliasresolver.goUpdates checker host contracts.
tsc/internal/ls/autoimport/aliasresolver_crash_test.goUpdates fake clone hosts.
tsc/internal/fourslash/tests/pnpAutoImportCompletion_test.goTests PnP auto-import completions.
tsc/internal/execute/watcher.goPasses PnP into watch builds.
tsc/internal/execute/tsctests/sys.goAdds PnP-aware test systems.
tsc/internal/execute/tsc/extendedconfigcache_test.goUpdates config test hosts.
tsc/internal/execute/tsc/emit_test.goUpdates test systems and hosts.
tsc/internal/execute/tsc/compile.goAdds PnP to system contracts.
tsc/internal/execute/tsc.goPasses PnP into compilation.
tsc/internal/execute/incremental/buildinfo_contentmapper_test.goUpdates incremental test hosts.
tsc/internal/execute/build/orchestrator.goPasses PnP into build orchestration.
tsc/internal/execute/build/host.goExposes PnP from build hosts.
tsc/internal/execute/build/compilerHost.goExposes PnP from compiler hosts.
tsc/internal/diagnostics/loc_generated.goUpdates generated import ordering.
tsc/internal/diagnostics/extraDiagnosticMessages.jsonAdds PnP diagnostics.
tsc/internal/diagnostics/diagnostics_generated.goGenerates PnP diagnostic definitions.
tsc/internal/core/compileroptions.goRefactors type-root discovery.
tsc/internal/compiler/projectreferencedtsfakinghost.goForwards PnP through reference hosts.
tsc/internal/compiler/program.goExposes PnP from programs.
tsc/internal/compiler/program_test.goUpdates compiler-host calls.
tsc/internal/compiler/host.goStores PnP on compiler hosts.
tsc/internal/compiler/emitHost.goExposes PnP during emit.
tsc/internal/compiler/emit_test.goUpdates compiler-host calls.
tsc/internal/compiler/contentmapper_test.goUpdates compiler-host calls.
tsc/internal/checker/checker_test.goUpdates compiler-host calls.
tsc/internal/api/server.goInitializes PnP for API sessions.
tsc/cmd/tsc/sys.goInitializes PnP for the CLI.
packages/vscode-typescript/src/client.tsRegisters the zip URI scheme.
Files not reviewed (2)
  • tsc/internal/diagnostics/diagnostics_generated.go: Generated file
  • tsc/internal/diagnostics/loc_generated.go: Generated file

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

Comment threadtsc/internal/module/resolver.go Outdated
Comment on lines +382 to +395
func (p *PnpApi) AppendPnpTypeRoots(nmTypes []string, currentDirectory string, compilerOptions *core.CompilerOptions, nmFromConfig bool) ([]string, bool) {
baseDir := compilerOptions.GetBaseDirFromOptions(currentDirectory)
pnpTypes := p.GetPnpTypeRoots(baseDir)

if len(nmTypes) > 0 {
return append(nmTypes, pnpTypes...), nmFromConfig
}

if len(pnpTypes) > 0 {
return pnpTypes, false
}

return nil, false
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

resolveFromTypeRoot which reads from compilerOptions.TypeRoots as a fallback when node_modules resolution fails (resolver.go@L478), is never denying getEffectiveTypeRoots calls. And the latter uses the same resolution logic no matter the value of compilerOptions.TypeRoots

So I think this suggested change doesn't make sense here

Comment threadtsc/internal/modulespecifiers/specifiers.go Outdated
Comment threadtsc/internal/pnp/manifestparser.go
Comment threadtsc/internal/pnp/pnpapi.go
Comment on lines +1732 to +1735
pnpApi := pnp.InitPnpApi(s.fs, cwd)
if pnpApi != nil {
s.fs = pnpvfs.From(s.fs)
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

As discussed in the old PR in typescript-go, it would be complex to initialize the PnP API at the project level, as we use the PnP resolver for global scope logic, like parsing the project config. Implementing it would also stray us away from the original TS PnP patch which could potentially introduce unknown regressions

However, I still generated a quick POC that changes how the PnP API is initialized, and it seems to properly work on multi-root projects. Let me know if you want me to refine this change and integrate it in this PR 🙇

Comment on lines +161 to +171
cachedReader, ok := pnpFS.cachedZipReadersMap[zipPath]
if ok {
usedReader = cachedReader
} else {
zipReader, err := zip.OpenReader(zipPath)
if err != nil {
return pnpFS.fs, path, ""
}

usedReader = zipReader
pnpFS.cachedZipReadersMap[zipPath] = usedReader

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I had memory issues with bounded cache, as I couldn't find a way to make Go GC properly clear closed zipReaders, since some parts of the Typescript process seem to keep references of these readers.

It's been a while since I looked into it, and we never had memory issues related to zip readers in our monorepo. I can have a look at it again if you really want to improve this part of the pnpvfs

Comment on lines +356 to +361
var prefix string
if tspath.IsZipPath(fileName) {
prefix = "zip:"
} else {
prefix = "file:"
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I don't think I can fix this stateless helper without adding an additional argument and heavily refactor usages of FileNameToDocumentURI

I could look into it if needed, but I don't know if this edge case should really be handled

(Note that neovim/coc-nvim use zipfile: and we could also take the time to handle these if we do refactor FileNameToDocumentURI)

Comment on lines +99 to +102
func (pnpFS *pnpFS) UseCaseSensitiveFileNames() bool {
// pnp fs is always case sensitive
return true
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The yarn pnp patch for Typescript <7.0 always used to set the case sensitiveness to true (source), so I think this change would still be valid in TS7

Comment threadtsc/testdata/fixtures/pnp/test-expectations.json Outdated
…ests
Co-Authored-By: ejc3 <ejc3@users.noreply.github.com>
@typescript-automation

Copy link
Copy Markdown

The TypeScript team hasn't accepted the linked issue #63769. If you can get it accepted, this PR will have a better chance of being reviewed.

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

Labels

For Uncommitted BugPR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Yarn PnP

2 participants

@GGomez99
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Yarn PnP module resolution support - #63919

Open
Guyllian Gomez (GGomez99) wants to merge 7 commits into
microsoft:mainfrom
GGomez99:guyllian.gomez/yarn-pnp
Open

Add Yarn PnP module resolution support#63919
Guyllian Gomez (GGomez99) wants to merge 7 commits into
microsoft:mainfrom
GGomez99:guyllian.gomez/yarn-pnp

Conversation

@GGomez99

@GGomez99Guyllian Gomez (GGomez99) commented Aug 20, 2026

Copy link
Copy Markdown

Closes#63769

See original PR: microsoft/typescript-go#1966
Git history has been lost as replaying all commits would have generated a huge amount of unnecessary conflicts

Co-authored with gun-yu (@gun-yu), EJ (@ejc3) and Valentin MELUSSON (@valentinmelusson), original commits were lost during the move from the old repo

Motivation

This PR adds Plug'n'Play support natively to Typescript Go, following this issue: #63769
It has been reviewed and supported by Maël Nison (@arcanis), the lead maintainer of Yarn, and the original author of Yarn PnP.

Datadog has a frontend monorepo using yarn with over 6k packages, and seeing how TS Strada struggles with our current scaling, we decided to invest time in adding a native Yarn PnP support for Typescript Go.
This PnP implementation is being actively used by all engineers at Datadog, including in IDEs, by agents, and it's currently being beta-tested in CI with a custom lage setup.

Changes

It's based on the main changes from the original yarn patch (99f3e13) that the community has been maintaining for years throughout Typescript Strada updates, except that we implemented the official PnP specification so it doesn't depend on third-party code.

Implemented features:

  • PnP VFS that handles virtual folders and zip files seamlessly, with caching and fallback to the original vfs if pnp is not available
  • Add PnP API and manifest handling, following the yarn PnP specification
  • Initialize the PnP API every time a Host is initialized for both build and LSP modes
  • Add PnP support when resolving modules in internal/module/resolver.go
  • Add PnP support for auto-imports and completion at internal/modulespecifiers/specifiers.go
  • Add PnP support for root types at internal/core/compileroptions.go
  • Handle zip paths when going to implementation with the LSP
  • Update the baseline testing framework to handle PnP when needed
  • PnP manifest auto-refresh by watching .pnp.cjs changes

Tests

  • Basic PnP setup
  • Types from transitive dependencies
  • Root types loading from PnP dependencies
  • Completion and autoimports

Ports the Yarn Plug'n'Play (PnP) support developed on
guyllian.gomez/yarn-pnp in typescript-go into this repo's layout,
adding a pnp resolver and vfs overlay so tsc/lsp can resolve modules
through .pnp.cjs/.pnp.data.json manifests without node_modules.
- internal/pnp: parses PnP manifests and resolves package/dependency
locations
- internal/vfs/pnpvfs: read-through vfs.FS overlay that exposes
zip-packed dependencies (zip:/path/to.zip/inner/path) as regular
files
- module resolver, autoimport, and project/session wiring to plug the
PnP resolver and vfs into the existing module resolution and LSP
pipelines
- new compiler/fourslash test fixtures and baselines under
tsc/testdata covering PnP resolution scenarios
- packages/vscode-typescript/src/client.ts: register the "zip" URI
scheme in the LSP client's documentSelector so the extension
attaches to files served from zip-packed PnP dependencies
Ported and squashed from the original branch history to reconcile
with divergent upstream refactors (module rename to
github.com/microsoft/TypeScript/tsc, auto-import infrastructure
rewrite) that landed in this repo's main before this port. Original
work also includes contributions from valentinmelusson (fix package
IDs for Yarn PnP submodule imports) and gun-yu (add pnp error
handling and test).
Co-Authored-By: valentinmelusson <valentin.melusson@datadoghq.com>
Co-Authored-By: gun-yu <163322737+gun-yu@users.noreply.github.com>
@typescript-automationtypescript-automationBot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Aug 20, 2026
@GGomez99
Guyllian Gomez (GGomez99) marked this pull request as ready for review August 20, 2026 15:59
CopilotAI balanced review requested due to automatic review settings August 20, 2026 15:59

CopilotAI 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.

Pull request overview

Adds native Yarn Plug’n’Play support across the compiler, language server, module resolution, and virtual filesystem.

Changes:

  • Implements PnP manifest parsing, dependency resolution, type roots, and auto-imports.
  • Adds zip/virtual-path filesystem support and manifest refresh handling.
  • Adds compiler, resolver, VFS, and fourslash coverage with baselines.

Reviewed changes

Copilot reviewed 84 out of 88 changed files in this pull request and generated 10 comments.

Show a summary per file
FileDescription
tsc/testdata/tests/cases/compiler/pnpTypeRootsResolution.tsTests PnP type resolution.
tsc/testdata/tests/cases/compiler/pnpTransitiveDependencies.tsTests transitive dependency isolation.
tsc/testdata/tests/cases/compiler/pnpSimpleTest.tsTests basic PnP imports.
tsc/testdata/tests/cases/compiler/pnpDeclarationEmitWorkspace.tsTests workspace declaration emit.
tsc/testdata/fixtures/pnp/test-expectations.jsonDefines PnP resolver scenarios.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.errors.txtDiagnostic baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.jsEmit baseline.
tsc/internal/vfs/pnpvfs/pnpvfs.goImplements PnP zip and virtual-path VFS.
tsc/internal/vfs/pnpvfs/pnpvfs_test.goTests the PnP VFS.
tsc/internal/vfs/cachedvfs/cachedvfs.goPropagates cache clearing.
tsc/internal/tspath/path.goDetects zip paths.
tsc/internal/tsoptions/tsoptionstest/vfsparseconfighost.goEnables PnP in config tests.
tsc/internal/tsoptions/tsconfigparsing.goExposes PnP through config hosts.
tsc/internal/tsoptions/contentmappers_test.goUpdates test host contracts.
tsc/internal/transpile/transpile.goUpdates compiler-host construction.
tsc/internal/transformers/tstransforms/importelision_test.goUpdates fake program contract.
tsc/internal/testutil/tsbaseline/js_emit_baseline.goPreserves PnP loader fixtures.
tsc/internal/testutil/harnessutil/harnessutil.goInitializes PnP in compiler tests.
tsc/internal/project/snapshot.goCarries and refreshes PnP state.
tsc/internal/project/session.goWires PnP into sessions and watches.
tsc/internal/project/projectcollectionbuilder.goBuilds PnP manifest watchers.
tsc/internal/project/project.goStores project PnP watchers.
tsc/internal/project/overlayfs.goInvalidates snapshots after installs.
tsc/internal/project/filechange.goAdds a PnP install change kind.
tsc/internal/project/extendedconfigcache_test.goUpdates PnP-aware config tests.
tsc/internal/project/configfileregistrybuilder.goExposes PnP during config parsing.
tsc/internal/project/compilerhost.goExposes PnP from project hosts.
tsc/internal/project/autoimport.goCarries PnP into auto-import cloning.
tsc/internal/pnp/pnpapi.goImplements the PnP resolution API.
tsc/internal/pnp/pnpapi_test.goTests manifest loading and resolution.
tsc/internal/pnp/pnp.goInitializes and identifies PnP loaders.
tsc/internal/pnp/manifestparser.goParses serialized PnP manifests.
tsc/internal/modulespecifiers/types.goExtends specifier host contracts.
tsc/internal/modulespecifiers/specifiers.goGenerates PnP package specifiers.
tsc/internal/modulespecifiers/specifiers_test.goUpdates specifier test hosts.
tsc/internal/module/types.goExtends module resolution hosts.
tsc/internal/module/resolver.goAdds PnP module and type resolution.
tsc/internal/module/resolver_test.goTests PnP package IDs.
tsc/internal/lsp/server.goInitializes PnP for LSP sessions.
tsc/internal/lsp/lsproto/lsp.goConverts zip document URIs.
tsc/internal/ls/lsconv/converters.goEmits zip document URIs.
tsc/internal/ls/lsconv/converters_test.goTests zip URI conversion.
tsc/internal/ls/host.goAdds PnP to language-service hosts.
tsc/internal/ls/findallreferences_test.goUpdates compiler-host construction.
tsc/internal/ls/file_rename_test.goUpdates test host contracts.
tsc/internal/ls/autoimport/util.goUses PnP for auto-import resolution.
tsc/internal/ls/autoimport/registry.goIndexes importable PnP packages.
tsc/internal/ls/autoimport/aliasresolver.goUpdates checker host contracts.
tsc/internal/ls/autoimport/aliasresolver_crash_test.goUpdates fake clone hosts.
tsc/internal/fourslash/tests/pnpAutoImportCompletion_test.goTests PnP auto-import completions.
tsc/internal/execute/watcher.goPasses PnP into watch builds.
tsc/internal/execute/tsctests/sys.goAdds PnP-aware test systems.
tsc/internal/execute/tsc/extendedconfigcache_test.goUpdates config test hosts.
tsc/internal/execute/tsc/emit_test.goUpdates test systems and hosts.
tsc/internal/execute/tsc/compile.goAdds PnP to system contracts.
tsc/internal/execute/tsc.goPasses PnP into compilation.
tsc/internal/execute/incremental/buildinfo_contentmapper_test.goUpdates incremental test hosts.
tsc/internal/execute/build/orchestrator.goPasses PnP into build orchestration.
tsc/internal/execute/build/host.goExposes PnP from build hosts.
tsc/internal/execute/build/compilerHost.goExposes PnP from compiler hosts.
tsc/internal/diagnostics/loc_generated.goUpdates generated import ordering.
tsc/internal/diagnostics/extraDiagnosticMessages.jsonAdds PnP diagnostics.
tsc/internal/diagnostics/diagnostics_generated.goGenerates PnP diagnostic definitions.
tsc/internal/core/compileroptions.goRefactors type-root discovery.
tsc/internal/compiler/projectreferencedtsfakinghost.goForwards PnP through reference hosts.
tsc/internal/compiler/program.goExposes PnP from programs.
tsc/internal/compiler/program_test.goUpdates compiler-host calls.
tsc/internal/compiler/host.goStores PnP on compiler hosts.
tsc/internal/compiler/emitHost.goExposes PnP during emit.
tsc/internal/compiler/emit_test.goUpdates compiler-host calls.
tsc/internal/compiler/contentmapper_test.goUpdates compiler-host calls.
tsc/internal/checker/checker_test.goUpdates compiler-host calls.
tsc/internal/api/server.goInitializes PnP for API sessions.
tsc/cmd/tsc/sys.goInitializes PnP for the CLI.
packages/vscode-typescript/src/client.tsRegisters the zip URI scheme.
Files not reviewed (2)
  • tsc/internal/diagnostics/diagnostics_generated.go: Generated file
  • tsc/internal/diagnostics/loc_generated.go: Generated file

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

Comment threadtsc/internal/module/resolver.go Outdated
Comment on lines +382 to +395
func (p *PnpApi) AppendPnpTypeRoots(nmTypes []string, currentDirectory string, compilerOptions *core.CompilerOptions, nmFromConfig bool) ([]string, bool) {
baseDir := compilerOptions.GetBaseDirFromOptions(currentDirectory)
pnpTypes := p.GetPnpTypeRoots(baseDir)

if len(nmTypes) > 0 {
return append(nmTypes, pnpTypes...), nmFromConfig
}

if len(pnpTypes) > 0 {
return pnpTypes, false
}

return nil, false
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

resolveFromTypeRoot which reads from compilerOptions.TypeRoots as a fallback when node_modules resolution fails (resolver.go@L478), is never denying getEffectiveTypeRoots calls. And the latter uses the same resolution logic no matter the value of compilerOptions.TypeRoots

So I think this suggested change doesn't make sense here

Comment threadtsc/internal/modulespecifiers/specifiers.go Outdated
Comment threadtsc/internal/pnp/manifestparser.go
Comment threadtsc/internal/pnp/pnpapi.go
Comment on lines +1732 to +1735
pnpApi := pnp.InitPnpApi(s.fs, cwd)
if pnpApi != nil {
s.fs = pnpvfs.From(s.fs)
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

As discussed in the old PR in typescript-go, it would be complex to initialize the PnP API at the project level, as we use the PnP resolver for global scope logic, like parsing the project config. Implementing it would also stray us away from the original TS PnP patch which could potentially introduce unknown regressions

However, I still generated a quick POC that changes how the PnP API is initialized, and it seems to properly work on multi-root projects. Let me know if you want me to refine this change and integrate it in this PR 🙇

Comment on lines +161 to +171
cachedReader, ok := pnpFS.cachedZipReadersMap[zipPath]
if ok {
usedReader = cachedReader
} else {
zipReader, err := zip.OpenReader(zipPath)
if err != nil {
return pnpFS.fs, path, ""
}

usedReader = zipReader
pnpFS.cachedZipReadersMap[zipPath] = usedReader

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I had memory issues with bounded cache, as I couldn't find a way to make Go GC properly clear closed zipReaders, since some parts of the Typescript process seem to keep references of these readers.

It's been a while since I looked into it, and we never had memory issues related to zip readers in our monorepo. I can have a look at it again if you really want to improve this part of the pnpvfs

Comment on lines +356 to +361
var prefix string
if tspath.IsZipPath(fileName) {
prefix = "zip:"
} else {
prefix = "file:"
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I don't think I can fix this stateless helper without adding an additional argument and heavily refactor usages of FileNameToDocumentURI

I could look into it if needed, but I don't know if this edge case should really be handled

(Note that neovim/coc-nvim use zipfile: and we could also take the time to handle these if we do refactor FileNameToDocumentURI)

Comment on lines +99 to +102
func (pnpFS *pnpFS) UseCaseSensitiveFileNames() bool {
// pnp fs is always case sensitive
return true
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The yarn pnp patch for Typescript <7.0 always used to set the case sensitiveness to true (source), so I think this change would still be valid in TS7

Comment threadtsc/testdata/fixtures/pnp/test-expectations.json Outdated
…ests
Co-Authored-By: ejc3 <ejc3@users.noreply.github.com>
@typescript-automation

Copy link
Copy Markdown

The TypeScript team hasn't accepted the linked issue #63769. If you can get it accepted, this PR will have a better chance of being reviewed.

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

Labels

For Uncommitted BugPR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Yarn PnP

2 participants

@GGomez99
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add Yarn PnP module resolution support - #63919

Open
Guyllian Gomez (GGomez99) wants to merge 7 commits into
microsoft:mainfrom
GGomez99:guyllian.gomez/yarn-pnp
Open

Add Yarn PnP module resolution support#63919
Guyllian Gomez (GGomez99) wants to merge 7 commits into
microsoft:mainfrom
GGomez99:guyllian.gomez/yarn-pnp

Conversation

@GGomez99

@GGomez99Guyllian Gomez (GGomez99) commented Aug 20, 2026

Copy link
Copy Markdown

Closes#63769

See original PR: microsoft/typescript-go#1966
Git history has been lost as replaying all commits would have generated a huge amount of unnecessary conflicts

Co-authored with gun-yu (@gun-yu), EJ (@ejc3) and Valentin MELUSSON (@valentinmelusson), original commits were lost during the move from the old repo

Motivation

This PR adds Plug'n'Play support natively to Typescript Go, following this issue: #63769
It has been reviewed and supported by Maël Nison (@arcanis), the lead maintainer of Yarn, and the original author of Yarn PnP.

Datadog has a frontend monorepo using yarn with over 6k packages, and seeing how TS Strada struggles with our current scaling, we decided to invest time in adding a native Yarn PnP support for Typescript Go.
This PnP implementation is being actively used by all engineers at Datadog, including in IDEs, by agents, and it's currently being beta-tested in CI with a custom lage setup.

Changes

It's based on the main changes from the original yarn patch (99f3e13) that the community has been maintaining for years throughout Typescript Strada updates, except that we implemented the official PnP specification so it doesn't depend on third-party code.

Implemented features:

  • PnP VFS that handles virtual folders and zip files seamlessly, with caching and fallback to the original vfs if pnp is not available
  • Add PnP API and manifest handling, following the yarn PnP specification
  • Initialize the PnP API every time a Host is initialized for both build and LSP modes
  • Add PnP support when resolving modules in internal/module/resolver.go
  • Add PnP support for auto-imports and completion at internal/modulespecifiers/specifiers.go
  • Add PnP support for root types at internal/core/compileroptions.go
  • Handle zip paths when going to implementation with the LSP
  • Update the baseline testing framework to handle PnP when needed
  • PnP manifest auto-refresh by watching .pnp.cjs changes

Tests

  • Basic PnP setup
  • Types from transitive dependencies
  • Root types loading from PnP dependencies
  • Completion and autoimports

Ports the Yarn Plug'n'Play (PnP) support developed on
guyllian.gomez/yarn-pnp in typescript-go into this repo's layout,
adding a pnp resolver and vfs overlay so tsc/lsp can resolve modules
through .pnp.cjs/.pnp.data.json manifests without node_modules.
- internal/pnp: parses PnP manifests and resolves package/dependency
locations
- internal/vfs/pnpvfs: read-through vfs.FS overlay that exposes
zip-packed dependencies (zip:/path/to.zip/inner/path) as regular
files
- module resolver, autoimport, and project/session wiring to plug the
PnP resolver and vfs into the existing module resolution and LSP
pipelines
- new compiler/fourslash test fixtures and baselines under
tsc/testdata covering PnP resolution scenarios
- packages/vscode-typescript/src/client.ts: register the "zip" URI
scheme in the LSP client's documentSelector so the extension
attaches to files served from zip-packed PnP dependencies
Ported and squashed from the original branch history to reconcile
with divergent upstream refactors (module rename to
github.com/microsoft/TypeScript/tsc, auto-import infrastructure
rewrite) that landed in this repo's main before this port. Original
work also includes contributions from valentinmelusson (fix package
IDs for Yarn PnP submodule imports) and gun-yu (add pnp error
handling and test).
Co-Authored-By: valentinmelusson <valentin.melusson@datadoghq.com>
Co-Authored-By: gun-yu <163322737+gun-yu@users.noreply.github.com>
@typescript-automationtypescript-automationBot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Aug 20, 2026
@GGomez99
Guyllian Gomez (GGomez99) marked this pull request as ready for review August 20, 2026 15:59
CopilotAI balanced review requested due to automatic review settings August 20, 2026 15:59

CopilotAI 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.

Pull request overview

Adds native Yarn Plug’n’Play support across the compiler, language server, module resolution, and virtual filesystem.

Changes:

  • Implements PnP manifest parsing, dependency resolution, type roots, and auto-imports.
  • Adds zip/virtual-path filesystem support and manifest refresh handling.
  • Adds compiler, resolver, VFS, and fourslash coverage with baselines.

Reviewed changes

Copilot reviewed 84 out of 88 changed files in this pull request and generated 10 comments.

Show a summary per file
FileDescription
tsc/testdata/tests/cases/compiler/pnpTypeRootsResolution.tsTests PnP type resolution.
tsc/testdata/tests/cases/compiler/pnpTransitiveDependencies.tsTests transitive dependency isolation.
tsc/testdata/tests/cases/compiler/pnpSimpleTest.tsTests basic PnP imports.
tsc/testdata/tests/cases/compiler/pnpDeclarationEmitWorkspace.tsTests workspace declaration emit.
tsc/testdata/fixtures/pnp/test-expectations.jsonDefines PnP resolver scenarios.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.errors.txtDiagnostic baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.jsEmit baseline.
tsc/internal/vfs/pnpvfs/pnpvfs.goImplements PnP zip and virtual-path VFS.
tsc/internal/vfs/pnpvfs/pnpvfs_test.goTests the PnP VFS.
tsc/internal/vfs/cachedvfs/cachedvfs.goPropagates cache clearing.
tsc/internal/tspath/path.goDetects zip paths.
tsc/internal/tsoptions/tsoptionstest/vfsparseconfighost.goEnables PnP in config tests.
tsc/internal/tsoptions/tsconfigparsing.goExposes PnP through config hosts.
tsc/internal/tsoptions/contentmappers_test.goUpdates test host contracts.
tsc/internal/transpile/transpile.goUpdates compiler-host construction.
tsc/internal/transformers/tstransforms/importelision_test.goUpdates fake program contract.
tsc/internal/testutil/tsbaseline/js_emit_baseline.goPreserves PnP loader fixtures.
tsc/internal/testutil/harnessutil/harnessutil.goInitializes PnP in compiler tests.
tsc/internal/project/snapshot.goCarries and refreshes PnP state.
tsc/internal/project/session.goWires PnP into sessions and watches.
tsc/internal/project/projectcollectionbuilder.goBuilds PnP manifest watchers.
tsc/internal/project/project.goStores project PnP watchers.
tsc/internal/project/overlayfs.goInvalidates snapshots after installs.
tsc/internal/project/filechange.goAdds a PnP install change kind.
tsc/internal/project/extendedconfigcache_test.goUpdates PnP-aware config tests.
tsc/internal/project/configfileregistrybuilder.goExposes PnP during config parsing.
tsc/internal/project/compilerhost.goExposes PnP from project hosts.
tsc/internal/project/autoimport.goCarries PnP into auto-import cloning.
tsc/internal/pnp/pnpapi.goImplements the PnP resolution API.
tsc/internal/pnp/pnpapi_test.goTests manifest loading and resolution.
tsc/internal/pnp/pnp.goInitializes and identifies PnP loaders.
tsc/internal/pnp/manifestparser.goParses serialized PnP manifests.
tsc/internal/modulespecifiers/types.goExtends specifier host contracts.
tsc/internal/modulespecifiers/specifiers.goGenerates PnP package specifiers.
tsc/internal/modulespecifiers/specifiers_test.goUpdates specifier test hosts.
tsc/internal/module/types.goExtends module resolution hosts.
tsc/internal/module/resolver.goAdds PnP module and type resolution.
tsc/internal/module/resolver_test.goTests PnP package IDs.
tsc/internal/lsp/server.goInitializes PnP for LSP sessions.
tsc/internal/lsp/lsproto/lsp.goConverts zip document URIs.
tsc/internal/ls/lsconv/converters.goEmits zip document URIs.
tsc/internal/ls/lsconv/converters_test.goTests zip URI conversion.
tsc/internal/ls/host.goAdds PnP to language-service hosts.
tsc/internal/ls/findallreferences_test.goUpdates compiler-host construction.
tsc/internal/ls/file_rename_test.goUpdates test host contracts.
tsc/internal/ls/autoimport/util.goUses PnP for auto-import resolution.
tsc/internal/ls/autoimport/registry.goIndexes importable PnP packages.
tsc/internal/ls/autoimport/aliasresolver.goUpdates checker host contracts.
tsc/internal/ls/autoimport/aliasresolver_crash_test.goUpdates fake clone hosts.
tsc/internal/fourslash/tests/pnpAutoImportCompletion_test.goTests PnP auto-import completions.
tsc/internal/execute/watcher.goPasses PnP into watch builds.
tsc/internal/execute/tsctests/sys.goAdds PnP-aware test systems.
tsc/internal/execute/tsc/extendedconfigcache_test.goUpdates config test hosts.
tsc/internal/execute/tsc/emit_test.goUpdates test systems and hosts.
tsc/internal/execute/tsc/compile.goAdds PnP to system contracts.
tsc/internal/execute/tsc.goPasses PnP into compilation.
tsc/internal/execute/incremental/buildinfo_contentmapper_test.goUpdates incremental test hosts.
tsc/internal/execute/build/orchestrator.goPasses PnP into build orchestration.
tsc/internal/execute/build/host.goExposes PnP from build hosts.
tsc/internal/execute/build/compilerHost.goExposes PnP from compiler hosts.
tsc/internal/diagnostics/loc_generated.goUpdates generated import ordering.
tsc/internal/diagnostics/extraDiagnosticMessages.jsonAdds PnP diagnostics.
tsc/internal/diagnostics/diagnostics_generated.goGenerates PnP diagnostic definitions.
tsc/internal/core/compileroptions.goRefactors type-root discovery.
tsc/internal/compiler/projectreferencedtsfakinghost.goForwards PnP through reference hosts.
tsc/internal/compiler/program.goExposes PnP from programs.
tsc/internal/compiler/program_test.goUpdates compiler-host calls.
tsc/internal/compiler/host.goStores PnP on compiler hosts.
tsc/internal/compiler/emitHost.goExposes PnP during emit.
tsc/internal/compiler/emit_test.goUpdates compiler-host calls.
tsc/internal/compiler/contentmapper_test.goUpdates compiler-host calls.
tsc/internal/checker/checker_test.goUpdates compiler-host calls.
tsc/internal/api/server.goInitializes PnP for API sessions.
tsc/cmd/tsc/sys.goInitializes PnP for the CLI.
packages/vscode-typescript/src/client.tsRegisters the zip URI scheme.
Files not reviewed (2)
  • tsc/internal/diagnostics/diagnostics_generated.go: Generated file
  • tsc/internal/diagnostics/loc_generated.go: Generated file

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

Comment threadtsc/internal/module/resolver.go Outdated
Comment on lines +382 to +395
func (p *PnpApi) AppendPnpTypeRoots(nmTypes []string, currentDirectory string, compilerOptions *core.CompilerOptions, nmFromConfig bool) ([]string, bool) {
baseDir := compilerOptions.GetBaseDirFromOptions(currentDirectory)
pnpTypes := p.GetPnpTypeRoots(baseDir)

if len(nmTypes) > 0 {
return append(nmTypes, pnpTypes...), nmFromConfig
}

if len(pnpTypes) > 0 {
return pnpTypes, false
}

return nil, false
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

resolveFromTypeRoot which reads from compilerOptions.TypeRoots as a fallback when node_modules resolution fails (resolver.go@L478), is never denying getEffectiveTypeRoots calls. And the latter uses the same resolution logic no matter the value of compilerOptions.TypeRoots

So I think this suggested change doesn't make sense here

Comment threadtsc/internal/modulespecifiers/specifiers.go Outdated
Comment threadtsc/internal/pnp/manifestparser.go
Comment threadtsc/internal/pnp/pnpapi.go
Comment on lines +1732 to +1735
pnpApi := pnp.InitPnpApi(s.fs, cwd)
if pnpApi != nil {
s.fs = pnpvfs.From(s.fs)
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

As discussed in the old PR in typescript-go, it would be complex to initialize the PnP API at the project level, as we use the PnP resolver for global scope logic, like parsing the project config. Implementing it would also stray us away from the original TS PnP patch which could potentially introduce unknown regressions

However, I still generated a quick POC that changes how the PnP API is initialized, and it seems to properly work on multi-root projects. Let me know if you want me to refine this change and integrate it in this PR 🙇

Comment on lines +161 to +171
cachedReader, ok := pnpFS.cachedZipReadersMap[zipPath]
if ok {
usedReader = cachedReader
} else {
zipReader, err := zip.OpenReader(zipPath)
if err != nil {
return pnpFS.fs, path, ""
}

usedReader = zipReader
pnpFS.cachedZipReadersMap[zipPath] = usedReader

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I had memory issues with bounded cache, as I couldn't find a way to make Go GC properly clear closed zipReaders, since some parts of the Typescript process seem to keep references of these readers.

It's been a while since I looked into it, and we never had memory issues related to zip readers in our monorepo. I can have a look at it again if you really want to improve this part of the pnpvfs

Comment on lines +356 to +361
var prefix string
if tspath.IsZipPath(fileName) {
prefix = "zip:"
} else {
prefix = "file:"
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I don't think I can fix this stateless helper without adding an additional argument and heavily refactor usages of FileNameToDocumentURI

I could look into it if needed, but I don't know if this edge case should really be handled

(Note that neovim/coc-nvim use zipfile: and we could also take the time to handle these if we do refactor FileNameToDocumentURI)

Comment on lines +99 to +102
func (pnpFS *pnpFS) UseCaseSensitiveFileNames() bool {
// pnp fs is always case sensitive
return true
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The yarn pnp patch for Typescript <7.0 always used to set the case sensitiveness to true (source), so I think this change would still be valid in TS7

Comment threadtsc/testdata/fixtures/pnp/test-expectations.json Outdated
…ests
Co-Authored-By: ejc3 <ejc3@users.noreply.github.com>
@typescript-automation

Copy link
Copy Markdown

The TypeScript team hasn't accepted the linked issue #63769. If you can get it accepted, this PR will have a better chance of being reviewed.

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

Labels

For Uncommitted BugPR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Yarn PnP

2 participants

@GGomez99
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Yarn PnP module resolution support - #63919

Open
Guyllian Gomez (GGomez99) wants to merge 7 commits into
microsoft:mainfrom
GGomez99:guyllian.gomez/yarn-pnp
Open

Add Yarn PnP module resolution support#63919
Guyllian Gomez (GGomez99) wants to merge 7 commits into
microsoft:mainfrom
GGomez99:guyllian.gomez/yarn-pnp

Conversation

@GGomez99

@GGomez99Guyllian Gomez (GGomez99) commented Aug 20, 2026

Copy link
Copy Markdown

Closes#63769

See original PR: microsoft/typescript-go#1966
Git history has been lost as replaying all commits would have generated a huge amount of unnecessary conflicts

Co-authored with gun-yu (@gun-yu), EJ (@ejc3) and Valentin MELUSSON (@valentinmelusson), original commits were lost during the move from the old repo

Motivation

This PR adds Plug'n'Play support natively to Typescript Go, following this issue: #63769
It has been reviewed and supported by Maël Nison (@arcanis), the lead maintainer of Yarn, and the original author of Yarn PnP.

Datadog has a frontend monorepo using yarn with over 6k packages, and seeing how TS Strada struggles with our current scaling, we decided to invest time in adding a native Yarn PnP support for Typescript Go.
This PnP implementation is being actively used by all engineers at Datadog, including in IDEs, by agents, and it's currently being beta-tested in CI with a custom lage setup.

Changes

It's based on the main changes from the original yarn patch (99f3e13) that the community has been maintaining for years throughout Typescript Strada updates, except that we implemented the official PnP specification so it doesn't depend on third-party code.

Implemented features:

  • PnP VFS that handles virtual folders and zip files seamlessly, with caching and fallback to the original vfs if pnp is not available
  • Add PnP API and manifest handling, following the yarn PnP specification
  • Initialize the PnP API every time a Host is initialized for both build and LSP modes
  • Add PnP support when resolving modules in internal/module/resolver.go
  • Add PnP support for auto-imports and completion at internal/modulespecifiers/specifiers.go
  • Add PnP support for root types at internal/core/compileroptions.go
  • Handle zip paths when going to implementation with the LSP
  • Update the baseline testing framework to handle PnP when needed
  • PnP manifest auto-refresh by watching .pnp.cjs changes

Tests

  • Basic PnP setup
  • Types from transitive dependencies
  • Root types loading from PnP dependencies
  • Completion and autoimports

Ports the Yarn Plug'n'Play (PnP) support developed on
guyllian.gomez/yarn-pnp in typescript-go into this repo's layout,
adding a pnp resolver and vfs overlay so tsc/lsp can resolve modules
through .pnp.cjs/.pnp.data.json manifests without node_modules.
- internal/pnp: parses PnP manifests and resolves package/dependency
locations
- internal/vfs/pnpvfs: read-through vfs.FS overlay that exposes
zip-packed dependencies (zip:/path/to.zip/inner/path) as regular
files
- module resolver, autoimport, and project/session wiring to plug the
PnP resolver and vfs into the existing module resolution and LSP
pipelines
- new compiler/fourslash test fixtures and baselines under
tsc/testdata covering PnP resolution scenarios
- packages/vscode-typescript/src/client.ts: register the "zip" URI
scheme in the LSP client's documentSelector so the extension
attaches to files served from zip-packed PnP dependencies
Ported and squashed from the original branch history to reconcile
with divergent upstream refactors (module rename to
github.com/microsoft/TypeScript/tsc, auto-import infrastructure
rewrite) that landed in this repo's main before this port. Original
work also includes contributions from valentinmelusson (fix package
IDs for Yarn PnP submodule imports) and gun-yu (add pnp error
handling and test).
Co-Authored-By: valentinmelusson <valentin.melusson@datadoghq.com>
Co-Authored-By: gun-yu <163322737+gun-yu@users.noreply.github.com>
@typescript-automationtypescript-automationBot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Aug 20, 2026
@GGomez99
Guyllian Gomez (GGomez99) marked this pull request as ready for review August 20, 2026 15:59
CopilotAI balanced review requested due to automatic review settings August 20, 2026 15:59

CopilotAI 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.

Pull request overview

Adds native Yarn Plug’n’Play support across the compiler, language server, module resolution, and virtual filesystem.

Changes:

  • Implements PnP manifest parsing, dependency resolution, type roots, and auto-imports.
  • Adds zip/virtual-path filesystem support and manifest refresh handling.
  • Adds compiler, resolver, VFS, and fourslash coverage with baselines.

Reviewed changes

Copilot reviewed 84 out of 88 changed files in this pull request and generated 10 comments.

Show a summary per file
FileDescription
tsc/testdata/tests/cases/compiler/pnpTypeRootsResolution.tsTests PnP type resolution.
tsc/testdata/tests/cases/compiler/pnpTransitiveDependencies.tsTests transitive dependency isolation.
tsc/testdata/tests/cases/compiler/pnpSimpleTest.tsTests basic PnP imports.
tsc/testdata/tests/cases/compiler/pnpDeclarationEmitWorkspace.tsTests workspace declaration emit.
tsc/testdata/fixtures/pnp/test-expectations.jsonDefines PnP resolver scenarios.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.errors.txtDiagnostic baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.jsEmit baseline.
tsc/internal/vfs/pnpvfs/pnpvfs.goImplements PnP zip and virtual-path VFS.
tsc/internal/vfs/pnpvfs/pnpvfs_test.goTests the PnP VFS.
tsc/internal/vfs/cachedvfs/cachedvfs.goPropagates cache clearing.
tsc/internal/tspath/path.goDetects zip paths.
tsc/internal/tsoptions/tsoptionstest/vfsparseconfighost.goEnables PnP in config tests.
tsc/internal/tsoptions/tsconfigparsing.goExposes PnP through config hosts.
tsc/internal/tsoptions/contentmappers_test.goUpdates test host contracts.
tsc/internal/transpile/transpile.goUpdates compiler-host construction.
tsc/internal/transformers/tstransforms/importelision_test.goUpdates fake program contract.
tsc/internal/testutil/tsbaseline/js_emit_baseline.goPreserves PnP loader fixtures.
tsc/internal/testutil/harnessutil/harnessutil.goInitializes PnP in compiler tests.
tsc/internal/project/snapshot.goCarries and refreshes PnP state.
tsc/internal/project/session.goWires PnP into sessions and watches.
tsc/internal/project/projectcollectionbuilder.goBuilds PnP manifest watchers.
tsc/internal/project/project.goStores project PnP watchers.
tsc/internal/project/overlayfs.goInvalidates snapshots after installs.
tsc/internal/project/filechange.goAdds a PnP install change kind.
tsc/internal/project/extendedconfigcache_test.goUpdates PnP-aware config tests.
tsc/internal/project/configfileregistrybuilder.goExposes PnP during config parsing.
tsc/internal/project/compilerhost.goExposes PnP from project hosts.
tsc/internal/project/autoimport.goCarries PnP into auto-import cloning.
tsc/internal/pnp/pnpapi.goImplements the PnP resolution API.
tsc/internal/pnp/pnpapi_test.goTests manifest loading and resolution.
tsc/internal/pnp/pnp.goInitializes and identifies PnP loaders.
tsc/internal/pnp/manifestparser.goParses serialized PnP manifests.
tsc/internal/modulespecifiers/types.goExtends specifier host contracts.
tsc/internal/modulespecifiers/specifiers.goGenerates PnP package specifiers.
tsc/internal/modulespecifiers/specifiers_test.goUpdates specifier test hosts.
tsc/internal/module/types.goExtends module resolution hosts.
tsc/internal/module/resolver.goAdds PnP module and type resolution.
tsc/internal/module/resolver_test.goTests PnP package IDs.
tsc/internal/lsp/server.goInitializes PnP for LSP sessions.
tsc/internal/lsp/lsproto/lsp.goConverts zip document URIs.
tsc/internal/ls/lsconv/converters.goEmits zip document URIs.
tsc/internal/ls/lsconv/converters_test.goTests zip URI conversion.
tsc/internal/ls/host.goAdds PnP to language-service hosts.
tsc/internal/ls/findallreferences_test.goUpdates compiler-host construction.
tsc/internal/ls/file_rename_test.goUpdates test host contracts.
tsc/internal/ls/autoimport/util.goUses PnP for auto-import resolution.
tsc/internal/ls/autoimport/registry.goIndexes importable PnP packages.
tsc/internal/ls/autoimport/aliasresolver.goUpdates checker host contracts.
tsc/internal/ls/autoimport/aliasresolver_crash_test.goUpdates fake clone hosts.
tsc/internal/fourslash/tests/pnpAutoImportCompletion_test.goTests PnP auto-import completions.
tsc/internal/execute/watcher.goPasses PnP into watch builds.
tsc/internal/execute/tsctests/sys.goAdds PnP-aware test systems.
tsc/internal/execute/tsc/extendedconfigcache_test.goUpdates config test hosts.
tsc/internal/execute/tsc/emit_test.goUpdates test systems and hosts.
tsc/internal/execute/tsc/compile.goAdds PnP to system contracts.
tsc/internal/execute/tsc.goPasses PnP into compilation.
tsc/internal/execute/incremental/buildinfo_contentmapper_test.goUpdates incremental test hosts.
tsc/internal/execute/build/orchestrator.goPasses PnP into build orchestration.
tsc/internal/execute/build/host.goExposes PnP from build hosts.
tsc/internal/execute/build/compilerHost.goExposes PnP from compiler hosts.
tsc/internal/diagnostics/loc_generated.goUpdates generated import ordering.
tsc/internal/diagnostics/extraDiagnosticMessages.jsonAdds PnP diagnostics.
tsc/internal/diagnostics/diagnostics_generated.goGenerates PnP diagnostic definitions.
tsc/internal/core/compileroptions.goRefactors type-root discovery.
tsc/internal/compiler/projectreferencedtsfakinghost.goForwards PnP through reference hosts.
tsc/internal/compiler/program.goExposes PnP from programs.
tsc/internal/compiler/program_test.goUpdates compiler-host calls.
tsc/internal/compiler/host.goStores PnP on compiler hosts.
tsc/internal/compiler/emitHost.goExposes PnP during emit.
tsc/internal/compiler/emit_test.goUpdates compiler-host calls.
tsc/internal/compiler/contentmapper_test.goUpdates compiler-host calls.
tsc/internal/checker/checker_test.goUpdates compiler-host calls.
tsc/internal/api/server.goInitializes PnP for API sessions.
tsc/cmd/tsc/sys.goInitializes PnP for the CLI.
packages/vscode-typescript/src/client.tsRegisters the zip URI scheme.
Files not reviewed (2)
  • tsc/internal/diagnostics/diagnostics_generated.go: Generated file
  • tsc/internal/diagnostics/loc_generated.go: Generated file

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

Comment threadtsc/internal/module/resolver.go Outdated
Comment on lines +382 to +395
func (p *PnpApi) AppendPnpTypeRoots(nmTypes []string, currentDirectory string, compilerOptions *core.CompilerOptions, nmFromConfig bool) ([]string, bool) {
baseDir := compilerOptions.GetBaseDirFromOptions(currentDirectory)
pnpTypes := p.GetPnpTypeRoots(baseDir)

if len(nmTypes) > 0 {
return append(nmTypes, pnpTypes...), nmFromConfig
}

if len(pnpTypes) > 0 {
return pnpTypes, false
}

return nil, false
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

resolveFromTypeRoot which reads from compilerOptions.TypeRoots as a fallback when node_modules resolution fails (resolver.go@L478), is never denying getEffectiveTypeRoots calls. And the latter uses the same resolution logic no matter the value of compilerOptions.TypeRoots

So I think this suggested change doesn't make sense here

Comment threadtsc/internal/modulespecifiers/specifiers.go Outdated
Comment threadtsc/internal/pnp/manifestparser.go
Comment threadtsc/internal/pnp/pnpapi.go
Comment on lines +1732 to +1735
pnpApi := pnp.InitPnpApi(s.fs, cwd)
if pnpApi != nil {
s.fs = pnpvfs.From(s.fs)
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

As discussed in the old PR in typescript-go, it would be complex to initialize the PnP API at the project level, as we use the PnP resolver for global scope logic, like parsing the project config. Implementing it would also stray us away from the original TS PnP patch which could potentially introduce unknown regressions

However, I still generated a quick POC that changes how the PnP API is initialized, and it seems to properly work on multi-root projects. Let me know if you want me to refine this change and integrate it in this PR 🙇

Comment on lines +161 to +171
cachedReader, ok := pnpFS.cachedZipReadersMap[zipPath]
if ok {
usedReader = cachedReader
} else {
zipReader, err := zip.OpenReader(zipPath)
if err != nil {
return pnpFS.fs, path, ""
}

usedReader = zipReader
pnpFS.cachedZipReadersMap[zipPath] = usedReader

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I had memory issues with bounded cache, as I couldn't find a way to make Go GC properly clear closed zipReaders, since some parts of the Typescript process seem to keep references of these readers.

It's been a while since I looked into it, and we never had memory issues related to zip readers in our monorepo. I can have a look at it again if you really want to improve this part of the pnpvfs

Comment on lines +356 to +361
var prefix string
if tspath.IsZipPath(fileName) {
prefix = "zip:"
} else {
prefix = "file:"
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I don't think I can fix this stateless helper without adding an additional argument and heavily refactor usages of FileNameToDocumentURI

I could look into it if needed, but I don't know if this edge case should really be handled

(Note that neovim/coc-nvim use zipfile: and we could also take the time to handle these if we do refactor FileNameToDocumentURI)

Comment on lines +99 to +102
func (pnpFS *pnpFS) UseCaseSensitiveFileNames() bool {
// pnp fs is always case sensitive
return true
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The yarn pnp patch for Typescript <7.0 always used to set the case sensitiveness to true (source), so I think this change would still be valid in TS7

Comment threadtsc/testdata/fixtures/pnp/test-expectations.json Outdated
…ests
Co-Authored-By: ejc3 <ejc3@users.noreply.github.com>
@typescript-automation

Copy link
Copy Markdown

The TypeScript team hasn't accepted the linked issue #63769. If you can get it accepted, this PR will have a better chance of being reviewed.

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

Labels

For Uncommitted BugPR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Yarn PnP

2 participants

@GGomez99
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Yarn PnP module resolution support - #63919

Open
Guyllian Gomez (GGomez99) wants to merge 7 commits into
microsoft:mainfrom
GGomez99:guyllian.gomez/yarn-pnp
Open

Add Yarn PnP module resolution support#63919
Guyllian Gomez (GGomez99) wants to merge 7 commits into
microsoft:mainfrom
GGomez99:guyllian.gomez/yarn-pnp

Conversation

@GGomez99

@GGomez99Guyllian Gomez (GGomez99) commented Aug 20, 2026

Copy link
Copy Markdown

Closes#63769

See original PR: microsoft/typescript-go#1966
Git history has been lost as replaying all commits would have generated a huge amount of unnecessary conflicts

Co-authored with gun-yu (@gun-yu), EJ (@ejc3) and Valentin MELUSSON (@valentinmelusson), original commits were lost during the move from the old repo

Motivation

This PR adds Plug'n'Play support natively to Typescript Go, following this issue: #63769
It has been reviewed and supported by Maël Nison (@arcanis), the lead maintainer of Yarn, and the original author of Yarn PnP.

Datadog has a frontend monorepo using yarn with over 6k packages, and seeing how TS Strada struggles with our current scaling, we decided to invest time in adding a native Yarn PnP support for Typescript Go.
This PnP implementation is being actively used by all engineers at Datadog, including in IDEs, by agents, and it's currently being beta-tested in CI with a custom lage setup.

Changes

It's based on the main changes from the original yarn patch (99f3e13) that the community has been maintaining for years throughout Typescript Strada updates, except that we implemented the official PnP specification so it doesn't depend on third-party code.

Implemented features:

  • PnP VFS that handles virtual folders and zip files seamlessly, with caching and fallback to the original vfs if pnp is not available
  • Add PnP API and manifest handling, following the yarn PnP specification
  • Initialize the PnP API every time a Host is initialized for both build and LSP modes
  • Add PnP support when resolving modules in internal/module/resolver.go
  • Add PnP support for auto-imports and completion at internal/modulespecifiers/specifiers.go
  • Add PnP support for root types at internal/core/compileroptions.go
  • Handle zip paths when going to implementation with the LSP
  • Update the baseline testing framework to handle PnP when needed
  • PnP manifest auto-refresh by watching .pnp.cjs changes

Tests

  • Basic PnP setup
  • Types from transitive dependencies
  • Root types loading from PnP dependencies
  • Completion and autoimports

Ports the Yarn Plug'n'Play (PnP) support developed on
guyllian.gomez/yarn-pnp in typescript-go into this repo's layout,
adding a pnp resolver and vfs overlay so tsc/lsp can resolve modules
through .pnp.cjs/.pnp.data.json manifests without node_modules.
- internal/pnp: parses PnP manifests and resolves package/dependency
locations
- internal/vfs/pnpvfs: read-through vfs.FS overlay that exposes
zip-packed dependencies (zip:/path/to.zip/inner/path) as regular
files
- module resolver, autoimport, and project/session wiring to plug the
PnP resolver and vfs into the existing module resolution and LSP
pipelines
- new compiler/fourslash test fixtures and baselines under
tsc/testdata covering PnP resolution scenarios
- packages/vscode-typescript/src/client.ts: register the "zip" URI
scheme in the LSP client's documentSelector so the extension
attaches to files served from zip-packed PnP dependencies
Ported and squashed from the original branch history to reconcile
with divergent upstream refactors (module rename to
github.com/microsoft/TypeScript/tsc, auto-import infrastructure
rewrite) that landed in this repo's main before this port. Original
work also includes contributions from valentinmelusson (fix package
IDs for Yarn PnP submodule imports) and gun-yu (add pnp error
handling and test).
Co-Authored-By: valentinmelusson <valentin.melusson@datadoghq.com>
Co-Authored-By: gun-yu <163322737+gun-yu@users.noreply.github.com>
@typescript-automationtypescript-automationBot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Aug 20, 2026
@GGomez99
Guyllian Gomez (GGomez99) marked this pull request as ready for review August 20, 2026 15:59
CopilotAI balanced review requested due to automatic review settings August 20, 2026 15:59

CopilotAI 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.

Pull request overview

Adds native Yarn Plug’n’Play support across the compiler, language server, module resolution, and virtual filesystem.

Changes:

  • Implements PnP manifest parsing, dependency resolution, type roots, and auto-imports.
  • Adds zip/virtual-path filesystem support and manifest refresh handling.
  • Adds compiler, resolver, VFS, and fourslash coverage with baselines.

Reviewed changes

Copilot reviewed 84 out of 88 changed files in this pull request and generated 10 comments.

Show a summary per file
FileDescription
tsc/testdata/tests/cases/compiler/pnpTypeRootsResolution.tsTests PnP type resolution.
tsc/testdata/tests/cases/compiler/pnpTransitiveDependencies.tsTests transitive dependency isolation.
tsc/testdata/tests/cases/compiler/pnpSimpleTest.tsTests basic PnP imports.
tsc/testdata/tests/cases/compiler/pnpDeclarationEmitWorkspace.tsTests workspace declaration emit.
tsc/testdata/fixtures/pnp/test-expectations.jsonDefines PnP resolver scenarios.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.errors.txtDiagnostic baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.jsEmit baseline.
tsc/internal/vfs/pnpvfs/pnpvfs.goImplements PnP zip and virtual-path VFS.
tsc/internal/vfs/pnpvfs/pnpvfs_test.goTests the PnP VFS.
tsc/internal/vfs/cachedvfs/cachedvfs.goPropagates cache clearing.
tsc/internal/tspath/path.goDetects zip paths.
tsc/internal/tsoptions/tsoptionstest/vfsparseconfighost.goEnables PnP in config tests.
tsc/internal/tsoptions/tsconfigparsing.goExposes PnP through config hosts.
tsc/internal/tsoptions/contentmappers_test.goUpdates test host contracts.
tsc/internal/transpile/transpile.goUpdates compiler-host construction.
tsc/internal/transformers/tstransforms/importelision_test.goUpdates fake program contract.
tsc/internal/testutil/tsbaseline/js_emit_baseline.goPreserves PnP loader fixtures.
tsc/internal/testutil/harnessutil/harnessutil.goInitializes PnP in compiler tests.
tsc/internal/project/snapshot.goCarries and refreshes PnP state.
tsc/internal/project/session.goWires PnP into sessions and watches.
tsc/internal/project/projectcollectionbuilder.goBuilds PnP manifest watchers.
tsc/internal/project/project.goStores project PnP watchers.
tsc/internal/project/overlayfs.goInvalidates snapshots after installs.
tsc/internal/project/filechange.goAdds a PnP install change kind.
tsc/internal/project/extendedconfigcache_test.goUpdates PnP-aware config tests.
tsc/internal/project/configfileregistrybuilder.goExposes PnP during config parsing.
tsc/internal/project/compilerhost.goExposes PnP from project hosts.
tsc/internal/project/autoimport.goCarries PnP into auto-import cloning.
tsc/internal/pnp/pnpapi.goImplements the PnP resolution API.
tsc/internal/pnp/pnpapi_test.goTests manifest loading and resolution.
tsc/internal/pnp/pnp.goInitializes and identifies PnP loaders.
tsc/internal/pnp/manifestparser.goParses serialized PnP manifests.
tsc/internal/modulespecifiers/types.goExtends specifier host contracts.
tsc/internal/modulespecifiers/specifiers.goGenerates PnP package specifiers.
tsc/internal/modulespecifiers/specifiers_test.goUpdates specifier test hosts.
tsc/internal/module/types.goExtends module resolution hosts.
tsc/internal/module/resolver.goAdds PnP module and type resolution.
tsc/internal/module/resolver_test.goTests PnP package IDs.
tsc/internal/lsp/server.goInitializes PnP for LSP sessions.
tsc/internal/lsp/lsproto/lsp.goConverts zip document URIs.
tsc/internal/ls/lsconv/converters.goEmits zip document URIs.
tsc/internal/ls/lsconv/converters_test.goTests zip URI conversion.
tsc/internal/ls/host.goAdds PnP to language-service hosts.
tsc/internal/ls/findallreferences_test.goUpdates compiler-host construction.
tsc/internal/ls/file_rename_test.goUpdates test host contracts.
tsc/internal/ls/autoimport/util.goUses PnP for auto-import resolution.
tsc/internal/ls/autoimport/registry.goIndexes importable PnP packages.
tsc/internal/ls/autoimport/aliasresolver.goUpdates checker host contracts.
tsc/internal/ls/autoimport/aliasresolver_crash_test.goUpdates fake clone hosts.
tsc/internal/fourslash/tests/pnpAutoImportCompletion_test.goTests PnP auto-import completions.
tsc/internal/execute/watcher.goPasses PnP into watch builds.
tsc/internal/execute/tsctests/sys.goAdds PnP-aware test systems.
tsc/internal/execute/tsc/extendedconfigcache_test.goUpdates config test hosts.
tsc/internal/execute/tsc/emit_test.goUpdates test systems and hosts.
tsc/internal/execute/tsc/compile.goAdds PnP to system contracts.
tsc/internal/execute/tsc.goPasses PnP into compilation.
tsc/internal/execute/incremental/buildinfo_contentmapper_test.goUpdates incremental test hosts.
tsc/internal/execute/build/orchestrator.goPasses PnP into build orchestration.
tsc/internal/execute/build/host.goExposes PnP from build hosts.
tsc/internal/execute/build/compilerHost.goExposes PnP from compiler hosts.
tsc/internal/diagnostics/loc_generated.goUpdates generated import ordering.
tsc/internal/diagnostics/extraDiagnosticMessages.jsonAdds PnP diagnostics.
tsc/internal/diagnostics/diagnostics_generated.goGenerates PnP diagnostic definitions.
tsc/internal/core/compileroptions.goRefactors type-root discovery.
tsc/internal/compiler/projectreferencedtsfakinghost.goForwards PnP through reference hosts.
tsc/internal/compiler/program.goExposes PnP from programs.
tsc/internal/compiler/program_test.goUpdates compiler-host calls.
tsc/internal/compiler/host.goStores PnP on compiler hosts.
tsc/internal/compiler/emitHost.goExposes PnP during emit.
tsc/internal/compiler/emit_test.goUpdates compiler-host calls.
tsc/internal/compiler/contentmapper_test.goUpdates compiler-host calls.
tsc/internal/checker/checker_test.goUpdates compiler-host calls.
tsc/internal/api/server.goInitializes PnP for API sessions.
tsc/cmd/tsc/sys.goInitializes PnP for the CLI.
packages/vscode-typescript/src/client.tsRegisters the zip URI scheme.
Files not reviewed (2)
  • tsc/internal/diagnostics/diagnostics_generated.go: Generated file
  • tsc/internal/diagnostics/loc_generated.go: Generated file

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

Comment threadtsc/internal/module/resolver.go Outdated
Comment on lines +382 to +395
func (p *PnpApi) AppendPnpTypeRoots(nmTypes []string, currentDirectory string, compilerOptions *core.CompilerOptions, nmFromConfig bool) ([]string, bool) {
baseDir := compilerOptions.GetBaseDirFromOptions(currentDirectory)
pnpTypes := p.GetPnpTypeRoots(baseDir)

if len(nmTypes) > 0 {
return append(nmTypes, pnpTypes...), nmFromConfig
}

if len(pnpTypes) > 0 {
return pnpTypes, false
}

return nil, false
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

resolveFromTypeRoot which reads from compilerOptions.TypeRoots as a fallback when node_modules resolution fails (resolver.go@L478), is never denying getEffectiveTypeRoots calls. And the latter uses the same resolution logic no matter the value of compilerOptions.TypeRoots

So I think this suggested change doesn't make sense here

Comment threadtsc/internal/modulespecifiers/specifiers.go Outdated
Comment threadtsc/internal/pnp/manifestparser.go
Comment threadtsc/internal/pnp/pnpapi.go
Comment on lines +1732 to +1735
pnpApi := pnp.InitPnpApi(s.fs, cwd)
if pnpApi != nil {
s.fs = pnpvfs.From(s.fs)
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

As discussed in the old PR in typescript-go, it would be complex to initialize the PnP API at the project level, as we use the PnP resolver for global scope logic, like parsing the project config. Implementing it would also stray us away from the original TS PnP patch which could potentially introduce unknown regressions

However, I still generated a quick POC that changes how the PnP API is initialized, and it seems to properly work on multi-root projects. Let me know if you want me to refine this change and integrate it in this PR 🙇

Comment on lines +161 to +171
cachedReader, ok := pnpFS.cachedZipReadersMap[zipPath]
if ok {
usedReader = cachedReader
} else {
zipReader, err := zip.OpenReader(zipPath)
if err != nil {
return pnpFS.fs, path, ""
}

usedReader = zipReader
pnpFS.cachedZipReadersMap[zipPath] = usedReader

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I had memory issues with bounded cache, as I couldn't find a way to make Go GC properly clear closed zipReaders, since some parts of the Typescript process seem to keep references of these readers.

It's been a while since I looked into it, and we never had memory issues related to zip readers in our monorepo. I can have a look at it again if you really want to improve this part of the pnpvfs

Comment on lines +356 to +361
var prefix string
if tspath.IsZipPath(fileName) {
prefix = "zip:"
} else {
prefix = "file:"
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I don't think I can fix this stateless helper without adding an additional argument and heavily refactor usages of FileNameToDocumentURI

I could look into it if needed, but I don't know if this edge case should really be handled

(Note that neovim/coc-nvim use zipfile: and we could also take the time to handle these if we do refactor FileNameToDocumentURI)

Comment on lines +99 to +102
func (pnpFS *pnpFS) UseCaseSensitiveFileNames() bool {
// pnp fs is always case sensitive
return true
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The yarn pnp patch for Typescript <7.0 always used to set the case sensitiveness to true (source), so I think this change would still be valid in TS7

Comment threadtsc/testdata/fixtures/pnp/test-expectations.json Outdated
…ests
Co-Authored-By: ejc3 <ejc3@users.noreply.github.com>
@typescript-automation

Copy link
Copy Markdown

The TypeScript team hasn't accepted the linked issue #63769. If you can get it accepted, this PR will have a better chance of being reviewed.

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

Labels

For Uncommitted BugPR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Yarn PnP

2 participants

@GGomez99
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add Yarn PnP module resolution support - #63919

Open
Guyllian Gomez (GGomez99) wants to merge 7 commits into
microsoft:mainfrom
GGomez99:guyllian.gomez/yarn-pnp
Open

Add Yarn PnP module resolution support#63919
Guyllian Gomez (GGomez99) wants to merge 7 commits into
microsoft:mainfrom
GGomez99:guyllian.gomez/yarn-pnp

Conversation

@GGomez99

@GGomez99Guyllian Gomez (GGomez99) commented Aug 20, 2026

Copy link
Copy Markdown

Closes#63769

See original PR: microsoft/typescript-go#1966
Git history has been lost as replaying all commits would have generated a huge amount of unnecessary conflicts

Co-authored with gun-yu (@gun-yu), EJ (@ejc3) and Valentin MELUSSON (@valentinmelusson), original commits were lost during the move from the old repo

Motivation

This PR adds Plug'n'Play support natively to Typescript Go, following this issue: #63769
It has been reviewed and supported by Maël Nison (@arcanis), the lead maintainer of Yarn, and the original author of Yarn PnP.

Datadog has a frontend monorepo using yarn with over 6k packages, and seeing how TS Strada struggles with our current scaling, we decided to invest time in adding a native Yarn PnP support for Typescript Go.
This PnP implementation is being actively used by all engineers at Datadog, including in IDEs, by agents, and it's currently being beta-tested in CI with a custom lage setup.

Changes

It's based on the main changes from the original yarn patch (99f3e13) that the community has been maintaining for years throughout Typescript Strada updates, except that we implemented the official PnP specification so it doesn't depend on third-party code.

Implemented features:

  • PnP VFS that handles virtual folders and zip files seamlessly, with caching and fallback to the original vfs if pnp is not available
  • Add PnP API and manifest handling, following the yarn PnP specification
  • Initialize the PnP API every time a Host is initialized for both build and LSP modes
  • Add PnP support when resolving modules in internal/module/resolver.go
  • Add PnP support for auto-imports and completion at internal/modulespecifiers/specifiers.go
  • Add PnP support for root types at internal/core/compileroptions.go
  • Handle zip paths when going to implementation with the LSP
  • Update the baseline testing framework to handle PnP when needed
  • PnP manifest auto-refresh by watching .pnp.cjs changes

Tests

  • Basic PnP setup
  • Types from transitive dependencies
  • Root types loading from PnP dependencies
  • Completion and autoimports

Ports the Yarn Plug'n'Play (PnP) support developed on
guyllian.gomez/yarn-pnp in typescript-go into this repo's layout,
adding a pnp resolver and vfs overlay so tsc/lsp can resolve modules
through .pnp.cjs/.pnp.data.json manifests without node_modules.
- internal/pnp: parses PnP manifests and resolves package/dependency
locations
- internal/vfs/pnpvfs: read-through vfs.FS overlay that exposes
zip-packed dependencies (zip:/path/to.zip/inner/path) as regular
files
- module resolver, autoimport, and project/session wiring to plug the
PnP resolver and vfs into the existing module resolution and LSP
pipelines
- new compiler/fourslash test fixtures and baselines under
tsc/testdata covering PnP resolution scenarios
- packages/vscode-typescript/src/client.ts: register the "zip" URI
scheme in the LSP client's documentSelector so the extension
attaches to files served from zip-packed PnP dependencies
Ported and squashed from the original branch history to reconcile
with divergent upstream refactors (module rename to
github.com/microsoft/TypeScript/tsc, auto-import infrastructure
rewrite) that landed in this repo's main before this port. Original
work also includes contributions from valentinmelusson (fix package
IDs for Yarn PnP submodule imports) and gun-yu (add pnp error
handling and test).
Co-Authored-By: valentinmelusson <valentin.melusson@datadoghq.com>
Co-Authored-By: gun-yu <163322737+gun-yu@users.noreply.github.com>
@typescript-automationtypescript-automationBot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Aug 20, 2026
@GGomez99
Guyllian Gomez (GGomez99) marked this pull request as ready for review August 20, 2026 15:59
CopilotAI balanced review requested due to automatic review settings August 20, 2026 15:59

CopilotAI 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.

Pull request overview

Adds native Yarn Plug’n’Play support across the compiler, language server, module resolution, and virtual filesystem.

Changes:

  • Implements PnP manifest parsing, dependency resolution, type roots, and auto-imports.
  • Adds zip/virtual-path filesystem support and manifest refresh handling.
  • Adds compiler, resolver, VFS, and fourslash coverage with baselines.

Reviewed changes

Copilot reviewed 84 out of 88 changed files in this pull request and generated 10 comments.

Show a summary per file
FileDescription
tsc/testdata/tests/cases/compiler/pnpTypeRootsResolution.tsTests PnP type resolution.
tsc/testdata/tests/cases/compiler/pnpTransitiveDependencies.tsTests transitive dependency isolation.
tsc/testdata/tests/cases/compiler/pnpSimpleTest.tsTests basic PnP imports.
tsc/testdata/tests/cases/compiler/pnpDeclarationEmitWorkspace.tsTests workspace declaration emit.
tsc/testdata/fixtures/pnp/test-expectations.jsonDefines PnP resolver scenarios.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.errors.txtDiagnostic baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.jsEmit baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.typesType baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.symbolsSymbol baseline.
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.jsEmit baseline.
tsc/internal/vfs/pnpvfs/pnpvfs.goImplements PnP zip and virtual-path VFS.
tsc/internal/vfs/pnpvfs/pnpvfs_test.goTests the PnP VFS.
tsc/internal/vfs/cachedvfs/cachedvfs.goPropagates cache clearing.
tsc/internal/tspath/path.goDetects zip paths.
tsc/internal/tsoptions/tsoptionstest/vfsparseconfighost.goEnables PnP in config tests.
tsc/internal/tsoptions/tsconfigparsing.goExposes PnP through config hosts.
tsc/internal/tsoptions/contentmappers_test.goUpdates test host contracts.
tsc/internal/transpile/transpile.goUpdates compiler-host construction.
tsc/internal/transformers/tstransforms/importelision_test.goUpdates fake program contract.
tsc/internal/testutil/tsbaseline/js_emit_baseline.goPreserves PnP loader fixtures.
tsc/internal/testutil/harnessutil/harnessutil.goInitializes PnP in compiler tests.
tsc/internal/project/snapshot.goCarries and refreshes PnP state.
tsc/internal/project/session.goWires PnP into sessions and watches.
tsc/internal/project/projectcollectionbuilder.goBuilds PnP manifest watchers.
tsc/internal/project/project.goStores project PnP watchers.
tsc/internal/project/overlayfs.goInvalidates snapshots after installs.
tsc/internal/project/filechange.goAdds a PnP install change kind.
tsc/internal/project/extendedconfigcache_test.goUpdates PnP-aware config tests.
tsc/internal/project/configfileregistrybuilder.goExposes PnP during config parsing.
tsc/internal/project/compilerhost.goExposes PnP from project hosts.
tsc/internal/project/autoimport.goCarries PnP into auto-import cloning.
tsc/internal/pnp/pnpapi.goImplements the PnP resolution API.
tsc/internal/pnp/pnpapi_test.goTests manifest loading and resolution.
tsc/internal/pnp/pnp.goInitializes and identifies PnP loaders.
tsc/internal/pnp/manifestparser.goParses serialized PnP manifests.
tsc/internal/modulespecifiers/types.goExtends specifier host contracts.
tsc/internal/modulespecifiers/specifiers.goGenerates PnP package specifiers.
tsc/internal/modulespecifiers/specifiers_test.goUpdates specifier test hosts.
tsc/internal/module/types.goExtends module resolution hosts.
tsc/internal/module/resolver.goAdds PnP module and type resolution.
tsc/internal/module/resolver_test.goTests PnP package IDs.
tsc/internal/lsp/server.goInitializes PnP for LSP sessions.
tsc/internal/lsp/lsproto/lsp.goConverts zip document URIs.
tsc/internal/ls/lsconv/converters.goEmits zip document URIs.
tsc/internal/ls/lsconv/converters_test.goTests zip URI conversion.
tsc/internal/ls/host.goAdds PnP to language-service hosts.
tsc/internal/ls/findallreferences_test.goUpdates compiler-host construction.
tsc/internal/ls/file_rename_test.goUpdates test host contracts.
tsc/internal/ls/autoimport/util.goUses PnP for auto-import resolution.
tsc/internal/ls/autoimport/registry.goIndexes importable PnP packages.
tsc/internal/ls/autoimport/aliasresolver.goUpdates checker host contracts.
tsc/internal/ls/autoimport/aliasresolver_crash_test.goUpdates fake clone hosts.
tsc/internal/fourslash/tests/pnpAutoImportCompletion_test.goTests PnP auto-import completions.
tsc/internal/execute/watcher.goPasses PnP into watch builds.
tsc/internal/execute/tsctests/sys.goAdds PnP-aware test systems.
tsc/internal/execute/tsc/extendedconfigcache_test.goUpdates config test hosts.
tsc/internal/execute/tsc/emit_test.goUpdates test systems and hosts.
tsc/internal/execute/tsc/compile.goAdds PnP to system contracts.
tsc/internal/execute/tsc.goPasses PnP into compilation.
tsc/internal/execute/incremental/buildinfo_contentmapper_test.goUpdates incremental test hosts.
tsc/internal/execute/build/orchestrator.goPasses PnP into build orchestration.
tsc/internal/execute/build/host.goExposes PnP from build hosts.
tsc/internal/execute/build/compilerHost.goExposes PnP from compiler hosts.
tsc/internal/diagnostics/loc_generated.goUpdates generated import ordering.
tsc/internal/diagnostics/extraDiagnosticMessages.jsonAdds PnP diagnostics.
tsc/internal/diagnostics/diagnostics_generated.goGenerates PnP diagnostic definitions.
tsc/internal/core/compileroptions.goRefactors type-root discovery.
tsc/internal/compiler/projectreferencedtsfakinghost.goForwards PnP through reference hosts.
tsc/internal/compiler/program.goExposes PnP from programs.
tsc/internal/compiler/program_test.goUpdates compiler-host calls.
tsc/internal/compiler/host.goStores PnP on compiler hosts.
tsc/internal/compiler/emitHost.goExposes PnP during emit.
tsc/internal/compiler/emit_test.goUpdates compiler-host calls.
tsc/internal/compiler/contentmapper_test.goUpdates compiler-host calls.
tsc/internal/checker/checker_test.goUpdates compiler-host calls.
tsc/internal/api/server.goInitializes PnP for API sessions.
tsc/cmd/tsc/sys.goInitializes PnP for the CLI.
packages/vscode-typescript/src/client.tsRegisters the zip URI scheme.
Files not reviewed (2)
  • tsc/internal/diagnostics/diagnostics_generated.go: Generated file
  • tsc/internal/diagnostics/loc_generated.go: Generated file

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

Comment threadtsc/internal/module/resolver.go Outdated
Comment on lines +382 to +395
func (p *PnpApi) AppendPnpTypeRoots(nmTypes []string, currentDirectory string, compilerOptions *core.CompilerOptions, nmFromConfig bool) ([]string, bool) {
baseDir := compilerOptions.GetBaseDirFromOptions(currentDirectory)
pnpTypes := p.GetPnpTypeRoots(baseDir)

if len(nmTypes) > 0 {
return append(nmTypes, pnpTypes...), nmFromConfig
}

if len(pnpTypes) > 0 {
return pnpTypes, false
}

return nil, false
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

resolveFromTypeRoot which reads from compilerOptions.TypeRoots as a fallback when node_modules resolution fails (resolver.go@L478), is never denying getEffectiveTypeRoots calls. And the latter uses the same resolution logic no matter the value of compilerOptions.TypeRoots

So I think this suggested change doesn't make sense here

Comment threadtsc/internal/modulespecifiers/specifiers.go Outdated
Comment threadtsc/internal/pnp/manifestparser.go
Comment threadtsc/internal/pnp/pnpapi.go
Comment on lines +1732 to +1735
pnpApi := pnp.InitPnpApi(s.fs, cwd)
if pnpApi != nil {
s.fs = pnpvfs.From(s.fs)
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

As discussed in the old PR in typescript-go, it would be complex to initialize the PnP API at the project level, as we use the PnP resolver for global scope logic, like parsing the project config. Implementing it would also stray us away from the original TS PnP patch which could potentially introduce unknown regressions

However, I still generated a quick POC that changes how the PnP API is initialized, and it seems to properly work on multi-root projects. Let me know if you want me to refine this change and integrate it in this PR 🙇

Comment on lines +161 to +171
cachedReader, ok := pnpFS.cachedZipReadersMap[zipPath]
if ok {
usedReader = cachedReader
} else {
zipReader, err := zip.OpenReader(zipPath)
if err != nil {
return pnpFS.fs, path, ""
}

usedReader = zipReader
pnpFS.cachedZipReadersMap[zipPath] = usedReader

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I had memory issues with bounded cache, as I couldn't find a way to make Go GC properly clear closed zipReaders, since some parts of the Typescript process seem to keep references of these readers.

It's been a while since I looked into it, and we never had memory issues related to zip readers in our monorepo. I can have a look at it again if you really want to improve this part of the pnpvfs

Comment on lines +356 to +361
var prefix string
if tspath.IsZipPath(fileName) {
prefix = "zip:"
} else {
prefix = "file:"
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I don't think I can fix this stateless helper without adding an additional argument and heavily refactor usages of FileNameToDocumentURI

I could look into it if needed, but I don't know if this edge case should really be handled

(Note that neovim/coc-nvim use zipfile: and we could also take the time to handle these if we do refactor FileNameToDocumentURI)

Comment on lines +99 to +102
func (pnpFS *pnpFS) UseCaseSensitiveFileNames() bool {
// pnp fs is always case sensitive
return true
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The yarn pnp patch for Typescript <7.0 always used to set the case sensitiveness to true (source), so I think this change would still be valid in TS7

Comment threadtsc/testdata/fixtures/pnp/test-expectations.json Outdated
…ests
Co-Authored-By: ejc3 <ejc3@users.noreply.github.com>
@typescript-automation

Copy link
Copy Markdown

The TypeScript team hasn't accepted the linked issue #63769. If you can get it accepted, this PR will have a better chance of being reviewed.

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

Labels

For Uncommitted BugPR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Yarn PnP

2 participants

@GGomez99