Add Yarn PnP module resolution support - #63919
Conversation
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>
There was a problem hiding this comment.
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
| File | Description |
|---|---|
tsc/testdata/tests/cases/compiler/pnpTypeRootsResolution.ts |
Tests PnP type resolution. |
tsc/testdata/tests/cases/compiler/pnpTransitiveDependencies.ts |
Tests transitive dependency isolation. |
tsc/testdata/tests/cases/compiler/pnpSimpleTest.ts |
Tests basic PnP imports. |
tsc/testdata/tests/cases/compiler/pnpDeclarationEmitWorkspace.ts |
Tests workspace declaration emit. |
tsc/testdata/fixtures/pnp/test-expectations.json |
Defines PnP resolver scenarios. |
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.types |
Type baseline. |
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.symbols |
Symbol baseline. |
tsc/testdata/baselines/reference/compiler/pnpTypeRootsResolution.js |
Emit baseline. |
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.types |
Type baseline. |
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.symbols |
Symbol baseline. |
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.js |
Emit baseline. |
tsc/testdata/baselines/reference/compiler/pnpTransitiveDependencies.errors.txt |
Diagnostic baseline. |
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.types |
Type baseline. |
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.symbols |
Symbol baseline. |
tsc/testdata/baselines/reference/compiler/pnpSimpleTest.js |
Emit baseline. |
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.types |
Type baseline. |
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.symbols |
Symbol baseline. |
tsc/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.js |
Emit baseline. |
tsc/internal/vfs/pnpvfs/pnpvfs.go |
Implements PnP zip and virtual-path VFS. |
tsc/internal/vfs/pnpvfs/pnpvfs_test.go |
Tests the PnP VFS. |
tsc/internal/vfs/cachedvfs/cachedvfs.go |
Propagates cache clearing. |
tsc/internal/tspath/path.go |
Detects zip paths. |
tsc/internal/tsoptions/tsoptionstest/vfsparseconfighost.go |
Enables PnP in config tests. |
tsc/internal/tsoptions/tsconfigparsing.go |
Exposes PnP through config hosts. |
tsc/internal/tsoptions/contentmappers_test.go |
Updates test host contracts. |
tsc/internal/transpile/transpile.go |
Updates compiler-host construction. |
tsc/internal/transformers/tstransforms/importelision_test.go |
Updates fake program contract. |
tsc/internal/testutil/tsbaseline/js_emit_baseline.go |
Preserves PnP loader fixtures. |
tsc/internal/testutil/harnessutil/harnessutil.go |
Initializes PnP in compiler tests. |
tsc/internal/project/snapshot.go |
Carries and refreshes PnP state. |
tsc/internal/project/session.go |
Wires PnP into sessions and watches. |
tsc/internal/project/projectcollectionbuilder.go |
Builds PnP manifest watchers. |
tsc/internal/project/project.go |
Stores project PnP watchers. |
tsc/internal/project/overlayfs.go |
Invalidates snapshots after installs. |
tsc/internal/project/filechange.go |
Adds a PnP install change kind. |
tsc/internal/project/extendedconfigcache_test.go |
Updates PnP-aware config tests. |
tsc/internal/project/configfileregistrybuilder.go |
Exposes PnP during config parsing. |
tsc/internal/project/compilerhost.go |
Exposes PnP from project hosts. |
tsc/internal/project/autoimport.go |
Carries PnP into auto-import cloning. |
tsc/internal/pnp/pnpapi.go |
Implements the PnP resolution API. |
tsc/internal/pnp/pnpapi_test.go |
Tests manifest loading and resolution. |
tsc/internal/pnp/pnp.go |
Initializes and identifies PnP loaders. |
tsc/internal/pnp/manifestparser.go |
Parses serialized PnP manifests. |
tsc/internal/modulespecifiers/types.go |
Extends specifier host contracts. |
tsc/internal/modulespecifiers/specifiers.go |
Generates PnP package specifiers. |
tsc/internal/modulespecifiers/specifiers_test.go |
Updates specifier test hosts. |
tsc/internal/module/types.go |
Extends module resolution hosts. |
tsc/internal/module/resolver.go |
Adds PnP module and type resolution. |
tsc/internal/module/resolver_test.go |
Tests PnP package IDs. |
tsc/internal/lsp/server.go |
Initializes PnP for LSP sessions. |
tsc/internal/lsp/lsproto/lsp.go |
Converts zip document URIs. |
tsc/internal/ls/lsconv/converters.go |
Emits zip document URIs. |
tsc/internal/ls/lsconv/converters_test.go |
Tests zip URI conversion. |
tsc/internal/ls/host.go |
Adds PnP to language-service hosts. |
tsc/internal/ls/findallreferences_test.go |
Updates compiler-host construction. |
tsc/internal/ls/file_rename_test.go |
Updates test host contracts. |
tsc/internal/ls/autoimport/util.go |
Uses PnP for auto-import resolution. |
tsc/internal/ls/autoimport/registry.go |
Indexes importable PnP packages. |
tsc/internal/ls/autoimport/aliasresolver.go |
Updates checker host contracts. |
tsc/internal/ls/autoimport/aliasresolver_crash_test.go |
Updates fake clone hosts. |
tsc/internal/fourslash/tests/pnpAutoImportCompletion_test.go |
Tests PnP auto-import completions. |
tsc/internal/execute/watcher.go |
Passes PnP into watch builds. |
tsc/internal/execute/tsctests/sys.go |
Adds PnP-aware test systems. |
tsc/internal/execute/tsc/extendedconfigcache_test.go |
Updates config test hosts. |
tsc/internal/execute/tsc/emit_test.go |
Updates test systems and hosts. |
tsc/internal/execute/tsc/compile.go |
Adds PnP to system contracts. |
tsc/internal/execute/tsc.go |
Passes PnP into compilation. |
tsc/internal/execute/incremental/buildinfo_contentmapper_test.go |
Updates incremental test hosts. |
tsc/internal/execute/build/orchestrator.go |
Passes PnP into build orchestration. |
tsc/internal/execute/build/host.go |
Exposes PnP from build hosts. |
tsc/internal/execute/build/compilerHost.go |
Exposes PnP from compiler hosts. |
tsc/internal/diagnostics/loc_generated.go |
Updates generated import ordering. |
tsc/internal/diagnostics/extraDiagnosticMessages.json |
Adds PnP diagnostics. |
tsc/internal/diagnostics/diagnostics_generated.go |
Generates PnP diagnostic definitions. |
tsc/internal/core/compileroptions.go |
Refactors type-root discovery. |
tsc/internal/compiler/projectreferencedtsfakinghost.go |
Forwards PnP through reference hosts. |
tsc/internal/compiler/program.go |
Exposes PnP from programs. |
tsc/internal/compiler/program_test.go |
Updates compiler-host calls. |
tsc/internal/compiler/host.go |
Stores PnP on compiler hosts. |
tsc/internal/compiler/emitHost.go |
Exposes PnP during emit. |
tsc/internal/compiler/emit_test.go |
Updates compiler-host calls. |
tsc/internal/compiler/contentmapper_test.go |
Updates compiler-host calls. |
tsc/internal/checker/checker_test.go |
Updates compiler-host calls. |
tsc/internal/api/server.go |
Initializes PnP for API sessions. |
tsc/cmd/tsc/sys.go |
Initializes PnP for the CLI. |
packages/vscode-typescript/src/client.ts |
Registers 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.
| 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 | ||
| } |
There was a problem hiding this comment.
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
| pnpApi := pnp.InitPnpApi(s.fs, cwd) | ||
| if pnpApi != nil { | ||
| s.fs = pnpvfs.From(s.fs) | ||
| } |
There was a problem hiding this comment.
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 🙇
| 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 |
There was a problem hiding this comment.
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
| var prefix string | ||
| if tspath.IsZipPath(fileName) { | ||
| prefix = "zip:" | ||
| } else { | ||
| prefix = "file:" | ||
| } |
There was a problem hiding this comment.
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)
| func (pnpFS *pnpFS) UseCaseSensitiveFileNames() bool { | ||
| // pnp fs is always case sensitive | ||
| return true | ||
| } |
There was a problem hiding this comment.
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
…ests Co-Authored-By: ejc3 <ejc3@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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:
Hostis initialized for both build and LSP modesinternal/module/resolver.gointernal/modulespecifiers/specifiers.gointernal/core/compileroptions.go.pnp.cjschangesTests