diff --git a/internal/checker/nodebuilderimpl.go b/internal/checker/nodebuilderimpl.go index f1f4c7ad2a..098eaf2a00 100644 --- a/internal/checker/nodebuilderimpl.go +++ b/internal/checker/nodebuilderimpl.go @@ -1211,6 +1211,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..95665f9b6b 100644 --- a/internal/compiler/emitHost.go +++ b/internal/compiler/emitHost.go @@ -6,9 +6,10 @@ import ( "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/modulespecifiers" "github.com/microsoft/typescript-go/internal/outputpaths" + "github.com/microsoft/typescript-go/internal/packagejson" "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" @@ -70,7 +71,7 @@ func (host *emitHost) GetNearestAncestorDirectoryWithPackageJson(dirname string) return host.program.GetNearestAncestorDirectoryWithPackageJson(dirname) } -func (host *emitHost) GetPackageJsonInfo(pkgJsonPath string) modulespecifiers.PackageJsonInfo { +func (host *emitHost) GetPackageJsonInfo(pkgJsonPath string) *packagejson.InfoCacheEntry { return host.program.GetPackageJsonInfo(pkgJsonPath) } @@ -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/fileloader.go b/internal/compiler/fileloader.go index e1e84650d3..6cba26889f 100644 --- a/internal/compiler/fileloader.go +++ b/internal/compiler/fileloader.go @@ -393,14 +393,20 @@ func (p *fileLoader) getDefaultLibFilePriority(a *ast.SourceFile) int { } func (p *fileLoader) loadSourceFileMetaData(fileName string) ast.SourceFileMetaData { - packageJsonScope := p.resolver.GetPackageJsonScopeIfApplicable(fileName) + packageJsonScope := p.resolver.GetPackageScopeForPath(fileName) + moduleResolutionKind := p.opts.Config.CompilerOptions().GetModuleResolutionKind() + var packageJsonType, packageJsonDirectory string if packageJsonScope.Exists() { packageJsonDirectory = packageJsonScope.PackageDirectory if value, ok := packageJsonScope.Contents.Type.GetValue(); ok { - packageJsonType = value + if !tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionMts, tspath.ExtensionCts, tspath.ExtensionMjs, tspath.ExtensionCjs}) && + core.ModuleResolutionKindNode16 <= moduleResolutionKind && moduleResolutionKind <= core.ModuleResolutionKindNodeNext || strings.Contains(fileName, "/node_modules/") { + packageJsonType = value + } } } + impliedNodeFormat := ast.GetImpliedNodeFormatForFile(fileName, packageJsonType) return ast.SourceFileMetaData{ PackageJsonType: packageJsonType, 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 cb6246b543..2372ed9ff5 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -17,12 +17,13 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/diagnostics" "github.com/microsoft/typescript-go/internal/module" - "github.com/microsoft/typescript-go/internal/modulespecifiers" "github.com/microsoft/typescript-go/internal/outputpaths" + "github.com/microsoft/typescript-go/internal/packagejson" "github.com/microsoft/typescript-go/internal/parser" "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,8 @@ type Program struct { // Cached unresolved imports for ATA unresolvedImportsOnce sync.Once unresolvedImports *collections.Set[string] + knownSymlinks *symlinks.KnownSymlinks + knownSymlinksOnce sync.Once } // FileExists implements checker.Program. @@ -93,7 +96,7 @@ func (p *Program) GetNearestAncestorDirectoryWithPackageJson(dirname string) str } // GetPackageJsonInfo implements checker.Program. -func (p *Program) GetPackageJsonInfo(pkgJsonPath string) modulespecifiers.PackageJsonInfo { +func (p *Program) GetPackageJsonInfo(pkgJsonPath string) *packagejson.InfoCacheEntry { scoped := p.resolver.GetPackageScopeForPath(pkgJsonPath) if scoped != nil && scoped.Exists() && scoped.PackageDirectory == tspath.GetDirectoryPath(pkgJsonPath) { return scoped @@ -232,6 +235,7 @@ func (p *Program) UpdateProgram(changedFilePath tspath.Path, newHost CompilerHos programDiagnostics: p.programDiagnostics, hasEmitBlockingDiagnostics: p.hasEmitBlockingDiagnostics, unresolvedImports: p.unresolvedImports, + knownSymlinks: p.knownSymlinks, } result.initCheckerPool() index := core.FindIndex(result.files, func(file *ast.SourceFile) bool { return file.Path() == newFile.Path() }) @@ -240,6 +244,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 } @@ -1635,6 +1643,86 @@ func (p *Program) SourceFileMayBeEmitted(sourceFile *ast.SourceFile, forceDtsEmi return sourceFileMayBeEmitted(sourceFile, p, forceDtsEmit) } +func (p *Program) GetSymlinkCache() *symlinks.KnownSymlinks { + p.knownSymlinksOnce.Do(func() { + if p.knownSymlinks == nil { + p.knownSymlinks = symlinks.NewKnownSymlink(p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) + + // Resolved modules store realpath information when they're resolved inside node_modules + if len(p.resolvedModules) > 0 || len(p.typeResolutionsInFile) > 0 { + p.knownSymlinks.SetSymlinksFromResolutions(p.ForEachResolvedModule, p.ForEachResolvedTypeReferenceDirective) + } + + // Check other dependencies for symlinks + var seenPackageJsons collections.Set[tspath.Path] + for filePath, meta := range p.sourceFileMetaDatas { + if meta.PackageJsonDirectory == "" || + !p.SourceFileMayBeEmitted(p.GetSourceFileByPath(filePath), false) || + !seenPackageJsons.AddIfAbsent(p.toPath(meta.PackageJsonDirectory)) { + continue + } + packageJsonName := tspath.CombinePaths(meta.PackageJsonDirectory, "package.json") + info := p.GetPackageJsonInfo(packageJsonName) + if info.GetContents() == nil { + continue + } + + for dep := range info.GetContents().GetRuntimeDependencyNames().Keys() { + // Skip work in common case: we already saved a symlink for this package directory + // in the node_modules adjacent to this package.json + possibleDirectoryPath := p.toPath(tspath.CombinePaths(meta.PackageJsonDirectory, "node_modules", dep)) + if p.knownSymlinks.HasDirectory(possibleDirectoryPath) { + continue + } + if !strings.HasPrefix(dep, "@types") { + possibleTypesDirectoryPath := p.toPath(tspath.CombinePaths(meta.PackageJsonDirectory, "node_modules", module.GetTypesPackageName(dep))) + if p.knownSymlinks.HasDirectory(possibleTypesDirectoryPath) { + continue + } + } + + if packageResolution := p.resolver.ResolvePackageDirectory(dep, packageJsonName, core.ResolutionModeCommonJS, nil); packageResolution.IsResolved() { + p.knownSymlinks.ProcessResolution( + tspath.CombinePaths(packageResolution.OriginalPath, "package.json"), + tspath.CombinePaths(packageResolution.ResolvedFileName, "package.json"), + ) + } + } + } + } + }) + 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/module/resolver.go b/internal/module/resolver.go index 1130604759..73aa90bdec 100644 --- a/internal/module/resolver.go +++ b/internal/module/resolver.go @@ -63,14 +63,15 @@ type resolutionState struct { tracer *tracer // request fields - name string - containingDirectory string - isConfigLookup bool - features NodeResolutionFeatures - esmMode bool - conditions []string - extensions extensions - compilerOptions *core.CompilerOptions + name string + containingDirectory string + isConfigLookup bool + features NodeResolutionFeatures + esmMode bool + conditions []string + extensions extensions + compilerOptions *core.CompilerOptions + resolvePackageDirectoryOnly bool // state fields candidateIsFromPackageJsonField bool @@ -177,19 +178,6 @@ func (r *Resolver) GetPackageScopeForPath(directory string) *packagejson.InfoCac return (&resolutionState{compilerOptions: r.compilerOptions, resolver: r}).getPackageScopeForPath(directory) } -func (r *Resolver) GetPackageJsonScopeIfApplicable(path string) *packagejson.InfoCacheEntry { - if tspath.FileExtensionIsOneOf(path, []string{tspath.ExtensionMts, tspath.ExtensionCts, tspath.ExtensionMjs, tspath.ExtensionCjs}) { - return nil - } - - moduleResolutionKind := r.compilerOptions.GetModuleResolutionKind() - if core.ModuleResolutionKindNode16 <= moduleResolutionKind && moduleResolutionKind <= core.ModuleResolutionKindNodeNext || strings.Contains(path, "/node_modules/") { - return r.GetPackageScopeForPath(tspath.GetDirectoryPath(path)) - } - - return nil -} - func (r *tracer) traceResolutionUsingProjectReference(redirectedReference ResolvedProjectReference) { if redirectedReference != nil && redirectedReference.CompilerOptions() != nil { r.write(diagnostics.Using_compiler_options_of_project_reference_redirect_0.Format(redirectedReference.ConfigName())) @@ -266,6 +254,17 @@ func (r *Resolver) ResolveModuleName(moduleName string, containingFile string, r return r.tryResolveFromTypingsLocation(moduleName, containingDirectory, result, traceBuilder), traceBuilder.getTraces() } +func (r *Resolver) ResolvePackageDirectory(moduleName string, containingFile string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) *ResolvedModule { + compilerOptions := GetCompilerOptionsWithRedirect(r.compilerOptions, redirectedReference) + containingDirectory := tspath.GetDirectoryPath(containingFile) + state := newResolutionState(moduleName, containingDirectory, false /*isTypeReferenceDirective*/, resolutionMode, compilerOptions, redirectedReference, r, nil) + state.resolvePackageDirectoryOnly = true + if result := state.loadModuleFromNearestNodeModulesDirectory(false /*typesScopeOnly*/); result != nil && result.path != "" { + return state.createResolvedModuleHandlingSymlink(result) + } + return nil +} + func (r *Resolver) tryResolveFromTypingsLocation(moduleName string, containingDirectory string, originalResult *ResolvedModule, traceBuilder *tracer) *ResolvedModule { if r.typingsLocation == "" || tspath.IsExternalModuleNameRelative(moduleName) || @@ -956,6 +955,16 @@ func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectory(ext extensi candidate := tspath.NormalizePath(tspath.CombinePaths(nodeModulesDirectory, moduleName)) packageName, rest := ParsePackageName(moduleName) packageDirectory := tspath.CombinePaths(nodeModulesDirectory, packageName) + if packageName == "" { + packageDirectory = candidate + } + + if r.resolvePackageDirectoryOnly { + if r.resolver.host.FS().DirectoryExists(packageDirectory) { + return &resolved{path: packageDirectory} + } + return continueSearching() + } var rootPackageInfo *packagejson.InfoCacheEntry // First look for a nested package.json, as in `node_modules/foo/bar/package.json` diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index d5a4c10767..3f171a47ee 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -64,8 +64,8 @@ func GetModuleSpecifiersWithInfo( getInfo(host.GetSourceOfProjectReferenceIfOutputIncluded(importingSourceFile), host), moduleSourceFile.FileName(), host, - // compilerOptions, - // options, + compilerOptions, + options, ) return computeModuleSpecifiers( @@ -163,7 +163,7 @@ 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); // } @@ -174,28 +174,9 @@ 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); - // // } - // // } - // } - allFileNames := make(map[string]ModulePath) paths := GetEachFileNameOfModule(info.ImportingSourceFileName, importedFileName, host, true) for _, p := range paths { @@ -231,16 +212,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 +253,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 +264,51 @@ 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; - // } - // }); - // }, - // ); + symlinkCache := host.GetSymlinkCache() + fullImportedFileName := tspath.GetNormalizedAbsolutePath(importedFileName, cwd) + if symlinkCache != nil { + tspath.ForEachAncestorDirectoryStoppingAtGlobalCache( + host.GetGlobalTypingsCacheLocation(), + tspath.GetDirectoryPath(fullImportedFileName), + func(realPathDirectory string) (bool, bool) { + symlinkSet, ok := symlinkCache.DirectoriesByRealpath().Load(tspath.ToPath(realPathDirectory, cwd, host.UseCaseSensitiveFileNames()).EnsureTrailingDirectorySeparator()) + if !ok { + 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, + }) + symlinkSet.Range(func(symlinkDirectory string) bool { + 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 true + }) + } + + return false, false + }, + ) + } if preferSymlinks { for _, p := range targets { @@ -810,6 +809,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 diff --git a/internal/modulespecifiers/specifiers_bench_test.go b/internal/modulespecifiers/specifiers_bench_test.go new file mode 100644 index 0000000000..7174c8aefa --- /dev/null +++ b/internal/modulespecifiers/specifiers_bench_test.go @@ -0,0 +1,228 @@ +package modulespecifiers + +import ( + "strings" + "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" + "github.com/microsoft/typescript-go/internal/tspath" +) + +func populateSymlinkCacheFromResolutions(importingFileName string, host *benchHost, 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 + } + + cwd := host.GetCurrentDirectory() + caseSensitive := host.UseCaseSensitiveFileNames() + + // Helper to resolve dependencies without creating intermediate slices + resolveDeps := func(deps map[string]string) { + for depName := range deps { + 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 { + resolveDeps(peerDeps) + } + if optionalDeps, ok := pkgJson.OptionalDependencies.GetValue(); ok { + resolveDeps(optionalDeps) + } +} + +func processResolution(links *symlinks.KnownSymlinks, originalPath string, resolvedFileName string, cwd string, caseSensitive bool) { + 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: realPath.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, "@")) +} + +type benchHost struct { + mockModuleSpecifierGenerationHost + resolveCount int + packageJson *packagejson.InfoCacheEntry +} + +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) *packagejson.InfoCacheEntry { + 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: &packagejson.InfoCacheEntry{ + PackageDirectory: "/project", + Contents: &packagejson.PackageJson{ + Fields: packagejson.Fields{ + DependencyFields: packagejson.DependencyFields{ + Dependencies: packagejson.ExpectedOf(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: &packagejson.InfoCacheEntry{ + PackageDirectory: "/project", + Contents: &packagejson.PackageJson{ + Fields: packagejson.Fields{ + DependencyFields: packagejson.DependencyFields{ + Dependencies: packagejson.ExpectedOf(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/modulespecifiers/specifiers_test.go b/internal/modulespecifiers/specifiers_test.go new file mode 100644 index 0000000000..96f98252fd --- /dev/null +++ b/internal/modulespecifiers/specifiers_test.go @@ -0,0 +1,258 @@ +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/packagejson" + "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) *packagejson.InfoCacheEntry { + 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 c83dde2d83..e164cc15da 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" ) @@ -38,14 +39,9 @@ type ModulePath struct { IsRedirect bool } -type PackageJsonInfo interface { - GetDirectory() string - GetContents() *packagejson.PackageJson -} - 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 @@ -59,7 +55,7 @@ type ModuleSpecifierGenerationHost interface { FileExists(path string) bool GetNearestAncestorDirectoryWithPackageJson(dirname string) string - GetPackageJsonInfo(pkgJsonPath string) PackageJsonInfo + GetPackageJsonInfo(pkgJsonPath string) *packagejson.InfoCacheEntry GetDefaultResolutionModeForFile(file ast.HasFileName) core.ResolutionMode GetResolvedModuleFromModuleSpecifier(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) *module.ResolvedModule GetModeForUsageLocation(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) core.ResolutionMode diff --git a/internal/packagejson/packagejson.go b/internal/packagejson/packagejson.go index 7ada92993e..191a639185 100644 --- a/internal/packagejson/packagejson.go +++ b/internal/packagejson/packagejson.go @@ -3,6 +3,7 @@ package packagejson import ( json "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" + "github.com/microsoft/typescript-go/internal/collections" ) type HeaderFields struct { @@ -55,6 +56,27 @@ func (df *DependencyFields) HasDependency(name string) bool { return false } +func (df *DependencyFields) GetRuntimeDependencyNames() *collections.Set[string] { + var count int + deps, _ := df.Dependencies.GetValue() + count += len(deps) + peerDeps, _ := df.PeerDependencies.GetValue() + count += len(peerDeps) + optDeps, _ := df.OptionalDependencies.GetValue() + count += len(optDeps) + names := collections.NewSetWithSizeHint[string](count) + for name := range deps { + names.Add(name) + } + for name := range peerDeps { + names.Add(name) + } + for name := range optDeps { + names.Add(name) + } + return names +} + type Fields struct { HeaderFields PathFields diff --git a/internal/symlinks/knownsymlinks.go b/internal/symlinks/knownsymlinks.go new file mode 100644 index 0000000000..712d858aa6 --- /dev/null +++ b/internal/symlinks/knownsymlinks.go @@ -0,0 +1,123 @@ +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/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.SyncMap[tspath.Path, *collections.SyncSet[string]] + files collections.SyncMap[tspath.Path, string] + cwd string + useCaseSensitiveFileNames bool +} + +func (cache *KnownSymlinks) HasDirectory(symlinkPath tspath.Path) bool { + _, ok := cache.directories.Load(symlinkPath.EnsureTrailingDirectorySeparator()) + return ok +} + +// 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.SyncMap[tspath.Path, *collections.SyncSet[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 { + set, _ := cache.directoriesByRealpath.LoadOrStore(realDirectory.RealPath, &collections.SyncSet[string]{}) + set.Add(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), +) { + 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_bench_test.go b/internal/symlinks/knownsymlinks_bench_test.go new file mode 100644 index 0000000000..79ea7281f7 --- /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++ + } + }) +} diff --git a/internal/symlinks/knownsymlinks_test.go b/internal/symlinks/knownsymlinks_test.go new file mode 100644 index 0000000000..74b46d24d3 --- /dev/null +++ b/internal/symlinks/knownsymlinks_test.go @@ -0,0 +1,290 @@ +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") + } +} + +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 + set, ok := cache.DirectoriesByRealpath().Load(realDirectory.RealPath) + if !ok || set.Size() == 0 { + t.Fatal("Expected realpath mapping to be created") + } + if !set.Has("/test/symlink") { + t.Error("Expected symlink '/test/symlink' to be in set") + } +} + +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) + + // 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..61e45f5fa2 100644 --- a/internal/transformers/tstransforms/importelision_test.go +++ b/internal/transformers/tstransforms/importelision_test.go @@ -8,8 +8,9 @@ import ( "github.com/microsoft/typescript-go/internal/checker" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/module" - "github.com/microsoft/typescript-go/internal/modulespecifiers" + "github.com/microsoft/typescript-go/internal/packagejson" "github.com/microsoft/typescript-go/internal/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,7 +70,15 @@ func (p *fakeProgram) GetNearestAncestorDirectoryWithPackageJson(dirname string) return "" } -func (p *fakeProgram) GetPackageJsonInfo(pkgJsonPath string) modulespecifiers.PackageJsonInfo { +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) *packagejson.InfoCacheEntry { 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 de09221507..0357309480 100644 --- a/internal/tspath/path.go +++ b/internal/tspath/path.go @@ -1128,6 +1128,20 @@ 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+"\\") +} + func CompareNumberOfDirectorySeparators(path1, path2 string) int { return cmp.Compare(strings.Count(path1, "/"), strings.Count(path2, "/")) } 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/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/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 deleted file mode 100644 index 67bb08a149..0000000000 --- a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference2.js.diff +++ /dev/null @@ -1,10 +0,0 @@ ---- old.declarationEmitReexportedSymlinkReference2.js -+++ new.declarationEmitReexportedSymlinkReference2.js -@@= skipped -73, +73 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 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 b09fdf0eb0..049a5cb1d0 100644 --- a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.js.diff +++ b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.js.diff @@ -6,6 +6,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 f86e06bd9b..0d69ee4818 100644 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js +++ b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js @@ -69,4 +69,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 deleted file mode 100644 index c7cdb09575..0000000000 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js.diff +++ /dev/null @@ -1,8 +0,0 @@ ---- old.symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js -+++ new.symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js -@@= skipped -68, +68 lines =@@ - - - //// [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 d638053907..e646bf58c4 100644 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js +++ b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js @@ -71,4 +71,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 deleted file mode 100644 index 1895fe4aa3..0000000000 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js.diff +++ /dev/null @@ -1,8 +0,0 @@ ---- old.symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js -+++ new.symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js -@@= skipped -70, +70 lines =@@ - - - //// [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 1e8b592fdc..9e37afb5b1 100644 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js +++ b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js @@ -71,4 +71,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 deleted file mode 100644 index 8e01d3b15a..0000000000 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js.diff +++ /dev/null @@ -1,8 +0,0 @@ ---- old.symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js -+++ new.symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js -@@= skipped -70, +70 lines =@@ - - - //// [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"; 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 +