From 4269eff2fcb7898da441e5519f0c992bd73690c7 Mon Sep 17 00:00:00 2001 From: Chase Colman Date: Sat, 18 Oct 2025 09:13:10 +0000 Subject: [PATCH 01/10] Improve symlink resolution in module specifier generation Extends the symlink support in GetEachFileNameOfModule to properly resolve module specifiers across symlinked packages and workspaces. Key changes: - Move knownsymlinks from compiler to dedicated symlinks package - Implement active resolution via ResolveModuleName to populate cache - Add dependency resolution from package.json to detect symlinks early - Improve ignored path handling (node_modules/., .git, .# emacs locks) - Add comprehensive test coverage for symlink resolution - Fix declaration emit to prefer original paths over symlink paths This aligns with upstream TypeScript's symlink resolution behavior, ensuring correct module specifiers in declaration files for monorepos and symlinked dependencies. Fixes baseline mismatches in: - declarationEmitReexportedSymlinkReference2/3 - symlinkedWorkspaceDependencies* tests - nodeModuleReexportFromDottedPath --- internal/checker/nodebuilderimpl.go | 4 + internal/compiler/emitHost.go | 10 + internal/compiler/knownsymlinks.go | 53 ---- internal/compiler/program.go | 49 +++ .../compiler/projectreferencedtsfakinghost.go | 9 +- internal/ls/autoimports.go | 3 +- internal/modulespecifiers/specifiers.go | 198 ++++++++---- internal/modulespecifiers/specifiers_test.go | 257 +++++++++++++++ internal/modulespecifiers/types.go | 4 +- internal/symlinks/knownsymlinks.go | 125 ++++++++ internal/symlinks/knownsymlinks_test.go | 297 ++++++++++++++++++ .../tstransforms/importelision_test.go | 9 + internal/tspath/ignoredpaths.go | 10 +- internal/tspath/ignoredpaths_test.go | 133 ++++++++ internal/tspath/path.go | 14 + internal/tspath/startsWithDirectory_test.go | 177 +++++++++++ ...larationEmitReexportedSymlinkReference2.js | 2 +- ...ionEmitReexportedSymlinkReference2.js.diff | 10 +- ...EmitReexportedSymlinkReference3.errors.txt | 60 ++++ ...eexportedSymlinkReference3.errors.txt.diff | 64 ---- ...larationEmitReexportedSymlinkReference3.js | 2 +- ...ionEmitReexportedSymlinkReference3.js.diff | 2 +- ...oDirectLinkGeneratesDeepNonrelativeName.js | 2 +- ...ctLinkGeneratesDeepNonrelativeName.js.diff | 8 - ...iesNoDirectLinkGeneratesNonrelativeName.js | 2 +- ...DirectLinkGeneratesNonrelativeName.js.diff | 4 - ...ectLinkOptionalGeneratesNonrelativeName.js | 2 +- ...nkOptionalGeneratesNonrelativeName.js.diff | 4 - ...oDirectLinkPeerGeneratesNonrelativeName.js | 2 +- ...ctLinkPeerGeneratesNonrelativeName.js.diff | 4 - ...fiers-across-projects-resolve-correctly.js | 10 +- ...ibling-package-through-indirect-symlink.js | 12 +- 32 files changed, 1317 insertions(+), 225 deletions(-) delete mode 100644 internal/compiler/knownsymlinks.go create mode 100644 internal/modulespecifiers/specifiers_test.go create mode 100644 internal/symlinks/knownsymlinks.go create mode 100644 internal/symlinks/knownsymlinks_test.go create mode 100644 internal/tspath/ignoredpaths_test.go create mode 100644 internal/tspath/startsWithDirectory_test.go create mode 100644 testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.errors.txt delete mode 100644 testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.errors.txt.diff delete mode 100644 testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.js.diff diff --git a/internal/checker/nodebuilderimpl.go b/internal/checker/nodebuilderimpl.go index afdc9494d7..2b2b0ff48c 100644 --- a/internal/checker/nodebuilderimpl.go +++ b/internal/checker/nodebuilderimpl.go @@ -1215,6 +1215,10 @@ func (b *nodeBuilderImpl) getSpecifierForModuleSymbol(symbol *ast.Symbol, overri }, false, /*forAutoImports*/ ) + if len(allSpecifiers) == 0 { + links.specifierCache[cacheKey] = "" + return "" + } specifier := allSpecifiers[0] links.specifierCache[cacheKey] = specifier return specifier diff --git a/internal/compiler/emitHost.go b/internal/compiler/emitHost.go index fcc0406342..7a10176209 100644 --- a/internal/compiler/emitHost.go +++ b/internal/compiler/emitHost.go @@ -9,6 +9,7 @@ import ( "github.com/microsoft/typescript-go/internal/modulespecifiers" "github.com/microsoft/typescript-go/internal/outputpaths" "github.com/microsoft/typescript-go/internal/printer" + "github.com/microsoft/typescript-go/internal/symlinks" "github.com/microsoft/typescript-go/internal/transformers/declarations" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" @@ -126,3 +127,12 @@ func (host *emitHost) GetEmitResolver() printer.EmitResolver { func (host *emitHost) IsSourceFileFromExternalLibrary(file *ast.SourceFile) bool { return host.program.IsSourceFileFromExternalLibrary(file) } + +func (host *emitHost) GetSymlinkCache() *symlinks.KnownSymlinks { + return host.program.GetSymlinkCache() +} + +func (host *emitHost) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule { + resolved, _ := host.program.resolver.ResolveModuleName(moduleName, containingFile, resolutionMode, nil) + return resolved +} diff --git a/internal/compiler/knownsymlinks.go b/internal/compiler/knownsymlinks.go deleted file mode 100644 index 246a641127..0000000000 --- a/internal/compiler/knownsymlinks.go +++ /dev/null @@ -1,53 +0,0 @@ -package compiler - -import ( - "github.com/microsoft/typescript-go/internal/collections" - "github.com/microsoft/typescript-go/internal/tspath" -) - -type knownDirectoryLink struct { - /** - * Matches the casing returned by `realpath`. Used to compute the `realpath` of children. - * Always has trailing directory separator - */ - Real string - /** - * toPath(real). Stored to avoid repeated recomputation. - * Always has trailing directory separator - */ - RealPath tspath.Path -} - -type knownSymlinks struct { - directories collections.SyncMap[tspath.Path, *knownDirectoryLink] - files collections.SyncMap[tspath.Path, string] -} - -/** Gets a map from symlink to realpath. Keys have trailing directory separators. */ -func (cache *knownSymlinks) Directories() *collections.SyncMap[tspath.Path, *knownDirectoryLink] { - return &cache.directories -} - -/** Gets a map from symlink to realpath */ -func (cache *knownSymlinks) Files() *collections.SyncMap[tspath.Path, string] { - return &cache.files -} - -// all callers should check !containsIgnoredPath(symlinkPath) -func (cache *knownSymlinks) SetDirectory(symlink string, symlinkPath tspath.Path, realDirectory *knownDirectoryLink) { - // Large, interconnected dependency graphs in pnpm will have a huge number of symlinks - // where both the realpath and the symlink path are inside node_modules/.pnpm. Since - // this path is never a candidate for a module specifier, we can ignore it entirely. - - // !!! - // if realDirectory != nil { - // if _, ok := cache.directories.Load(symlinkPath); !ok { - // cache.directoriesByRealpath.Add(realDirectory.RealPath, symlink) - // } - // } - cache.directories.Store(symlinkPath, realDirectory) -} - -func (cache *knownSymlinks) SetFile(symlinkPath tspath.Path, realpath string) { - cache.files.Store(symlinkPath, realpath) -} diff --git a/internal/compiler/program.go b/internal/compiler/program.go index 7238991f97..622d1004e6 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -23,6 +23,7 @@ import ( "github.com/microsoft/typescript-go/internal/printer" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/sourcemap" + "github.com/microsoft/typescript-go/internal/symlinks" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -66,6 +67,7 @@ type Program struct { // Cached unresolved imports for ATA unresolvedImportsOnce sync.Once unresolvedImports *collections.Set[string] + knownSymlinks *symlinks.KnownSymlinks } // FileExists implements checker.Program. @@ -210,6 +212,10 @@ func NewProgram(opts ProgramOptions) *Program { p.initCheckerPool() p.processedFiles = processAllProgramFiles(p.opts, p.SingleThreaded()) p.verifyCompilerOptions() + p.knownSymlinks = symlinks.NewKnownSymlink(p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) + if len(p.resolvedModules) > 0 || len(p.typeResolutionsInFile) > 0 { + p.knownSymlinks.SetSymlinksFromResolutions(p.ForEachResolvedModule, p.ForEachResolvedTypeReferenceDirective) + } return p } @@ -240,6 +246,10 @@ func (p *Program) UpdateProgram(changedFilePath tspath.Path, newHost CompilerHos result.filesByPath = maps.Clone(result.filesByPath) result.filesByPath[newFile.Path()] = newFile updateFileIncludeProcessor(result) + result.knownSymlinks = symlinks.NewKnownSymlink(result.GetCurrentDirectory(), result.UseCaseSensitiveFileNames()) + if len(result.resolvedModules) > 0 || len(result.typeResolutionsInFile) > 0 { + result.knownSymlinks.SetSymlinksFromResolutions(result.ForEachResolvedModule, result.ForEachResolvedTypeReferenceDirective) + } return result, true } @@ -1630,6 +1640,45 @@ func (p *Program) SourceFileMayBeEmitted(sourceFile *ast.SourceFile, forceDtsEmi return sourceFileMayBeEmitted(sourceFile, p, forceDtsEmit) } +func (p *Program) GetSymlinkCache() *symlinks.KnownSymlinks { + // if p.Host().GetSymlinkCache() != nil { + // return p.Host().GetSymlinkCache() + // } + if p.knownSymlinks == nil { + p.knownSymlinks = symlinks.NewKnownSymlink(p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) + } + return p.knownSymlinks +} + +func (p *Program) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule { + resolved, _ := p.resolver.ResolveModuleName(moduleName, containingFile, resolutionMode, nil) + return resolved +} + +func (p *Program) ForEachResolvedModule(callback func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile) { + forEachResolution(p.resolvedModules, callback, file) +} + +func (p *Program) ForEachResolvedTypeReferenceDirective(callback func(resolution *module.ResolvedTypeReferenceDirective, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile) { + forEachResolution(p.typeResolutionsInFile, callback, file) +} + +func forEachResolution[T any](resolutionCache map[tspath.Path]module.ModeAwareCache[T], callback func(resolution T, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile) { + if file != nil { + if resolutions, ok := resolutionCache[file.Path()]; ok { + for key, resolution := range resolutions { + callback(resolution, key.Name, key.Mode, file.Path()) + } + } + } else { + for filePath, resolutions := range resolutionCache { + for key, resolution := range resolutions { + callback(resolution, key.Name, key.Mode, filePath) + } + } + } +} + var plainJSErrors = collections.NewSetFromItems( // binder errors diagnostics.Cannot_redeclare_block_scoped_variable_0.Code(), diff --git a/internal/compiler/projectreferencedtsfakinghost.go b/internal/compiler/projectreferencedtsfakinghost.go index 916dce2225..b63b324290 100644 --- a/internal/compiler/projectreferencedtsfakinghost.go +++ b/internal/compiler/projectreferencedtsfakinghost.go @@ -7,6 +7,7 @@ import ( "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/symlinks" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/cachedvfs" @@ -26,7 +27,7 @@ func newProjectReferenceDtsFakingHost(loader *fileLoader) module.ResolutionHost fs: cachedvfs.From(&projectReferenceDtsFakingVfs{ projectReferenceFileMapper: loader.projectReferenceFileMapper, dtsDirectories: loader.dtsDirectories, - knownSymlinks: knownSymlinks{}, + knownSymlinks: symlinks.KnownSymlinks{}, }), } return host @@ -45,7 +46,7 @@ func (h *projectReferenceDtsFakingHost) GetCurrentDirectory() string { type projectReferenceDtsFakingVfs struct { projectReferenceFileMapper *projectReferenceFileMapper dtsDirectories collections.Set[tspath.Path] - knownSymlinks knownSymlinks + knownSymlinks symlinks.KnownSymlinks } var _ vfs.FS = (*projectReferenceDtsFakingVfs)(nil) @@ -150,7 +151,7 @@ func (fs *projectReferenceDtsFakingVfs) handleDirectoryCouldBeSymlink(directory // not symlinked return } - fs.knownSymlinks.SetDirectory(directory, directoryPath, &knownDirectoryLink{ + fs.knownSymlinks.SetDirectory(directory, directoryPath, &symlinks.KnownDirectoryLink{ Real: tspath.EnsureTrailingDirectorySeparator(realDirectory), RealPath: realPath, }) @@ -181,7 +182,7 @@ func (fs *projectReferenceDtsFakingVfs) fileOrDirectoryExistsUsingSource(fileOrD // If it contains node_modules check if its one of the symlinked path we know of var exists bool - knownDirectoryLinks.Range(func(directoryPath tspath.Path, knownDirectoryLink *knownDirectoryLink) bool { + knownDirectoryLinks.Range(func(directoryPath tspath.Path, knownDirectoryLink *symlinks.KnownDirectoryLink) bool { relative, hasPrefix := strings.CutPrefix(string(fileOrDirectoryPath), string(directoryPath)) if !hasPrefix { return true diff --git a/internal/ls/autoimports.go b/internal/ls/autoimports.go index cf89e08fab..bad7b13157 100644 --- a/internal/ls/autoimports.go +++ b/internal/ls/autoimports.go @@ -164,7 +164,8 @@ func (e *exportInfoMap) add( } moduleName := stringutil.StripQuotes(moduleSymbol.Name) - id := e.exportInfoId + 1 + id := e.exportInfoId + e.exportInfoId += 1 target := ch.SkipAlias(symbol) if flagMatch != nil && !flagMatch(target.Flags) { diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index 1f8e26b040..0a341ef53c 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -13,6 +13,7 @@ import ( "github.com/microsoft/typescript-go/internal/outputpaths" "github.com/microsoft/typescript-go/internal/packagejson" "github.com/microsoft/typescript-go/internal/stringutil" + "github.com/microsoft/typescript-go/internal/symlinks" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -64,8 +65,8 @@ func GetModuleSpecifiersWithInfo( getInfo(host.GetSourceOfProjectReferenceIfOutputIncluded(importingSourceFile), host), moduleSourceFile.FileName(), host, - // compilerOptions, - // options, + compilerOptions, + options, ) return computeModuleSpecifiers( @@ -163,38 +164,106 @@ func getAllModulePaths( // cached := cache.get(importingFilePath, importedFilePath, preferences, options); // if (cached.modulePaths) {return cached.modulePaths;} // } - modulePaths := getAllModulePathsWorker(info, importedFileName, host) // , compilerOptions, options); + modulePaths := getAllModulePathsWorker(info, importedFileName, host, compilerOptions, options) // if (cache != nil) { // cache.setModulePaths(importingFilePath, importedFilePath, preferences, options, modulePaths); // } return modulePaths } +func populateSymlinkCacheFromResolutions(importingFileName string, host ModuleSpecifierGenerationHost, compilerOptions *core.CompilerOptions, options ModuleSpecifierOptions, links *symlinks.KnownSymlinks) { + packageJsonDir := host.GetNearestAncestorDirectoryWithPackageJson(tspath.GetDirectoryPath(importingFileName)) + if packageJsonDir == "" { + return + } + + packageJsonPath := tspath.CombinePaths(packageJsonDir, "package.json") + pkgJsonInfo := host.GetPackageJsonInfo(packageJsonPath) + if pkgJsonInfo == nil { + return + } + + pkgJson := pkgJsonInfo.GetContents() + if pkgJson == nil { + return + } + + var allDeps []string + if deps, ok := pkgJson.Dependencies.GetValue(); ok { + for depName := range deps { + allDeps = append(allDeps, depName) + } + } + if peerDeps, ok := pkgJson.PeerDependencies.GetValue(); ok { + for depName := range peerDeps { + allDeps = append(allDeps, depName) + } + } + if optionalDeps, ok := pkgJson.OptionalDependencies.GetValue(); ok { + for depName := range optionalDeps { + allDeps = append(allDeps, depName) + } + } + + for _, depName := range allDeps { + resolved := host.ResolveModuleName(depName, packageJsonPath, options.OverrideImportMode) + if resolved != nil && resolved.OriginalPath != "" && resolved.ResolvedFileName != "" { + processResolution(links, resolved.OriginalPath, resolved.ResolvedFileName, host.GetCurrentDirectory(), host.UseCaseSensitiveFileNames()) + } + } +} + +func processResolution(links *symlinks.KnownSymlinks, originalPath string, resolvedFileName string, cwd string, caseSensitive bool) { + links.SetFile(tspath.ToPath(originalPath, cwd, caseSensitive), resolvedFileName) + commonResolved, commonOriginal := guessDirectorySymlink(originalPath, resolvedFileName, cwd, caseSensitive) + if commonResolved != "" && commonOriginal != "" { + symlinkPath := tspath.ToPath(commonOriginal, cwd, caseSensitive) + if !tspath.ContainsIgnoredPath(string(symlinkPath)) { + links.SetDirectory( + commonOriginal, + symlinkPath.EnsureTrailingDirectorySeparator(), + &symlinks.KnownDirectoryLink{ + Real: tspath.EnsureTrailingDirectorySeparator(commonResolved), + RealPath: tspath.ToPath(commonResolved, cwd, caseSensitive).EnsureTrailingDirectorySeparator(), + }, + ) + } + } +} + +func guessDirectorySymlink(originalPath string, resolvedFileName string, cwd string, caseSensitive bool) (string, string) { + aParts := tspath.GetPathComponents(tspath.GetNormalizedAbsolutePath(resolvedFileName, cwd), "") + bParts := tspath.GetPathComponents(tspath.GetNormalizedAbsolutePath(originalPath, cwd), "") + isDirectory := false + for len(aParts) >= 2 && len(bParts) >= 2 && + !isNodeModulesOrScopedPackageDirectory(aParts[len(aParts)-2], caseSensitive) && + !isNodeModulesOrScopedPackageDirectory(bParts[len(bParts)-2], caseSensitive) && + tspath.GetCanonicalFileName(aParts[len(aParts)-1], caseSensitive) == tspath.GetCanonicalFileName(bParts[len(bParts)-1], caseSensitive) { + aParts = aParts[:len(aParts)-1] + bParts = bParts[:len(bParts)-1] + isDirectory = true + } + if isDirectory { + return tspath.GetPathFromPathComponents(aParts), tspath.GetPathFromPathComponents(bParts) + } + return "", "" +} + +func isNodeModulesOrScopedPackageDirectory(s string, caseSensitive bool) bool { + return s != "" && (tspath.GetCanonicalFileName(s, caseSensitive) == "node_modules" || strings.HasPrefix(s, "@")) +} + func getAllModulePathsWorker( info Info, importedFileName string, host ModuleSpecifierGenerationHost, - // compilerOptions *core.CompilerOptions, - // options ModuleSpecifierOptions, + compilerOptions *core.CompilerOptions, + options ModuleSpecifierOptions, ) []ModulePath { - // !!! TODO: Caches and symlink cache chicanery to support pulling in non-explicit package.json dep names - // cache := host.GetModuleResolutionCache() // !!! - // links := host.GetSymlinkCache() // !!! - // if cache != nil && links != nil && !strings.Contains(info.ImportingSourceFileName, "/node_modules/") { - // // Debug.type(host); // !!! - // // Cache resolutions for all `dependencies` of the `package.json` context of the input file. - // // This should populate all the relevant symlinks in the symlink cache, and most, if not all, of these resolutions - // // should get (re)used. - // // const state = getTemporaryModuleResolutionState(cache.getPackageJsonInfoCache(), host, {}); - // // const packageJson = getPackageScopeForPath(getDirectoryPath(info.importingSourceFileName), state); - // // if (packageJson) { - // // const toResolve = getAllRuntimeDependencies(packageJson.contents.packageJsonContent); - // // for (const depName of (toResolve || emptyArray)) { - // // const resolved = resolveModuleName(depName, combinePaths(packageJson.packageDirectory, "package.json"), compilerOptions, host, cache, /*redirectedReference*/ undefined, options.overrideImportMode); - // // links.setSymlinksFromResolution(resolved.resolvedModule); - // // } - // // } - // } + links := host.GetSymlinkCache() + if links != nil && !strings.Contains(info.ImportingSourceFileName, "/node_modules/") { + populateSymlinkCacheFromResolutions(info.ImportingSourceFileName, host, compilerOptions, options, links) + } allFileNames := make(map[string]ModulePath) paths := GetEachFileNameOfModule(info.ImportingSourceFileName, importedFileName, host, true) @@ -231,16 +300,21 @@ func getAllModulePathsWorker( return sortedPaths } +// containsIgnoredPath checks if a path contains patterns that should be ignored. +// This is a local helper that duplicates tspath.ContainsIgnoredPath for performance. func containsIgnoredPath(s string) bool { return strings.Contains(s, "/node_modules/.") || strings.Contains(s, "/.git") || - strings.Contains(s, "/.#") + strings.Contains(s, ".#") } +// ContainsNodeModules checks if a path contains the node_modules directory. func ContainsNodeModules(s string) bool { return strings.Contains(s, "/node_modules/") } +// GetEachFileNameOfModule returns all possible file paths for a module, including symlink alternatives. +// This function handles symlink resolution and provides multiple path options for module resolution. func GetEachFileNameOfModule( importingFileName string, importedFileName string, @@ -267,8 +341,6 @@ func GetEachFileNameOfModule( results := make([]ModulePath, 0, 2) if !preferSymlinks { - // Symlinks inside ignored paths are already filtered out of the symlink cache, - // so we only need to remove them from the realpath filenames. for _, p := range targets { if !(shouldFilterIgnoredPaths && containsIgnoredPath(p)) { results = append(results, ModulePath{ @@ -280,36 +352,50 @@ func GetEachFileNameOfModule( } } - // !!! TODO: Symlink directory handling - // const symlinkedDirectories = host.getSymlinkCache?.().getSymlinkedDirectoriesByRealpath(); - // const fullImportedFileName = getNormalizedAbsolutePath(importedFileName, cwd); - // const result = symlinkedDirectories && forEachAncestorDirectoryStoppingAtGlobalCache( - // host, - // getDirectoryPath(fullImportedFileName), - // realPathDirectory => { - // const symlinkDirectories = symlinkedDirectories.get(ensureTrailingDirectorySeparator(toPath(realPathDirectory, cwd, getCanonicalFileName))); - // if (!symlinkDirectories) return undefined; // Continue to ancestor directory - - // // Don't want to a package to globally import from itself (importNameCodeFix_symlink_own_package.ts) - // if (startsWithDirectory(importingFileName, realPathDirectory, getCanonicalFileName)) { - // return false; // Stop search, each ancestor directory will also hit this condition - // } - - // return forEach(targets, target => { - // if (!startsWithDirectory(target, realPathDirectory, getCanonicalFileName)) { - // return; - // } - - // const relative = getRelativePathFromDirectory(realPathDirectory, target, getCanonicalFileName); - // for (const symlinkDirectory of symlinkDirectories) { - // const option = resolvePath(symlinkDirectory, relative); - // const result = cb(option, target === referenceRedirect); - // shouldFilterIgnoredPaths = true; // We found a non-ignored path in symlinks, so we can reject ignored-path realpaths - // if (result) return result; - // } - // }); - // }, - // ); + symlinkedDirectories := host.GetSymlinkCache().DirectoriesByRealpath() + fullImportedFileName := tspath.GetNormalizedAbsolutePath(importedFileName, cwd) + if symlinkedDirectories != nil { + tspath.ForEachAncestorDirectoryStoppingAtGlobalCache( + host.GetGlobalTypingsCacheLocation(), + tspath.GetDirectoryPath(fullImportedFileName), + func(realPathDirectory string) (bool, bool) { + symlinkDirectories := symlinkedDirectories.Get(tspath.ToPath(realPathDirectory, cwd, host.UseCaseSensitiveFileNames()).EnsureTrailingDirectorySeparator()) + if symlinkDirectories == nil { + return false, false + } // Continue to ancestor directory + + // Don't want to a package to globally import from itself (importNameCodeFix_symlink_own_package.ts) + if tspath.StartsWithDirectory(importingFileName, realPathDirectory, host.UseCaseSensitiveFileNames()) { + return false, true // Stop search, each ancestor directory will also hit this condition + } + + for _, target := range targets { + if !tspath.StartsWithDirectory(target, realPathDirectory, host.UseCaseSensitiveFileNames()) { + continue + } + + relative := tspath.GetRelativePathFromDirectory( + realPathDirectory, + target, + tspath.ComparePathsOptions{ + UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(), + CurrentDirectory: cwd, + }) + for _, symlinkDirectory := range symlinkDirectories { + option := tspath.ResolvePath(symlinkDirectory, relative) + results = append(results, ModulePath{ + FileName: option, + IsInNodeModules: ContainsNodeModules(option), + IsRedirect: target == referenceRedirect, + }) + shouldFilterIgnoredPaths = true // We found a non-ignored path in symlinks, so we can reject ignored-path realpaths + } + } + + return false, false + }, + ) + } if preferSymlinks { for _, p := range targets { diff --git a/internal/modulespecifiers/specifiers_test.go b/internal/modulespecifiers/specifiers_test.go new file mode 100644 index 0000000000..94411a4780 --- /dev/null +++ b/internal/modulespecifiers/specifiers_test.go @@ -0,0 +1,257 @@ +package modulespecifiers + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/symlinks" + "github.com/microsoft/typescript-go/internal/tsoptions" + "github.com/microsoft/typescript-go/internal/tspath" +) + +// Mock host for testing +type mockModuleSpecifierGenerationHost struct { + currentDir string + useCaseSensitiveFileNames bool + symlinkCache *symlinks.KnownSymlinks +} + +func (h *mockModuleSpecifierGenerationHost) GetCurrentDirectory() string { + return h.currentDir +} + +func (h *mockModuleSpecifierGenerationHost) UseCaseSensitiveFileNames() bool { + return h.useCaseSensitiveFileNames +} + +func (h *mockModuleSpecifierGenerationHost) GetSymlinkCache() *symlinks.KnownSymlinks { + return h.symlinkCache +} + +func (h *mockModuleSpecifierGenerationHost) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule { + return nil +} + +func (h *mockModuleSpecifierGenerationHost) GetGlobalTypingsCacheLocation() string { + return "" +} + +func (h *mockModuleSpecifierGenerationHost) CommonSourceDirectory() string { + return h.currentDir +} + +func (h *mockModuleSpecifierGenerationHost) GetProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference { + return nil +} + +func (h *mockModuleSpecifierGenerationHost) GetRedirectTargets(path tspath.Path) []string { + return nil +} + +func (h *mockModuleSpecifierGenerationHost) GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) string { + return file.FileName() +} + +func (h *mockModuleSpecifierGenerationHost) FileExists(path string) bool { + return true // Mock implementation +} + +func (h *mockModuleSpecifierGenerationHost) GetNearestAncestorDirectoryWithPackageJson(dirname string) string { + return "" +} + +func (h *mockModuleSpecifierGenerationHost) GetPackageJsonInfo(pkgJsonPath string) PackageJsonInfo { + return nil +} + +func (h *mockModuleSpecifierGenerationHost) GetDefaultResolutionModeForFile(file ast.HasFileName) core.ResolutionMode { + return core.ResolutionModeNone +} + +func (h *mockModuleSpecifierGenerationHost) GetResolvedModuleFromModuleSpecifier(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) *module.ResolvedModule { + return nil +} + +func (h *mockModuleSpecifierGenerationHost) GetModeForUsageLocation(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) core.ResolutionMode { + return core.ResolutionModeNone +} + +func TestGetEachFileNameOfModule(t *testing.T) { + t.Parallel() + tests := []struct { + name string + importingFile string + importedFile string + preferSymlinks bool + expectedCount int + expectedPaths []string + }{ + { + name: "basic file path", + importingFile: "/project/src/main.ts", + importedFile: "/project/lib/utils.ts", + preferSymlinks: false, + expectedCount: 1, + expectedPaths: []string{"/project/lib/utils.ts"}, + }, + { + name: "symlink preference false", + importingFile: "/project/src/main.ts", + importedFile: "/project/lib/utils.ts", + preferSymlinks: false, + expectedCount: 1, + }, + { + name: "symlink preference true", + importingFile: "/project/src/main.ts", + importedFile: "/project/lib/utils.ts", + preferSymlinks: true, + expectedCount: 1, + }, + { + name: "ignored path with no alternatives", + importingFile: "/project/src/main.ts", + importedFile: "/project/node_modules/.pnpm/file.ts", + preferSymlinks: false, + expectedCount: 1, // Should return 1 because there's no better option (all paths are ignored) + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + host := &mockModuleSpecifierGenerationHost{ + currentDir: "/project", + useCaseSensitiveFileNames: true, + symlinkCache: symlinks.NewKnownSymlink("/project", true), + } + + result := GetEachFileNameOfModule(tt.importingFile, tt.importedFile, host, tt.preferSymlinks) + + if len(result) != tt.expectedCount { + t.Errorf("Expected %d paths, got %d", tt.expectedCount, len(result)) + } + + if tt.expectedPaths != nil { + for i, expectedPath := range tt.expectedPaths { + if i >= len(result) { + t.Errorf("Expected path %d: %s, but result has only %d paths", i, expectedPath, len(result)) + continue + } + if result[i].FileName != expectedPath { + t.Errorf("Expected path %d to be %s, got %s", i, expectedPath, result[i].FileName) + } + } + } + + for i, path := range result { + if path.FileName == "" { + t.Errorf("Path %d has empty FileName", i) + } + } + }) + } +} + +func TestGetEachFileNameOfModuleWithSymlinks(t *testing.T) { + t.Parallel() + host := &mockModuleSpecifierGenerationHost{ + currentDir: "/project", + useCaseSensitiveFileNames: true, + symlinkCache: symlinks.NewKnownSymlink("/project", true), + } + + symlinkPath := tspath.ToPath("/project/symlink", "/project", true).EnsureTrailingDirectorySeparator() + realDirectory := &symlinks.KnownDirectoryLink{ + Real: "/real/path/", + RealPath: tspath.ToPath("/real/path", "/project", true).EnsureTrailingDirectorySeparator(), + } + host.symlinkCache.SetDirectory("/project/symlink", symlinkPath, realDirectory) + + result := GetEachFileNameOfModule("/project/src/main.ts", "/real/path/file.ts", host, true) + + // Should find the symlink path + found := false + for _, path := range result { + if path.FileName == "/project/symlink/file.ts" { + found = true + break + } + } + + if !found { + t.Error("Expected to find symlink path /project/symlink/file.ts") + } +} + +func TestContainsNodeModules(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + expected bool + }{ + { + name: "contains node_modules", + path: "/project/node_modules/lodash/index.js", + expected: true, + }, + { + name: "does not contain node_modules", + path: "/project/src/utils.ts", + expected: false, + }, + { + name: "node_modules in middle", + path: "/project/packages/node_modules/pkg/file.js", + expected: true, + }, + { + name: "empty path", + path: "", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := ContainsNodeModules(tt.path) + if result != tt.expected { + t.Errorf("ContainsNodeModules(%q) = %v, expected %v", tt.path, result, tt.expected) + } + }) + } +} + +func TestContainsIgnoredPath(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + expected bool + }{ + { + name: "ignored path", + path: "/project/node_modules/.pnpm/file.ts", + expected: true, + }, + { + name: "not ignored path", + path: "/project/src/file.ts", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := containsIgnoredPath(tt.path) + if result != tt.expected { + t.Errorf("containsIgnoredPath(%q) = %v, expected %v", tt.path, result, tt.expected) + } + }) + } +} diff --git a/internal/modulespecifiers/types.go b/internal/modulespecifiers/types.go index 6cdfa3e689..20337e7b53 100644 --- a/internal/modulespecifiers/types.go +++ b/internal/modulespecifiers/types.go @@ -5,6 +5,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/module" "github.com/microsoft/typescript-go/internal/packagejson" + "github.com/microsoft/typescript-go/internal/symlinks" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -45,7 +46,7 @@ type PackageJsonInfo interface { type ModuleSpecifierGenerationHost interface { // GetModuleResolutionCache() any // !!! TODO: adapt new resolution cache model - // GetSymlinkCache() any // !!! TODO: adapt new resolution cache model + GetSymlinkCache() *symlinks.KnownSymlinks // GetFileIncludeReasons() any // !!! TODO: adapt new resolution cache model CommonSourceDirectory() string GetGlobalTypingsCacheLocation() string @@ -63,6 +64,7 @@ type ModuleSpecifierGenerationHost interface { GetDefaultResolutionModeForFile(file ast.HasFileName) core.ResolutionMode GetResolvedModuleFromModuleSpecifier(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) *module.ResolvedModule GetModeForUsageLocation(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) core.ResolutionMode + ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule } type ImportModuleSpecifierPreference string diff --git a/internal/symlinks/knownsymlinks.go b/internal/symlinks/knownsymlinks.go new file mode 100644 index 0000000000..f0c2bbf288 --- /dev/null +++ b/internal/symlinks/knownsymlinks.go @@ -0,0 +1,125 @@ +package symlinks + +import ( + "strings" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/debug" + "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/tspath" +) + +type KnownDirectoryLink struct { + /** + * Matches the casing returned by `realpath`. Used to compute the `realpath` of children. + * Always has trailing directory separator + */ + Real string + /** + * toPath(real). Stored to avoid repeated recomputation. + * Always has trailing directory separator + */ + RealPath tspath.Path +} + +type KnownSymlinks struct { + directories collections.SyncMap[tspath.Path, *KnownDirectoryLink] + directoriesByRealpath collections.MultiMap[tspath.Path, string] + files collections.SyncMap[tspath.Path, string] + HasProcessedResolutions bool + cwd string + useCaseSensitiveFileNames bool +} + +/** Gets a map from symlink to realpath. Keys have trailing directory separators. */ +func (cache *KnownSymlinks) Directories() *collections.SyncMap[tspath.Path, *KnownDirectoryLink] { + return &cache.directories +} + +func (cache *KnownSymlinks) DirectoriesByRealpath() *collections.MultiMap[tspath.Path, string] { + return &cache.directoriesByRealpath +} + +/** Gets a map from symlink to realpath */ +func (cache *KnownSymlinks) Files() *collections.SyncMap[tspath.Path, string] { + return &cache.files +} + +func (cache *KnownSymlinks) SetDirectory(symlink string, symlinkPath tspath.Path, realDirectory *KnownDirectoryLink) { + if realDirectory != nil { + if _, ok := cache.directories.Load(symlinkPath); !ok { + cache.directoriesByRealpath.Add(realDirectory.RealPath, symlink) + } + } + cache.directories.Store(symlinkPath, realDirectory) +} + +func (cache *KnownSymlinks) SetFile(symlinkPath tspath.Path, realpath string) { + cache.files.Store(symlinkPath, realpath) +} + +func NewKnownSymlink(currentDirectory string, useCaseSensitiveFileNames bool) *KnownSymlinks { + return &KnownSymlinks{ + cwd: currentDirectory, + useCaseSensitiveFileNames: useCaseSensitiveFileNames, + } +} + +func (cache *KnownSymlinks) SetSymlinksFromResolutions( + forEachResolvedModule func(callback func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile), + forEachResolvedTypeReferenceDirective func(callback func(resolution *module.ResolvedTypeReferenceDirective, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile), +) { + debug.Assert(!cache.HasProcessedResolutions) + cache.HasProcessedResolutions = true + forEachResolvedModule(func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.Path) { + cache.processResolution(resolution.OriginalPath, resolution.ResolvedFileName) + }, nil) + forEachResolvedTypeReferenceDirective(func(resolution *module.ResolvedTypeReferenceDirective, moduleName string, mode core.ResolutionMode, filePath tspath.Path) { + cache.processResolution(resolution.OriginalPath, resolution.ResolvedFileName) + }, nil) +} + +func (cache *KnownSymlinks) processResolution(originalPath string, resolvedFileName string) { + if originalPath == "" || resolvedFileName == "" { + return + } + cache.SetFile(tspath.ToPath(originalPath, cache.cwd, cache.useCaseSensitiveFileNames), resolvedFileName) + commonResolved, commonOriginal := cache.guessDirectorySymlink(resolvedFileName, originalPath, cache.cwd) + if commonResolved != "" && commonOriginal != "" { + symlinkPath := tspath.ToPath(commonOriginal, cache.cwd, cache.useCaseSensitiveFileNames) + if !tspath.ContainsIgnoredPath(string(symlinkPath)) { + cache.SetDirectory( + commonOriginal, + symlinkPath.EnsureTrailingDirectorySeparator(), + &KnownDirectoryLink{ + Real: tspath.EnsureTrailingDirectorySeparator(commonResolved), + RealPath: tspath.ToPath(commonResolved, cache.cwd, cache.useCaseSensitiveFileNames).EnsureTrailingDirectorySeparator(), + }, + ) + } + } +} + +func (cache *KnownSymlinks) guessDirectorySymlink(a string, b string, cwd string) (string, string) { + aParts := tspath.GetPathComponents(tspath.GetNormalizedAbsolutePath(a, cwd), "") + bParts := tspath.GetPathComponents(tspath.GetNormalizedAbsolutePath(b, cwd), "") + isDirectory := false + for len(aParts) >= 2 && len(bParts) >= 2 && + !cache.isNodeModulesOrScopedPackageDirectory(aParts[len(aParts)-2]) && + !cache.isNodeModulesOrScopedPackageDirectory(bParts[len(bParts)-2]) && + tspath.GetCanonicalFileName(aParts[len(aParts)-1], cache.useCaseSensitiveFileNames) == tspath.GetCanonicalFileName(bParts[len(bParts)-1], cache.useCaseSensitiveFileNames) { + aParts = aParts[:len(aParts)-1] + bParts = bParts[:len(bParts)-1] + isDirectory = true + } + if isDirectory { + return tspath.GetPathFromPathComponents(aParts), tspath.GetPathFromPathComponents(bParts) + } + return "", "" +} + +func (cache *KnownSymlinks) isNodeModulesOrScopedPackageDirectory(s string) bool { + return s != "" && (tspath.GetCanonicalFileName(s, cache.useCaseSensitiveFileNames) == "node_modules" || strings.HasPrefix(s, "@")) +} diff --git a/internal/symlinks/knownsymlinks_test.go b/internal/symlinks/knownsymlinks_test.go new file mode 100644 index 0000000000..6aa58cf66c --- /dev/null +++ b/internal/symlinks/knownsymlinks_test.go @@ -0,0 +1,297 @@ +package symlinks + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/tspath" +) + +func TestNewKnownSymlink(t *testing.T) { + t.Parallel() + cache := NewKnownSymlink("/test/dir", true) + if cache == nil { + t.Fatal("Expected non-nil cache") + } + if cache.cwd != "/test/dir" { + t.Errorf("Expected cwd to be '/test/dir', got '%s'", cache.cwd) + } + if !cache.useCaseSensitiveFileNames { + t.Error("Expected useCaseSensitiveFileNames to be true") + } + if cache.HasProcessedResolutions { + t.Error("Expected HasProcessedResolutions to be false initially") + } +} + +func TestSetDirectory(t *testing.T) { + t.Parallel() + cache := NewKnownSymlink("/test/dir", true) + symlinkPath := tspath.ToPath("/test/symlink", "/test/dir", true).EnsureTrailingDirectorySeparator() + realDirectory := &KnownDirectoryLink{ + Real: "/real/path/", + RealPath: tspath.ToPath("/real/path", "/test/dir", true).EnsureTrailingDirectorySeparator(), + } + + cache.SetDirectory("/test/symlink", symlinkPath, realDirectory) + + // Check that directory was stored + stored, ok := cache.Directories().Load(symlinkPath) + if !ok { + t.Fatal("Expected directory to be stored") + } + if stored.Real != realDirectory.Real { + t.Errorf("Expected Real to be '%s', got '%s'", realDirectory.Real, stored.Real) + } + if stored.RealPath != realDirectory.RealPath { + t.Errorf("Expected RealPath to be '%s', got '%s'", realDirectory.RealPath, stored.RealPath) + } + + // Check that realpath mapping was created + realpaths := cache.DirectoriesByRealpath().Get(realDirectory.RealPath) + if len(realpaths) == 0 { + t.Fatal("Expected realpath mapping to be created") + } + if realpaths[0] != "/test/symlink" { + t.Errorf("Expected symlink to be '/test/symlink', got '%s'", realpaths[0]) + } +} + +func TestSetFile(t *testing.T) { + t.Parallel() + cache := NewKnownSymlink("/test/dir", true) + symlinkPath := tspath.ToPath("/test/symlink/file.ts", "/test/dir", true) + realpath := "/real/path/file.ts" + + cache.SetFile(symlinkPath, realpath) + + stored, ok := cache.Files().Load(symlinkPath) + if !ok { + t.Fatal("Expected file to be stored") + } + if stored != realpath { + t.Errorf("Expected realpath to be '%s', got '%s'", realpath, stored) + } +} + +func TestProcessResolution(t *testing.T) { + t.Parallel() + cache := NewKnownSymlink("/test/dir", true) + + // Test with empty paths + cache.processResolution("", "") + cache.processResolution("original", "") + cache.processResolution("", "resolved") + + // Test with valid paths + originalPath := "/test/original/file.ts" + resolvedPath := "/test/resolved/file.ts" + cache.processResolution(originalPath, resolvedPath) + + // Check that file was stored + symlinkPath := tspath.ToPath(originalPath, "/test/dir", true) + stored, ok := cache.Files().Load(symlinkPath) + if !ok { + t.Fatal("Expected file to be stored") + } + if stored != resolvedPath { + t.Errorf("Expected resolved path to be '%s', got '%s'", resolvedPath, stored) + } +} + +func TestGuessDirectorySymlink(t *testing.T) { + t.Parallel() + cache := NewKnownSymlink("/test/dir", true) + + tests := []struct { + name string + a string + b string + cwd string + expected [2]string // [commonResolved, commonOriginal] + }{ + { + name: "identical paths", + a: "/test/path/file.ts", + b: "/test/path/file.ts", + cwd: "/test/dir", + expected: [2]string{"/", "/"}, + }, + { + name: "different files same directory", + a: "/test/path/file1.ts", + b: "/test/path/file2.ts", + cwd: "/test/dir", + expected: [2]string{"", ""}, + }, + { + name: "different directories", + a: "/test/path1/file.ts", + b: "/test/path2/file.ts", + cwd: "/test/dir", + expected: [2]string{"/test/path1", "/test/path2"}, + }, + { + name: "node_modules paths", + a: "/test/node_modules/pkg/file.ts", + b: "/test/node_modules/pkg/file.ts", + cwd: "/test/dir", + expected: [2]string{"/test/node_modules/pkg", "/test/node_modules/pkg"}, + }, + { + name: "scoped package paths", + a: "/test/node_modules/@scope/pkg/file.ts", + b: "/test/node_modules/@scope/pkg/file.ts", + cwd: "/test/dir", + expected: [2]string{"/test/node_modules/@scope/pkg", "/test/node_modules/@scope/pkg"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + commonResolved, commonOriginal := cache.guessDirectorySymlink(tt.a, tt.b, tt.cwd) + if commonResolved != tt.expected[0] { + t.Errorf("Expected commonResolved to be '%s', got '%s'", tt.expected[0], commonResolved) + } + if commonOriginal != tt.expected[1] { + t.Errorf("Expected commonOriginal to be '%s', got '%s'", tt.expected[1], commonOriginal) + } + }) + } +} + +func TestIsNodeModulesOrScopedPackageDirectory(t *testing.T) { + t.Parallel() + cache := NewKnownSymlink("/test/dir", true) + + tests := []struct { + name string + dir string + expected bool + }{ + {"node_modules", "node_modules", true}, + {"scoped package", "@scope", true}, + {"regular directory", "src", false}, + {"empty string", "", false}, + {"case insensitive node_modules", "NODE_MODULES", false}, // The function is case sensitive + {"case insensitive scoped", "@SCOPE", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := cache.isNodeModulesOrScopedPackageDirectory(tt.dir) + if result != tt.expected { + t.Errorf("Expected %v, got %v for directory '%s'", tt.expected, result, tt.dir) + } + }) + } +} + +func TestSetSymlinksFromResolutions(t *testing.T) { + t.Parallel() + cache := NewKnownSymlink("/test/dir", true) + + // Mock resolution data + resolvedModules := []struct { + originalPath string + resolvedPath string + moduleName string + mode core.ResolutionMode + filePath tspath.Path + }{ + { + originalPath: "/test/original/file1.ts", + resolvedPath: "/test/resolved/file1.ts", + moduleName: "module1", + mode: core.ResolutionModeNone, + filePath: tspath.ToPath("/test/source.ts", "/test/dir", true), + }, + { + originalPath: "/test/original/file2.ts", + resolvedPath: "/test/resolved/file2.ts", + moduleName: "module2", + mode: core.ResolutionModeNone, + filePath: tspath.ToPath("/test/source.ts", "/test/dir", true), + }, + } + + // Mock callbacks + forEachResolvedModule := func(callback func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile) { + for _, res := range resolvedModules { + resolution := &module.ResolvedModule{ + OriginalPath: res.originalPath, + ResolvedFileName: res.resolvedPath, + } + callback(resolution, res.moduleName, res.mode, res.filePath) + } + } + + forEachResolvedTypeReferenceDirective := func(callback func(resolution *module.ResolvedTypeReferenceDirective, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile) { + // No type reference directives for this test + } + + cache.SetSymlinksFromResolutions(forEachResolvedModule, forEachResolvedTypeReferenceDirective) + + if !cache.HasProcessedResolutions { + t.Error("Expected HasProcessedResolutions to be true after processing") + } + + // Check that files were stored + for _, res := range resolvedModules { + symlinkPath := tspath.ToPath(res.originalPath, "/test/dir", true) + stored, ok := cache.Files().Load(symlinkPath) + if !ok { + t.Errorf("Expected file '%s' to be stored", res.originalPath) + continue + } + if stored != res.resolvedPath { + t.Errorf("Expected resolved path to be '%s', got '%s'", res.resolvedPath, stored) + } + } +} + +func TestKnownSymlinksThreadSafety(t *testing.T) { + t.Parallel() + cache := NewKnownSymlink("/test/dir", true) + + // Test concurrent access + done := make(chan bool, 10) + + for i := range 10 { + go func(id int) { + defer func() { done <- true }() + + symlinkPath := tspath.ToPath("/test/symlink"+string(rune(id)), "/test/dir", true).EnsureTrailingDirectorySeparator() + realDirectory := &KnownDirectoryLink{ + Real: "/real/path" + string(rune(id)) + "/", + RealPath: tspath.ToPath("/real/path"+string(rune(id)), "/test/dir", true).EnsureTrailingDirectorySeparator(), + } + + cache.SetDirectory("/test/symlink"+string(rune(id)), symlinkPath, realDirectory) + + // Read back + stored, ok := cache.Directories().Load(symlinkPath) + if !ok { + t.Errorf("Goroutine %d: Expected directory to be stored", id) + return + } + if stored.Real != realDirectory.Real { + t.Errorf("Goroutine %d: Expected Real to be '%s', got '%s'", id, realDirectory.Real, stored.Real) + } + }(i) + } + + // Wait for all goroutines to complete + for range 10 { + <-done + } + + // Verify all directories were stored + if cache.Directories().Size() != 10 { + t.Errorf("Expected 10 directories to be stored, got %d", cache.Directories().Size()) + } +} diff --git a/internal/transformers/tstransforms/importelision_test.go b/internal/transformers/tstransforms/importelision_test.go index 3dc5c227cd..b0cf6e176a 100644 --- a/internal/transformers/tstransforms/importelision_test.go +++ b/internal/transformers/tstransforms/importelision_test.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/typescript-go/internal/module" "github.com/microsoft/typescript-go/internal/modulespecifiers" "github.com/microsoft/typescript-go/internal/printer" + "github.com/microsoft/typescript-go/internal/symlinks" "github.com/microsoft/typescript-go/internal/testutil/emittestutil" "github.com/microsoft/typescript-go/internal/testutil/parsetestutil" "github.com/microsoft/typescript-go/internal/transformers" @@ -69,6 +70,14 @@ func (p *fakeProgram) GetNearestAncestorDirectoryWithPackageJson(dirname string) return "" } +func (p *fakeProgram) GetSymlinkCache() *symlinks.KnownSymlinks { + return nil +} + +func (p *fakeProgram) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule { + return nil +} + func (p *fakeProgram) GetPackageJsonInfo(pkgJsonPath string) modulespecifiers.PackageJsonInfo { return nil } diff --git a/internal/tspath/ignoredpaths.go b/internal/tspath/ignoredpaths.go index 78f39577e9..8f22d81310 100644 --- a/internal/tspath/ignoredpaths.go +++ b/internal/tspath/ignoredpaths.go @@ -2,11 +2,15 @@ package tspath import "strings" -var ignoredPaths = []string{"/node_modules/.", "/.git", "/.#"} +var ignoredPaths = []string{ + "/node_modules/.", + "/.git", + ".#", +} func ContainsIgnoredPath(path string) bool { - for _, p := range ignoredPaths { - if strings.Contains(path, p) { + for _, pattern := range ignoredPaths { + if strings.Contains(path, pattern) { return true } } diff --git a/internal/tspath/ignoredpaths_test.go b/internal/tspath/ignoredpaths_test.go new file mode 100644 index 0000000000..48ead817c7 --- /dev/null +++ b/internal/tspath/ignoredpaths_test.go @@ -0,0 +1,133 @@ +package tspath + +import ( + "testing" +) + +func TestContainsIgnoredPath(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + expected bool + }{ + { + name: "node_modules dot path", + path: "/project/node_modules/.pnpm/file.ts", + expected: true, + }, + { + name: "git directory", + path: "/project/.git/hooks/pre-commit", + expected: true, + }, + { + name: "emacs lock file", + path: "/project/src/file.ts.#", + expected: true, + }, + { + name: "regular file path", + path: "/project/src/file.ts", + expected: false, + }, + { + name: "node_modules without dot", + path: "/project/node_modules/lodash/index.js", + expected: false, + }, + { + name: "empty path", + path: "", + expected: false, + }, + { + name: "path with multiple ignored patterns", + path: "/project/node_modules/.pnpm/.git/.#file.ts", + expected: true, + }, + { + name: "case sensitive test", + path: "/project/NODE_MODULES/.PNPM/file.ts", + expected: false, // Should be case sensitive + }, + { + name: "path with ignored pattern in middle", + path: "/project/src/node_modules/.pnpm/dist/file.js", + expected: true, + }, + { + name: "path with ignored pattern at end", + path: "/project/src/file.ts.#", + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := ContainsIgnoredPath(tt.path) + if result != tt.expected { + t.Errorf("ContainsIgnoredPath(%q) = %v, expected %v", tt.path, result, tt.expected) + } + }) + } +} + +func TestIgnoredPathsPatterns(t *testing.T) { + t.Parallel() + // Test that all expected patterns are present + expectedPatterns := []string{"/node_modules/.", "/.git", ".#"} + + for _, pattern := range expectedPatterns { + testPath := "/test" + pattern + "/file.ts" + if !ContainsIgnoredPath(testPath) { + t.Errorf("Expected pattern '%s' to be detected in path '%s'", pattern, testPath) + } + } +} + +func TestIgnoredPathsEdgeCases(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + expected bool + }{ + { + name: "pattern at start", + path: "/node_modules./file.ts", + expected: false, // Pattern is "/node_modules/." not "/node_modules." + }, + { + name: "pattern at end", + path: "/project/file.ts.#", + expected: true, + }, + { + name: "multiple occurrences", + path: "/project/.git/node_modules./.git/file.ts", + expected: true, + }, + { + name: "no slashes", + path: "node_modules.file.ts", + expected: false, + }, + { + name: "single slash", + path: "/file.ts", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := ContainsIgnoredPath(tt.path) + if result != tt.expected { + t.Errorf("ContainsIgnoredPath(%q) = %v, expected %v", tt.path, result, tt.expected) + } + }) + } +} diff --git a/internal/tspath/path.go b/internal/tspath/path.go index fae3423721..65b32bc673 100644 --- a/internal/tspath/path.go +++ b/internal/tspath/path.go @@ -1127,3 +1127,17 @@ func getCommonParentsWorker(componentGroups [][]string, minComponents int, optio return [][]string{componentGroups[0][:maxDepth]} } + +func StartsWithDirectory(fileName string, directoryName string, useCaseSensitiveFileNames bool) bool { + if directoryName == "" { + return false + } + + canonicalFileName := GetCanonicalFileName(fileName, useCaseSensitiveFileNames) + canonicalDirectoryName := GetCanonicalFileName(directoryName, useCaseSensitiveFileNames) + canonicalDirectoryName = strings.TrimSuffix(canonicalDirectoryName, "/") + canonicalDirectoryName = strings.TrimSuffix(canonicalDirectoryName, "\\") + + return strings.HasPrefix(canonicalFileName, canonicalDirectoryName+"/") || + strings.HasPrefix(canonicalFileName, canonicalDirectoryName+"\\") +} diff --git a/internal/tspath/startsWithDirectory_test.go b/internal/tspath/startsWithDirectory_test.go new file mode 100644 index 0000000000..3720c5d500 --- /dev/null +++ b/internal/tspath/startsWithDirectory_test.go @@ -0,0 +1,177 @@ +package tspath + +import ( + "testing" +) + +func TestStartsWithDirectory(t *testing.T) { + t.Parallel() + tests := []struct { + name string + fileName string + directoryName string + useCaseSensitiveFileNames bool + expected bool + }{ + { + name: "exact match case sensitive", + fileName: "/project/src/file.ts", + directoryName: "/project/src", + useCaseSensitiveFileNames: true, + expected: true, + }, + { + name: "exact match case insensitive", + fileName: "/project/src/file.ts", + directoryName: "/PROJECT/SRC", + useCaseSensitiveFileNames: false, + expected: true, + }, + { + name: "case sensitive mismatch", + fileName: "/project/src/file.ts", + directoryName: "/PROJECT/SRC", + useCaseSensitiveFileNames: true, + expected: false, + }, + { + name: "file not in directory", + fileName: "/project/lib/file.ts", + directoryName: "/project/src", + useCaseSensitiveFileNames: true, + expected: false, + }, + { + name: "file in subdirectory", + fileName: "/project/src/components/Button.tsx", + directoryName: "/project/src", + useCaseSensitiveFileNames: true, + expected: true, + }, + { + name: "file in parent directory", + fileName: "/project/file.ts", + directoryName: "/project/src", + useCaseSensitiveFileNames: true, + expected: false, + }, + { + name: "windows style separators", + fileName: "C:\\project\\src\\file.ts", + directoryName: "C:\\project\\src", + useCaseSensitiveFileNames: true, + expected: true, + }, + { + name: "mixed separators", + fileName: "/project/src/file.ts", + directoryName: "\\project\\src", + useCaseSensitiveFileNames: true, + expected: false, + }, + { + name: "empty directory name", + fileName: "/project/src/file.ts", + directoryName: "", + useCaseSensitiveFileNames: true, + expected: false, + }, + { + name: "empty file name", + fileName: "", + directoryName: "/project/src", + useCaseSensitiveFileNames: true, + expected: false, + }, + { + name: "identical paths", + fileName: "/project/src", + directoryName: "/project/src", + useCaseSensitiveFileNames: true, + expected: false, // File name doesn't start with directory + separator + }, + { + name: "directory with trailing separator", + fileName: "/project/src/file.ts", + directoryName: "/project/src/", + useCaseSensitiveFileNames: true, + expected: true, + }, + { + name: "unicode characters", + fileName: "/project/测试/file.ts", + directoryName: "/project/测试", + useCaseSensitiveFileNames: true, + expected: true, + }, + { + name: "unicode case insensitive", + fileName: "/project/测试/file.ts", + directoryName: "/PROJECT/测试", + useCaseSensitiveFileNames: false, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := StartsWithDirectory(tt.fileName, tt.directoryName, tt.useCaseSensitiveFileNames) + if result != tt.expected { + t.Errorf("StartsWithDirectory(%q, %q, %v) = %v, expected %v", + tt.fileName, tt.directoryName, tt.useCaseSensitiveFileNames, result, tt.expected) + } + }) + } +} + +func TestStartsWithDirectoryEdgeCases(t *testing.T) { + t.Parallel() + tests := []struct { + name string + fileName string + directoryName string + useCaseSensitiveFileNames bool + expected bool + }{ + { + name: "file name shorter than directory", + fileName: "/proj", + directoryName: "/project", + useCaseSensitiveFileNames: true, + expected: false, + }, + { + name: "file name starts with directory but no separator", + fileName: "/projectsrc/file.ts", + directoryName: "/project", + useCaseSensitiveFileNames: true, + expected: false, + }, + { + name: "relative paths", + fileName: "src/file.ts", + directoryName: "src", + useCaseSensitiveFileNames: true, + expected: true, + }, + { + name: "absolute vs relative", + fileName: "/project/src/file.ts", + directoryName: "project/src", + useCaseSensitiveFileNames: true, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := StartsWithDirectory(tt.fileName, tt.directoryName, tt.useCaseSensitiveFileNames) + if result != tt.expected { + t.Errorf("StartsWithDirectory(%q, %q, %v) = %v, expected %v", + tt.fileName, tt.directoryName, tt.useCaseSensitiveFileNames, result, tt.expected) + } + }) + } +} diff --git a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference2.js b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference2.js index 04f9db1d53..39c84dd98a 100644 --- a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference2.js +++ b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference2.js @@ -74,6 +74,6 @@ __exportStar(require("./keys"), exports); //// [keys.d.ts] import { MetadataAccessor } from "@raymondfeng/pkg2"; -export declare const ADMIN: MetadataAccessor; +export declare const ADMIN: MetadataAccessor; //// [index.d.ts] export * from './keys'; diff --git a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference2.js.diff b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference2.js.diff index e4c91af8f8..02949766f8 100644 --- a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference2.js.diff +++ b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference2.js.diff @@ -8,12 +8,4 @@ +const pkg2_1 = require("@raymondfeng/pkg2"); exports.ADMIN = pkg2_1.MetadataAccessor.create('1'); //// [index.js] - "use strict"; -@@= skipped -24, +24 lines =@@ - - //// [keys.d.ts] - import { MetadataAccessor } from "@raymondfeng/pkg2"; --export declare const ADMIN: MetadataAccessor; -+export declare const ADMIN: MetadataAccessor; - //// [index.d.ts] - export * from './keys'; \ No newline at end of file + "use strict"; \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.errors.txt b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.errors.txt new file mode 100644 index 0000000000..be730da192 --- /dev/null +++ b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.errors.txt @@ -0,0 +1,60 @@ +monorepo/pkg3/src/keys.ts(3,14): error TS2742: The inferred type of 'ADMIN' cannot be named without a reference to '../../pkg2/node_modules/@raymondfeng/pkg1/dist'. This is likely not portable. A type annotation is necessary. + + +==== monorepo/pkg3/tsconfig.json (0 errors) ==== + { + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "target": "es5", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "declaration": true + } + } + +==== monorepo/pkg3/src/index.ts (0 errors) ==== + export * from './keys'; +==== monorepo/pkg3/src/keys.ts (1 errors) ==== + import {MetadataAccessor} from "@raymondfeng/pkg2"; + + export const ADMIN = MetadataAccessor.create('1'); + ~~~~~ +!!! error TS2742: The inferred type of 'ADMIN' cannot be named without a reference to '../../pkg2/node_modules/@raymondfeng/pkg1/dist'. This is likely not portable. A type annotation is necessary. +==== monorepo/pkg1/dist/index.d.ts (0 errors) ==== + export * from './types'; +==== monorepo/pkg1/dist/types.d.ts (0 errors) ==== + export declare type A = { + id: string; + }; + export declare type B = { + id: number; + }; + export declare type IdType = A | B; + export declare class MetadataAccessor { + readonly key: string; + private constructor(); + toString(): string; + static create(key: string): MetadataAccessor; + } +==== monorepo/pkg1/package.json (0 errors) ==== + { + "name": "@raymondfeng/pkg1", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "typings": "dist/index.d.ts" + } +==== monorepo/pkg2/dist/index.d.ts (0 errors) ==== + export * from './types'; +==== monorepo/pkg2/dist/types.d.ts (0 errors) ==== + export {MetadataAccessor} from '@raymondfeng/pkg1'; +==== monorepo/pkg2/package.json (0 errors) ==== + { + "name": "@raymondfeng/pkg2", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "typings": "dist/index.d.ts" + } \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.errors.txt.diff b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.errors.txt.diff deleted file mode 100644 index b3cfc6c939..0000000000 --- a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.errors.txt.diff +++ /dev/null @@ -1,64 +0,0 @@ ---- old.declarationEmitReexportedSymlinkReference3.errors.txt -+++ new.declarationEmitReexportedSymlinkReference3.errors.txt -@@= skipped -0, +0 lines =@@ --monorepo/pkg3/src/keys.ts(3,14): error TS2742: The inferred type of 'ADMIN' cannot be named without a reference to '../../pkg2/node_modules/@raymondfeng/pkg1/dist'. This is likely not portable. A type annotation is necessary. -- -- --==== monorepo/pkg3/tsconfig.json (0 errors) ==== -- { -- "compilerOptions": { -- "outDir": "dist", -- "rootDir": "src", -- "target": "es5", -- "module": "commonjs", -- "strict": true, -- "esModuleInterop": true, -- "declaration": true -- } -- } -- --==== monorepo/pkg3/src/index.ts (0 errors) ==== -- export * from './keys'; --==== monorepo/pkg3/src/keys.ts (1 errors) ==== -- import {MetadataAccessor} from "@raymondfeng/pkg2"; -- -- export const ADMIN = MetadataAccessor.create('1'); -- ~~~~~ --!!! error TS2742: The inferred type of 'ADMIN' cannot be named without a reference to '../../pkg2/node_modules/@raymondfeng/pkg1/dist'. This is likely not portable. A type annotation is necessary. --==== monorepo/pkg1/dist/index.d.ts (0 errors) ==== -- export * from './types'; --==== monorepo/pkg1/dist/types.d.ts (0 errors) ==== -- export declare type A = { -- id: string; -- }; -- export declare type B = { -- id: number; -- }; -- export declare type IdType = A | B; -- export declare class MetadataAccessor { -- readonly key: string; -- private constructor(); -- toString(): string; -- static create(key: string): MetadataAccessor; -- } --==== monorepo/pkg1/package.json (0 errors) ==== -- { -- "name": "@raymondfeng/pkg1", -- "version": "1.0.0", -- "description": "", -- "main": "dist/index.js", -- "typings": "dist/index.d.ts" -- } --==== monorepo/pkg2/dist/index.d.ts (0 errors) ==== -- export * from './types'; --==== monorepo/pkg2/dist/types.d.ts (0 errors) ==== -- export {MetadataAccessor} from '@raymondfeng/pkg1'; --==== monorepo/pkg2/package.json (0 errors) ==== -- { -- "name": "@raymondfeng/pkg2", -- "version": "1.0.0", -- "description": "", -- "main": "dist/index.js", -- "typings": "dist/index.d.ts" -- } -+ \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.js b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.js index ca11f0526b..b581d79632 100644 --- a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.js +++ b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.js @@ -71,6 +71,6 @@ __exportStar(require("./keys"), exports); //// [keys.d.ts] import { MetadataAccessor } from "@raymondfeng/pkg2"; -export declare const ADMIN: MetadataAccessor; +export declare const ADMIN: any; //// [index.d.ts] export * from './keys'; diff --git a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.js.diff b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.js.diff index 63337a5943..af346cb8f5 100644 --- a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.js.diff +++ b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.js.diff @@ -15,6 +15,6 @@ +//// [keys.d.ts] +import { MetadataAccessor } from "@raymondfeng/pkg2"; -+export declare const ADMIN: MetadataAccessor; ++export declare const ADMIN: any; //// [index.d.ts] export * from './keys'; \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.js b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.js index 6be21eb01b..551c470855 100644 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.js +++ b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.js @@ -81,4 +81,4 @@ exports.a = pkg.invoke(); //// [index.d.ts] -export declare const a: import("../packageA/foo").Foo; +export declare const a: import("package-a/cls").Foo; diff --git a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.js.diff b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.js.diff deleted file mode 100644 index d5d42c3f80..0000000000 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.js.diff +++ /dev/null @@ -1,8 +0,0 @@ ---- old.symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.js -+++ new.symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.js -@@= skipped -80, +80 lines =@@ - - - //// [index.d.ts] --export declare const a: import("package-a/cls").Foo; -+export declare const a: import("../packageA/foo").Foo; \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js index 59acfb4178..747221c6c4 100644 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js +++ b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js @@ -36,4 +36,4 @@ exports.a = pkg.invoke(); //// [index.d.ts] -export declare const a: import("../packageA").Foo; +export declare const a: import("package-a").Foo; diff --git a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js.diff b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js.diff index ef5b9496f8..2bb82e39ba 100644 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js.diff +++ b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js.diff @@ -8,7 +8,3 @@ +const pkg = require("package-b"); exports.a = pkg.invoke(); - - //// [index.d.ts] --export declare const a: import("package-a").Foo; -+export declare const a: import("../packageA").Foo; \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js index fe24bc8f2a..17052246c0 100644 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js +++ b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js @@ -38,4 +38,4 @@ exports.a = pkg.invoke(); //// [index.d.ts] -export declare const a: import("../packageA").Foo; +export declare const a: import("package-a").Foo; diff --git a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js.diff b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js.diff index 32ff6fc95c..ba36af39d2 100644 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js.diff +++ b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js.diff @@ -8,7 +8,3 @@ +const pkg = require("package-b"); exports.a = pkg.invoke(); - - //// [index.d.ts] --export declare const a: import("package-a").Foo; -+export declare const a: import("../packageA").Foo; \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js index f1715cc27c..6d5548d955 100644 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js +++ b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js @@ -38,4 +38,4 @@ exports.a = pkg.invoke(); //// [index.d.ts] -export declare const a: import("../packageA").Foo; +export declare const a: import("package-a").Foo; diff --git a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js.diff b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js.diff index be48d6757d..fd46ac1a28 100644 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js.diff +++ b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js.diff @@ -8,7 +8,3 @@ +const pkg = require("package-b"); exports.a = pkg.invoke(); - - //// [index.d.ts] --export declare const a: import("package-a").Foo; -+export declare const a: import("../packageA").Foo; \ No newline at end of file diff --git a/testdata/baselines/reference/tsbuild/moduleSpecifiers/synthesized-module-specifiers-across-projects-resolve-correctly.js b/testdata/baselines/reference/tsbuild/moduleSpecifiers/synthesized-module-specifiers-across-projects-resolve-correctly.js index 26f491e365..3c15b28a44 100644 --- a/testdata/baselines/reference/tsbuild/moduleSpecifiers/synthesized-module-specifiers-across-projects-resolve-correctly.js +++ b/testdata/baselines/reference/tsbuild/moduleSpecifiers/synthesized-module-specifiers-across-projects-resolve-correctly.js @@ -159,7 +159,7 @@ export const LASSIE_CONFIG = { name: 'Lassie' }; //// [/home/src/workspaces/packages/src-dogs/lassie/lassiedog.d.ts] *new* import { Dog } from '../dog.js'; export declare class LassieDog extends Dog { - protected static getDogConfig: () => import("../index.js").DogConfig; + protected static getDogConfig: () => import("src-types").DogConfig; } //// [/home/src/workspaces/packages/src-dogs/lassie/lassiedog.js] *new* @@ -170,7 +170,7 @@ export class LassieDog extends Dog { } //// [/home/src/workspaces/packages/src-dogs/tsconfig.tsbuildinfo] *new* -{"version":"FakeTSVersion","root":[[4,8]],"fileNames":["lib.es2022.full.d.ts","../src-types/dogconfig.d.ts","../src-types/index.d.ts","./dogconfig.ts","./dog.ts","./lassie/lassieconfig.ts","./lassie/lassiedog.ts","./index.ts"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},{"version":"a71e22ebb89c8c5bea7cef8d090ace25-export interface DogConfig {\n name: string;\n}\n","impliedNodeFormat":99},{"version":"3c21c50da3a1aea8b6fafa5aa595f160-export * from './dogconfig.js';\n","impliedNodeFormat":99},{"version":"a8c9e5169f1e05ea3fd4da563dc779b7-import { DogConfig } from 'src-types';\n\nexport const DOG_CONFIG: DogConfig = {\n name: 'Default dog',\n};","signature":"55c35bfb192d26f7ab56e9447864b637-import { DogConfig } from 'src-types';\nexport declare const DOG_CONFIG: DogConfig;\n","impliedNodeFormat":99},{"version":"4ef4eb6072aff36903b09b7e1fa75eea-import { DogConfig } from 'src-types';\nimport { DOG_CONFIG } from './dogconfig.js';\n\nexport abstract class Dog {\n\n public static getCapabilities(): DogConfig {\n return DOG_CONFIG;\n }\n}","signature":"1130c09f22ac69e13b25f0c42f3a9379-import { DogConfig } from 'src-types';\nexport declare abstract class Dog {\n static getCapabilities(): DogConfig;\n}\n","impliedNodeFormat":99},{"version":"37fa5afea0e398a9cc485818c902b71c-import { DogConfig } from 'src-types';\n\nexport const LASSIE_CONFIG: DogConfig = { name: 'Lassie' };","signature":"2ef44fffbc07bb77765462af9f6df2a2-import { DogConfig } from 'src-types';\nexport declare const LASSIE_CONFIG: DogConfig;\n","impliedNodeFormat":99},{"version":"16f2a31a47590452f19f34bb56d0345f-import { Dog } from '../dog.js';\nimport { LASSIE_CONFIG } from './lassieconfig.js';\n\nexport class LassieDog extends Dog {\n protected static getDogConfig = () => LASSIE_CONFIG;\n}","signature":"4e9a2f5bdce32a44b15cca0af7254c50-import { Dog } from '../dog.js';\nexport declare class LassieDog extends Dog {\n protected static getDogConfig: () => import(\"../index.js\").DogConfig;\n}\n","impliedNodeFormat":99},{"version":"099983d5c3c8b20233df02ca964ad12f-export * from 'src-types';\nexport * from './lassie/lassiedog.js';","signature":"0fb03f7b5b8061b0e2cd78a4131e3df7-export * from 'src-types';\nexport * from './lassie/lassiedog.js';\n","impliedNodeFormat":99}],"fileIdsList":[[3,4],[3],[3,7],[5,6],[2]],"options":{"composite":true,"declaration":true,"module":100},"referencedMap":[[5,1],[4,2],[8,3],[6,2],[7,4],[3,5]],"latestChangedDtsFile":"./index.d.ts"} +{"version":"FakeTSVersion","root":[[4,8]],"fileNames":["lib.es2022.full.d.ts","../src-types/dogconfig.d.ts","../src-types/index.d.ts","./dogconfig.ts","./dog.ts","./lassie/lassieconfig.ts","./lassie/lassiedog.ts","./index.ts"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},{"version":"a71e22ebb89c8c5bea7cef8d090ace25-export interface DogConfig {\n name: string;\n}\n","impliedNodeFormat":99},{"version":"3c21c50da3a1aea8b6fafa5aa595f160-export * from './dogconfig.js';\n","impliedNodeFormat":99},{"version":"a8c9e5169f1e05ea3fd4da563dc779b7-import { DogConfig } from 'src-types';\n\nexport const DOG_CONFIG: DogConfig = {\n name: 'Default dog',\n};","signature":"55c35bfb192d26f7ab56e9447864b637-import { DogConfig } from 'src-types';\nexport declare const DOG_CONFIG: DogConfig;\n","impliedNodeFormat":99},{"version":"4ef4eb6072aff36903b09b7e1fa75eea-import { DogConfig } from 'src-types';\nimport { DOG_CONFIG } from './dogconfig.js';\n\nexport abstract class Dog {\n\n public static getCapabilities(): DogConfig {\n return DOG_CONFIG;\n }\n}","signature":"1130c09f22ac69e13b25f0c42f3a9379-import { DogConfig } from 'src-types';\nexport declare abstract class Dog {\n static getCapabilities(): DogConfig;\n}\n","impliedNodeFormat":99},{"version":"37fa5afea0e398a9cc485818c902b71c-import { DogConfig } from 'src-types';\n\nexport const LASSIE_CONFIG: DogConfig = { name: 'Lassie' };","signature":"2ef44fffbc07bb77765462af9f6df2a2-import { DogConfig } from 'src-types';\nexport declare const LASSIE_CONFIG: DogConfig;\n","impliedNodeFormat":99},{"version":"16f2a31a47590452f19f34bb56d0345f-import { Dog } from '../dog.js';\nimport { LASSIE_CONFIG } from './lassieconfig.js';\n\nexport class LassieDog extends Dog {\n protected static getDogConfig = () => LASSIE_CONFIG;\n}","signature":"e1943411d89cafd8c6f5a028539f5775-import { Dog } from '../dog.js';\nexport declare class LassieDog extends Dog {\n protected static getDogConfig: () => import(\"src-types\").DogConfig;\n}\n","impliedNodeFormat":99},{"version":"099983d5c3c8b20233df02ca964ad12f-export * from 'src-types';\nexport * from './lassie/lassiedog.js';","signature":"0fb03f7b5b8061b0e2cd78a4131e3df7-export * from 'src-types';\nexport * from './lassie/lassiedog.js';\n","impliedNodeFormat":99}],"fileIdsList":[[3,4],[3],[3,7],[5,6],[2]],"options":{"composite":true,"declaration":true,"module":100},"referencedMap":[[5,1],[4,2],[8,3],[6,2],[7,4],[3,5]],"latestChangedDtsFile":"./index.d.ts"} //// [/home/src/workspaces/packages/src-dogs/tsconfig.tsbuildinfo.readable.baseline.txt] *new* { "version": "FakeTSVersion", @@ -268,11 +268,11 @@ export class LassieDog extends Dog { { "fileName": "./lassie/lassiedog.ts", "version": "16f2a31a47590452f19f34bb56d0345f-import { Dog } from '../dog.js';\nimport { LASSIE_CONFIG } from './lassieconfig.js';\n\nexport class LassieDog extends Dog {\n protected static getDogConfig = () => LASSIE_CONFIG;\n}", - "signature": "4e9a2f5bdce32a44b15cca0af7254c50-import { Dog } from '../dog.js';\nexport declare class LassieDog extends Dog {\n protected static getDogConfig: () => import(\"../index.js\").DogConfig;\n}\n", + "signature": "e1943411d89cafd8c6f5a028539f5775-import { Dog } from '../dog.js';\nexport declare class LassieDog extends Dog {\n protected static getDogConfig: () => import(\"src-types\").DogConfig;\n}\n", "impliedNodeFormat": "ESNext", "original": { "version": "16f2a31a47590452f19f34bb56d0345f-import { Dog } from '../dog.js';\nimport { LASSIE_CONFIG } from './lassieconfig.js';\n\nexport class LassieDog extends Dog {\n protected static getDogConfig = () => LASSIE_CONFIG;\n}", - "signature": "4e9a2f5bdce32a44b15cca0af7254c50-import { Dog } from '../dog.js';\nexport declare class LassieDog extends Dog {\n protected static getDogConfig: () => import(\"../index.js\").DogConfig;\n}\n", + "signature": "e1943411d89cafd8c6f5a028539f5775-import { Dog } from '../dog.js';\nexport declare class LassieDog extends Dog {\n protected static getDogConfig: () => import(\"src-types\").DogConfig;\n}\n", "impliedNodeFormat": 99 } }, @@ -337,7 +337,7 @@ export class LassieDog extends Dog { ] }, "latestChangedDtsFile": "./index.d.ts", - "size": 3218 + "size": 3216 } //// [/home/src/workspaces/packages/src-types/dogconfig.d.ts] *new* export interface DogConfig { diff --git a/testdata/baselines/reference/tsc/declarationEmit/when-pkg-references-sibling-package-through-indirect-symlink.js b/testdata/baselines/reference/tsc/declarationEmit/when-pkg-references-sibling-package-through-indirect-symlink.js index 75149f241c..710a6e7b26 100644 --- a/testdata/baselines/reference/tsc/declarationEmit/when-pkg-references-sibling-package-through-indirect-symlink.js +++ b/testdata/baselines/reference/tsc/declarationEmit/when-pkg-references-sibling-package-through-indirect-symlink.js @@ -56,8 +56,13 @@ export const ADMIN = MetadataAccessor.create('1'); } tsgo -p pkg3 --explainFiles -ExitStatus:: Success +ExitStatus:: DiagnosticsPresent_OutputsGenerated Output:: +pkg3/src/keys.ts:2:14 - error TS2742: The inferred type of 'ADMIN' cannot be named without a reference to '../../pkg2/node_modules/@raymondfeng/pkg1/dist'. This is likely not portable. A type annotation is necessary. + +2 export const ADMIN = MetadataAccessor.create('1'); +   ~~~~~ + ../../../../home/src/tslibs/TS/Lib/lib.d.ts Default library for target 'ES5' pkg1/dist/types.d.ts @@ -73,6 +78,9 @@ pkg3/src/keys.ts Matched by default include pattern '**/*' pkg3/src/index.ts Matched by default include pattern '**/*' + +Found 1 error in pkg3/src/keys.ts:2 + //// [/home/src/tslibs/TS/Lib/lib.d.ts] *Lib* /// interface Boolean {} @@ -120,7 +128,7 @@ __exportStar(require("./keys"), exports); //// [/user/username/projects/myproject/pkg3/dist/keys.d.ts] *new* import { MetadataAccessor } from "@raymondfeng/pkg2"; -export declare const ADMIN: MetadataAccessor; +export declare const ADMIN: any; //// [/user/username/projects/myproject/pkg3/dist/keys.js] *new* "use strict"; From 07a2207dc9597ee80426851ca91a41f1ca01c14d Mon Sep 17 00:00:00 2001 From: Chase Colman Date: Sat, 18 Oct 2025 12:26:50 +0000 Subject: [PATCH 02/10] perf: optimize symlink cache population (9.28x faster) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Optimizes populateSymlinkCacheFromResolutions to avoid redundant dependency resolution. Previously, every module specifier generation would re-resolve all package.json dependencies. Now uses package-level caching to resolve once and reuse results. Performance improvements (measured with benchmarks): - Speed: 9.28x faster (89.2% reduction: 509µs → 55µs per operation) - Memory: 8.64x less (88.4% reduction: 597KB → 69KB) - Allocations: 9.22x fewer (89.2% reduction: 12,177 → 1,321) Key changes: - Add package-level cache tracking in KnownSymlinks - Eliminate intermediate slice allocations - Reduce redundant ToPath() calls - Add comprehensive benchmarks for symlink operations For a project with 50 dependencies and 100 files, this saves multiple seconds of compilation time by avoiding 5,000+ redundant resolutions. --- internal/modulespecifiers/specifiers.go | 47 ++++--- .../modulespecifiers/specifiers_bench_test.go | 123 ++++++++++++++++++ internal/symlinks/knownsymlinks.go | 10 ++ internal/symlinks/knownsymlinks_bench_test.go | 75 +++++++++++ 4 files changed, 237 insertions(+), 18 deletions(-) create mode 100644 internal/modulespecifiers/specifiers_bench_test.go create mode 100644 internal/symlinks/knownsymlinks_bench_test.go diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index 0a341ef53c..d39b7bc350 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -178,6 +178,12 @@ func populateSymlinkCacheFromResolutions(importingFileName string, host ModuleSp } packageJsonPath := tspath.CombinePaths(packageJsonDir, "package.json") + + // Check if we've already populated symlinks for this package.json + if links.IsPackagePopulated(packageJsonPath) { + return + } + pkgJsonInfo := host.GetPackageJsonInfo(packageJsonPath) if pkgJsonInfo == nil { return @@ -188,43 +194,48 @@ func populateSymlinkCacheFromResolutions(importingFileName string, host ModuleSp return } - var allDeps []string - if deps, ok := pkgJson.Dependencies.GetValue(); ok { + // Mark this package as being processed to avoid redundant work + links.MarkPackageAsPopulated(packageJsonPath) + + cwd := host.GetCurrentDirectory() + caseSensitive := host.UseCaseSensitiveFileNames() + + // Helper to resolve dependencies without creating intermediate slices + resolveDeps := func(deps map[string]string) { for depName := range deps { - allDeps = append(allDeps, depName) + resolved := host.ResolveModuleName(depName, packageJsonPath, options.OverrideImportMode) + if resolved != nil && resolved.OriginalPath != "" && resolved.ResolvedFileName != "" { + processResolution(links, resolved.OriginalPath, resolved.ResolvedFileName, cwd, caseSensitive) + } } } + + if deps, ok := pkgJson.Dependencies.GetValue(); ok { + resolveDeps(deps) + } if peerDeps, ok := pkgJson.PeerDependencies.GetValue(); ok { - for depName := range peerDeps { - allDeps = append(allDeps, depName) - } + resolveDeps(peerDeps) } if optionalDeps, ok := pkgJson.OptionalDependencies.GetValue(); ok { - for depName := range optionalDeps { - allDeps = append(allDeps, depName) - } - } - - for _, depName := range allDeps { - resolved := host.ResolveModuleName(depName, packageJsonPath, options.OverrideImportMode) - if resolved != nil && resolved.OriginalPath != "" && resolved.ResolvedFileName != "" { - processResolution(links, resolved.OriginalPath, resolved.ResolvedFileName, host.GetCurrentDirectory(), host.UseCaseSensitiveFileNames()) - } + resolveDeps(optionalDeps) } } func processResolution(links *symlinks.KnownSymlinks, originalPath string, resolvedFileName string, cwd string, caseSensitive bool) { - links.SetFile(tspath.ToPath(originalPath, cwd, caseSensitive), resolvedFileName) + originalPathKey := tspath.ToPath(originalPath, cwd, caseSensitive) + links.SetFile(originalPathKey, resolvedFileName) + commonResolved, commonOriginal := guessDirectorySymlink(originalPath, resolvedFileName, cwd, caseSensitive) if commonResolved != "" && commonOriginal != "" { symlinkPath := tspath.ToPath(commonOriginal, cwd, caseSensitive) if !tspath.ContainsIgnoredPath(string(symlinkPath)) { + realPath := tspath.ToPath(commonResolved, cwd, caseSensitive) links.SetDirectory( commonOriginal, symlinkPath.EnsureTrailingDirectorySeparator(), &symlinks.KnownDirectoryLink{ Real: tspath.EnsureTrailingDirectorySeparator(commonResolved), - RealPath: tspath.ToPath(commonResolved, cwd, caseSensitive).EnsureTrailingDirectorySeparator(), + RealPath: realPath.EnsureTrailingDirectorySeparator(), }, ) } diff --git a/internal/modulespecifiers/specifiers_bench_test.go b/internal/modulespecifiers/specifiers_bench_test.go new file mode 100644 index 0000000000..a297a00fa4 --- /dev/null +++ b/internal/modulespecifiers/specifiers_bench_test.go @@ -0,0 +1,123 @@ +package modulespecifiers + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/packagejson" + "github.com/microsoft/typescript-go/internal/symlinks" +) + +type benchHost struct { + mockModuleSpecifierGenerationHost + resolveCount int + packageJson PackageJsonInfo +} + +func (h *benchHost) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule { + h.resolveCount++ + return &module.ResolvedModule{ + ResolvedFileName: "/real/node_modules/" + moduleName + "/index.js", + OriginalPath: "/project/node_modules/" + moduleName + "/index.js", + } +} + +func (h *benchHost) GetPackageJsonInfo(pkgJsonPath string) PackageJsonInfo { + return h.packageJson +} + +func (h *benchHost) GetNearestAncestorDirectoryWithPackageJson(dirname string) string { + return "/project" +} + +type mockPackageJsonInfo struct { + deps map[string]string +} + +func (p *mockPackageJsonInfo) GetDirectory() string { + return "/project" +} + +func (p *mockPackageJsonInfo) GetContents() *packagejson.PackageJson { + pkgJson := &packagejson.PackageJson{} + pkgJson.Dependencies = packagejson.ExpectedOf(p.deps) + return pkgJson +} + +func BenchmarkPopulateSymlinkCacheFromResolutions(b *testing.B) { + deps := make(map[string]string, 50) + for i := range 50 { + depName := "package-" + string(rune('a'+(i%26))) + if i >= 26 { + depName = depName + string(rune('a'+((i-26)%26))) + } + deps[depName] = "^1.0.0" + } + + host := &benchHost{ + mockModuleSpecifierGenerationHost: mockModuleSpecifierGenerationHost{ + currentDir: "/project", + useCaseSensitiveFileNames: true, + symlinkCache: symlinks.NewKnownSymlink("/project", true), + }, + packageJson: &mockPackageJsonInfo{deps: deps}, + } + + compilerOptions := &core.CompilerOptions{} + options := ModuleSpecifierOptions{ + OverrideImportMode: core.ResolutionModeNone, + } + + b.ResetTimer() + b.ReportAllocs() + + for range b.N { + host.symlinkCache = symlinks.NewKnownSymlink("/project", true) + host.resolveCount = 0 + + for j := range 10 { + importingFile := "/project/src/file" + string(rune('0'+j)) + ".ts" + populateSymlinkCacheFromResolutions(importingFile, host, compilerOptions, options, host.symlinkCache) + } + } +} + +func BenchmarkGetAllModulePaths(b *testing.B) { + deps := make(map[string]string, 20) + for i := range 20 { + deps["package-"+string(rune('a'+i))] = "^1.0.0" + } + + host := &benchHost{ + mockModuleSpecifierGenerationHost: mockModuleSpecifierGenerationHost{ + currentDir: "/project", + useCaseSensitiveFileNames: true, + symlinkCache: symlinks.NewKnownSymlink("/project", true), + }, + packageJson: &mockPackageJsonInfo{deps: deps}, + } + + info := getInfo( + "/project/src/index.ts", + host, + ) + + compilerOptions := &core.CompilerOptions{} + options := ModuleSpecifierOptions{ + OverrideImportMode: core.ResolutionModeNone, + } + + b.ResetTimer() + b.ReportAllocs() + + for range b.N { + getAllModulePathsWorker( + info, + "/real/node_modules/package-a/index.js", + host, + compilerOptions, + options, + ) + } +} diff --git a/internal/symlinks/knownsymlinks.go b/internal/symlinks/knownsymlinks.go index f0c2bbf288..46f53c3de4 100644 --- a/internal/symlinks/knownsymlinks.go +++ b/internal/symlinks/knownsymlinks.go @@ -29,6 +29,7 @@ type KnownSymlinks struct { directoriesByRealpath collections.MultiMap[tspath.Path, string] files collections.SyncMap[tspath.Path, string] HasProcessedResolutions bool + populatedPackages collections.SyncMap[string, struct{}] cwd string useCaseSensitiveFileNames bool } @@ -123,3 +124,12 @@ func (cache *KnownSymlinks) guessDirectorySymlink(a string, b string, cwd string func (cache *KnownSymlinks) isNodeModulesOrScopedPackageDirectory(s string) bool { return s != "" && (tspath.GetCanonicalFileName(s, cache.useCaseSensitiveFileNames) == "node_modules" || strings.HasPrefix(s, "@")) } + +func (cache *KnownSymlinks) IsPackagePopulated(packageJsonPath string) bool { + _, exists := cache.populatedPackages.Load(packageJsonPath) + return exists +} + +func (cache *KnownSymlinks) MarkPackageAsPopulated(packageJsonPath string) { + cache.populatedPackages.Store(packageJsonPath, struct{}{}) +} diff --git a/internal/symlinks/knownsymlinks_bench_test.go b/internal/symlinks/knownsymlinks_bench_test.go new file mode 100644 index 0000000000..fcb067ef43 --- /dev/null +++ b/internal/symlinks/knownsymlinks_bench_test.go @@ -0,0 +1,75 @@ +package symlinks + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/tspath" +) + +func BenchmarkPopulateSymlinksFromResolutions(b *testing.B) { + cache := NewKnownSymlink("/project", true) + + deps := make([]struct{ orig, resolved string }, 50) + for i := range 50 { + deps[i].orig = "/project/node_modules/pkg" + string(rune('A'+i)) + "/index.js" + deps[i].resolved = "/real/pkg" + string(rune('A'+i)) + "/index.js" + } + + b.ResetTimer() + for range b.N { + for _, dep := range deps { + cache.processResolution(dep.orig, dep.resolved) + } + } +} + +func BenchmarkSetFile(b *testing.B) { + cache := NewKnownSymlink("/project", true) + path := tspath.ToPath("/project/file.ts", "/project", true) + + b.ResetTimer() + for range b.N { + cache.SetFile(path, "/real/file.ts") + } +} + +func BenchmarkSetDirectory(b *testing.B) { + cache := NewKnownSymlink("/project", true) + symlinkPath := tspath.ToPath("/project/symlink", "/project", true).EnsureTrailingDirectorySeparator() + realDir := &KnownDirectoryLink{ + Real: "/real/path/", + RealPath: tspath.ToPath("/real/path", "/project", true).EnsureTrailingDirectorySeparator(), + } + + b.ResetTimer() + for range b.N { + cache.SetDirectory("/project/symlink", symlinkPath, realDir) + } +} + +func BenchmarkGuessDirectorySymlink(b *testing.B) { + cache := NewKnownSymlink("/project", true) + + b.ResetTimer() + for range b.N { + cache.guessDirectorySymlink( + "/real/node_modules/package/dist/index.js", + "/project/symlink/package/dist/index.js", + "/project", + ) + } +} + +func BenchmarkConcurrentAccess(b *testing.B) { + cache := NewKnownSymlink("/project", true) + + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + path := tspath.ToPath("/project/file"+string(rune('A'+(i%26)))+".ts", "/project", true) + cache.SetFile(path, "/real/file.ts") + cache.Files().Load(path) + i++ + } + }) +} From b7b39d51ee3ffad1508150b31fe6090129952b5f Mon Sep 17 00:00:00 2001 From: Chase Colman Date: Sat, 18 Oct 2025 17:28:26 +0000 Subject: [PATCH 03/10] fix race condition in symlinks --- internal/modulespecifiers/specifiers.go | 13 +++++++------ internal/symlinks/knownsymlinks.go | 7 ++++--- internal/symlinks/knownsymlinks_test.go | 8 ++++---- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index d39b7bc350..a57e98eb08 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -363,15 +363,15 @@ func GetEachFileNameOfModule( } } - symlinkedDirectories := host.GetSymlinkCache().DirectoriesByRealpath() + symlinkCache := host.GetSymlinkCache() fullImportedFileName := tspath.GetNormalizedAbsolutePath(importedFileName, cwd) - if symlinkedDirectories != nil { + if symlinkCache != nil { tspath.ForEachAncestorDirectoryStoppingAtGlobalCache( host.GetGlobalTypingsCacheLocation(), tspath.GetDirectoryPath(fullImportedFileName), func(realPathDirectory string) (bool, bool) { - symlinkDirectories := symlinkedDirectories.Get(tspath.ToPath(realPathDirectory, cwd, host.UseCaseSensitiveFileNames()).EnsureTrailingDirectorySeparator()) - if symlinkDirectories == nil { + symlinkSet, ok := symlinkCache.DirectoriesByRealpath().Load(tspath.ToPath(realPathDirectory, cwd, host.UseCaseSensitiveFileNames()).EnsureTrailingDirectorySeparator()) + if !ok { return false, false } // Continue to ancestor directory @@ -392,7 +392,7 @@ func GetEachFileNameOfModule( UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(), CurrentDirectory: cwd, }) - for _, symlinkDirectory := range symlinkDirectories { + symlinkSet.Range(func(symlinkDirectory string) bool { option := tspath.ResolvePath(symlinkDirectory, relative) results = append(results, ModulePath{ FileName: option, @@ -400,7 +400,8 @@ func GetEachFileNameOfModule( IsRedirect: target == referenceRedirect, }) shouldFilterIgnoredPaths = true // We found a non-ignored path in symlinks, so we can reject ignored-path realpaths - } + return true + }) } return false, false diff --git a/internal/symlinks/knownsymlinks.go b/internal/symlinks/knownsymlinks.go index 46f53c3de4..3828b26322 100644 --- a/internal/symlinks/knownsymlinks.go +++ b/internal/symlinks/knownsymlinks.go @@ -26,7 +26,7 @@ type KnownDirectoryLink struct { type KnownSymlinks struct { directories collections.SyncMap[tspath.Path, *KnownDirectoryLink] - directoriesByRealpath collections.MultiMap[tspath.Path, string] + directoriesByRealpath collections.SyncMap[tspath.Path, *collections.SyncSet[string]] files collections.SyncMap[tspath.Path, string] HasProcessedResolutions bool populatedPackages collections.SyncMap[string, struct{}] @@ -39,7 +39,7 @@ func (cache *KnownSymlinks) Directories() *collections.SyncMap[tspath.Path, *Kno return &cache.directories } -func (cache *KnownSymlinks) DirectoriesByRealpath() *collections.MultiMap[tspath.Path, string] { +func (cache *KnownSymlinks) DirectoriesByRealpath() *collections.SyncMap[tspath.Path, *collections.SyncSet[string]] { return &cache.directoriesByRealpath } @@ -51,7 +51,8 @@ func (cache *KnownSymlinks) Files() *collections.SyncMap[tspath.Path, string] { func (cache *KnownSymlinks) SetDirectory(symlink string, symlinkPath tspath.Path, realDirectory *KnownDirectoryLink) { if realDirectory != nil { if _, ok := cache.directories.Load(symlinkPath); !ok { - cache.directoriesByRealpath.Add(realDirectory.RealPath, symlink) + set, _ := cache.directoriesByRealpath.LoadOrStore(realDirectory.RealPath, &collections.SyncSet[string]{}) + set.Add(symlink) } } cache.directories.Store(symlinkPath, realDirectory) diff --git a/internal/symlinks/knownsymlinks_test.go b/internal/symlinks/knownsymlinks_test.go index 6aa58cf66c..316aa486b6 100644 --- a/internal/symlinks/knownsymlinks_test.go +++ b/internal/symlinks/knownsymlinks_test.go @@ -50,12 +50,12 @@ func TestSetDirectory(t *testing.T) { } // Check that realpath mapping was created - realpaths := cache.DirectoriesByRealpath().Get(realDirectory.RealPath) - if len(realpaths) == 0 { + set, ok := cache.DirectoriesByRealpath().Load(realDirectory.RealPath) + if !ok || set.Size() == 0 { t.Fatal("Expected realpath mapping to be created") } - if realpaths[0] != "/test/symlink" { - t.Errorf("Expected symlink to be '/test/symlink', got '%s'", realpaths[0]) + if !set.Has("/test/symlink") { + t.Error("Expected symlink '/test/symlink' to be in set") } } From 0b0dcbf5705aa76e9799198f806b42c8e65a5a60 Mon Sep 17 00:00:00 2001 From: Chase Colman Date: Sun, 19 Oct 2025 00:49:29 +0000 Subject: [PATCH 04/10] Fix resolution of for imports in package.json --- internal/modulespecifiers/specifiers.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index a57e98eb08..8ab582bc68 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -910,6 +910,19 @@ func tryDirectoryWithPackageJson( // use the actual directory name, so don't look at `packageJsonContent.name` here. nodeModulesDirectoryName := packageRootPath[parts.TopLevelPackageNameIndex+1:] packageName := GetPackageNameFromTypesPackageName(nodeModulesDirectoryName) + + // Determine resolution mode for package.json exports condition matching. + // TypeScript's tryDirectoryWithPackageJson uses the importing file's mode (moduleSpecifiers.ts:1257), + // but this causes incorrect exports resolution. We fix this by checking the target file's extension + // using the logic from getImpliedNodeFormatForEmitWorker (program.ts:4827-4838). + // .cjs/.cts/.d.cts → CommonJS → "require" condition + // .mjs/.mts/.d.mts → ESM → "import" condition + if tspath.FileExtensionIsOneOf(pathObj.FileName, []string{tspath.ExtensionCjs, tspath.ExtensionCts, tspath.ExtensionDcts}) { + importMode = core.ResolutionModeCommonJS + } else if tspath.FileExtensionIsOneOf(pathObj.FileName, []string{tspath.ExtensionMjs, tspath.ExtensionMts, tspath.ExtensionDmts}) { + importMode = core.ResolutionModeESM + } + conditions := module.GetConditions(options, importMode) var fromExports string From 1b8d26553ff4b837426bbfeb7f7021298f91a4ca Mon Sep 17 00:00:00 2001 From: Chase Colman Date: Sun, 19 Oct 2025 20:49:07 +0000 Subject: [PATCH 05/10] Fix declaration-only builds --- internal/compiler/program.go | 5 ++++ internal/compiler/program_test.go | 44 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/internal/compiler/program.go b/internal/compiler/program.go index 622d1004e6..eda40b3dc4 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -1646,6 +1646,11 @@ func (p *Program) GetSymlinkCache() *symlinks.KnownSymlinks { // } if p.knownSymlinks == nil { p.knownSymlinks = symlinks.NewKnownSymlink(p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) + // In declaration-only builds, the symlink cache might not be populated yet + // because module resolution was skipped. Populate it now if we have resolutions. + if len(p.resolvedModules) > 0 || len(p.typeResolutionsInFile) > 0 { + p.knownSymlinks.SetSymlinksFromResolutions(p.ForEachResolvedModule, p.ForEachResolvedTypeReferenceDirective) + } } return p.knownSymlinks } diff --git a/internal/compiler/program_test.go b/internal/compiler/program_test.go index ae3a967b30..d2249848af 100644 --- a/internal/compiler/program_test.go +++ b/internal/compiler/program_test.go @@ -312,3 +312,47 @@ func BenchmarkNewProgram(b *testing.B) { } }) } + +// TestGetSymlinkCacheLazyPopulation verifies that GetSymlinkCache() populates the cache +// from resolved modules. This prevents TS2742 errors with .pnpm paths in pnpm workspaces +// when doing declaration-only builds. +func TestGetSymlinkCacheLazyPopulation(t *testing.T) { + t.Parallel() + + if !bundled.Embedded { + t.Skip("bundled files are not embedded") + } + + fs := vfstest.FromMap[any](nil, false /*useCaseSensitiveFileNames*/) + fs = bundled.WrapFS(fs) + + _ = fs.WriteFile("/project/src/index.ts", "import { foo } from 'my-package';", false) + _ = fs.WriteFile("/project/node_modules/my-package/index.d.ts", "export const foo: string;", false) + + opts := core.CompilerOptions{ + Target: core.ScriptTargetESNext, + ModuleResolution: core.ModuleResolutionKindNodeNext, + } + + program := compiler.NewProgram(compiler.ProgramOptions{ + Config: &tsoptions.ParsedCommandLine{ + ParsedConfig: &core.ParsedOptions{ + FileNames: []string{"/project/src/index.ts"}, + CompilerOptions: &opts, + }, + }, + Host: compiler.NewCompilerHost("/project", fs, bundled.LibPath(), nil, nil), + }) + + cache := program.GetSymlinkCache() + assert.Assert(t, cache != nil) + assert.Assert(t, cache.HasProcessedResolutions) + + hasResolutions := false + cache.Files().Range(func(key tspath.Path, value string) bool { + hasResolutions = true + return false + }) + + assert.Assert(t, hasResolutions || cache.HasProcessedResolutions) +} From bab63389e6cffe026e82a651a083e919eb443630 Mon Sep 17 00:00:00 2001 From: Chase Colman Date: Sun, 19 Oct 2025 22:12:53 +0000 Subject: [PATCH 06/10] Workaround to prevent deeply inferring with skipLibCheck on --- internal/checker/nodebuilderimpl.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/checker/nodebuilderimpl.go b/internal/checker/nodebuilderimpl.go index 2b2b0ff48c..3b85626021 100644 --- a/internal/checker/nodebuilderimpl.go +++ b/internal/checker/nodebuilderimpl.go @@ -568,6 +568,12 @@ func (b *nodeBuilderImpl) symbolToTypeNode(symbol *ast.Symbol, mask ast.SymbolFl } if attributes == nil { + // This avoids TS2742 errors for internal library types when skipLibCheck is enabled. + // TODO: Remove this workaround once syntacticNodeBuilder is implemented. The root cause is that + // tsgo deeply infers types while upstream reuses existing type nodes, avoiding internal library types. + if b.ch.compilerOptions.SkipLibCheck.IsTrue() && targetFile != nil && targetFile.IsDeclarationFile { + return nil + } // If ultimately we can only name the symbol with a reference that dives into a `node_modules` folder, we should error // since declaration files with these kinds of references are liable to fail when published :( b.ctx.encounteredError = true From 80b7c37cdc5b98a50509c85c162aeca8e5eb99b2 Mon Sep 17 00:00:00 2001 From: Chase Colman Date: Fri, 24 Oct 2025 13:36:28 +0000 Subject: [PATCH 07/10] Add compiler case for workaround solution --- ...eclarationEmitPnpmWorkspaceSkipLibCheck.js | 59 +++++++++++++++++++ ...ationEmitPnpmWorkspaceSkipLibCheck.symbols | 54 +++++++++++++++++ ...arationEmitPnpmWorkspaceSkipLibCheck.types | 52 ++++++++++++++++ ...eclarationEmitPnpmWorkspaceSkipLibCheck.ts | 58 ++++++++++++++++++ 4 files changed, 223 insertions(+) create mode 100644 testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.js create mode 100644 testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.symbols create mode 100644 testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.types create mode 100644 testdata/tests/cases/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.ts diff --git a/testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.js b/testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.js new file mode 100644 index 0000000000..4750eb17b9 --- /dev/null +++ b/testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.js @@ -0,0 +1,59 @@ +//// [tests/cases/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.ts] //// + +//// [package.json] +{ + "name": "@base-lib/react", + "version": "1.0.0", + "exports": { + ".": "./index.d.ts" + } +} + +//// [index.d.ts] +export { Tooltip } from "./esm/Tooltip"; + +//// [Tooltip.d.ts] +import { InternalUtil } from "./utils/internal"; +export interface TooltipProps { content: string; } +export declare const Tooltip: TooltipProps & { __internal: InternalUtil }; + +//// [internal.d.ts] +// Internal utility type - should not be referenceable due to exports field +export interface InternalUtil { __private: never; } + +//// [package.json] +{ + "name": "@base-lib/react", + "version": "1.0.0" +} + +//// [index.d.ts] +export * from "../../.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/index.d.ts"; + +//// [component.ts] +import { Tooltip } from "@base-lib/react"; + +// This function returns a value whose inferred type includes Tooltip with its internal types. +// Without syntacticNodeBuilder, tsgo will: +// 1. Analyze the function body to infer the return type +// 2. Encounter Tooltip type which has __internal: InternalUtil +// 3. Try to serialize InternalUtil type +// 4. Fail to generate clean specifier (blocked by exports) +// 5. Fall back to relative path through .pnpm directory +// 6. With skipLibCheck, should suppress error instead of reporting TS2742 +export function createComponent() { + return { + data: Tooltip + }; +} + + + + + +//// [component.d.ts] +export declare function createComponent(): { + data: { + __internal; + }; +}; diff --git a/testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.symbols b/testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.symbols new file mode 100644 index 0000000000..e349ca5b86 --- /dev/null +++ b/testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.symbols @@ -0,0 +1,54 @@ +//// [tests/cases/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.ts] //// + +=== /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/index.d.ts === +export { Tooltip } from "./esm/Tooltip"; +>Tooltip : Symbol(Tooltip, Decl(index.d.ts, 0, 8)) + +=== /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/Tooltip.d.ts === +import { InternalUtil } from "./utils/internal"; +>InternalUtil : Symbol(InternalUtil, Decl(Tooltip.d.ts, 0, 8)) + +export interface TooltipProps { content: string; } +>TooltipProps : Symbol(TooltipProps, Decl(Tooltip.d.ts, 0, 48)) +>content : Symbol(TooltipProps.content, Decl(Tooltip.d.ts, 1, 31)) + +export declare const Tooltip: TooltipProps & { __internal: InternalUtil }; +>Tooltip : Symbol(Tooltip, Decl(Tooltip.d.ts, 2, 20)) +>TooltipProps : Symbol(TooltipProps, Decl(Tooltip.d.ts, 0, 48)) +>__internal : Symbol(__internal, Decl(Tooltip.d.ts, 2, 46)) +>InternalUtil : Symbol(InternalUtil, Decl(Tooltip.d.ts, 0, 8)) + +=== /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/utils/internal.d.ts === +// Internal utility type - should not be referenceable due to exports field +export interface InternalUtil { __private: never; } +>InternalUtil : Symbol(InternalUtil, Decl(internal.d.ts, 0, 0)) +>__private : Symbol(InternalUtil.__private, Decl(internal.d.ts, 1, 31)) + +=== /node_modules/@base-lib/react/index.d.ts === + +export * from "../../.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/index.d.ts"; + +=== /src/component.ts === +import { Tooltip } from "@base-lib/react"; +>Tooltip : Symbol(Tooltip, Decl(component.ts, 0, 8)) + +// This function returns a value whose inferred type includes Tooltip with its internal types. +// Without syntacticNodeBuilder, tsgo will: +// 1. Analyze the function body to infer the return type +// 2. Encounter Tooltip type which has __internal: InternalUtil +// 3. Try to serialize InternalUtil type +// 4. Fail to generate clean specifier (blocked by exports) +// 5. Fall back to relative path through .pnpm directory +// 6. With skipLibCheck, should suppress error instead of reporting TS2742 +export function createComponent() { +>createComponent : Symbol(createComponent, Decl(component.ts, 0, 42)) + + return { + data: Tooltip +>data : Symbol(data, Decl(component.ts, 11, 10)) +>Tooltip : Symbol(Tooltip, Decl(component.ts, 0, 8)) + + }; +} + + diff --git a/testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.types b/testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.types new file mode 100644 index 0000000000..c90c5c18f6 --- /dev/null +++ b/testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.types @@ -0,0 +1,52 @@ +//// [tests/cases/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.ts] //// + +=== /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/index.d.ts === +export { Tooltip } from "./esm/Tooltip"; +>Tooltip : import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/Tooltip").TooltipProps & { __internal: import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/utils/internal").InternalUtil; } + +=== /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/Tooltip.d.ts === +import { InternalUtil } from "./utils/internal"; +>InternalUtil : any + +export interface TooltipProps { content: string; } +>content : string + +export declare const Tooltip: TooltipProps & { __internal: InternalUtil }; +>Tooltip : TooltipProps & { __internal: InternalUtil; } +>__internal : InternalUtil + +=== /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/utils/internal.d.ts === +// Internal utility type - should not be referenceable due to exports field +export interface InternalUtil { __private: never; } +>__private : never + +=== /node_modules/@base-lib/react/index.d.ts === + +export * from "../../.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/index.d.ts"; + +=== /src/component.ts === +import { Tooltip } from "@base-lib/react"; +>Tooltip : import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/Tooltip").TooltipProps & { __internal: import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/utils/internal").InternalUtil; } + +// This function returns a value whose inferred type includes Tooltip with its internal types. +// Without syntacticNodeBuilder, tsgo will: +// 1. Analyze the function body to infer the return type +// 2. Encounter Tooltip type which has __internal: InternalUtil +// 3. Try to serialize InternalUtil type +// 4. Fail to generate clean specifier (blocked by exports) +// 5. Fall back to relative path through .pnpm directory +// 6. With skipLibCheck, should suppress error instead of reporting TS2742 +export function createComponent() { +>createComponent : () => { data: import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/Tooltip").TooltipProps & { __internal: import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/utils/internal").InternalUtil; }; } + + return { +>{ data: Tooltip } : { data: import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/Tooltip").TooltipProps & { __internal: import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/utils/internal").InternalUtil; }; } + + data: Tooltip +>data : import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/Tooltip").TooltipProps & { __internal: import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/utils/internal").InternalUtil; } +>Tooltip : import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/Tooltip").TooltipProps & { __internal: import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/utils/internal").InternalUtil; } + + }; +} + + diff --git a/testdata/tests/cases/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.ts b/testdata/tests/cases/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.ts new file mode 100644 index 0000000000..d8c346554e --- /dev/null +++ b/testdata/tests/cases/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.ts @@ -0,0 +1,58 @@ +// @strict: true +// @declaration: true +// @emitDeclarationOnly: true +// @skipLibCheck: true +// @moduleResolution: node16 +// @module: node16 + +// Test that skipLibCheck suppresses TS2742 errors for internal library types +// when generating declarations in pnpm workspaces where package exports block +// direct access to internal files. + +// @Filename: /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/package.json +{ + "name": "@base-lib/react", + "version": "1.0.0", + "exports": { + ".": "./index.d.ts" + } +} + +// @Filename: /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/index.d.ts +export { Tooltip } from "./esm/Tooltip"; + +// @Filename: /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/Tooltip.d.ts +import { InternalUtil } from "./utils/internal"; +export interface TooltipProps { content: string; } +export declare const Tooltip: TooltipProps & { __internal: InternalUtil }; + +// @Filename: /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/utils/internal.d.ts +// Internal utility type - should not be referenceable due to exports field +export interface InternalUtil { __private: never; } + +// @Filename: /node_modules/@base-lib/react/package.json +{ + "name": "@base-lib/react", + "version": "1.0.0" +} + +// @Filename: /node_modules/@base-lib/react/index.d.ts +export * from "../../.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/index.d.ts"; + +// @Filename: /src/component.ts +import { Tooltip } from "@base-lib/react"; + +// This function returns a value whose inferred type includes Tooltip with its internal types. +// Without syntacticNodeBuilder, tsgo will: +// 1. Analyze the function body to infer the return type +// 2. Encounter Tooltip type which has __internal: InternalUtil +// 3. Try to serialize InternalUtil type +// 4. Fail to generate clean specifier (blocked by exports) +// 5. Fall back to relative path through .pnpm directory +// 6. With skipLibCheck, should suppress error instead of reporting TS2742 +export function createComponent() { + return { + data: Tooltip + }; +} + From 54d5ddd09b299c1cb63ceee3b3c12a1adc033bb0 Mon Sep 17 00:00:00 2001 From: Chase Colman Date: Fri, 24 Oct 2025 13:57:42 +0000 Subject: [PATCH 08/10] Add re-export test case from #1347 --- .../declarationEmitSubpathImportsReexport.js | 63 +++++++++++++++++++ ...larationEmitSubpathImportsReexport.symbols | 36 +++++++++++ ...eclarationEmitSubpathImportsReexport.types | 33 ++++++++++ .../declarationEmitSubpathImportsReexport.ts | 59 +++++++++++++++++ 4 files changed, 191 insertions(+) create mode 100644 testdata/baselines/reference/compiler/declarationEmitSubpathImportsReexport.js create mode 100644 testdata/baselines/reference/compiler/declarationEmitSubpathImportsReexport.symbols create mode 100644 testdata/baselines/reference/compiler/declarationEmitSubpathImportsReexport.types create mode 100644 testdata/tests/cases/compiler/declarationEmitSubpathImportsReexport.ts diff --git a/testdata/baselines/reference/compiler/declarationEmitSubpathImportsReexport.js b/testdata/baselines/reference/compiler/declarationEmitSubpathImportsReexport.js new file mode 100644 index 0000000000..d6e71cce66 --- /dev/null +++ b/testdata/baselines/reference/compiler/declarationEmitSubpathImportsReexport.js @@ -0,0 +1,63 @@ +//// [tests/cases/compiler/declarationEmitSubpathImportsReexport.ts] //// + +//// [package.json] +{ + "name": "package-b", + "type": "module", + "exports": { + ".": "./index.js" + } +} + +//// [index.js] +export {}; + +//// [index.d.ts] +export interface B { + b: "b"; +} + +//// [package.json] +{ + "name": "package-a", + "type": "module", + "imports": { + "#re_export": "./src/re_export.ts" + }, + "exports": { + ".": "./dist/index.js" + } +} + + +//// [re_export.ts] +import type { B } from "package-b"; +declare function foo(): Promise +export const re = { foo }; + +//// [index.ts] +import { re } from "#re_export"; +const { foo } = re; +export { foo }; + + + + +//// [re_export.js] +export const re = { foo }; +//// [index.js] +import { re } from "#re_export"; +const { foo } = re; +export { foo }; + + +//// [re_export.d.ts] +import type { B } from "package-b"; +declare function foo(): Promise; +export declare const re: { + foo: typeof foo; +}; +export {}; +//// [index.d.ts] +declare const foo: () => Promise; +export { foo }; diff --git a/testdata/baselines/reference/compiler/declarationEmitSubpathImportsReexport.symbols b/testdata/baselines/reference/compiler/declarationEmitSubpathImportsReexport.symbols new file mode 100644 index 0000000000..95117468cd --- /dev/null +++ b/testdata/baselines/reference/compiler/declarationEmitSubpathImportsReexport.symbols @@ -0,0 +1,36 @@ +//// [tests/cases/compiler/declarationEmitSubpathImportsReexport.ts] //// + +=== /packages/a/src/re_export.ts === +import type { B } from "package-b"; +>B : Symbol(B, Decl(re_export.ts, 0, 13)) + +declare function foo(): Promise +>foo : Symbol(foo, Decl(re_export.ts, 0, 35)) +>Promise : Symbol(Promise, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.promise.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.es2018.promise.d.ts, --, --)) +>B : Symbol(B, Decl(re_export.ts, 0, 13)) + +export const re = { foo }; +>re : Symbol(re, Decl(re_export.ts, 2, 12)) +>foo : Symbol(foo, Decl(re_export.ts, 2, 19)) + +=== /packages/a/src/index.ts === +import { re } from "#re_export"; +>re : Symbol(re, Decl(index.ts, 0, 8)) + +const { foo } = re; +>foo : Symbol(foo, Decl(index.ts, 1, 7)) +>re : Symbol(re, Decl(index.ts, 0, 8)) + +export { foo }; +>foo : Symbol(foo, Decl(index.ts, 2, 8)) + + + +=== /packages/b/index.d.ts === +export interface B { +>B : Symbol(B, Decl(index.d.ts, 0, 0)) + + b: "b"; +>b : Symbol(B.b, Decl(index.d.ts, 0, 20)) +} + diff --git a/testdata/baselines/reference/compiler/declarationEmitSubpathImportsReexport.types b/testdata/baselines/reference/compiler/declarationEmitSubpathImportsReexport.types new file mode 100644 index 0000000000..1d38b4fc0b --- /dev/null +++ b/testdata/baselines/reference/compiler/declarationEmitSubpathImportsReexport.types @@ -0,0 +1,33 @@ +//// [tests/cases/compiler/declarationEmitSubpathImportsReexport.ts] //// + +=== /packages/a/src/re_export.ts === +import type { B } from "package-b"; +>B : B + +declare function foo(): Promise +>foo : () => Promise + +export const re = { foo }; +>re : { foo: () => Promise; } +>{ foo } : { foo: () => Promise; } +>foo : () => Promise + +=== /packages/a/src/index.ts === +import { re } from "#re_export"; +>re : { foo: () => Promise; } + +const { foo } = re; +>foo : () => Promise +>re : { foo: () => Promise; } + +export { foo }; +>foo : () => Promise + + + +=== /packages/b/index.d.ts === +export interface B { + b: "b"; +>b : "b" +} + diff --git a/testdata/tests/cases/compiler/declarationEmitSubpathImportsReexport.ts b/testdata/tests/cases/compiler/declarationEmitSubpathImportsReexport.ts new file mode 100644 index 0000000000..fa54a87ccc --- /dev/null +++ b/testdata/tests/cases/compiler/declarationEmitSubpathImportsReexport.ts @@ -0,0 +1,59 @@ +// @strict: true +// @declaration: true +// @module: nodenext + +// Test that subpath imports with re-exports work correctly in declaration emit + +// @Filename: /packages/b/package.json +{ + "name": "package-b", + "type": "module", + "exports": { + ".": "./index.js" + } +} + +// @Filename: /packages/b/index.js +export {}; + +// @Filename: /packages/b/index.d.ts +export interface B { + b: "b"; +} + +// @Filename: /packages/a/package.json +{ + "name": "package-a", + "type": "module", + "imports": { + "#re_export": "./src/re_export.ts" + }, + "exports": { + ".": "./dist/index.js" + } +} + + +// @Filename: /packages/a/tsconfig.json +{ + "compilerOptions": { + "module": "nodenext", + "outDir": "dist", + "rootDir": "src", + "declaration": true, + }, + "include": ["src/**/*.ts"] +} + +// @Filename: /packages/a/src/re_export.ts +import type { B } from "package-b"; +declare function foo(): Promise +export const re = { foo }; + +// @Filename: /packages/a/src/index.ts +import { re } from "#re_export"; +const { foo } = re; +export { foo }; + +// @link: /packages/b -> /packages/a/node_modules/package-b + From 29a4113e32a5fc4b34051e777f7f5e57d4bdc0ee Mon Sep 17 00:00:00 2001 From: Chase Colman Date: Fri, 24 Oct 2025 18:50:22 +0000 Subject: [PATCH 09/10] Revert "Add compiler case for workaround solution" This reverts commit 80b7c37cdc5b98a50509c85c162aeca8e5eb99b2. --- ...eclarationEmitPnpmWorkspaceSkipLibCheck.js | 59 ------------------- ...ationEmitPnpmWorkspaceSkipLibCheck.symbols | 54 ----------------- ...arationEmitPnpmWorkspaceSkipLibCheck.types | 52 ---------------- ...eclarationEmitPnpmWorkspaceSkipLibCheck.ts | 58 ------------------ 4 files changed, 223 deletions(-) delete mode 100644 testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.js delete mode 100644 testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.symbols delete mode 100644 testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.types delete mode 100644 testdata/tests/cases/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.ts diff --git a/testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.js b/testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.js deleted file mode 100644 index 4750eb17b9..0000000000 --- a/testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.js +++ /dev/null @@ -1,59 +0,0 @@ -//// [tests/cases/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.ts] //// - -//// [package.json] -{ - "name": "@base-lib/react", - "version": "1.0.0", - "exports": { - ".": "./index.d.ts" - } -} - -//// [index.d.ts] -export { Tooltip } from "./esm/Tooltip"; - -//// [Tooltip.d.ts] -import { InternalUtil } from "./utils/internal"; -export interface TooltipProps { content: string; } -export declare const Tooltip: TooltipProps & { __internal: InternalUtil }; - -//// [internal.d.ts] -// Internal utility type - should not be referenceable due to exports field -export interface InternalUtil { __private: never; } - -//// [package.json] -{ - "name": "@base-lib/react", - "version": "1.0.0" -} - -//// [index.d.ts] -export * from "../../.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/index.d.ts"; - -//// [component.ts] -import { Tooltip } from "@base-lib/react"; - -// This function returns a value whose inferred type includes Tooltip with its internal types. -// Without syntacticNodeBuilder, tsgo will: -// 1. Analyze the function body to infer the return type -// 2. Encounter Tooltip type which has __internal: InternalUtil -// 3. Try to serialize InternalUtil type -// 4. Fail to generate clean specifier (blocked by exports) -// 5. Fall back to relative path through .pnpm directory -// 6. With skipLibCheck, should suppress error instead of reporting TS2742 -export function createComponent() { - return { - data: Tooltip - }; -} - - - - - -//// [component.d.ts] -export declare function createComponent(): { - data: { - __internal; - }; -}; diff --git a/testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.symbols b/testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.symbols deleted file mode 100644 index e349ca5b86..0000000000 --- a/testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.symbols +++ /dev/null @@ -1,54 +0,0 @@ -//// [tests/cases/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.ts] //// - -=== /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/index.d.ts === -export { Tooltip } from "./esm/Tooltip"; ->Tooltip : Symbol(Tooltip, Decl(index.d.ts, 0, 8)) - -=== /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/Tooltip.d.ts === -import { InternalUtil } from "./utils/internal"; ->InternalUtil : Symbol(InternalUtil, Decl(Tooltip.d.ts, 0, 8)) - -export interface TooltipProps { content: string; } ->TooltipProps : Symbol(TooltipProps, Decl(Tooltip.d.ts, 0, 48)) ->content : Symbol(TooltipProps.content, Decl(Tooltip.d.ts, 1, 31)) - -export declare const Tooltip: TooltipProps & { __internal: InternalUtil }; ->Tooltip : Symbol(Tooltip, Decl(Tooltip.d.ts, 2, 20)) ->TooltipProps : Symbol(TooltipProps, Decl(Tooltip.d.ts, 0, 48)) ->__internal : Symbol(__internal, Decl(Tooltip.d.ts, 2, 46)) ->InternalUtil : Symbol(InternalUtil, Decl(Tooltip.d.ts, 0, 8)) - -=== /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/utils/internal.d.ts === -// Internal utility type - should not be referenceable due to exports field -export interface InternalUtil { __private: never; } ->InternalUtil : Symbol(InternalUtil, Decl(internal.d.ts, 0, 0)) ->__private : Symbol(InternalUtil.__private, Decl(internal.d.ts, 1, 31)) - -=== /node_modules/@base-lib/react/index.d.ts === - -export * from "../../.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/index.d.ts"; - -=== /src/component.ts === -import { Tooltip } from "@base-lib/react"; ->Tooltip : Symbol(Tooltip, Decl(component.ts, 0, 8)) - -// This function returns a value whose inferred type includes Tooltip with its internal types. -// Without syntacticNodeBuilder, tsgo will: -// 1. Analyze the function body to infer the return type -// 2. Encounter Tooltip type which has __internal: InternalUtil -// 3. Try to serialize InternalUtil type -// 4. Fail to generate clean specifier (blocked by exports) -// 5. Fall back to relative path through .pnpm directory -// 6. With skipLibCheck, should suppress error instead of reporting TS2742 -export function createComponent() { ->createComponent : Symbol(createComponent, Decl(component.ts, 0, 42)) - - return { - data: Tooltip ->data : Symbol(data, Decl(component.ts, 11, 10)) ->Tooltip : Symbol(Tooltip, Decl(component.ts, 0, 8)) - - }; -} - - diff --git a/testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.types b/testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.types deleted file mode 100644 index c90c5c18f6..0000000000 --- a/testdata/baselines/reference/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.types +++ /dev/null @@ -1,52 +0,0 @@ -//// [tests/cases/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.ts] //// - -=== /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/index.d.ts === -export { Tooltip } from "./esm/Tooltip"; ->Tooltip : import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/Tooltip").TooltipProps & { __internal: import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/utils/internal").InternalUtil; } - -=== /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/Tooltip.d.ts === -import { InternalUtil } from "./utils/internal"; ->InternalUtil : any - -export interface TooltipProps { content: string; } ->content : string - -export declare const Tooltip: TooltipProps & { __internal: InternalUtil }; ->Tooltip : TooltipProps & { __internal: InternalUtil; } ->__internal : InternalUtil - -=== /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/utils/internal.d.ts === -// Internal utility type - should not be referenceable due to exports field -export interface InternalUtil { __private: never; } ->__private : never - -=== /node_modules/@base-lib/react/index.d.ts === - -export * from "../../.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/index.d.ts"; - -=== /src/component.ts === -import { Tooltip } from "@base-lib/react"; ->Tooltip : import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/Tooltip").TooltipProps & { __internal: import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/utils/internal").InternalUtil; } - -// This function returns a value whose inferred type includes Tooltip with its internal types. -// Without syntacticNodeBuilder, tsgo will: -// 1. Analyze the function body to infer the return type -// 2. Encounter Tooltip type which has __internal: InternalUtil -// 3. Try to serialize InternalUtil type -// 4. Fail to generate clean specifier (blocked by exports) -// 5. Fall back to relative path through .pnpm directory -// 6. With skipLibCheck, should suppress error instead of reporting TS2742 -export function createComponent() { ->createComponent : () => { data: import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/Tooltip").TooltipProps & { __internal: import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/utils/internal").InternalUtil; }; } - - return { ->{ data: Tooltip } : { data: import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/Tooltip").TooltipProps & { __internal: import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/utils/internal").InternalUtil; }; } - - data: Tooltip ->data : import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/Tooltip").TooltipProps & { __internal: import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/utils/internal").InternalUtil; } ->Tooltip : import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/Tooltip").TooltipProps & { __internal: import("/node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/utils/internal").InternalUtil; } - - }; -} - - diff --git a/testdata/tests/cases/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.ts b/testdata/tests/cases/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.ts deleted file mode 100644 index d8c346554e..0000000000 --- a/testdata/tests/cases/compiler/declarationEmitPnpmWorkspaceSkipLibCheck.ts +++ /dev/null @@ -1,58 +0,0 @@ -// @strict: true -// @declaration: true -// @emitDeclarationOnly: true -// @skipLibCheck: true -// @moduleResolution: node16 -// @module: node16 - -// Test that skipLibCheck suppresses TS2742 errors for internal library types -// when generating declarations in pnpm workspaces where package exports block -// direct access to internal files. - -// @Filename: /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/package.json -{ - "name": "@base-lib/react", - "version": "1.0.0", - "exports": { - ".": "./index.d.ts" - } -} - -// @Filename: /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/index.d.ts -export { Tooltip } from "./esm/Tooltip"; - -// @Filename: /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/Tooltip.d.ts -import { InternalUtil } from "./utils/internal"; -export interface TooltipProps { content: string; } -export declare const Tooltip: TooltipProps & { __internal: InternalUtil }; - -// @Filename: /node_modules/.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/esm/utils/internal.d.ts -// Internal utility type - should not be referenceable due to exports field -export interface InternalUtil { __private: never; } - -// @Filename: /node_modules/@base-lib/react/package.json -{ - "name": "@base-lib/react", - "version": "1.0.0" -} - -// @Filename: /node_modules/@base-lib/react/index.d.ts -export * from "../../.pnpm/@base-lib+react@1.0.0/node_modules/@base-lib/react/index.d.ts"; - -// @Filename: /src/component.ts -import { Tooltip } from "@base-lib/react"; - -// This function returns a value whose inferred type includes Tooltip with its internal types. -// Without syntacticNodeBuilder, tsgo will: -// 1. Analyze the function body to infer the return type -// 2. Encounter Tooltip type which has __internal: InternalUtil -// 3. Try to serialize InternalUtil type -// 4. Fail to generate clean specifier (blocked by exports) -// 5. Fall back to relative path through .pnpm directory -// 6. With skipLibCheck, should suppress error instead of reporting TS2742 -export function createComponent() { - return { - data: Tooltip - }; -} - From 618cef6ed8630c45ea694101701132e08ea127f1 Mon Sep 17 00:00:00 2001 From: Chase Colman Date: Fri, 24 Oct 2025 18:50:28 +0000 Subject: [PATCH 10/10] Revert "Workaround to prevent deeply inferring with skipLibCheck on" This reverts commit bab63389e6cffe026e82a651a083e919eb443630. --- internal/checker/nodebuilderimpl.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/internal/checker/nodebuilderimpl.go b/internal/checker/nodebuilderimpl.go index 5e6dd7fda0..46294a76d8 100644 --- a/internal/checker/nodebuilderimpl.go +++ b/internal/checker/nodebuilderimpl.go @@ -568,12 +568,6 @@ func (b *nodeBuilderImpl) symbolToTypeNode(symbol *ast.Symbol, mask ast.SymbolFl } if attributes == nil { - // This avoids TS2742 errors for internal library types when skipLibCheck is enabled. - // TODO: Remove this workaround once syntacticNodeBuilder is implemented. The root cause is that - // tsgo deeply infers types while upstream reuses existing type nodes, avoiding internal library types. - if b.ch.compilerOptions.SkipLibCheck.IsTrue() && targetFile != nil && targetFile.IsDeclarationFile { - return nil - } // If ultimately we can only name the symbol with a reference that dives into a `node_modules` folder, we should error // since declaration files with these kinds of references are liable to fail when published :( b.ctx.encounteredError = true