diff --git a/_extension/src/client.ts b/_extension/src/client.ts index b759501e97..916bedec52 100644 --- a/_extension/src/client.ts +++ b/_extension/src/client.ts @@ -28,7 +28,14 @@ export class Client { this.clientOptions = { documentSelector: [ ...jsTsLanguageModes.map(language => ({ scheme: "file", language })), - ...jsTsLanguageModes.map(language => ({ scheme: "untitled", language })), + ...jsTsLanguageModes.map(language => ({ + scheme: "untitled", + language, + })), + ...jsTsLanguageModes.map(language => ({ + scheme: "zip", + language, + })), ], outputChannel: this.outputChannel, traceOutputChannel: this.traceOutputChannel, diff --git a/cmd/tsgo/sys.go b/cmd/tsgo/sys.go index e5b2b8d569..4593195034 100644 --- a/cmd/tsgo/sys.go +++ b/cmd/tsgo/sys.go @@ -8,9 +8,11 @@ import ( "github.com/microsoft/typescript-go/internal/bundled" "github.com/microsoft/typescript-go/internal/execute/tsc" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/osvfs" + "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" "golang.org/x/term" ) @@ -66,9 +68,16 @@ func newSystem() *osSys { os.Exit(int(tsc.ExitStatusInvalidProject_OutputsSkipped)) } + var fs vfs.FS = osvfs.FS() + + pnpApi := pnp.InitPnpApi(fs, tspath.NormalizePath(cwd)) + if pnpApi != nil { + fs = pnpvfs.From(fs) + } + return &osSys{ cwd: tspath.NormalizePath(cwd), - fs: bundled.WrapFS(osvfs.FS()), + fs: bundled.WrapFS(fs), defaultLibraryPath: bundled.LibPath(), writer: os.Stdout, start: time.Now(), diff --git a/internal/api/server.go b/internal/api/server.go index 5a3ce4213b..68e4a9141c 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -15,10 +15,12 @@ import ( "github.com/microsoft/typescript-go/internal/bundled" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/project" "github.com/microsoft/typescript-go/internal/project/logging" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/osvfs" + "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" ) //go:generate go tool golang.org/x/tools/cmd/stringer -type=MessageType -output=stringer_generated.go @@ -93,12 +95,19 @@ func NewServer(options *ServerOptions) *Server { panic("Cwd is required") } + var fs vfs.FS = osvfs.FS() + + pnpApi := pnp.InitPnpApi(fs, options.Cwd) + if pnpApi != nil { + fs = pnpvfs.From(fs) + } + server := &Server{ r: bufio.NewReader(options.In), w: bufio.NewWriter(options.Out), stderr: options.Err, cwd: options.Cwd, - fs: bundled.WrapFS(osvfs.FS()), + fs: bundled.WrapFS(fs), defaultLibraryPath: options.DefaultLibraryPath, } logger := logging.NewLogger(options.Err) diff --git a/internal/core/compileroptions.go b/internal/core/compileroptions.go index aae6fed160..64e3b8fd9f 100644 --- a/internal/core/compileroptions.go +++ b/internal/core/compileroptions.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -322,6 +323,14 @@ func (options *CompilerOptions) GetEffectiveTypeRoots(currentDirectory string) ( } } + nmTypes, nmFromConfig := options.GetNodeModulesTypeRoots(baseDir) + + typeRoots, nmFromConfig := pnp.AppendPnpTypeRoots(nmTypes, baseDir, nmFromConfig) + + return typeRoots, nmFromConfig +} + +func (options *CompilerOptions) GetNodeModulesTypeRoots(baseDir string) (result []string, fromConfig bool) { typeRoots := make([]string, 0, strings.Count(baseDir, "/")) tspath.ForEachAncestorDirectory(baseDir, func(dir string) (any, bool) { typeRoots = append(typeRoots, tspath.CombinePaths(dir, "node_modules", "@types")) diff --git a/internal/ls/autoimports.go b/internal/ls/autoimports.go index 1e586cc070..b822e16926 100644 --- a/internal/ls/autoimports.go +++ b/internal/ls/autoimports.go @@ -18,6 +18,7 @@ import ( "github.com/microsoft/typescript-go/internal/module" "github.com/microsoft/typescript-go/internal/modulespecifiers" "github.com/microsoft/typescript-go/internal/packagejson" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/stringutil" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -418,6 +419,11 @@ func (l *LanguageService) isImportable( // } fromPath := fromFile.FileName() + pnpApi := pnp.GetPnpApi(fromPath) + if pnpApi != nil { + return pnpApi.IsImportable(fromPath, toFile.FileName()) + } + useCaseSensitiveFileNames := moduleSpecifierResolutionHost.UseCaseSensitiveFileNames() globalTypingsCache := l.GetProgram().GetGlobalTypingsCacheLocation() modulePaths := modulespecifiers.GetEachFileNameOfModule( diff --git a/internal/ls/converters.go b/internal/ls/converters.go index 2fb9d262a3..9f68536282 100644 --- a/internal/ls/converters.go +++ b/internal/ls/converters.go @@ -128,7 +128,14 @@ func FileNameToDocumentURI(fileName string) lsproto.DocumentUri { parts[i] = extraEscapeReplacer.Replace(url.PathEscape(part)) } - return lsproto.DocumentUri("file://" + volume + strings.Join(parts, "/")) + var prefix string + if tspath.IsZipPath(fileName) { + prefix = "zip:" + } else { + prefix = "file:" + } + + return lsproto.DocumentUri(prefix + "//" + volume + strings.Join(parts, "/")) } func (c *Converters) LineAndCharacterToPosition(script Script, lineAndCharacter lsproto.Position) core.TextPos { diff --git a/internal/ls/converters_test.go b/internal/ls/converters_test.go index 25fc94bce2..2faa0af22e 100644 --- a/internal/ls/converters_test.go +++ b/internal/ls/converters_test.go @@ -31,6 +31,9 @@ func TestDocumentURIToFileName(t *testing.T) { {"file://localhost/c%24/GitDevelopment/express", "//localhost/c$/GitDevelopment/express"}, {"file:///c%3A/test%20with%20%2525/c%23code", "c:/test with %25/c#code"}, + {"zip:///path/to/archive.zip/file.ts", "/path/to/archive.zip/file.ts"}, + {"zip:///d:/work/tsgo932/lib/archive.zip/utils.ts", "d:/work/tsgo932/lib/archive.zip/utils.ts"}, + {"untitled:Untitled-1", "^/untitled/ts-nul-authority/Untitled-1"}, {"untitled:Untitled-1#fragment", "^/untitled/ts-nul-authority/Untitled-1#fragment"}, {"untitled:c:/Users/jrieken/Code/abc.txt", "^/untitled/ts-nul-authority/c:/Users/jrieken/Code/abc.txt"}, @@ -69,6 +72,9 @@ func TestFileNameToDocumentURI(t *testing.T) { {"//localhost/c$/GitDevelopment/express", "file://localhost/c%24/GitDevelopment/express"}, {"c:/test with %25/c#code", "file:///c%3A/test%20with%20%2525/c%23code"}, + {"/path/to/archive.zip/file.ts", "zip:///path/to/archive.zip/file.ts"}, + {"d:/work/tsgo932/lib/archive.zip/utils.ts", "zip:///d%3A/work/tsgo932/lib/archive.zip/utils.ts"}, + {"^/untitled/ts-nul-authority/Untitled-1", "untitled:Untitled-1"}, {"^/untitled/ts-nul-authority/c:/Users/jrieken/Code/abc.txt", "untitled:c:/Users/jrieken/Code/abc.txt"}, {"^/untitled/ts-nul-authority///wsl%2Bubuntu/home/jabaile/work/TypeScript-go/newfile.ts", "untitled://wsl%2Bubuntu/home/jabaile/work/TypeScript-go/newfile.ts"}, diff --git a/internal/ls/string_completions.go b/internal/ls/string_completions.go index 749ad880c6..eaee300342 100644 --- a/internal/ls/string_completions.go +++ b/internal/ls/string_completions.go @@ -522,6 +522,7 @@ func getStringLiteralCompletionsFromModuleNames( program *compiler.Program, ) *stringLiteralCompletions { // !!! needs `getModeForUsageLocationWorker` + // TODO investigate if we will need to update this for pnp, once available return nil } diff --git a/internal/lsp/lsproto/lsp.go b/internal/lsp/lsproto/lsp.go index dd172077c2..ea59298bcb 100644 --- a/internal/lsp/lsproto/lsp.go +++ b/internal/lsp/lsproto/lsp.go @@ -14,7 +14,7 @@ import ( type DocumentUri string // !!! func (uri DocumentUri) FileName() string { - if strings.HasPrefix(string(uri), "file://") { + if strings.HasPrefix(string(uri), "file://") || strings.HasPrefix(string(uri), "zip:") { parsed := core.Must(url.Parse(string(uri))) if parsed.Host != "" { return "//" + parsed.Host + parsed.Path diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 275e1c92df..4e85a276d8 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -16,11 +16,13 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/ls" "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/project" "github.com/microsoft/typescript-go/internal/project/ata" "github.com/microsoft/typescript-go/internal/project/logging" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" "golang.org/x/sync/errgroup" "golang.org/x/text/language" ) @@ -699,6 +701,12 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali cwd = s.cwd } + fs := s.fs + pnpApi := pnp.InitPnpApi(fs, cwd) + if pnpApi != nil { + fs = pnpvfs.From(fs) + } + s.session = project.NewSession(&project.SessionInit{ Options: &project.SessionOptions{ CurrentDirectory: cwd, @@ -709,7 +717,7 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali LoggingEnabled: true, DebounceDelay: 500 * time.Millisecond, }, - FS: s.fs, + FS: fs, Logger: s.logger, Client: s, NpmExecutor: s, diff --git a/internal/module/resolver.go b/internal/module/resolver.go index 7f83beb76d..2bfc3f1423 100644 --- a/internal/module/resolver.go +++ b/internal/module/resolver.go @@ -11,6 +11,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/diagnostics" "github.com/microsoft/typescript-go/internal/packagejson" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/semver" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -475,7 +476,7 @@ func (r *resolutionState) resolveNodeLikeWorker() *ResolvedModule { resolved := r.nodeLoadModuleByRelativeName(r.extensions, candidate, false, true) return r.createResolvedModule( resolved, - resolved != nil && strings.Contains(resolved.path, "/node_modules/"), + resolved != nil && (tspath.IsExternalLibraryImport(resolved.path)), ) } return r.createResolvedModule(nil, false) @@ -914,6 +915,12 @@ func (r *resolutionState) loadModuleFromNearestNodeModulesDirectory(typesScopeOn } func (r *resolutionState) loadModuleFromNearestNodeModulesDirectoryWorker(ext extensions, mode core.ResolutionMode, typesScopeOnly bool) *resolved { + pnpApi := pnp.GetPnpApi(r.containingDirectory) + if pnpApi != nil { + // !!! stop at global cache + return r.loadModuleFromImmediateNodeModulesDirectoryPnP(ext, r.containingDirectory, typesScopeOnly) + } + result, _ := tspath.ForEachAncestorDirectory( r.containingDirectory, func(directory string) (result *resolved, stop bool) { @@ -953,11 +960,52 @@ func (r *resolutionState) loadModuleFromImmediateNodeModulesDirectory(extensions return continueSearching() } +/* +With Plug and Play, we directly resolve the path of the moduleName using the PnP API, instead of searching for it in the node_modules directory + +See github.com/microsoft/typescript-go/internal/pnp package for more details +*/ +func (r *resolutionState) loadModuleFromImmediateNodeModulesDirectoryPnP(extensions extensions, directory string, typesScopeOnly bool) *resolved { + if !typesScopeOnly { + if packageResult := r.loadModuleFromPnpResolution(extensions, r.name, directory); !packageResult.shouldContinueSearching() { + return packageResult + } + } + + if extensions&extensionsDeclaration != 0 { + result := r.loadModuleFromPnpResolution(extensionsDeclaration, "@types/"+r.mangleScopedPackageName(r.name), directory) + + return result + } + + return nil +} + +func (r *resolutionState) loadModuleFromPnpResolution(ext extensions, moduleName string, issuer string) *resolved { + pnpApi := pnp.GetPnpApi(issuer) + + if pnpApi != nil { + packageName, rest := ParsePackageName(moduleName) + // TODO: bubble up yarn resolution errors, instead of _ + packageDirectory, _ := pnpApi.ResolveToUnqualified(packageName, issuer) + if packageDirectory != "" { + candidate := tspath.NormalizePath(tspath.CombinePaths(packageDirectory, rest)) + return r.loadModuleFromSpecificNodeModulesDirectoryImpl(ext, true /* nodeModulesDirectoryExists */, candidate, rest, packageDirectory) + } + } + + return nil +} + func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectory(ext extensions, moduleName string, nodeModulesDirectory string, nodeModulesDirectoryExists bool) *resolved { candidate := tspath.NormalizePath(tspath.CombinePaths(nodeModulesDirectory, moduleName)) packageName, rest := ParsePackageName(moduleName) packageDirectory := tspath.CombinePaths(nodeModulesDirectory, packageName) + return r.loadModuleFromSpecificNodeModulesDirectoryImpl(ext, nodeModulesDirectoryExists, candidate, rest, packageDirectory) +} + +func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectoryImpl(ext extensions, nodeModulesDirectoryExists bool, candidate string, rest string, packageDirectory string) *resolved { var rootPackageInfo *packagejson.InfoCacheEntry // First look for a nested package.json, as in `node_modules/foo/bar/package.json` packageInfo := r.getPackageJsonInfo(candidate, !nodeModulesDirectoryExists) @@ -1036,7 +1084,7 @@ func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectory(ext extensi } func (r *resolutionState) createResolvedModuleHandlingSymlink(resolved *resolved) *ResolvedModule { - isExternalLibraryImport := resolved != nil && strings.Contains(resolved.path, "/node_modules/") + isExternalLibraryImport := resolved != nil && (tspath.IsExternalLibraryImport(resolved.path)) if r.compilerOptions.PreserveSymlinks != core.TSTrue && isExternalLibraryImport && resolved.originalPath == "" && @@ -1084,7 +1132,7 @@ func (r *resolutionState) createResolvedTypeReferenceDirective(resolved *resolve resolvedTypeReferenceDirective.ResolvedFileName = resolved.path resolvedTypeReferenceDirective.Primary = primary resolvedTypeReferenceDirective.PackageId = resolved.packageId - resolvedTypeReferenceDirective.IsExternalLibraryImport = strings.Contains(resolved.path, "/node_modules/") + resolvedTypeReferenceDirective.IsExternalLibraryImport = tspath.IsExternalLibraryImport(resolved.path) if r.compilerOptions.PreserveSymlinks != core.TSTrue { originalPath, resolvedFileName := r.getOriginalAndResolvedFileName(resolved.path) @@ -1740,8 +1788,19 @@ func (r *resolutionState) readPackageJsonPeerDependencies(packageJsonInfo *packa } nodeModules := packageDirectory[:nodeModulesIndex+len("/node_modules")] + "/" builder := strings.Builder{} + pnpApi := pnp.GetPnpApi(packageJsonInfo.PackageDirectory) for name := range peerDependencies.Value { - peerPackageJson := r.getPackageJsonInfo(nodeModules+name /*onlyRecordFailures*/, false) + var peerDependencyPath string + + if pnpApi != nil { + peerDependencyPath, _ = pnpApi.ResolveToUnqualified(name, packageDirectory) + } + + if peerDependencyPath == "" { + peerDependencyPath = nodeModules + name + } + + peerPackageJson := r.getPackageJsonInfo(peerDependencyPath, false /*onlyRecordFailures*/) if peerPackageJson != nil { version := peerPackageJson.Contents.Version.Value builder.WriteString("+") diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index fd040fc14e..e872f17620 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -12,6 +12,7 @@ import ( "github.com/microsoft/typescript-go/internal/module" "github.com/microsoft/typescript-go/internal/outputpaths" "github.com/microsoft/typescript-go/internal/packagejson" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/stringutil" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -273,7 +274,7 @@ func GetEachFileNameOfModule( if !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) { results = append(results, ModulePath{ FileName: p, - IsInNodeModules: ContainsNodeModules(p), + IsInNodeModules: ContainsNodeModules(p) || pnp.IsInPnpModule(importingFileName, p), IsRedirect: referenceRedirect == p, }) } @@ -316,7 +317,7 @@ func GetEachFileNameOfModule( if !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) { results = append(results, ModulePath{ FileName: p, - IsInNodeModules: ContainsNodeModules(p), + IsInNodeModules: ContainsNodeModules(p) || pnp.IsInPnpModule(importingFileName, p), IsRedirect: referenceRedirect == p, }) } @@ -681,6 +682,131 @@ func tryGetModuleNameFromRootDirs( return processEnding(shortest, allowedEndings, compilerOptions, host) } +// TODO: This code partially duplicates tryGetModuleNameAsNodeModule, is it better to keep it isolated from the node module version or should we merge them? +func tryGetModuleNameAsPnpPackage( + pathObj ModulePath, + info Info, + importingSourceFile SourceFileForSpecifierGeneration, + host ModuleSpecifierGenerationHost, + options *core.CompilerOptions, + userPreferences UserPreferences, + packageNameOnly bool, + overrideMode core.ResolutionMode, +) string { + pnpApi := pnp.GetPnpApi(importingSourceFile.FileName()) + if pnpApi == nil { + return "" + } + + pnpPackageName := "" + fromLocator, _ := pnpApi.FindLocator(importingSourceFile.FileName()) + toLocator, _ := pnpApi.FindLocator(pathObj.FileName) + + // Don't use the package name when the imported file is inside + // the source directory (prefer a relative path instead) + if fromLocator == toLocator { + return "" + } + + if fromLocator != nil && toLocator != nil { + fromInfo := pnpApi.GetPackage(fromLocator) + + useToLocator := false + + for i := range fromInfo.PackageDependencies { + isAlias := fromInfo.PackageDependencies[i].IsAlias() + if isAlias && fromInfo.PackageDependencies[i].AliasName == toLocator.Name && fromInfo.PackageDependencies[i].Reference == toLocator.Reference { + useToLocator = true + break + } else if fromInfo.PackageDependencies[i].Ident == toLocator.Name && fromInfo.PackageDependencies[i].Reference == toLocator.Reference { + useToLocator = true + break + } + } + + if useToLocator { + pnpPackageName = toLocator.Name + } + } + + var parts *NodeModulePathParts + if toLocator != nil { + toInfo := pnpApi.GetPackage(toLocator) + packageRootAbsolutePath := pnpApi.GetPackageLocationAbsolutePath(toInfo) + parts = &NodeModulePathParts{ + TopLevelNodeModulesIndex: -1, + TopLevelPackageNameIndex: -1, + PackageRootIndex: len(packageRootAbsolutePath), + FileNameIndex: strings.LastIndex(pathObj.FileName, "/"), + } + } + + if parts == nil { + return "" + } + + // Simplify the full file path to something that can be resolved by Node. + preferences := getModuleSpecifierPreferences(userPreferences, host, options, importingSourceFile, "") + allowedEndings := preferences.getAllowedEndingsInPreferredOrder(core.ResolutionModeNone) + + moduleSpecifier := pathObj.FileName + isPackageRootPath := false + if !packageNameOnly { + packageRootIndex := parts.PackageRootIndex + var moduleFileName string + for true { + // If the module could be imported by a directory name, use that directory's name + pkgJsonResults := tryDirectoryWithPackageJson( + *parts, + pathObj, + importingSourceFile, + host, + overrideMode, + options, + allowedEndings, + pnpPackageName, + ) + moduleFileToTry := pkgJsonResults.moduleFileToTry + packageRootPath := pkgJsonResults.packageRootPath + blockedByExports := pkgJsonResults.blockedByExports + verbatimFromExports := pkgJsonResults.verbatimFromExports + if blockedByExports { + return "" // File is under this package.json, but is not publicly exported - there's no way to name it via `node_modules` resolution + } + if verbatimFromExports { + return moduleFileToTry + } + //} + if len(packageRootPath) > 0 { + moduleSpecifier = packageRootPath + isPackageRootPath = true + break + } + if len(moduleFileName) == 0 { + moduleFileName = moduleFileToTry + } + // try with next level of directory + packageRootIndex = core.IndexAfter(pathObj.FileName, "/", packageRootIndex+1) + if packageRootIndex == -1 { + moduleSpecifier = processEnding(moduleFileName, allowedEndings, options, host) + break + } + } + } + + if pathObj.IsRedirect && !isPackageRootPath { + return "" + } + + // If the module was found in @types, get the actual Node package name + nodeModulesDirectoryName := moduleSpecifier[parts.TopLevelPackageNameIndex+1:] + if pnpPackageName != "" { + nodeModulesDirectoryName = pnpPackageName + moduleSpecifier[parts.PackageRootIndex:] + } + + return GetPackageNameFromTypesPackageName(nodeModulesDirectoryName) +} + func tryGetModuleNameAsNodeModule( pathObj ModulePath, info Info, @@ -691,6 +817,11 @@ func tryGetModuleNameAsNodeModule( packageNameOnly bool, overrideMode core.ResolutionMode, ) string { + pnpModuleName := tryGetModuleNameAsPnpPackage(pathObj, info, importingSourceFile, host, options, userPreferences, packageNameOnly, overrideMode) + if pnpModuleName != "" { + return pnpModuleName + } + parts := GetNodeModulePathParts(pathObj.FileName) if parts == nil { return "" @@ -716,6 +847,7 @@ func tryGetModuleNameAsNodeModule( overrideMode, options, allowedEndings, + "", ) moduleFileToTry := pkgJsonResults.moduleFileToTry packageRootPath := pkgJsonResults.packageRootPath @@ -778,6 +910,7 @@ func tryDirectoryWithPackageJson( overrideMode core.ResolutionMode, options *core.CompilerOptions, allowedEndings []ModuleSpecifierEnding, + packageNameOverride string, ) pkgJsonDirAttemptResult { rootIdx := parts.PackageRootIndex if rootIdx == -1 { @@ -809,7 +942,10 @@ func tryDirectoryWithPackageJson( // name in the package.json content via url/filepath dependency specifiers. We need to // use the actual directory name, so don't look at `packageJsonContent.name` here. nodeModulesDirectoryName := packageRootPath[parts.TopLevelPackageNameIndex+1:] - packageName := GetPackageNameFromTypesPackageName(nodeModulesDirectoryName) + packageName := packageNameOverride + if packageName == "" { + packageName = GetPackageNameFromTypesPackageName(nodeModulesDirectoryName) + } conditions := module.GetConditions(options, importMode) var fromExports string diff --git a/internal/pnp/manifestparser.go b/internal/pnp/manifestparser.go new file mode 100644 index 0000000000..8e75f2edc6 --- /dev/null +++ b/internal/pnp/manifestparser.go @@ -0,0 +1,324 @@ +package pnp + +import ( + "errors" + "fmt" + "strings" + + "github.com/go-json-experiment/json" + + "github.com/dlclark/regexp2" + "github.com/microsoft/typescript-go/internal/tspath" +) + +type LinkType string + +const ( + LinkTypeSoft LinkType = "SOFT" + LinkTypeHard LinkType = "HARD" +) + +type PackageDependency struct { + Ident string + Reference string // Either the direct reference or alias reference + AliasName string // Empty if not an alias +} + +func (pd PackageDependency) IsAlias() bool { + return pd.AliasName != "" +} + +type PackageInfo struct { + PackageLocation string `json:"packageLocation"` + PackageDependencies []PackageDependency `json:"packageDependencies,omitempty"` + LinkType LinkType `json:"linkType,omitempty"` + DiscardFromLookup bool `json:"discardFromLookup,omitempty"` + PackagePeers []string `json:"packagePeers,omitempty"` +} + +type Locator struct { + Name string `json:"name"` + Reference string `json:"reference"` +} + +type FallbackExclusion struct { + Name string `json:"name"` + Entries []string `json:"entries"` +} + +type PackageTrieData struct { + ident string + reference string + info *PackageInfo +} + +type PackageRegistryTrie struct { + pathSegment string + childrenPathSegments map[string]*PackageRegistryTrie + packageData *PackageTrieData +} + +type PnpManifestData struct { + dirPath string + + ignorePatternData *regexp2.Regexp + enableTopLevelFallback bool + + fallbackPool [][2]string + fallbackExclusionMap map[string]*FallbackExclusion + + dependencyTreeRoots []Locator + + // Nested maps for package registry (ident -> reference -> PackageInfo) + packageRegistryMap map[string]map[string]*PackageInfo + packageRegistryTrie *PackageRegistryTrie +} + +func parseManifestFromPath(fs PnpApiFS, manifestDir string) (*PnpManifestData, error) { + pnpDataString := "" + + data, ok := fs.ReadFile(tspath.CombinePaths(manifestDir, ".pnp.data.json")) + if ok { + pnpDataString = data + } else { + pnpScriptString, ok := fs.ReadFile(tspath.CombinePaths(manifestDir, ".pnp.cjs")) + if !ok { + return nil, errors.New("failed to read .pnp.cjs file") + } + + manifestRegex := regexp2.MustCompile(`(const[ \r\n]+RAW_RUNTIME_STATE[ \r\n]*=[ \r\n]*|hydrateRuntimeState\(JSON\.parse\()'`, regexp2.None) + matches, err := manifestRegex.FindStringMatch(pnpScriptString) + if err != nil || matches == nil { + return nil, errors.New("We failed to locate the PnP data payload inside its manifest file. Did you manually edit the file?") + } + + start := matches.Index + matches.Length + var b strings.Builder + b.Grow(len(pnpScriptString)) + for i := start; i < len(pnpScriptString); i++ { + if pnpScriptString[i] == '\'' { + break + } + + if pnpScriptString[i] != '\\' { + b.WriteByte(pnpScriptString[i]) + } + } + pnpDataString = b.String() + } + + return parseManifestFromData(pnpDataString, manifestDir) +} + +func parseManifestFromData(pnpDataString string, manifestDir string) (*PnpManifestData, error) { + var rawData map[string]interface{} + if err := json.Unmarshal([]byte(pnpDataString), &rawData); err != nil { + return nil, fmt.Errorf("failed to parse JSON PnP data: %w", err) + } + + pnpData, err := parsePnpManifest(rawData, manifestDir) + if err != nil { + return nil, fmt.Errorf("failed to parse PnP data: %w", err) + } + + return pnpData, nil +} + +// TODO add error handling for corrupted data +func parsePnpManifest(rawData map[string]interface{}, manifestDir string) (*PnpManifestData, error) { + data := &PnpManifestData{dirPath: manifestDir} + + if roots, ok := rawData["dependencyTreeRoots"].([]interface{}); ok { + for _, root := range roots { + if rootMap, ok := root.(map[string]interface{}); ok { + data.dependencyTreeRoots = append(data.dependencyTreeRoots, Locator{ + Name: getField(rootMap, "name", parseString), + Reference: getField(rootMap, "reference", parseString), + }) + } + } + } + + ignorePatternData := getField(rawData, "ignorePatternData", parseString) + if ignorePatternData != "" { + ignorePatternDataRegexp, err := regexp2.Compile(ignorePatternData, regexp2.None) + if err != nil { + return nil, fmt.Errorf("failed to compile ignore pattern data: %w", err) + } + + data.ignorePatternData = ignorePatternDataRegexp + } + + data.enableTopLevelFallback = getField(rawData, "enableTopLevelFallback", parseBool) + + data.fallbackPool = getField(rawData, "fallbackPool", parseStringPairs) + + data.fallbackExclusionMap = make(map[string]*FallbackExclusion) + + if exclusions, ok := rawData["fallbackExclusionList"].([]interface{}); ok { + for _, exclusion := range exclusions { + if exclusionArr, ok := exclusion.([]interface{}); ok && len(exclusionArr) == 2 { + name := parseString(exclusionArr[0]) + entries := parseStringArray(exclusionArr[1]) + exclusionEntry := &FallbackExclusion{ + Name: name, + Entries: entries, + } + data.fallbackExclusionMap[exclusionEntry.Name] = exclusionEntry + } + } + } + + data.packageRegistryMap = make(map[string]map[string]*PackageInfo) + + if registryData, ok := rawData["packageRegistryData"].([]interface{}); ok { + for _, entry := range registryData { + if entryArr, ok := entry.([]interface{}); ok && len(entryArr) == 2 { + ident := parseString(entryArr[0]) + + if data.packageRegistryMap[ident] == nil { + data.packageRegistryMap[ident] = make(map[string]*PackageInfo) + } + + if versions, ok := entryArr[1].([]interface{}); ok { + for _, version := range versions { + if versionArr, ok := version.([]interface{}); ok && len(versionArr) == 2 { + reference := parseString(versionArr[0]) + + if infoMap, ok := versionArr[1].(map[string]interface{}); ok { + packageInfo := &PackageInfo{ + PackageLocation: getField(infoMap, "packageLocation", parseString), + PackageDependencies: getField(infoMap, "packageDependencies", parsePackageDependencies), + LinkType: LinkType(getField(infoMap, "linkType", parseString)), + DiscardFromLookup: getField(infoMap, "discardFromLookup", parseBool), + PackagePeers: getField(infoMap, "packagePeers", parseStringArray), + } + + data.packageRegistryMap[ident][reference] = packageInfo + data.addPackageToTrie(ident, reference, packageInfo) + } + } + } + } + } + } + } + + return data, nil +} + +func (data *PnpManifestData) addPackageToTrie(ident string, reference string, packageInfo *PackageInfo) { + if data.packageRegistryTrie == nil { + data.packageRegistryTrie = &PackageRegistryTrie{ + pathSegment: "", + childrenPathSegments: make(map[string]*PackageRegistryTrie), + packageData: nil, + } + } + + packageData := &PackageTrieData{ + ident: ident, + reference: reference, + info: packageInfo, + } + + packagePath := tspath.RemoveTrailingDirectorySeparator(packageInfo.PackageLocation) + packagePathSegments := strings.Split(packagePath, "/") + + currentTrie := data.packageRegistryTrie + + for _, segment := range packagePathSegments { + if currentTrie.childrenPathSegments[segment] == nil { + currentTrie.childrenPathSegments[segment] = &PackageRegistryTrie{ + pathSegment: segment, + childrenPathSegments: make(map[string]*PackageRegistryTrie), + packageData: nil, + } + } + + currentTrie = currentTrie.childrenPathSegments[segment] + } + + currentTrie.packageData = packageData +} + +// Helper functions for parsing JSON values - following patterns from tsoptions.parseString, etc. +func parseString(value interface{}) string { + if str, ok := value.(string); ok { + return str + } + return "" +} + +func parseBool(value interface{}) bool { + if val, ok := value.(bool); ok { + return val + } + return false +} + +func parseStringArray(value interface{}) []string { + if arr, ok := value.([]interface{}); ok { + if arr == nil { + return nil + } + result := make([]string, 0, len(arr)) + for _, v := range arr { + if str, ok := v.(string); ok { + result = append(result, str) + } + } + return result + } + return nil +} + +func parseStringPairs(value interface{}) [][2]string { + var result [][2]string + if arr, ok := value.([]interface{}); ok { + for _, item := range arr { + if pair, ok := item.([]interface{}); ok && len(pair) == 2 { + result = append(result, [2]string{ + parseString(pair[0]), + parseString(pair[1]), + }) + } + } + } + return result +} + +func parsePackageDependencies(value interface{}) []PackageDependency { + var result []PackageDependency + if arr, ok := value.([]interface{}); ok { + for _, item := range arr { + if pair, ok := item.([]interface{}); ok && len(pair) == 2 { + ident := parseString(pair[0]) + + // Check if second element is string (simple reference) or array (alias) + if str, ok := pair[1].(string); ok { + result = append(result, PackageDependency{ + Ident: ident, + Reference: str, + AliasName: "", + }) + } else if aliasPair, ok := pair[1].([]interface{}); ok && len(aliasPair) == 2 { + result = append(result, PackageDependency{ + Ident: ident, + Reference: parseString(aliasPair[1]), + AliasName: parseString(aliasPair[0]), + }) + } + } + } + } + return result +} + +func getField[T any](m map[string]interface{}, key string, parser func(interface{}) T) T { + if val, exists := m[key]; exists { + return parser(val) + } + var zero T + return zero +} diff --git a/internal/pnp/pnp.go b/internal/pnp/pnp.go new file mode 100644 index 0000000000..2832bb3791 --- /dev/null +++ b/internal/pnp/pnp.go @@ -0,0 +1,142 @@ +package pnp + +import ( + "runtime" + "strconv" + "strings" + "sync" + "sync/atomic" + "testing" +) + +var ( + isPnpApiInitialized atomic.Uint32 + cachedPnpApi *PnpApi + pnpMu sync.Mutex + // testPnpCache stores per-goroutine PnP APIs for test isolation + // Key is goroutine ID (as int) + testPnpCache sync.Map // map[int]*PnpApi +) + +// getGoroutineID returns the current goroutine ID +// It is usually not recommended to work with goroutine IDs, but it is the most non-intrusive way to setup a parallel testing environment for PnP API +func getGoroutineID() int { + var buf [64]byte + n := runtime.Stack(buf[:], false) + idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0] + id, _ := strconv.Atoi(idField) + return id +} + +// Sets the PnP API for the given manifest data and manifest directory, used for testing +// This creates a goroutine-specific cache entry that won't interfere with other parallel tests. +func OverridePnpApi(fs PnpApiFS, manifestDataRaw string) *PnpApi { + if manifestDataRaw == "" { + return nil + } + + var pnpApi *PnpApi + + manifestData, err := parseManifestFromData(manifestDataRaw, "/") + if err != nil { + pnpApi = nil + } else if manifestData != nil { + pnpApi = &PnpApi{ + fs: fs, + url: "/", + manifest: manifestData, + } + } + + // Store in goroutine-specific cache using goroutine ID + // This allows parallel tests to have isolated PnP configurations + gid := getGoroutineID() + testPnpCache.Store(gid, pnpApi) + + return pnpApi +} + +// ClearTestPnpCache clears the test-specific PnP API cache for the current goroutine +func ClearTestPnpCache() { + gid := getGoroutineID() + testPnpCache.Delete(gid) +} + +func InitPnpApi(fs PnpApiFS, filePath string) *PnpApi { + pnpMu.Lock() + defer pnpMu.Unlock() + // Double-check after acquiring lock + if isPnpApiInitialized.Load() == 1 { + return cachedPnpApi + } + + pnpApi := &PnpApi{fs: fs, url: filePath} + + manifestData, err := pnpApi.findClosestPnpManifest() + if err == nil { + pnpApi.manifest = manifestData + cachedPnpApi = pnpApi + } else { + // Couldn't load PnP API + cachedPnpApi = nil + } + + isPnpApiInitialized.Store(1) + return cachedPnpApi +} + +// GetPnpApi returns the PnP API for the given file path. Will return nil if the PnP API is not available or not initialized +func GetPnpApi(filePath string) *PnpApi { + // If in a test, check for PnP API overrides + if testing.Testing() { + gid := getGoroutineID() + if api, ok := testPnpCache.Load(gid); ok { + return api.(*PnpApi) + } + } + + // Check if PnP API is already initialized using atomic read (no lock needed) + if isPnpApiInitialized.Load() == 1 { + return cachedPnpApi + } + + return nil +} + +// Clears the singleton PnP API cache +func ClearPnpCache() { + pnpMu.Lock() + defer pnpMu.Unlock() + cachedPnpApi = nil + isPnpApiInitialized.Store(0) +} + +func IsInPnpModule(fromFileName string, toFileName string) bool { + pnpApi := GetPnpApi(fromFileName) + if pnpApi == nil { + return false + } + + fromLocator, _ := pnpApi.FindLocator(fromFileName) + toLocator, _ := pnpApi.FindLocator(toFileName) + // The targeted filename is in a pnp module different from the requesting filename + return fromLocator != nil && toLocator != nil && fromLocator.Name != toLocator.Name +} + +func AppendPnpTypeRoots(nmTypes []string, baseDir string, nmFromConfig bool) ([]string, bool) { + pnpTypes := []string{} + pnpApi := GetPnpApi(baseDir) + if pnpApi != nil { + pnpTypes = pnpApi.GetPnpTypeRoots(baseDir) + } + + if len(nmTypes) > 0 { + return append(nmTypes, pnpTypes...), nmFromConfig + } + + if len(pnpTypes) > 0 { + return pnpTypes, false + } + + return nil, false +} diff --git a/internal/pnp/pnpapi.go b/internal/pnp/pnpapi.go new file mode 100644 index 0000000000..e3e949dcd6 --- /dev/null +++ b/internal/pnp/pnpapi.go @@ -0,0 +1,319 @@ +package pnp + +/* + * Yarn Plug'n'Play (generally referred to as Yarn PnP) is the default installation strategy in modern releases of Yarn. + * Yarn PnP generates a single Node.js loader file in place of the typical node_modules folder. + * This loader file, named .pnp.cjs, contains all information about your project's dependency tree, informing your tools as to + * the location of the packages on the disk and letting them know how to resolve require and import calls. + * + * The full specification is available at https://yarnpkg.com/advanced/pnp-spec + */ + +import ( + "errors" + "fmt" + "strings" + + "github.com/microsoft/typescript-go/internal/tspath" +) + +type PnpApi struct { + fs PnpApiFS + url string + manifest *PnpManifestData +} + +// FS abstraction used by the PnpApi to access the file system +// We can't use the vfs.FS interface because it creates an import cycle: core -> pnp -> vfs -> core +type PnpApiFS interface { + FileExists(path string) bool + ReadFile(path string) (contents string, ok bool) +} + +func (p *PnpApi) RefreshManifest() error { + var newData *PnpManifestData + var err error + + if p.manifest == nil { + newData, err = p.findClosestPnpManifest() + } else { + newData, err = parseManifestFromPath(p.fs, p.manifest.dirPath) + } + + if err != nil { + return err + } + + p.manifest = newData + return nil +} + +func (p *PnpApi) ResolveToUnqualified(specifier string, parentPath string) (string, error) { + if p.manifest == nil { + panic("ResolveToUnqualified called with no PnP manifest available") + } + + ident, modulePath, err := p.ParseBareIdentifier(specifier) + if err != nil { + // Skipping resolution + return "", nil + } + + parentLocator, err := p.FindLocator(parentPath) + if err != nil || parentLocator == nil { + // Skipping resolution + return "", nil + } + + parentPkg := p.GetPackage(parentLocator) + + var referenceOrAlias *PackageDependency + for _, dep := range parentPkg.PackageDependencies { + if dep.Ident == ident { + referenceOrAlias = &dep + break + } + } + + // If not found, try fallback if enabled + if referenceOrAlias == nil { + if p.manifest.enableTopLevelFallback { + excluded := false + if exclusion, ok := p.manifest.fallbackExclusionMap[parentLocator.Name]; ok { + for _, entry := range exclusion.Entries { + if entry == parentLocator.Reference { + excluded = true + break + } + } + } + if !excluded { + fallback := p.ResolveViaFallback(ident) + if fallback != nil { + referenceOrAlias = fallback + } + } + } + } + + // undeclared dependency + if referenceOrAlias == nil { + if parentLocator.Name == "" { + return "", fmt.Errorf("Your application tried to access %s, but it isn't declared in your dependencies; this makes the require call ambiguous and unsound.\n\nRequired package: %s\nRequired by: %s", ident, ident, parentPath) + } + return "", fmt.Errorf("%s tried to access %s, but it isn't declared in your dependencies; this makes the require call ambiguous and unsound.\n\nRequired package: %s\nRequired by: %s", parentLocator.Name, ident, ident, parentPath) + } + + // unfulfilled peer dependency + if !referenceOrAlias.IsAlias() && referenceOrAlias.Reference == "" { + if parentLocator.Name == "" { + return "", fmt.Errorf("Your application tried to access %s (a peer dependency); this isn't allowed as there is no ancestor to satisfy the requirement. Use a devDependency if needed.\n\nRequired package: %s\nRequired by: %s", ident, ident, parentPath) + } + return "", fmt.Errorf("%s tried to access %s (a peer dependency) but it isn't provided by its ancestors/your application; this makes the require call ambiguous and unsound.\n\nRequired package: %s\nRequired by: %s", parentLocator.Name, ident, ident, parentPath) + } + + var dependencyPkg *PackageInfo + if referenceOrAlias.IsAlias() { + dependencyPkg = p.GetPackage(&Locator{Name: referenceOrAlias.AliasName, Reference: referenceOrAlias.Reference}) + } else { + dependencyPkg = p.GetPackage(&Locator{Name: referenceOrAlias.Ident, Reference: referenceOrAlias.Reference}) + } + + return tspath.CombinePaths(p.manifest.dirPath, dependencyPkg.PackageLocation, modulePath), nil +} + +func (p *PnpApi) findClosestPnpManifest() (*PnpManifestData, error) { + directoryPath := p.url + + for { + pnpPath := tspath.CombinePaths(directoryPath, ".pnp.cjs") + if p.fs.FileExists(pnpPath) { + return parseManifestFromPath(p.fs, directoryPath) + } + + directoryPath = tspath.GetDirectoryPath(directoryPath) + if tspath.IsDiskPathRoot(directoryPath) { + return nil, errors.New("no PnP manifest found") + } + } +} + +func (p *PnpApi) GetPackage(locator *Locator) *PackageInfo { + packageRegistryMap := p.manifest.packageRegistryMap + packageInfo, ok := packageRegistryMap[locator.Name][locator.Reference] + if !ok { + panic(locator.Name + " should have an entry in the package registry") + } + + return packageInfo +} + +func (p *PnpApi) FindLocator(parentPath string) (*Locator, error) { + relativePath := tspath.GetRelativePathFromDirectory(p.manifest.dirPath, parentPath, + tspath.ComparePathsOptions{UseCaseSensitiveFileNames: true}) + + if p.manifest.ignorePatternData != nil { + match, err := p.manifest.ignorePatternData.MatchString(relativePath) + if err != nil { + return nil, err + } + + if match { + return nil, nil + } + } + + var relativePathWithDot string + if strings.HasPrefix(relativePath, "../") { + relativePathWithDot = relativePath + } else { + relativePathWithDot = "./" + relativePath + } + + var bestLength int + var bestLocator *Locator + pathSegments := strings.Split(relativePathWithDot, "/") + currentTrie := p.manifest.packageRegistryTrie + + // Go down the trie, looking for the latest defined packageInfo that matches the path + for index, segment := range pathSegments { + currentTrie = currentTrie.childrenPathSegments[segment] + + if currentTrie == nil || currentTrie.childrenPathSegments == nil { + break + } + + if currentTrie.packageData != nil && index >= bestLength { + bestLength = index + bestLocator = &Locator{Name: currentTrie.packageData.ident, Reference: currentTrie.packageData.reference} + } + } + + if bestLocator == nil { + return nil, fmt.Errorf("no package found for path %s", relativePath) + } + + return bestLocator, nil +} + +func (p *PnpApi) ResolveViaFallback(name string) *PackageDependency { + topLevelPkg := p.GetPackage(&Locator{Name: "", Reference: ""}) + + if topLevelPkg != nil { + for _, dep := range topLevelPkg.PackageDependencies { + if dep.Ident == name { + return &dep + } + } + } + + for _, dep := range p.manifest.fallbackPool { + if dep[0] == name { + return &PackageDependency{ + Ident: dep[0], + Reference: dep[1], + AliasName: "", + } + } + } + + return nil +} + +func (p *PnpApi) ParseBareIdentifier(specifier string) (ident string, modulePath string, err error) { + if len(specifier) == 0 { + return "", "", fmt.Errorf("Empty specifier: %s", specifier) + } + + firstSlash := strings.Index(specifier, "/") + + if specifier[0] == '@' { + if firstSlash == -1 { + return "", "", fmt.Errorf("Invalid specifier: %s", specifier) + } + + secondSlash := strings.Index(specifier[firstSlash+1:], "/") + + if secondSlash == -1 { + ident = specifier + } else { + ident = specifier[:firstSlash+1+secondSlash] + } + } else { + firstSlash := strings.Index(specifier, "/") + + if firstSlash == -1 { + ident = specifier + } else { + ident = specifier[:firstSlash] + } + } + + modulePath = specifier[len(ident):] + + return ident, modulePath, nil +} + +func (p *PnpApi) GetPnpTypeRoots(currentDirectory string) []string { + if p.manifest == nil { + return []string{} + } + + currentDirectory = tspath.NormalizePath(currentDirectory) + + currentPackage, err := p.FindLocator(currentDirectory) + if err != nil { + return []string{} + } + + if currentPackage == nil { + return []string{} + } + + packageDependencies := p.GetPackage(currentPackage).PackageDependencies + + typeRoots := []string{} + for _, dep := range packageDependencies { + if strings.HasPrefix(dep.Ident, "@types/") && dep.Reference != "" { + packageInfo := p.GetPackage(&Locator{Name: dep.Ident, Reference: dep.Reference}) + typeRoots = append(typeRoots, tspath.GetDirectoryPath( + tspath.CombinePaths(p.manifest.dirPath, packageInfo.PackageLocation), + )) + } + } + + return typeRoots +} + +func (p *PnpApi) IsImportable(fromFileName string, toFileName string) bool { + fromLocator, errFromLocator := p.FindLocator(fromFileName) + toLocator, errToLocator := p.FindLocator(toFileName) + + if fromLocator == nil || toLocator == nil || errFromLocator != nil || errToLocator != nil { + return false + } + + fromInfo := p.GetPackage(fromLocator) + for _, dep := range fromInfo.PackageDependencies { + if dep.Reference == toLocator.Reference { + if dep.IsAlias() && dep.AliasName == toLocator.Name { + return true + } + + if dep.Ident == toLocator.Name { + return true + } + } + } + + return false +} + +func (p *PnpApi) GetPackageLocationAbsolutePath(packageInfo *PackageInfo) string { + if packageInfo == nil { + return "" + } + + packageLocation := packageInfo.PackageLocation + return tspath.CombinePaths(p.manifest.dirPath, packageLocation) +} diff --git a/internal/testutil/harnessutil/harnessutil.go b/internal/testutil/harnessutil/harnessutil.go index 772eb169c0..a842ac091b 100644 --- a/internal/testutil/harnessutil/harnessutil.go +++ b/internal/testutil/harnessutil/harnessutil.go @@ -23,6 +23,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/outputpaths" "github.com/microsoft/typescript-go/internal/parser" + "github.com/microsoft/typescript-go/internal/pnp" "github.com/microsoft/typescript-go/internal/repo" "github.com/microsoft/typescript-go/internal/sourcemap" "github.com/microsoft/typescript-go/internal/testutil" @@ -208,6 +209,11 @@ func CompileFilesEx( fs = bundled.WrapFS(fs) fs = NewOutputRecorderFS(fs) + manifestData, _ := fs.ReadFile("/.pnp.data.json") + // Instantiate a unique PnP API per goroutine, or disable PnP if no manifestData found + pnp.OverridePnpApi(fs, manifestData) + defer pnp.ClearTestPnpCache() + host := createCompilerHost(fs, bundled.LibPath(), currentDirectory) var configFile *tsoptions.TsConfigSourceFile var errors []*ast.Diagnostic diff --git a/internal/tspath/path.go b/internal/tspath/path.go index fae3423721..468d3468ca 100644 --- a/internal/tspath/path.go +++ b/internal/tspath/path.go @@ -869,6 +869,17 @@ func IsExternalModuleNameRelative(moduleName string) bool { return PathIsRelative(moduleName) || IsRootedDiskPath(moduleName) } +func IsExternalLibraryImport(path string) bool { + // When PnP is enabled, some internal libraries can be resolved as virtual packages, which should be treated as external libraries + // Since these virtual pnp packages don't have a `/node_modules/` folder, we need to check for the presence of `/__virtual__/` in the path + // See https://yarnpkg.com/advanced/lexicon#virtual-package for more details + return strings.Contains(path, "/node_modules/") || isPnpVirtualPath(path) +} + +func isPnpVirtualPath(path string) bool { + return strings.Contains(path, "/__virtual__/") +} + type ComparePathsOptions struct { UseCaseSensitiveFileNames bool CurrentDirectory string @@ -1018,6 +1029,10 @@ func HasExtension(fileName string) bool { return strings.Contains(GetBaseFileName(fileName), ".") } +func IsZipPath(path string) bool { + return strings.Contains(path, ".zip/") || strings.HasSuffix(path, ".zip") +} + func SplitVolumePath(path string) (volume string, rest string, ok bool) { if len(path) >= 2 && IsVolumeCharacter(path[0]) && path[1] == ':' { return strings.ToLower(path[0:2]), path[2:], true diff --git a/internal/vfs/pnpvfs/pnpvfs.go b/internal/vfs/pnpvfs/pnpvfs.go new file mode 100644 index 0000000000..d88894a21f --- /dev/null +++ b/internal/vfs/pnpvfs/pnpvfs.go @@ -0,0 +1,235 @@ +package pnpvfs + +import ( + "archive/zip" + "path" + "strconv" + "strings" + "sync" + "time" + + "github.com/microsoft/typescript-go/internal/tspath" + "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/iovfs" +) + +type pnpFS struct { + fs vfs.FS + cachedZipReadersMap map[string]*zip.ReadCloser + cacheReaderMutex sync.Mutex +} + +var _ vfs.FS = (*pnpFS)(nil) + +func From(fs vfs.FS) *pnpFS { + pnpFS := &pnpFS{ + fs: fs, + cachedZipReadersMap: make(map[string]*zip.ReadCloser), + } + + return pnpFS +} + +func (pnpFS *pnpFS) DirectoryExists(path string) bool { + path, _, _ = resolveVirtual(path) + + if strings.HasSuffix(path, ".zip") { + return pnpFS.fs.FileExists(path) + } + + fs, formattedPath, _ := getMatchingFS(pnpFS, path) + + return fs.DirectoryExists(formattedPath) +} + +func (pnpFS *pnpFS) FileExists(path string) bool { + path, _, _ = resolveVirtual(path) + + if strings.HasSuffix(path, ".zip") { + return pnpFS.fs.FileExists(path) + } + + fs, formattedPath, _ := getMatchingFS(pnpFS, path) + return fs.FileExists(formattedPath) +} + +func (pnpFS *pnpFS) GetAccessibleEntries(path string) vfs.Entries { + path, hash, basePath := resolveVirtual(path) + + fs, formattedPath, zipPath := getMatchingFS(pnpFS, path) + entries := fs.GetAccessibleEntries(formattedPath) + + for i, dir := range entries.Directories { + fullPath := tspath.CombinePaths(zipPath+formattedPath, dir) + entries.Directories[i] = makeVirtualPath(basePath, hash, fullPath) + } + + for i, file := range entries.Files { + fullPath := tspath.CombinePaths(zipPath+formattedPath, file) + entries.Files[i] = makeVirtualPath(basePath, hash, fullPath) + } + + return entries +} + +func (pnpFS *pnpFS) ReadFile(path string) (contents string, ok bool) { + path, _, _ = resolveVirtual(path) + + fs, formattedPath, _ := getMatchingFS(pnpFS, path) + return fs.ReadFile(formattedPath) +} + +func (pnpFS *pnpFS) Chtimes(path string, mtime time.Time, atime time.Time) error { + path, _, _ = resolveVirtual(path) + + fs, formattedPath, _ := getMatchingFS(pnpFS, path) + return fs.Chtimes(formattedPath, mtime, atime) +} + +func (pnpFS *pnpFS) Realpath(path string) string { + path, hash, basePath := resolveVirtual(path) + + fs, formattedPath, zipPath := getMatchingFS(pnpFS, path) + fullPath := zipPath + fs.Realpath(formattedPath) + return makeVirtualPath(basePath, hash, fullPath) +} + +func (pnpFS *pnpFS) Remove(path string) error { + path, _, _ = resolveVirtual(path) + + fs, formattedPath, _ := getMatchingFS(pnpFS, path) + return fs.Remove(formattedPath) +} + +func (pnpFS *pnpFS) Stat(path string) vfs.FileInfo { + path, _, _ = resolveVirtual(path) + + fs, formattedPath, _ := getMatchingFS(pnpFS, path) + return fs.Stat(formattedPath) +} + +func (pnpFS *pnpFS) UseCaseSensitiveFileNames() bool { + // pnp fs is always case sensitive + return true +} + +func (pnpFS *pnpFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { + root, hash, basePath := resolveVirtual(root) + + fs, formattedPath, zipPath := getMatchingFS(pnpFS, root) + return fs.WalkDir(formattedPath, (func(path string, d vfs.DirEntry, err error) error { + fullPath := zipPath + path + return walkFn(makeVirtualPath(basePath, hash, fullPath), d, err) + })) +} + +func (pnpFS *pnpFS) WriteFile(path string, data string, writeByteOrderMark bool) error { + path, _, _ = resolveVirtual(path) + + fs, formattedPath, zipPath := getMatchingFS(pnpFS, path) + if zipPath != "" { + panic("cannot write to zip file") + } + + return fs.WriteFile(formattedPath, data, writeByteOrderMark) +} + +func splitZipPath(path string) (string, string) { + parts := strings.Split(path, ".zip/") + if len(parts) < 2 { + return path, "/" + } + return parts[0] + ".zip", "/" + parts[1] +} + +func getMatchingFS(pnpFS *pnpFS, path string) (vfs.FS, string, string) { + if !tspath.IsZipPath(path) { + return pnpFS.fs, path, "" + } + + zipPath, internalPath := splitZipPath(path) + + zipStat := pnpFS.fs.Stat(zipPath) + if zipStat == nil { + return pnpFS.fs, path, "" + } + + var usedReader *zip.ReadCloser + + pnpFS.cacheReaderMutex.Lock() + defer pnpFS.cacheReaderMutex.Unlock() + + 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 + } + + return iovfs.From(usedReader, pnpFS.fs.UseCaseSensitiveFileNames()), internalPath, zipPath +} + +// Virtual paths are used to make different paths resolve to the same real file or folder, which is necessary in some cases when PnP is enabled +// See https://yarnpkg.com/advanced/lexicon#virtual-package and https://yarnpkg.com/advanced/pnpapi#resolvevirtual for more details +func resolveVirtual(path string) (realPath string, hash string, basePath string) { + idx := strings.Index(path, "/__virtual__/") + if idx == -1 { + return path, "", "" + } + + base := path[:idx] + rest := path[idx+len("/__virtual__/"):] + parts := strings.SplitN(rest, "/", 3) + if len(parts) < 3 { + // Not enough parts to match the pattern, return as is + return path, "", "" + } + hash = parts[0] + subpath := parts[2] + depth, err := strconv.Atoi(parts[1]) + if err != nil || depth < 0 { + // Invalid n, return as is + return path, "", "" + } + + basePath = path[:idx] + "/__virtual__" + + // Apply dirname n times to base + for range depth { + base = tspath.GetDirectoryPath(base) + } + // Join base and subpath + if base == "/" { + return "/" + subpath, hash, basePath + } + + return tspath.CombinePaths(base, subpath), hash, basePath +} + +func makeVirtualPath(basePath string, hash string, targetPath string) string { + if basePath == "" || hash == "" { + return targetPath + } + + relativePath := tspath.GetRelativePathFromDirectory( + tspath.GetDirectoryPath(basePath), + targetPath, + tspath.ComparePathsOptions{UseCaseSensitiveFileNames: true}) + + segments := strings.Split(relativePath, "/") + + depth := 0 + for depth < len(segments) && segments[depth] == ".." { + depth++ + } + + subPath := strings.Join(segments[depth:], "/") + + return path.Join(basePath, hash, strconv.Itoa(depth), subPath) +} diff --git a/internal/vfs/pnpvfs/pnpvfs_test.go b/internal/vfs/pnpvfs/pnpvfs_test.go new file mode 100644 index 0000000000..f5d12fa775 --- /dev/null +++ b/internal/vfs/pnpvfs/pnpvfs_test.go @@ -0,0 +1,296 @@ +package pnpvfs_test + +import ( + "archive/zip" + "fmt" + "os" + "strings" + "testing" + + "github.com/microsoft/typescript-go/internal/testutil" + "github.com/microsoft/typescript-go/internal/tspath" + "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/osvfs" + "github.com/microsoft/typescript-go/internal/vfs/pnpvfs" + "github.com/microsoft/typescript-go/internal/vfs/vfstest" + "gotest.tools/v3/assert" +) + +func createTestZip(t *testing.T, files map[string]string) string { + t.Helper() + + tmpDir := t.TempDir() + zipPath := tspath.CombinePaths(tmpDir, "test.zip") + + file, err := os.Create(zipPath) + assert.NilError(t, err) + defer file.Close() + + w := zip.NewWriter(file) + defer w.Close() + + for name, content := range files { + f, err := w.Create(name) + assert.NilError(t, err) + _, err = f.Write([]byte(content)) + assert.NilError(t, err) + } + + return zipPath +} + +func TestPnpVfs_BasicFileOperations(t *testing.T) { + t.Parallel() + + underlyingFS := vfstest.FromMap(map[string]string{ + "/project/src/index.ts": "export const hello = 'world';", + "/project/package.json": `{"name": "test"}`, + }, true) + + fs := pnpvfs.From(underlyingFS) + assert.Assert(t, fs.FileExists("/project/src/index.ts")) + assert.Assert(t, !fs.FileExists("/project/nonexistent.ts")) + + content, ok := fs.ReadFile("/project/src/index.ts") + assert.Assert(t, ok) + assert.Equal(t, "export const hello = 'world';", content) + + assert.Assert(t, fs.DirectoryExists("/project/src")) + assert.Assert(t, !fs.DirectoryExists("/project/nonexistent")) + + var files []string + err := fs.WalkDir("/", func(path string, d vfs.DirEntry, err error) error { + if !d.IsDir() { + files = append(files, path) + } + return nil + }) + + assert.NilError(t, err) + assert.DeepEqual(t, files, []string{"/project/package.json", "/project/src/index.ts"}) + + err = fs.WriteFile("/project/src/index.ts", "export const hello = 'world2';", false) + assert.NilError(t, err) + + content, ok = fs.ReadFile("/project/src/index.ts") + assert.Assert(t, ok) + assert.Equal(t, "export const hello = 'world2';", content) +} + +func TestPnpVfs_ZipFileDetection(t *testing.T) { + t.Parallel() + + zipFiles := map[string]string{ + "src/index.ts": "export const hello = 'world';", + "package.json": `{"name": "test-project"}`, + } + + zipPath := createTestZip(t, zipFiles) + + underlyingFS := vfstest.FromMap(map[string]string{ + zipPath: "zip content placeholder", + }, true) + + fs := pnpvfs.From(underlyingFS) + + fmt.Println(zipPath) + assert.Assert(t, fs.FileExists(zipPath)) + + zipInternalPath := zipPath + "/src/index.ts" + assert.Assert(t, fs.FileExists(zipInternalPath)) + + content, ok := fs.ReadFile(zipInternalPath) + assert.Assert(t, ok) + assert.Equal(t, content, zipFiles["src/index.ts"]) +} + +func TestPnpVfs_ErrorHandling(t *testing.T) { + t.Parallel() + + fs := pnpvfs.From(osvfs.FS()) + + t.Run("NonexistentZipFile", func(t *testing.T) { + t.Parallel() + + result := fs.FileExists("/nonexistent/path/archive.zip/file.txt") + assert.Assert(t, !result) + + _, ok := fs.ReadFile("/nonexistent/archive.zip/file.txt") + assert.Assert(t, !ok) + }) + + t.Run("InvalidZipFile", func(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + fakePath := tspath.CombinePaths(tmpDir, "fake.zip") + err := os.WriteFile(fakePath, []byte("not a zip file"), 0o644) + assert.NilError(t, err) + + result := fs.FileExists(fakePath + "/file.txt") + assert.Assert(t, !result) + }) + + t.Run("WriteToZipFile", func(t *testing.T) { + t.Parallel() + + zipFiles := map[string]string{ + "src/index.ts": "export const hello = 'world';", + } + zipPath := createTestZip(t, zipFiles) + + testutil.AssertPanics(t, func() { + _ = fs.WriteFile(zipPath+"/src/index.ts", "hello, world", false) + }, "cannot write to zip file") + }) +} + +func TestPnpVfs_CaseSensitivity(t *testing.T) { + t.Parallel() + + sensitiveFS := pnpvfs.From(vfstest.FromMap(map[string]string{}, true)) + assert.Assert(t, sensitiveFS.UseCaseSensitiveFileNames()) + insensitiveFS := pnpvfs.From(vfstest.FromMap(map[string]string{}, false)) + // pnpvfs is always case sensitive + assert.Assert(t, insensitiveFS.UseCaseSensitiveFileNames()) +} + +func TestPnpVfs_FallbackToRegularFiles(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + regularFile := tspath.CombinePaths(tmpDir, "regular.ts") + err := os.WriteFile(regularFile, []byte("regular content"), 0o644) + assert.NilError(t, err) + + fs := pnpvfs.From(osvfs.FS()) + + assert.Assert(t, fs.FileExists(regularFile)) + + content, ok := fs.ReadFile(regularFile) + assert.Assert(t, ok) + assert.Equal(t, "regular content", content) + assert.Assert(t, fs.DirectoryExists(tmpDir)) +} + +func TestZipPath_Detection(t *testing.T) { + t.Parallel() + + testCases := []struct { + path string + shouldBeZip bool + }{ + {"/normal/path/file.txt", false}, + {"/path/to/archive.zip", true}, + {"/path/to/archive.zip/internal/file.txt", true}, + {"/path/archive.zip/nested/dir/file.ts", true}, + {"/path/file.zip.txt", false}, + {"/absolute/archive.zip", true}, + {"/absolute/archive.zip/file.txt", true}, + } + + for _, tc := range testCases { + t.Run(tc.path, func(t *testing.T) { + t.Parallel() + assert.Assert(t, tspath.IsZipPath(tc.path) == tc.shouldBeZip) + }) + } +} + +func TestPnpVfs_VirtualPathHandling(t *testing.T) { + t.Parallel() + + underlyingFS := vfstest.FromMap(map[string]string{ + "/project/packages/packageA/indexA.ts": "export const helloA = 'world';", + "/project/packages/packageA/package.json": `{"name": "packageA"}`, + "/project/packages/packageB/indexB.ts": "export const helloB = 'world';", + "/project/packages/packageB/package.json": `{"name": "packageB"}`, + }, true) + + fs := pnpvfs.From(underlyingFS) + assert.Assert(t, fs.FileExists("/project/packages/__virtual__/packageA-virtual-123456/0/packageA/package.json")) + assert.Assert(t, fs.FileExists("/project/packages/subfolder/__virtual__/packageA-virtual-123456/1/packageA/package.json")) + + content, ok := fs.ReadFile("/project/packages/__virtual__/packageB-virtual-123456/0/packageB/package.json") + assert.Assert(t, ok) + assert.Equal(t, `{"name": "packageB"}`, content) + + assert.Assert(t, fs.DirectoryExists("/project/packages/__virtual__/packageB-virtual-123456/0/packageB")) + assert.Assert(t, !fs.DirectoryExists("/project/packages/__virtual__/packageB-virtual-123456/0/nonexistent")) + + entries := fs.GetAccessibleEntries("/project/packages/__virtual__/packageB-virtual-123456/0/packageB") + assert.DeepEqual(t, entries.Files, []string{ + "/project/packages/__virtual__/packageB-virtual-123456/0/packageB/indexB.ts", + "/project/packages/__virtual__/packageB-virtual-123456/0/packageB/package.json", + }) + assert.DeepEqual(t, entries.Directories, []string(nil)) + + files := []string{} + err := fs.WalkDir("/project/packages/__virtual__/packageB-virtual-123456/0/packageB", func(path string, d vfs.DirEntry, err error) error { + if !d.IsDir() { + files = append(files, path) + } + return nil + }) + assert.NilError(t, err) + assert.DeepEqual(t, files, []string{ + "/project/packages/__virtual__/packageB-virtual-123456/0/packageB/indexB.ts", + "/project/packages/__virtual__/packageB-virtual-123456/0/packageB/package.json", + }) +} + +func TestPnpVfs_RealZipIntegration(t *testing.T) { + t.Parallel() + + zipFiles := map[string]string{ + "src/index.ts": "export const hello = 'world';", + "src/utils/helpers.ts": "export function add(a: number, b: number) { return a + b; }", + "package.json": `{"name": "test-project", "version": "1.0.0"}`, + "tsconfig.json": `{"compilerOptions": {"target": "es2020"}}`, + } + + zipPath := createTestZip(t, zipFiles) + fs := pnpvfs.From(osvfs.FS()) + + assert.Assert(t, fs.FileExists(zipPath)) + + indexPath := zipPath + "/src/index.ts" + packagePath := zipPath + "/package.json" + assert.Assert(t, fs.FileExists(indexPath)) + assert.Assert(t, fs.FileExists(packagePath)) + assert.Assert(t, fs.DirectoryExists(zipPath+"/src")) + + content, ok := fs.ReadFile(indexPath) + assert.Assert(t, ok) + assert.Equal(t, content, zipFiles["src/index.ts"]) + + content, ok = fs.ReadFile(packagePath) + assert.Assert(t, ok) + assert.Equal(t, content, zipFiles["package.json"]) + + entries := fs.GetAccessibleEntries(zipPath) + assert.DeepEqual(t, entries.Files, []string{zipPath + "/package.json", zipPath + "/tsconfig.json"}) + assert.DeepEqual(t, entries.Directories, []string{zipPath + "/src"}) + + entries = fs.GetAccessibleEntries(zipPath + "/src") + assert.DeepEqual(t, entries.Files, []string{zipPath + "/src/index.ts"}) + assert.DeepEqual(t, entries.Directories, []string{zipPath + "/src/utils"}) + + assert.Equal(t, fs.Realpath(indexPath), indexPath) + + files := []string{} + err := fs.WalkDir(zipPath, func(path string, d vfs.DirEntry, err error) error { + if !d.IsDir() { + files = append(files, path) + } + return nil + }) + assert.NilError(t, err) + assert.DeepEqual(t, files, []string{zipPath + "/package.json", zipPath + "/src/index.ts", zipPath + "/src/utils/helpers.ts", zipPath + "/tsconfig.json"}) + + assert.Assert(t, fs.FileExists(zipPath+"/src/__virtual__/src-virtual-123456/0/index.ts")) + + splitZipPath := strings.Split(zipPath, "/") + beforeZipVirtualPath := strings.Join(splitZipPath[0:len(splitZipPath)-2], "/") + "/__virtual__/zip-virtual-123456/0/" + strings.Join(splitZipPath[len(splitZipPath)-2:], "/") + "/src/index.ts" + assert.Assert(t, fs.FileExists(beforeZipVirtualPath)) +} diff --git a/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.js b/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.js new file mode 100644 index 0000000000..3c97ccfecf --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.js @@ -0,0 +1,117 @@ +//// [tests/cases/compiler/pnpDeclarationEmitWorkspace.ts] //// + +//// [.pnp.cjs] +module.exports = {}; + +//// [.pnp.data.json] +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "workspace:packages/package-a"] + ] + }] + ]], + ["package-a", [ + ["workspace:packages/package-a", { + "packageLocation": "./packages/package-a/", + "packageDependencies": [] + }] + ]] + ] +} + +//// [package.json] +{ + "name": "project", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "package-a": "workspace:*" + } +} + +//// [package.json] +{ + "name": "package-a", + "exports": { + "./other-subpath": { + "types": "./index.d.ts", + "default": "./index.ts" + } + }, + "dependencies": { + "package-b": "workspace:*" + } +} + +//// [index.d.ts] +export interface BaseConfig { + timeout: number; + retries: number; +} + +export interface DataOptions { + format: "json" | "xml"; + encoding: string; +} + +export interface ServiceConfig extends BaseConfig { + endpoint: string; + options: DataOptions; +} + +export type ConfigFactory = (endpoint: string) => ServiceConfig; + +export declare function createServiceConfig(endpoint: string): ServiceConfig; + +//// [index.ts] +import type { ServiceConfig, ConfigFactory } from 'package-a/other-subpath'; +import { createServiceConfig } from 'package-a/other-subpath'; + +export function initializeService(url: string): ServiceConfig { + return createServiceConfig(url); +} + +export const factory = createServiceConfig; + +export interface AppConfig { + service: ServiceConfig; + debug: boolean; +} + + +//// [index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.factory = void 0; +exports.initializeService = initializeService; +const other_subpath_1 = require("package-a/other-subpath"); +function initializeService(url) { + return (0, other_subpath_1.createServiceConfig)(url); +} +exports.factory = other_subpath_1.createServiceConfig; + + +//// [index.d.ts] +import type { ServiceConfig } from 'package-a/other-subpath'; +import { createServiceConfig } from 'package-a/other-subpath'; +export declare function initializeService(url: string): ServiceConfig; +export declare const factory: typeof createServiceConfig; +export interface AppConfig { + service: ServiceConfig; + debug: boolean; +} diff --git a/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.symbols b/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.symbols new file mode 100644 index 0000000000..c99e148d54 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.symbols @@ -0,0 +1,78 @@ +//// [tests/cases/compiler/pnpDeclarationEmitWorkspace.ts] //// + +=== /src/index.ts === +import type { ServiceConfig, ConfigFactory } from 'package-a/other-subpath'; +>ServiceConfig : Symbol(ServiceConfig, Decl(index.ts, 0, 13)) +>ConfigFactory : Symbol(ConfigFactory, Decl(index.ts, 0, 28)) + +import { createServiceConfig } from 'package-a/other-subpath'; +>createServiceConfig : Symbol(createServiceConfig, Decl(index.ts, 1, 8)) + +export function initializeService(url: string): ServiceConfig { +>initializeService : Symbol(initializeService, Decl(index.ts, 1, 62)) +>url : Symbol(url, Decl(index.ts, 3, 34)) +>ServiceConfig : Symbol(ServiceConfig, Decl(index.ts, 0, 13)) + + return createServiceConfig(url); +>createServiceConfig : Symbol(createServiceConfig, Decl(index.ts, 1, 8)) +>url : Symbol(url, Decl(index.ts, 3, 34)) +} + +export const factory = createServiceConfig; +>factory : Symbol(factory, Decl(index.ts, 7, 12)) +>createServiceConfig : Symbol(createServiceConfig, Decl(index.ts, 1, 8)) + +export interface AppConfig { +>AppConfig : Symbol(AppConfig, Decl(index.ts, 7, 43)) + + service: ServiceConfig; +>service : Symbol(AppConfig.service, Decl(index.ts, 9, 28)) +>ServiceConfig : Symbol(ServiceConfig, Decl(index.ts, 0, 13)) + + debug: boolean; +>debug : Symbol(AppConfig.debug, Decl(index.ts, 10, 25)) +} + +=== /packages/package-a/index.d.ts === +export interface BaseConfig { +>BaseConfig : Symbol(BaseConfig, Decl(index.d.ts, 0, 0)) + + timeout: number; +>timeout : Symbol(BaseConfig.timeout, Decl(index.d.ts, 0, 29)) + + retries: number; +>retries : Symbol(BaseConfig.retries, Decl(index.d.ts, 1, 18)) +} + +export interface DataOptions { +>DataOptions : Symbol(DataOptions, Decl(index.d.ts, 3, 1)) + + format: "json" | "xml"; +>format : Symbol(DataOptions.format, Decl(index.d.ts, 5, 30)) + + encoding: string; +>encoding : Symbol(DataOptions.encoding, Decl(index.d.ts, 6, 25)) +} + +export interface ServiceConfig extends BaseConfig { +>ServiceConfig : Symbol(ServiceConfig, Decl(index.d.ts, 8, 1)) +>BaseConfig : Symbol(BaseConfig, Decl(index.d.ts, 0, 0)) + + endpoint: string; +>endpoint : Symbol(ServiceConfig.endpoint, Decl(index.d.ts, 10, 51)) + + options: DataOptions; +>options : Symbol(ServiceConfig.options, Decl(index.d.ts, 11, 19)) +>DataOptions : Symbol(DataOptions, Decl(index.d.ts, 3, 1)) +} + +export type ConfigFactory = (endpoint: string) => ServiceConfig; +>ConfigFactory : Symbol(ConfigFactory, Decl(index.d.ts, 13, 1)) +>endpoint : Symbol(endpoint, Decl(index.d.ts, 15, 29)) +>ServiceConfig : Symbol(ServiceConfig, Decl(index.d.ts, 8, 1)) + +export declare function createServiceConfig(endpoint: string): ServiceConfig; +>createServiceConfig : Symbol(createServiceConfig, Decl(index.d.ts, 15, 64)) +>endpoint : Symbol(endpoint, Decl(index.d.ts, 17, 44)) +>ServiceConfig : Symbol(ServiceConfig, Decl(index.d.ts, 8, 1)) + diff --git a/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.types b/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.types new file mode 100644 index 0000000000..9e51fc8ba3 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpDeclarationEmitWorkspace.types @@ -0,0 +1,65 @@ +//// [tests/cases/compiler/pnpDeclarationEmitWorkspace.ts] //// + +=== /src/index.ts === +import type { ServiceConfig, ConfigFactory } from 'package-a/other-subpath'; +>ServiceConfig : ServiceConfig +>ConfigFactory : ConfigFactory + +import { createServiceConfig } from 'package-a/other-subpath'; +>createServiceConfig : (endpoint: string) => ServiceConfig + +export function initializeService(url: string): ServiceConfig { +>initializeService : (url: string) => ServiceConfig +>url : string + + return createServiceConfig(url); +>createServiceConfig(url) : ServiceConfig +>createServiceConfig : (endpoint: string) => ServiceConfig +>url : string +} + +export const factory = createServiceConfig; +>factory : (endpoint: string) => ServiceConfig +>createServiceConfig : (endpoint: string) => ServiceConfig + +export interface AppConfig { + service: ServiceConfig; +>service : ServiceConfig + + debug: boolean; +>debug : boolean +} + +=== /packages/package-a/index.d.ts === +export interface BaseConfig { + timeout: number; +>timeout : number + + retries: number; +>retries : number +} + +export interface DataOptions { + format: "json" | "xml"; +>format : "json" | "xml" + + encoding: string; +>encoding : string +} + +export interface ServiceConfig extends BaseConfig { + endpoint: string; +>endpoint : string + + options: DataOptions; +>options : DataOptions +} + +export type ConfigFactory = (endpoint: string) => ServiceConfig; +>ConfigFactory : ConfigFactory +>endpoint : string + +export declare function createServiceConfig(endpoint: string): ServiceConfig; +>createServiceConfig : (endpoint: string) => ServiceConfig +>endpoint : string + diff --git a/testdata/baselines/reference/compiler/pnpSimpleTest.js b/testdata/baselines/reference/compiler/pnpSimpleTest.js new file mode 100644 index 0000000000..4f17149ae5 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpSimpleTest.js @@ -0,0 +1,110 @@ +//// [tests/cases/compiler/pnpSimpleTest.ts] //// + +//// [.pnp.cjs] +module.exports = {}; + +//// [.pnp.data.json] +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "npm:1.0.0"], + ["package-b", "npm:2.0.0"] + ] + }] + ]], + ["package-a", [ + ["npm:1.0.0", { + "packageLocation": "./.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/", + "packageDependencies": [] + }] + ]], + ["package-b", [ + ["npm:2.0.0", { + "packageLocation": "./.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/", + "packageDependencies": [] + }] + ]] + ] +} + +//// [package.json] +{ + "name": "project", + "dependencies": { + "package-a": "npm:1.0.0", + "package-b": "npm:2.0.0" + } +} + +//// [package.json] +{ + "name": "package-a", + "version": "1.0.0", + "exports": { + ".": "./index.js" + }, + "types": "index.d.ts" +} + +//// [index.js] +exports.helperA = function(value) { + return "Helper A: " + value; +}; + +//// [index.d.ts] +export declare function helperA(value: string): string; + +//// [package.json] +{ + "name": "package-b", + "version": "2.0.0", + "exports": { + ".": "./index.js" + }, + "types": "index.d.ts" +} + +//// [index.js] +exports.helperB = function(value) { + return "Helper B: " + value; +}; + +//// [index.d.ts] +export declare function helperB(value: number): string; + +//// [index.ts] +// Workspace package that imports both third-party dependencies +import { helperA } from 'package-a'; +import { helperB } from 'package-b'; + +export function processData(text: string, num: number): string { + const resultA = helperA(text); + const resultB = helperB(num); + return `${resultA} | ${resultB}`; +} + +//// [index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.processData = processData; +// Workspace package that imports both third-party dependencies +const package_a_1 = require("package-a"); +const package_b_1 = require("package-b"); +function processData(text, num) { + const resultA = (0, package_a_1.helperA)(text); + const resultB = (0, package_b_1.helperB)(num); + return `${resultA} | ${resultB}`; +} diff --git a/testdata/baselines/reference/compiler/pnpSimpleTest.symbols b/testdata/baselines/reference/compiler/pnpSimpleTest.symbols new file mode 100644 index 0000000000..c2e6e68b83 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpSimpleTest.symbols @@ -0,0 +1,39 @@ +//// [tests/cases/compiler/pnpSimpleTest.ts] //// + +=== /.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/index.d.ts === +export declare function helperA(value: string): string; +>helperA : Symbol(helperA, Decl(index.d.ts, 0, 0)) +>value : Symbol(value, Decl(index.d.ts, 0, 32)) + +=== /.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/index.d.ts === +export declare function helperB(value: number): string; +>helperB : Symbol(helperB, Decl(index.d.ts, 0, 0)) +>value : Symbol(value, Decl(index.d.ts, 0, 32)) + +=== /src/index.ts === +// Workspace package that imports both third-party dependencies +import { helperA } from 'package-a'; +>helperA : Symbol(helperA, Decl(index.ts, 1, 8)) + +import { helperB } from 'package-b'; +>helperB : Symbol(helperB, Decl(index.ts, 2, 8)) + +export function processData(text: string, num: number): string { +>processData : Symbol(processData, Decl(index.ts, 2, 36)) +>text : Symbol(text, Decl(index.ts, 4, 28)) +>num : Symbol(num, Decl(index.ts, 4, 41)) + + const resultA = helperA(text); +>resultA : Symbol(resultA, Decl(index.ts, 5, 7)) +>helperA : Symbol(helperA, Decl(index.ts, 1, 8)) +>text : Symbol(text, Decl(index.ts, 4, 28)) + + const resultB = helperB(num); +>resultB : Symbol(resultB, Decl(index.ts, 6, 7)) +>helperB : Symbol(helperB, Decl(index.ts, 2, 8)) +>num : Symbol(num, Decl(index.ts, 4, 41)) + + return `${resultA} | ${resultB}`; +>resultA : Symbol(resultA, Decl(index.ts, 5, 7)) +>resultB : Symbol(resultB, Decl(index.ts, 6, 7)) +} diff --git a/testdata/baselines/reference/compiler/pnpSimpleTest.types b/testdata/baselines/reference/compiler/pnpSimpleTest.types new file mode 100644 index 0000000000..90ce12017b --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpSimpleTest.types @@ -0,0 +1,42 @@ +//// [tests/cases/compiler/pnpSimpleTest.ts] //// + +=== /.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/index.d.ts === +export declare function helperA(value: string): string; +>helperA : (value: string) => string +>value : string + +=== /.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/index.d.ts === +export declare function helperB(value: number): string; +>helperB : (value: number) => string +>value : number + +=== /src/index.ts === +// Workspace package that imports both third-party dependencies +import { helperA } from 'package-a'; +>helperA : (value: string) => string + +import { helperB } from 'package-b'; +>helperB : (value: number) => string + +export function processData(text: string, num: number): string { +>processData : (text: string, num: number) => string +>text : string +>num : number + + const resultA = helperA(text); +>resultA : string +>helperA(text) : string +>helperA : (value: string) => string +>text : string + + const resultB = helperB(num); +>resultB : string +>helperB(num) : string +>helperB : (value: number) => string +>num : number + + return `${resultA} | ${resultB}`; +>`${resultA} | ${resultB}` : string +>resultA : string +>resultB : string +} diff --git a/testdata/baselines/reference/compiler/pnpTransitiveDependencies.errors.txt b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.errors.txt new file mode 100644 index 0000000000..a3063b9048 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.errors.txt @@ -0,0 +1,121 @@ +/src/index.ts(5,36): error TS2307: Cannot find module 'package-b' or its corresponding type declarations. + + +==== /.pnp.cjs (0 errors) ==== + module.exports = {}; + +==== /.pnp.data.json (0 errors) ==== + { + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "workspace:packages/package-a"] + ] + }] + ]], + ["package-a", [ + ["workspace:packages/package-a", { + "packageLocation": "./packages/package-a/", + "packageDependencies": [ + ["package-b", "workspace:packages/package-b"] + ] + }] + ]], + ["package-b", [ + ["workspace:packages/package-b", { + "packageLocation": "./packages/package-b/", + "packageDependencies": [] + }] + ]] + ] + } + +==== /package.json (0 errors) ==== + { + "name": "project", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "package-a": "workspace:packages/package-a" + } + } + +==== /packages/package-a/package.json (0 errors) ==== + { + "name": "package-a", + "version": "1.0.0", + "exports": { + ".": "./index.ts" + }, + "dependencies": { + "package-b": "workspace:packages/package-b" + } + } + +==== /packages/package-a/index.ts (0 errors) ==== + import type { ConfigOptions } from 'package-b'; + + export interface HelperResult { + message: string; + config: ConfigOptions; + } + + export function helperA(value: string, config: ConfigOptions): HelperResult { + return { + message: "Helper A: " + value, + config: config + }; + } + +==== /packages/package-b/package.json (0 errors) ==== + { + "name": "package-b", + "version": "2.0.0", + "exports": { + ".": "./index.ts" + } + } + +==== /packages/package-b/index.ts (0 errors) ==== + export interface ConfigOptions { + enabled: boolean; + timeout: number; + } + + export function helperB(value: number): string { + return "Helper B: " + value; + } + +==== /src/index.ts (1 errors) ==== + // Test that the project can import package-a directly + // package-a's types depend on package-b's types (ConfigOptions) + import { helperA } from 'package-a'; + import type { HelperResult } from 'package-a'; + import type { ConfigOptions } from 'package-b'; // This should error - package-b is not a direct dependency + ~~~~~~~~~~~ +!!! error TS2307: Cannot find module 'package-b' or its corresponding type declarations. + + export function useDirectDependency(text: string): HelperResult { + const config: ConfigOptions = { enabled: true, timeout: 5000 }; + return helperA(text, config); + } + + // Test that the project CANNOT import package-b directly even though package-a uses it + // This should cause an error since package-b is not in project's dependencies + export function attemptDirectImport(): ConfigOptions { + return { enabled: false, timeout: 1000 }; + } + \ No newline at end of file diff --git a/testdata/baselines/reference/compiler/pnpTransitiveDependencies.js b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.js new file mode 100644 index 0000000000..b55c670226 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.js @@ -0,0 +1,153 @@ +//// [tests/cases/compiler/pnpTransitiveDependencies.ts] //// + +//// [.pnp.cjs] +module.exports = {}; + +//// [.pnp.data.json] +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "workspace:packages/package-a"] + ] + }] + ]], + ["package-a", [ + ["workspace:packages/package-a", { + "packageLocation": "./packages/package-a/", + "packageDependencies": [ + ["package-b", "workspace:packages/package-b"] + ] + }] + ]], + ["package-b", [ + ["workspace:packages/package-b", { + "packageLocation": "./packages/package-b/", + "packageDependencies": [] + }] + ]] + ] +} + +//// [package.json] +{ + "name": "project", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "package-a": "workspace:packages/package-a" + } +} + +//// [package.json] +{ + "name": "package-a", + "version": "1.0.0", + "exports": { + ".": "./index.ts" + }, + "dependencies": { + "package-b": "workspace:packages/package-b" + } +} + +//// [index.ts] +import type { ConfigOptions } from 'package-b'; + +export interface HelperResult { + message: string; + config: ConfigOptions; +} + +export function helperA(value: string, config: ConfigOptions): HelperResult { + return { + message: "Helper A: " + value, + config: config + }; +} + +//// [package.json] +{ + "name": "package-b", + "version": "2.0.0", + "exports": { + ".": "./index.ts" + } +} + +//// [index.ts] +export interface ConfigOptions { + enabled: boolean; + timeout: number; +} + +export function helperB(value: number): string { + return "Helper B: " + value; +} + +//// [index.ts] +// Test that the project can import package-a directly +// package-a's types depend on package-b's types (ConfigOptions) +import { helperA } from 'package-a'; +import type { HelperResult } from 'package-a'; +import type { ConfigOptions } from 'package-b'; // This should error - package-b is not a direct dependency + +export function useDirectDependency(text: string): HelperResult { + const config: ConfigOptions = { enabled: true, timeout: 5000 }; + return helperA(text, config); +} + +// Test that the project CANNOT import package-b directly even though package-a uses it +// This should cause an error since package-b is not in project's dependencies +export function attemptDirectImport(): ConfigOptions { + return { enabled: false, timeout: 1000 }; +} + + +//// [index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.helperB = helperB; +function helperB(value) { + return "Helper B: " + value; +} +//// [index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.helperA = helperA; +function helperA(value, config) { + return { + message: "Helper A: " + value, + config: config + }; +} +//// [index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.useDirectDependency = useDirectDependency; +exports.attemptDirectImport = attemptDirectImport; +// Test that the project can import package-a directly +// package-a's types depend on package-b's types (ConfigOptions) +const package_a_1 = require("package-a"); +function useDirectDependency(text) { + const config = { enabled: true, timeout: 5000 }; + return (0, package_a_1.helperA)(text, config); +} +// Test that the project CANNOT import package-b directly even though package-a uses it +// This should cause an error since package-b is not in project's dependencies +function attemptDirectImport() { + return { enabled: false, timeout: 1000 }; +} diff --git a/testdata/baselines/reference/compiler/pnpTransitiveDependencies.symbols b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.symbols new file mode 100644 index 0000000000..752518b2eb --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.symbols @@ -0,0 +1,95 @@ +//// [tests/cases/compiler/pnpTransitiveDependencies.ts] //// + +=== /packages/package-a/index.ts === +import type { ConfigOptions } from 'package-b'; +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 0, 13)) + +export interface HelperResult { +>HelperResult : Symbol(HelperResult, Decl(index.ts, 0, 47)) + + message: string; +>message : Symbol(HelperResult.message, Decl(index.ts, 2, 31)) + + config: ConfigOptions; +>config : Symbol(HelperResult.config, Decl(index.ts, 3, 18)) +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 0, 13)) +} + +export function helperA(value: string, config: ConfigOptions): HelperResult { +>helperA : Symbol(helperA, Decl(index.ts, 5, 1)) +>value : Symbol(value, Decl(index.ts, 7, 24)) +>config : Symbol(config, Decl(index.ts, 7, 38)) +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 0, 13)) +>HelperResult : Symbol(HelperResult, Decl(index.ts, 0, 47)) + + return { + message: "Helper A: " + value, +>message : Symbol(message, Decl(index.ts, 8, 10)) +>value : Symbol(value, Decl(index.ts, 7, 24)) + + config: config +>config : Symbol(config, Decl(index.ts, 9, 34)) +>config : Symbol(config, Decl(index.ts, 7, 38)) + + }; +} + +=== /packages/package-b/index.ts === +export interface ConfigOptions { +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 0, 0)) + + enabled: boolean; +>enabled : Symbol(ConfigOptions.enabled, Decl(index.ts, 0, 32)) + + timeout: number; +>timeout : Symbol(ConfigOptions.timeout, Decl(index.ts, 1, 19)) +} + +export function helperB(value: number): string { +>helperB : Symbol(helperB, Decl(index.ts, 3, 1)) +>value : Symbol(value, Decl(index.ts, 5, 24)) + + return "Helper B: " + value; +>value : Symbol(value, Decl(index.ts, 5, 24)) +} + +=== /src/index.ts === +// Test that the project can import package-a directly +// package-a's types depend on package-b's types (ConfigOptions) +import { helperA } from 'package-a'; +>helperA : Symbol(helperA, Decl(index.ts, 2, 8)) + +import type { HelperResult } from 'package-a'; +>HelperResult : Symbol(HelperResult, Decl(index.ts, 3, 13)) + +import type { ConfigOptions } from 'package-b'; // This should error - package-b is not a direct dependency +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 4, 13)) + +export function useDirectDependency(text: string): HelperResult { +>useDirectDependency : Symbol(useDirectDependency, Decl(index.ts, 4, 47)) +>text : Symbol(text, Decl(index.ts, 6, 36)) +>HelperResult : Symbol(HelperResult, Decl(index.ts, 3, 13)) + + const config: ConfigOptions = { enabled: true, timeout: 5000 }; +>config : Symbol(config, Decl(index.ts, 7, 7)) +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 4, 13)) +>enabled : Symbol(enabled, Decl(index.ts, 7, 33)) +>timeout : Symbol(timeout, Decl(index.ts, 7, 48)) + + return helperA(text, config); +>helperA : Symbol(helperA, Decl(index.ts, 2, 8)) +>text : Symbol(text, Decl(index.ts, 6, 36)) +>config : Symbol(config, Decl(index.ts, 7, 7)) +} + +// Test that the project CANNOT import package-b directly even though package-a uses it +// This should cause an error since package-b is not in project's dependencies +export function attemptDirectImport(): ConfigOptions { +>attemptDirectImport : Symbol(attemptDirectImport, Decl(index.ts, 9, 1)) +>ConfigOptions : Symbol(ConfigOptions, Decl(index.ts, 4, 13)) + + return { enabled: false, timeout: 1000 }; +>enabled : Symbol(enabled, Decl(index.ts, 14, 10)) +>timeout : Symbol(timeout, Decl(index.ts, 14, 26)) +} + diff --git a/testdata/baselines/reference/compiler/pnpTransitiveDependencies.types b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.types new file mode 100644 index 0000000000..67a56d6ef3 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTransitiveDependencies.types @@ -0,0 +1,98 @@ +//// [tests/cases/compiler/pnpTransitiveDependencies.ts] //// + +=== /packages/package-a/index.ts === +import type { ConfigOptions } from 'package-b'; +>ConfigOptions : ConfigOptions + +export interface HelperResult { + message: string; +>message : string + + config: ConfigOptions; +>config : ConfigOptions +} + +export function helperA(value: string, config: ConfigOptions): HelperResult { +>helperA : (value: string, config: ConfigOptions) => HelperResult +>value : string +>config : ConfigOptions + + return { +>{ message: "Helper A: " + value, config: config } : { message: string; config: ConfigOptions; } + + message: "Helper A: " + value, +>message : string +>"Helper A: " + value : string +>"Helper A: " : "Helper A: " +>value : string + + config: config +>config : ConfigOptions +>config : ConfigOptions + + }; +} + +=== /packages/package-b/index.ts === +export interface ConfigOptions { + enabled: boolean; +>enabled : boolean + + timeout: number; +>timeout : number +} + +export function helperB(value: number): string { +>helperB : (value: number) => string +>value : number + + return "Helper B: " + value; +>"Helper B: " + value : string +>"Helper B: " : "Helper B: " +>value : number +} + +=== /src/index.ts === +// Test that the project can import package-a directly +// package-a's types depend on package-b's types (ConfigOptions) +import { helperA } from 'package-a'; +>helperA : (value: string, config: import("/packages/package-b/index").ConfigOptions) => HelperResult + +import type { HelperResult } from 'package-a'; +>HelperResult : HelperResult + +import type { ConfigOptions } from 'package-b'; // This should error - package-b is not a direct dependency +>ConfigOptions : any + +export function useDirectDependency(text: string): HelperResult { +>useDirectDependency : (text: string) => HelperResult +>text : string + + const config: ConfigOptions = { enabled: true, timeout: 5000 }; +>config : ConfigOptions +>{ enabled: true, timeout: 5000 } : { enabled: boolean; timeout: number; } +>enabled : boolean +>true : true +>timeout : number +>5000 : 5000 + + return helperA(text, config); +>helperA(text, config) : HelperResult +>helperA : (value: string, config: import("/packages/package-b/index").ConfigOptions) => HelperResult +>text : string +>config : ConfigOptions +} + +// Test that the project CANNOT import package-b directly even though package-a uses it +// This should cause an error since package-b is not in project's dependencies +export function attemptDirectImport(): ConfigOptions { +>attemptDirectImport : () => ConfigOptions + + return { enabled: false, timeout: 1000 }; +>{ enabled: false, timeout: 1000 } : { enabled: boolean; timeout: number; } +>enabled : boolean +>false : false +>timeout : number +>1000 : 1000 +} + diff --git a/testdata/baselines/reference/compiler/pnpTypeRootsResolution.js b/testdata/baselines/reference/compiler/pnpTypeRootsResolution.js new file mode 100644 index 0000000000..c1e9014271 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTypeRootsResolution.js @@ -0,0 +1,103 @@ +//// [tests/cases/compiler/pnpTypeRootsResolution.ts] //// + +//// [.pnp.cjs] +module.exports = {}; + +//// [.pnp.data.json] +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["server-lib", "npm:2.0.0"], + ["@types/server-lib", "npm:2.0.0"] + ] + }] + ]], + ["server-lib", [ + ["npm:2.0.0", { + "packageLocation": "./.yarn/cache/server-lib-npm-2.0.0-ijkl9012/node_modules/server-lib/", + "packageDependencies": [] + }] + ]], + ["@types/server-lib", [ + ["npm:2.0.0", { + "packageLocation": "./.yarn/cache/@types-server-lib-npm-2.0.0-mnop3456/node_modules/@types/server-lib/", + "packageDependencies": [ + ["@types/runtime", "npm:3.0.0"] + ] + }] + ]] + ] +} + +//// [package.json] +{ + "name": "project", + "dependencies": { + "server-lib": "2.0.0" + }, + "devDependencies": { + "@types/server-lib": "2.0.0", + } +} + +//// [package.json] +{ + "name": "server-lib", + "version": "2.0.0" +} + +//// [package.json] +{ + "name": "@types/server-lib", + "version": "2.0.0", + "types": "index.d.ts" +} + +//// [index.d.ts] +export interface Request { + params: Record; + query: Record; +} + +export interface Response { + send(body: Record): void; + json(body: Record): void; +} + +export declare function createServer(): Record; + +//// [index.ts] +// Test that TypeScript can resolve @types packages through PnP +import type { Request, Response } from 'server-lib'; +import { createServer } from 'server-lib'; + +export function handleRequest(req: Request, res: Response): void { + res.json({ data: 'Hello, world!' }); +} + +export const server = createServer(); + + +//// [index.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.server = void 0; +exports.handleRequest = handleRequest; +const server_lib_1 = require("server-lib"); +function handleRequest(req, res) { + res.json({ data: 'Hello, world!' }); +} +exports.server = (0, server_lib_1.createServer)(); diff --git a/testdata/baselines/reference/compiler/pnpTypeRootsResolution.symbols b/testdata/baselines/reference/compiler/pnpTypeRootsResolution.symbols new file mode 100644 index 0000000000..739cb2367e --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTypeRootsResolution.symbols @@ -0,0 +1,60 @@ +//// [tests/cases/compiler/pnpTypeRootsResolution.ts] //// + +=== /.yarn/cache/@types-server-lib-npm-2.0.0-mnop3456/node_modules/@types/server-lib/index.d.ts === +export interface Request { +>Request : Symbol(Request, Decl(index.d.ts, 0, 0)) + + params: Record; +>params : Symbol(Request.params, Decl(index.d.ts, 0, 26)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) + + query: Record; +>query : Symbol(Request.query, Decl(index.d.ts, 1, 34)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) +} + +export interface Response { +>Response : Symbol(Response, Decl(index.d.ts, 3, 1)) + + send(body: Record): void; +>send : Symbol(Response.send, Decl(index.d.ts, 5, 27)) +>body : Symbol(body, Decl(index.d.ts, 6, 7)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) + + json(body: Record): void; +>json : Symbol(Response.json, Decl(index.d.ts, 6, 44)) +>body : Symbol(body, Decl(index.d.ts, 7, 7)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) +} + +export declare function createServer(): Record; +>createServer : Symbol(createServer, Decl(index.d.ts, 8, 1)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) + +=== /src/index.ts === +// Test that TypeScript can resolve @types packages through PnP +import type { Request, Response } from 'server-lib'; +>Request : Symbol(Request, Decl(index.ts, 1, 13)) +>Response : Symbol(Response, Decl(index.ts, 1, 22)) + +import { createServer } from 'server-lib'; +>createServer : Symbol(createServer, Decl(index.ts, 2, 8)) + +export function handleRequest(req: Request, res: Response): void { +>handleRequest : Symbol(handleRequest, Decl(index.ts, 2, 42)) +>req : Symbol(req, Decl(index.ts, 4, 30)) +>Request : Symbol(Request, Decl(index.ts, 1, 13)) +>res : Symbol(res, Decl(index.ts, 4, 43)) +>Response : Symbol(Response, Decl(index.ts, 1, 22)) + + res.json({ data: 'Hello, world!' }); +>res.json : Symbol(Response.json, Decl(index.d.ts, 6, 44)) +>res : Symbol(res, Decl(index.ts, 4, 43)) +>json : Symbol(Response.json, Decl(index.d.ts, 6, 44)) +>data : Symbol(data, Decl(index.ts, 5, 12)) +} + +export const server = createServer(); +>server : Symbol(server, Decl(index.ts, 8, 12)) +>createServer : Symbol(createServer, Decl(index.ts, 2, 8)) + diff --git a/testdata/baselines/reference/compiler/pnpTypeRootsResolution.types b/testdata/baselines/reference/compiler/pnpTypeRootsResolution.types new file mode 100644 index 0000000000..963f0b9c68 --- /dev/null +++ b/testdata/baselines/reference/compiler/pnpTypeRootsResolution.types @@ -0,0 +1,53 @@ +//// [tests/cases/compiler/pnpTypeRootsResolution.ts] //// + +=== /.yarn/cache/@types-server-lib-npm-2.0.0-mnop3456/node_modules/@types/server-lib/index.d.ts === +export interface Request { + params: Record; +>params : Record + + query: Record; +>query : Record +} + +export interface Response { + send(body: Record): void; +>send : (body: Record) => void +>body : Record + + json(body: Record): void; +>json : (body: Record) => void +>body : Record +} + +export declare function createServer(): Record; +>createServer : () => Record + +=== /src/index.ts === +// Test that TypeScript can resolve @types packages through PnP +import type { Request, Response } from 'server-lib'; +>Request : Request +>Response : Response + +import { createServer } from 'server-lib'; +>createServer : () => Record + +export function handleRequest(req: Request, res: Response): void { +>handleRequest : (req: Request, res: Response) => void +>req : Request +>res : Response + + res.json({ data: 'Hello, world!' }); +>res.json({ data: 'Hello, world!' }) : void +>res.json : (body: Record) => void +>res : Response +>json : (body: Record) => void +>{ data: 'Hello, world!' } : { data: string; } +>data : string +>'Hello, world!' : "Hello, world!" +} + +export const server = createServer(); +>server : Record +>createServer() : Record +>createServer : () => Record + diff --git a/testdata/tests/cases/compiler/pnpDeclarationEmitWorkspace.ts b/testdata/tests/cases/compiler/pnpDeclarationEmitWorkspace.ts new file mode 100644 index 0000000000..a66ac2db0c --- /dev/null +++ b/testdata/tests/cases/compiler/pnpDeclarationEmitWorkspace.ts @@ -0,0 +1,106 @@ +// @strict: true +// @declaration: true +// @currentDirectory: / + +// @filename: /.pnp.cjs +module.exports = {}; + +// @filename: /.pnp.data.json +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "workspace:packages/package-a"] + ] + }] + ]], + ["package-a", [ + ["workspace:packages/package-a", { + "packageLocation": "./packages/package-a/", + "packageDependencies": [] + }] + ]] + ] +} + +// @filename: /package.json +{ + "name": "project", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "package-a": "workspace:*" + } +} + +// @filename: /tsconfig.json +{ + "compilerOptions": { + "declaration": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} + +// @filename: /packages/package-a/package.json +{ + "name": "package-a", + "exports": { + "./other-subpath": { + "types": "./index.d.ts", + "default": "./index.ts" + } + }, + "dependencies": { + "package-b": "workspace:*" + } +} + +// @filename: /packages/package-a/index.d.ts +export interface BaseConfig { + timeout: number; + retries: number; +} + +export interface DataOptions { + format: "json" | "xml"; + encoding: string; +} + +export interface ServiceConfig extends BaseConfig { + endpoint: string; + options: DataOptions; +} + +export type ConfigFactory = (endpoint: string) => ServiceConfig; + +export declare function createServiceConfig(endpoint: string): ServiceConfig; + +// @filename: /src/index.ts +import type { ServiceConfig, ConfigFactory } from 'package-a/other-subpath'; +import { createServiceConfig } from 'package-a/other-subpath'; + +export function initializeService(url: string): ServiceConfig { + return createServiceConfig(url); +} + +export const factory = createServiceConfig; + +export interface AppConfig { + service: ServiceConfig; + debug: boolean; +} diff --git a/testdata/tests/cases/compiler/pnpSimpleTest.ts b/testdata/tests/cases/compiler/pnpSimpleTest.ts new file mode 100644 index 0000000000..7c4b2603ee --- /dev/null +++ b/testdata/tests/cases/compiler/pnpSimpleTest.ts @@ -0,0 +1,97 @@ +// @strict: true + +// @filename: /.pnp.cjs +module.exports = {}; + +// @filename: /.pnp.data.json +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "npm:1.0.0"], + ["package-b", "npm:2.0.0"] + ] + }] + ]], + ["package-a", [ + ["npm:1.0.0", { + "packageLocation": "./.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/", + "packageDependencies": [] + }] + ]], + ["package-b", [ + ["npm:2.0.0", { + "packageLocation": "./.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/", + "packageDependencies": [] + }] + ]] + ] +} + +// @filename: package.json +{ + "name": "project", + "dependencies": { + "package-a": "npm:1.0.0", + "package-b": "npm:2.0.0" + } +} + +// @filename: /.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/package.json +{ + "name": "package-a", + "version": "1.0.0", + "exports": { + ".": "./index.js" + }, + "types": "index.d.ts" +} + +// @filename: /.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/index.js +exports.helperA = function(value) { + return "Helper A: " + value; +}; + +// @filename: /.yarn/cache/package-a-npm-1.0.0-abcd1234/node_modules/package-a/index.d.ts +export declare function helperA(value: string): string; + +// @filename: /.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/package.json +{ + "name": "package-b", + "version": "2.0.0", + "exports": { + ".": "./index.js" + }, + "types": "index.d.ts" +} + +// @filename: /.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/index.js +exports.helperB = function(value) { + return "Helper B: " + value; +}; + +// @filename: /.yarn/cache/package-b-npm-2.0.0-efgh5678/node_modules/package-b/index.d.ts +export declare function helperB(value: number): string; + +// @filename: /src/index.ts +// Workspace package that imports both third-party dependencies +import { helperA } from 'package-a'; +import { helperB } from 'package-b'; + +export function processData(text: string, num: number): string { + const resultA = helperA(text); + const resultB = helperB(num); + return `${resultA} | ${resultB}`; +} \ No newline at end of file diff --git a/testdata/tests/cases/compiler/pnpTransitiveDependencies.ts b/testdata/tests/cases/compiler/pnpTransitiveDependencies.ts new file mode 100644 index 0000000000..41fdc6d77e --- /dev/null +++ b/testdata/tests/cases/compiler/pnpTransitiveDependencies.ts @@ -0,0 +1,117 @@ +// @strict: true + +// @filename: /.pnp.cjs +module.exports = {}; + +// @filename: /.pnp.data.json +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["package-a", "workspace:packages/package-a"] + ] + }] + ]], + ["package-a", [ + ["workspace:packages/package-a", { + "packageLocation": "./packages/package-a/", + "packageDependencies": [ + ["package-b", "workspace:packages/package-b"] + ] + }] + ]], + ["package-b", [ + ["workspace:packages/package-b", { + "packageLocation": "./packages/package-b/", + "packageDependencies": [] + }] + ]] + ] +} + +// @filename: /package.json +{ + "name": "project", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "package-a": "workspace:packages/package-a" + } +} + +// @filename: /packages/package-a/package.json +{ + "name": "package-a", + "version": "1.0.0", + "exports": { + ".": "./index.ts" + }, + "dependencies": { + "package-b": "workspace:packages/package-b" + } +} + +// @filename: /packages/package-a/index.ts +import type { ConfigOptions } from 'package-b'; + +export interface HelperResult { + message: string; + config: ConfigOptions; +} + +export function helperA(value: string, config: ConfigOptions): HelperResult { + return { + message: "Helper A: " + value, + config: config + }; +} + +// @filename: /packages/package-b/package.json +{ + "name": "package-b", + "version": "2.0.0", + "exports": { + ".": "./index.ts" + } +} + +// @filename: /packages/package-b/index.ts +export interface ConfigOptions { + enabled: boolean; + timeout: number; +} + +export function helperB(value: number): string { + return "Helper B: " + value; +} + +// @filename: /src/index.ts +// Test that the project can import package-a directly +// package-a's types depend on package-b's types (ConfigOptions) +import { helperA } from 'package-a'; +import type { HelperResult } from 'package-a'; +import type { ConfigOptions } from 'package-b'; // This should error - package-b is not a direct dependency + +export function useDirectDependency(text: string): HelperResult { + const config: ConfigOptions = { enabled: true, timeout: 5000 }; + return helperA(text, config); +} + +// Test that the project CANNOT import package-b directly even though package-a uses it +// This should cause an error since package-b is not in project's dependencies +export function attemptDirectImport(): ConfigOptions { + return { enabled: false, timeout: 1000 }; +} diff --git a/testdata/tests/cases/compiler/pnpTypeRootsResolution.ts b/testdata/tests/cases/compiler/pnpTypeRootsResolution.ts new file mode 100644 index 0000000000..8515344153 --- /dev/null +++ b/testdata/tests/cases/compiler/pnpTypeRootsResolution.ts @@ -0,0 +1,91 @@ +// @strict: true + +// @filename: /.pnp.cjs +module.exports = {}; + +// @filename: /.pnp.data.json +{ + "dependencyTreeRoots": [ + { + "name": "project", + "reference": "workspace:." + } + ], + "ignorePatternData": null, + "enableTopLevelFallback": false, + "fallbackPool": [], + "fallbackExclusionList": [], + "packageRegistryData": [ + ["project", [ + ["workspace:.", { + "packageLocation": "./", + "packageDependencies": [ + ["server-lib", "npm:2.0.0"], + ["@types/server-lib", "npm:2.0.0"] + ] + }] + ]], + ["server-lib", [ + ["npm:2.0.0", { + "packageLocation": "./.yarn/cache/server-lib-npm-2.0.0-ijkl9012/node_modules/server-lib/", + "packageDependencies": [] + }] + ]], + ["@types/server-lib", [ + ["npm:2.0.0", { + "packageLocation": "./.yarn/cache/@types-server-lib-npm-2.0.0-mnop3456/node_modules/@types/server-lib/", + "packageDependencies": [ + ["@types/runtime", "npm:3.0.0"] + ] + }] + ]] + ] +} + +// @filename: /package.json +{ + "name": "project", + "dependencies": { + "server-lib": "2.0.0" + }, + "devDependencies": { + "@types/server-lib": "2.0.0", + } +} + +// @filename: /.yarn/cache/server-lib-npm-2.0.0-ijkl9012/node_modules/server-lib/package.json +{ + "name": "server-lib", + "version": "2.0.0" +} + +// @filename: /.yarn/cache/@types-server-lib-npm-2.0.0-mnop3456/node_modules/@types/server-lib/package.json +{ + "name": "@types/server-lib", + "version": "2.0.0", + "types": "index.d.ts" +} + +// @filename: /.yarn/cache/@types-server-lib-npm-2.0.0-mnop3456/node_modules/@types/server-lib/index.d.ts +export interface Request { + params: Record; + query: Record; +} + +export interface Response { + send(body: Record): void; + json(body: Record): void; +} + +export declare function createServer(): Record; + +// @filename: /src/index.ts +// Test that TypeScript can resolve @types packages through PnP +import type { Request, Response } from 'server-lib'; +import { createServer } from 'server-lib'; + +export function handleRequest(req: Request, res: Response): void { + res.json({ data: 'Hello, world!' }); +} + +export const server = createServer();