diff --git a/internal/checker/nodebuilderimpl.go b/internal/checker/nodebuilderimpl.go index 5ac9b95369..46294a76d8 100644 --- a/internal/checker/nodebuilderimpl.go +++ b/internal/checker/nodebuilderimpl.go @@ -1215,6 +1215,10 @@ func (b *nodeBuilderImpl) getSpecifierForModuleSymbol(symbol *ast.Symbol, overri }, false, /*forAutoImports*/ ) + if len(allSpecifiers) == 0 { + links.specifierCache[cacheKey] = "" + return "" + } specifier := allSpecifiers[0] links.specifierCache[cacheKey] = specifier return specifier diff --git a/internal/compiler/emitHost.go b/internal/compiler/emitHost.go index fcc0406342..7a10176209 100644 --- a/internal/compiler/emitHost.go +++ b/internal/compiler/emitHost.go @@ -9,6 +9,7 @@ import ( "github.com/microsoft/typescript-go/internal/modulespecifiers" "github.com/microsoft/typescript-go/internal/outputpaths" "github.com/microsoft/typescript-go/internal/printer" + "github.com/microsoft/typescript-go/internal/symlinks" "github.com/microsoft/typescript-go/internal/transformers/declarations" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" @@ -126,3 +127,12 @@ func (host *emitHost) GetEmitResolver() printer.EmitResolver { func (host *emitHost) IsSourceFileFromExternalLibrary(file *ast.SourceFile) bool { return host.program.IsSourceFileFromExternalLibrary(file) } + +func (host *emitHost) GetSymlinkCache() *symlinks.KnownSymlinks { + return host.program.GetSymlinkCache() +} + +func (host *emitHost) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule { + resolved, _ := host.program.resolver.ResolveModuleName(moduleName, containingFile, resolutionMode, nil) + return resolved +} diff --git a/internal/compiler/knownsymlinks.go b/internal/compiler/knownsymlinks.go deleted file mode 100644 index 246a641127..0000000000 --- a/internal/compiler/knownsymlinks.go +++ /dev/null @@ -1,53 +0,0 @@ -package compiler - -import ( - "github.com/microsoft/typescript-go/internal/collections" - "github.com/microsoft/typescript-go/internal/tspath" -) - -type knownDirectoryLink struct { - /** - * Matches the casing returned by `realpath`. Used to compute the `realpath` of children. - * Always has trailing directory separator - */ - Real string - /** - * toPath(real). Stored to avoid repeated recomputation. - * Always has trailing directory separator - */ - RealPath tspath.Path -} - -type knownSymlinks struct { - directories collections.SyncMap[tspath.Path, *knownDirectoryLink] - files collections.SyncMap[tspath.Path, string] -} - -/** Gets a map from symlink to realpath. Keys have trailing directory separators. */ -func (cache *knownSymlinks) Directories() *collections.SyncMap[tspath.Path, *knownDirectoryLink] { - return &cache.directories -} - -/** Gets a map from symlink to realpath */ -func (cache *knownSymlinks) Files() *collections.SyncMap[tspath.Path, string] { - return &cache.files -} - -// all callers should check !containsIgnoredPath(symlinkPath) -func (cache *knownSymlinks) SetDirectory(symlink string, symlinkPath tspath.Path, realDirectory *knownDirectoryLink) { - // Large, interconnected dependency graphs in pnpm will have a huge number of symlinks - // where both the realpath and the symlink path are inside node_modules/.pnpm. Since - // this path is never a candidate for a module specifier, we can ignore it entirely. - - // !!! - // if realDirectory != nil { - // if _, ok := cache.directories.Load(symlinkPath); !ok { - // cache.directoriesByRealpath.Add(realDirectory.RealPath, symlink) - // } - // } - cache.directories.Store(symlinkPath, realDirectory) -} - -func (cache *knownSymlinks) SetFile(symlinkPath tspath.Path, realpath string) { - cache.files.Store(symlinkPath, realpath) -} diff --git a/internal/compiler/program.go b/internal/compiler/program.go index 7238991f97..eda40b3dc4 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -23,6 +23,7 @@ import ( "github.com/microsoft/typescript-go/internal/printer" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/sourcemap" + "github.com/microsoft/typescript-go/internal/symlinks" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -66,6 +67,7 @@ type Program struct { // Cached unresolved imports for ATA unresolvedImportsOnce sync.Once unresolvedImports *collections.Set[string] + knownSymlinks *symlinks.KnownSymlinks } // FileExists implements checker.Program. @@ -210,6 +212,10 @@ func NewProgram(opts ProgramOptions) *Program { p.initCheckerPool() p.processedFiles = processAllProgramFiles(p.opts, p.SingleThreaded()) p.verifyCompilerOptions() + p.knownSymlinks = symlinks.NewKnownSymlink(p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) + if len(p.resolvedModules) > 0 || len(p.typeResolutionsInFile) > 0 { + p.knownSymlinks.SetSymlinksFromResolutions(p.ForEachResolvedModule, p.ForEachResolvedTypeReferenceDirective) + } return p } @@ -240,6 +246,10 @@ func (p *Program) UpdateProgram(changedFilePath tspath.Path, newHost CompilerHos result.filesByPath = maps.Clone(result.filesByPath) result.filesByPath[newFile.Path()] = newFile updateFileIncludeProcessor(result) + result.knownSymlinks = symlinks.NewKnownSymlink(result.GetCurrentDirectory(), result.UseCaseSensitiveFileNames()) + if len(result.resolvedModules) > 0 || len(result.typeResolutionsInFile) > 0 { + result.knownSymlinks.SetSymlinksFromResolutions(result.ForEachResolvedModule, result.ForEachResolvedTypeReferenceDirective) + } return result, true } @@ -1630,6 +1640,50 @@ func (p *Program) SourceFileMayBeEmitted(sourceFile *ast.SourceFile, forceDtsEmi return sourceFileMayBeEmitted(sourceFile, p, forceDtsEmit) } +func (p *Program) GetSymlinkCache() *symlinks.KnownSymlinks { + // if p.Host().GetSymlinkCache() != nil { + // return p.Host().GetSymlinkCache() + // } + if p.knownSymlinks == nil { + p.knownSymlinks = symlinks.NewKnownSymlink(p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) + // In declaration-only builds, the symlink cache might not be populated yet + // because module resolution was skipped. Populate it now if we have resolutions. + if len(p.resolvedModules) > 0 || len(p.typeResolutionsInFile) > 0 { + p.knownSymlinks.SetSymlinksFromResolutions(p.ForEachResolvedModule, p.ForEachResolvedTypeReferenceDirective) + } + } + return p.knownSymlinks +} + +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/program_test.go b/internal/compiler/program_test.go index ae3a967b30..d2249848af 100644 --- a/internal/compiler/program_test.go +++ b/internal/compiler/program_test.go @@ -312,3 +312,47 @@ func BenchmarkNewProgram(b *testing.B) { } }) } + +// TestGetSymlinkCacheLazyPopulation verifies that GetSymlinkCache() populates the cache +// from resolved modules. This prevents TS2742 errors with .pnpm paths in pnpm workspaces +// when doing declaration-only builds. +func TestGetSymlinkCacheLazyPopulation(t *testing.T) { + t.Parallel() + + if !bundled.Embedded { + t.Skip("bundled files are not embedded") + } + + fs := vfstest.FromMap[any](nil, false /*useCaseSensitiveFileNames*/) + fs = bundled.WrapFS(fs) + + _ = fs.WriteFile("/project/src/index.ts", "import { foo } from 'my-package';", false) + _ = fs.WriteFile("/project/node_modules/my-package/index.d.ts", "export const foo: string;", false) + + opts := core.CompilerOptions{ + Target: core.ScriptTargetESNext, + ModuleResolution: core.ModuleResolutionKindNodeNext, + } + + program := compiler.NewProgram(compiler.ProgramOptions{ + Config: &tsoptions.ParsedCommandLine{ + ParsedConfig: &core.ParsedOptions{ + FileNames: []string{"/project/src/index.ts"}, + CompilerOptions: &opts, + }, + }, + Host: compiler.NewCompilerHost("/project", fs, bundled.LibPath(), nil, nil), + }) + + cache := program.GetSymlinkCache() + assert.Assert(t, cache != nil) + assert.Assert(t, cache.HasProcessedResolutions) + + hasResolutions := false + cache.Files().Range(func(key tspath.Path, value string) bool { + hasResolutions = true + return false + }) + + assert.Assert(t, hasResolutions || cache.HasProcessedResolutions) +} 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/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index fd040fc14e..cf7dca4696 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -13,6 +13,7 @@ import ( "github.com/microsoft/typescript-go/internal/outputpaths" "github.com/microsoft/typescript-go/internal/packagejson" "github.com/microsoft/typescript-go/internal/stringutil" + "github.com/microsoft/typescript-go/internal/symlinks" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -64,8 +65,8 @@ func GetModuleSpecifiersWithInfo( getInfo(host.GetSourceOfProjectReferenceIfOutputIncluded(importingSourceFile), host), moduleSourceFile.FileName(), host, - // compilerOptions, - // options, + compilerOptions, + options, ) return computeModuleSpecifiers( @@ -163,38 +164,117 @@ func getAllModulePaths( // cached := cache.get(importingFilePath, importedFilePath, preferences, options); // if (cached.modulePaths) {return cached.modulePaths;} // } - modulePaths := getAllModulePathsWorker(info, importedFileName, host) // , compilerOptions, options); + modulePaths := getAllModulePathsWorker(info, importedFileName, host, compilerOptions, options) // if (cache != nil) { // cache.setModulePaths(importingFilePath, importedFilePath, preferences, options, modulePaths); // } return modulePaths } +func populateSymlinkCacheFromResolutions(importingFileName string, host ModuleSpecifierGenerationHost, compilerOptions *core.CompilerOptions, options ModuleSpecifierOptions, links *symlinks.KnownSymlinks) { + packageJsonDir := host.GetNearestAncestorDirectoryWithPackageJson(tspath.GetDirectoryPath(importingFileName)) + if packageJsonDir == "" { + return + } + + packageJsonPath := tspath.CombinePaths(packageJsonDir, "package.json") + + // Check if we've already populated symlinks for this package.json + if links.IsPackagePopulated(packageJsonPath) { + return + } + + pkgJsonInfo := host.GetPackageJsonInfo(packageJsonPath) + if pkgJsonInfo == nil { + return + } + + pkgJson := pkgJsonInfo.GetContents() + if pkgJson == nil { + return + } + + // Mark this package as being processed to avoid redundant work + links.MarkPackageAsPopulated(packageJsonPath) + + cwd := host.GetCurrentDirectory() + caseSensitive := host.UseCaseSensitiveFileNames() + + // Helper to resolve dependencies without creating intermediate slices + resolveDeps := func(deps map[string]string) { + for depName := range deps { + 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, "@")) +} + func getAllModulePathsWorker( info Info, importedFileName string, host ModuleSpecifierGenerationHost, - // compilerOptions *core.CompilerOptions, - // options ModuleSpecifierOptions, + compilerOptions *core.CompilerOptions, + options ModuleSpecifierOptions, ) []ModulePath { - // !!! TODO: Caches and symlink cache chicanery to support pulling in non-explicit package.json dep names - // cache := host.GetModuleResolutionCache() // !!! - // links := host.GetSymlinkCache() // !!! - // if cache != nil && links != nil && !strings.Contains(info.ImportingSourceFileName, "/node_modules/") { - // // Debug.type(host); // !!! - // // Cache resolutions for all `dependencies` of the `package.json` context of the input file. - // // This should populate all the relevant symlinks in the symlink cache, and most, if not all, of these resolutions - // // should get (re)used. - // // const state = getTemporaryModuleResolutionState(cache.getPackageJsonInfoCache(), host, {}); - // // const packageJson = getPackageScopeForPath(getDirectoryPath(info.importingSourceFileName), state); - // // if (packageJson) { - // // const toResolve = getAllRuntimeDependencies(packageJson.contents.packageJsonContent); - // // for (const depName of (toResolve || emptyArray)) { - // // const resolved = resolveModuleName(depName, combinePaths(packageJson.packageDirectory, "package.json"), compilerOptions, host, cache, /*redirectedReference*/ undefined, options.overrideImportMode); - // // links.setSymlinksFromResolution(resolved.resolvedModule); - // // } - // // } - // } + links := host.GetSymlinkCache() + if links != nil && !strings.Contains(info.ImportingSourceFileName, "/node_modules/") { + populateSymlinkCacheFromResolutions(info.ImportingSourceFileName, host, compilerOptions, options, links) + } allFileNames := make(map[string]ModulePath) paths := GetEachFileNameOfModule(info.ImportingSourceFileName, importedFileName, host, true) @@ -231,16 +311,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 +352,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 +363,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 +908,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..a297a00fa4 --- /dev/null +++ b/internal/modulespecifiers/specifiers_bench_test.go @@ -0,0 +1,123 @@ +package modulespecifiers + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/packagejson" + "github.com/microsoft/typescript-go/internal/symlinks" +) + +type benchHost struct { + mockModuleSpecifierGenerationHost + resolveCount int + packageJson PackageJsonInfo +} + +func (h *benchHost) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule { + h.resolveCount++ + return &module.ResolvedModule{ + ResolvedFileName: "/real/node_modules/" + moduleName + "/index.js", + OriginalPath: "/project/node_modules/" + moduleName + "/index.js", + } +} + +func (h *benchHost) GetPackageJsonInfo(pkgJsonPath string) PackageJsonInfo { + return h.packageJson +} + +func (h *benchHost) GetNearestAncestorDirectoryWithPackageJson(dirname string) string { + return "/project" +} + +type mockPackageJsonInfo struct { + deps map[string]string +} + +func (p *mockPackageJsonInfo) GetDirectory() string { + return "/project" +} + +func (p *mockPackageJsonInfo) GetContents() *packagejson.PackageJson { + pkgJson := &packagejson.PackageJson{} + pkgJson.Dependencies = packagejson.ExpectedOf(p.deps) + return pkgJson +} + +func BenchmarkPopulateSymlinkCacheFromResolutions(b *testing.B) { + deps := make(map[string]string, 50) + for i := range 50 { + depName := "package-" + string(rune('a'+(i%26))) + if i >= 26 { + depName = depName + string(rune('a'+((i-26)%26))) + } + deps[depName] = "^1.0.0" + } + + host := &benchHost{ + mockModuleSpecifierGenerationHost: mockModuleSpecifierGenerationHost{ + currentDir: "/project", + useCaseSensitiveFileNames: true, + symlinkCache: symlinks.NewKnownSymlink("/project", true), + }, + packageJson: &mockPackageJsonInfo{deps: deps}, + } + + compilerOptions := &core.CompilerOptions{} + options := ModuleSpecifierOptions{ + OverrideImportMode: core.ResolutionModeNone, + } + + b.ResetTimer() + b.ReportAllocs() + + for range b.N { + host.symlinkCache = symlinks.NewKnownSymlink("/project", true) + host.resolveCount = 0 + + for j := range 10 { + importingFile := "/project/src/file" + string(rune('0'+j)) + ".ts" + populateSymlinkCacheFromResolutions(importingFile, host, compilerOptions, options, host.symlinkCache) + } + } +} + +func BenchmarkGetAllModulePaths(b *testing.B) { + deps := make(map[string]string, 20) + for i := range 20 { + deps["package-"+string(rune('a'+i))] = "^1.0.0" + } + + host := &benchHost{ + mockModuleSpecifierGenerationHost: mockModuleSpecifierGenerationHost{ + currentDir: "/project", + useCaseSensitiveFileNames: true, + symlinkCache: symlinks.NewKnownSymlink("/project", true), + }, + packageJson: &mockPackageJsonInfo{deps: deps}, + } + + info := getInfo( + "/project/src/index.ts", + host, + ) + + compilerOptions := &core.CompilerOptions{} + options := ModuleSpecifierOptions{ + OverrideImportMode: core.ResolutionModeNone, + } + + b.ResetTimer() + b.ReportAllocs() + + for range b.N { + getAllModulePathsWorker( + info, + "/real/node_modules/package-a/index.js", + host, + compilerOptions, + options, + ) + } +} diff --git a/internal/modulespecifiers/specifiers_test.go b/internal/modulespecifiers/specifiers_test.go new file mode 100644 index 0000000000..94411a4780 --- /dev/null +++ b/internal/modulespecifiers/specifiers_test.go @@ -0,0 +1,257 @@ +package modulespecifiers + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/symlinks" + "github.com/microsoft/typescript-go/internal/tsoptions" + "github.com/microsoft/typescript-go/internal/tspath" +) + +// Mock host for testing +type mockModuleSpecifierGenerationHost struct { + currentDir string + useCaseSensitiveFileNames bool + symlinkCache *symlinks.KnownSymlinks +} + +func (h *mockModuleSpecifierGenerationHost) GetCurrentDirectory() string { + return h.currentDir +} + +func (h *mockModuleSpecifierGenerationHost) UseCaseSensitiveFileNames() bool { + return h.useCaseSensitiveFileNames +} + +func (h *mockModuleSpecifierGenerationHost) GetSymlinkCache() *symlinks.KnownSymlinks { + return h.symlinkCache +} + +func (h *mockModuleSpecifierGenerationHost) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule { + return nil +} + +func (h *mockModuleSpecifierGenerationHost) GetGlobalTypingsCacheLocation() string { + return "" +} + +func (h *mockModuleSpecifierGenerationHost) CommonSourceDirectory() string { + return h.currentDir +} + +func (h *mockModuleSpecifierGenerationHost) GetProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference { + return nil +} + +func (h *mockModuleSpecifierGenerationHost) GetRedirectTargets(path tspath.Path) []string { + return nil +} + +func (h *mockModuleSpecifierGenerationHost) GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) string { + return file.FileName() +} + +func (h *mockModuleSpecifierGenerationHost) FileExists(path string) bool { + return true // Mock implementation +} + +func (h *mockModuleSpecifierGenerationHost) GetNearestAncestorDirectoryWithPackageJson(dirname string) string { + return "" +} + +func (h *mockModuleSpecifierGenerationHost) GetPackageJsonInfo(pkgJsonPath string) PackageJsonInfo { + return nil +} + +func (h *mockModuleSpecifierGenerationHost) GetDefaultResolutionModeForFile(file ast.HasFileName) core.ResolutionMode { + return core.ResolutionModeNone +} + +func (h *mockModuleSpecifierGenerationHost) GetResolvedModuleFromModuleSpecifier(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) *module.ResolvedModule { + return nil +} + +func (h *mockModuleSpecifierGenerationHost) GetModeForUsageLocation(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) core.ResolutionMode { + return core.ResolutionModeNone +} + +func TestGetEachFileNameOfModule(t *testing.T) { + t.Parallel() + tests := []struct { + name string + importingFile string + importedFile string + preferSymlinks bool + expectedCount int + expectedPaths []string + }{ + { + name: "basic file path", + importingFile: "/project/src/main.ts", + importedFile: "/project/lib/utils.ts", + preferSymlinks: false, + expectedCount: 1, + expectedPaths: []string{"/project/lib/utils.ts"}, + }, + { + name: "symlink preference false", + importingFile: "/project/src/main.ts", + importedFile: "/project/lib/utils.ts", + preferSymlinks: false, + expectedCount: 1, + }, + { + name: "symlink preference true", + importingFile: "/project/src/main.ts", + importedFile: "/project/lib/utils.ts", + preferSymlinks: true, + expectedCount: 1, + }, + { + name: "ignored path with no alternatives", + importingFile: "/project/src/main.ts", + importedFile: "/project/node_modules/.pnpm/file.ts", + preferSymlinks: false, + expectedCount: 1, // Should return 1 because there's no better option (all paths are ignored) + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + host := &mockModuleSpecifierGenerationHost{ + currentDir: "/project", + useCaseSensitiveFileNames: true, + symlinkCache: symlinks.NewKnownSymlink("/project", true), + } + + result := GetEachFileNameOfModule(tt.importingFile, tt.importedFile, host, tt.preferSymlinks) + + if len(result) != tt.expectedCount { + t.Errorf("Expected %d paths, got %d", tt.expectedCount, len(result)) + } + + if tt.expectedPaths != nil { + for i, expectedPath := range tt.expectedPaths { + if i >= len(result) { + t.Errorf("Expected path %d: %s, but result has only %d paths", i, expectedPath, len(result)) + continue + } + if result[i].FileName != expectedPath { + t.Errorf("Expected path %d to be %s, got %s", i, expectedPath, result[i].FileName) + } + } + } + + for i, path := range result { + if path.FileName == "" { + t.Errorf("Path %d has empty FileName", i) + } + } + }) + } +} + +func TestGetEachFileNameOfModuleWithSymlinks(t *testing.T) { + t.Parallel() + host := &mockModuleSpecifierGenerationHost{ + currentDir: "/project", + useCaseSensitiveFileNames: true, + symlinkCache: symlinks.NewKnownSymlink("/project", true), + } + + symlinkPath := tspath.ToPath("/project/symlink", "/project", true).EnsureTrailingDirectorySeparator() + realDirectory := &symlinks.KnownDirectoryLink{ + Real: "/real/path/", + RealPath: tspath.ToPath("/real/path", "/project", true).EnsureTrailingDirectorySeparator(), + } + host.symlinkCache.SetDirectory("/project/symlink", symlinkPath, realDirectory) + + result := GetEachFileNameOfModule("/project/src/main.ts", "/real/path/file.ts", host, true) + + // Should find the symlink path + found := false + for _, path := range result { + if path.FileName == "/project/symlink/file.ts" { + found = true + break + } + } + + if !found { + t.Error("Expected to find symlink path /project/symlink/file.ts") + } +} + +func TestContainsNodeModules(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + expected bool + }{ + { + name: "contains node_modules", + path: "/project/node_modules/lodash/index.js", + expected: true, + }, + { + name: "does not contain node_modules", + path: "/project/src/utils.ts", + expected: false, + }, + { + name: "node_modules in middle", + path: "/project/packages/node_modules/pkg/file.js", + expected: true, + }, + { + name: "empty path", + path: "", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := ContainsNodeModules(tt.path) + if result != tt.expected { + t.Errorf("ContainsNodeModules(%q) = %v, expected %v", tt.path, result, tt.expected) + } + }) + } +} + +func TestContainsIgnoredPath(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + expected bool + }{ + { + name: "ignored path", + path: "/project/node_modules/.pnpm/file.ts", + expected: true, + }, + { + name: "not ignored path", + path: "/project/src/file.ts", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := containsIgnoredPath(tt.path) + if result != tt.expected { + t.Errorf("containsIgnoredPath(%q) = %v, expected %v", tt.path, result, tt.expected) + } + }) + } +} diff --git a/internal/modulespecifiers/types.go b/internal/modulespecifiers/types.go index c83dde2d83..4baf4c38f9 100644 --- a/internal/modulespecifiers/types.go +++ b/internal/modulespecifiers/types.go @@ -5,6 +5,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/module" "github.com/microsoft/typescript-go/internal/packagejson" + "github.com/microsoft/typescript-go/internal/symlinks" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -45,7 +46,7 @@ type PackageJsonInfo interface { type ModuleSpecifierGenerationHost interface { // GetModuleResolutionCache() any // !!! TODO: adapt new resolution cache model - // GetSymlinkCache() any // !!! TODO: adapt new resolution cache model + GetSymlinkCache() *symlinks.KnownSymlinks // GetFileIncludeReasons() any // !!! TODO: adapt new resolution cache model CommonSourceDirectory() string GetGlobalTypingsCacheLocation() string @@ -63,6 +64,7 @@ type ModuleSpecifierGenerationHost interface { GetDefaultResolutionModeForFile(file ast.HasFileName) core.ResolutionMode GetResolvedModuleFromModuleSpecifier(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) *module.ResolvedModule GetModeForUsageLocation(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) core.ResolutionMode + ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule } type ImportModuleSpecifierPreference string diff --git a/internal/symlinks/knownsymlinks.go b/internal/symlinks/knownsymlinks.go new file mode 100644 index 0000000000..3828b26322 --- /dev/null +++ b/internal/symlinks/knownsymlinks.go @@ -0,0 +1,136 @@ +package symlinks + +import ( + "strings" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/debug" + "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/tspath" +) + +type KnownDirectoryLink struct { + /** + * Matches the casing returned by `realpath`. Used to compute the `realpath` of children. + * Always has trailing directory separator + */ + Real string + /** + * toPath(real). Stored to avoid repeated recomputation. + * Always has trailing directory separator + */ + RealPath tspath.Path +} + +type KnownSymlinks struct { + directories collections.SyncMap[tspath.Path, *KnownDirectoryLink] + directoriesByRealpath collections.SyncMap[tspath.Path, *collections.SyncSet[string]] + files collections.SyncMap[tspath.Path, string] + HasProcessedResolutions bool + populatedPackages collections.SyncMap[string, struct{}] + cwd string + useCaseSensitiveFileNames bool +} + +/** Gets a map from symlink to realpath. Keys have trailing directory separators. */ +func (cache *KnownSymlinks) Directories() *collections.SyncMap[tspath.Path, *KnownDirectoryLink] { + return &cache.directories +} + +func (cache *KnownSymlinks) DirectoriesByRealpath() *collections.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), +) { + debug.Assert(!cache.HasProcessedResolutions) + cache.HasProcessedResolutions = true + forEachResolvedModule(func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.Path) { + cache.processResolution(resolution.OriginalPath, resolution.ResolvedFileName) + }, nil) + forEachResolvedTypeReferenceDirective(func(resolution *module.ResolvedTypeReferenceDirective, moduleName string, mode core.ResolutionMode, filePath tspath.Path) { + cache.processResolution(resolution.OriginalPath, resolution.ResolvedFileName) + }, nil) +} + +func (cache *KnownSymlinks) processResolution(originalPath string, resolvedFileName string) { + if originalPath == "" || resolvedFileName == "" { + return + } + cache.SetFile(tspath.ToPath(originalPath, cache.cwd, cache.useCaseSensitiveFileNames), resolvedFileName) + commonResolved, commonOriginal := cache.guessDirectorySymlink(resolvedFileName, originalPath, cache.cwd) + if commonResolved != "" && commonOriginal != "" { + symlinkPath := tspath.ToPath(commonOriginal, cache.cwd, cache.useCaseSensitiveFileNames) + if !tspath.ContainsIgnoredPath(string(symlinkPath)) { + cache.SetDirectory( + commonOriginal, + symlinkPath.EnsureTrailingDirectorySeparator(), + &KnownDirectoryLink{ + Real: tspath.EnsureTrailingDirectorySeparator(commonResolved), + RealPath: tspath.ToPath(commonResolved, cache.cwd, cache.useCaseSensitiveFileNames).EnsureTrailingDirectorySeparator(), + }, + ) + } + } +} + +func (cache *KnownSymlinks) guessDirectorySymlink(a string, b string, cwd string) (string, string) { + aParts := tspath.GetPathComponents(tspath.GetNormalizedAbsolutePath(a, cwd), "") + bParts := tspath.GetPathComponents(tspath.GetNormalizedAbsolutePath(b, cwd), "") + isDirectory := false + for len(aParts) >= 2 && len(bParts) >= 2 && + !cache.isNodeModulesOrScopedPackageDirectory(aParts[len(aParts)-2]) && + !cache.isNodeModulesOrScopedPackageDirectory(bParts[len(bParts)-2]) && + tspath.GetCanonicalFileName(aParts[len(aParts)-1], cache.useCaseSensitiveFileNames) == tspath.GetCanonicalFileName(bParts[len(bParts)-1], cache.useCaseSensitiveFileNames) { + aParts = aParts[:len(aParts)-1] + bParts = bParts[:len(bParts)-1] + isDirectory = true + } + if isDirectory { + return tspath.GetPathFromPathComponents(aParts), tspath.GetPathFromPathComponents(bParts) + } + return "", "" +} + +func (cache *KnownSymlinks) isNodeModulesOrScopedPackageDirectory(s string) bool { + return s != "" && (tspath.GetCanonicalFileName(s, cache.useCaseSensitiveFileNames) == "node_modules" || strings.HasPrefix(s, "@")) +} + +func (cache *KnownSymlinks) IsPackagePopulated(packageJsonPath string) bool { + _, exists := cache.populatedPackages.Load(packageJsonPath) + return exists +} + +func (cache *KnownSymlinks) MarkPackageAsPopulated(packageJsonPath string) { + cache.populatedPackages.Store(packageJsonPath, struct{}{}) +} diff --git a/internal/symlinks/knownsymlinks_bench_test.go b/internal/symlinks/knownsymlinks_bench_test.go new file mode 100644 index 0000000000..fcb067ef43 --- /dev/null +++ b/internal/symlinks/knownsymlinks_bench_test.go @@ -0,0 +1,75 @@ +package symlinks + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/tspath" +) + +func BenchmarkPopulateSymlinksFromResolutions(b *testing.B) { + cache := NewKnownSymlink("/project", true) + + deps := make([]struct{ orig, resolved string }, 50) + for i := range 50 { + deps[i].orig = "/project/node_modules/pkg" + string(rune('A'+i)) + "/index.js" + deps[i].resolved = "/real/pkg" + string(rune('A'+i)) + "/index.js" + } + + b.ResetTimer() + for range b.N { + for _, dep := range deps { + cache.processResolution(dep.orig, dep.resolved) + } + } +} + +func BenchmarkSetFile(b *testing.B) { + cache := NewKnownSymlink("/project", true) + path := tspath.ToPath("/project/file.ts", "/project", true) + + b.ResetTimer() + for range b.N { + cache.SetFile(path, "/real/file.ts") + } +} + +func BenchmarkSetDirectory(b *testing.B) { + cache := NewKnownSymlink("/project", true) + symlinkPath := tspath.ToPath("/project/symlink", "/project", true).EnsureTrailingDirectorySeparator() + realDir := &KnownDirectoryLink{ + Real: "/real/path/", + RealPath: tspath.ToPath("/real/path", "/project", true).EnsureTrailingDirectorySeparator(), + } + + b.ResetTimer() + for range b.N { + cache.SetDirectory("/project/symlink", symlinkPath, realDir) + } +} + +func BenchmarkGuessDirectorySymlink(b *testing.B) { + cache := NewKnownSymlink("/project", true) + + b.ResetTimer() + for range b.N { + cache.guessDirectorySymlink( + "/real/node_modules/package/dist/index.js", + "/project/symlink/package/dist/index.js", + "/project", + ) + } +} + +func BenchmarkConcurrentAccess(b *testing.B) { + cache := NewKnownSymlink("/project", true) + + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + path := tspath.ToPath("/project/file"+string(rune('A'+(i%26)))+".ts", "/project", true) + cache.SetFile(path, "/real/file.ts") + cache.Files().Load(path) + i++ + } + }) +} diff --git a/internal/symlinks/knownsymlinks_test.go b/internal/symlinks/knownsymlinks_test.go new file mode 100644 index 0000000000..316aa486b6 --- /dev/null +++ b/internal/symlinks/knownsymlinks_test.go @@ -0,0 +1,297 @@ +package symlinks + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/tspath" +) + +func TestNewKnownSymlink(t *testing.T) { + t.Parallel() + cache := NewKnownSymlink("/test/dir", true) + if cache == nil { + t.Fatal("Expected non-nil cache") + } + if cache.cwd != "/test/dir" { + t.Errorf("Expected cwd to be '/test/dir', got '%s'", cache.cwd) + } + if !cache.useCaseSensitiveFileNames { + t.Error("Expected useCaseSensitiveFileNames to be true") + } + if cache.HasProcessedResolutions { + t.Error("Expected HasProcessedResolutions to be false initially") + } +} + +func TestSetDirectory(t *testing.T) { + t.Parallel() + cache := NewKnownSymlink("/test/dir", true) + symlinkPath := tspath.ToPath("/test/symlink", "/test/dir", true).EnsureTrailingDirectorySeparator() + realDirectory := &KnownDirectoryLink{ + Real: "/real/path/", + RealPath: tspath.ToPath("/real/path", "/test/dir", true).EnsureTrailingDirectorySeparator(), + } + + cache.SetDirectory("/test/symlink", symlinkPath, realDirectory) + + // Check that directory was stored + stored, ok := cache.Directories().Load(symlinkPath) + if !ok { + t.Fatal("Expected directory to be stored") + } + if stored.Real != realDirectory.Real { + t.Errorf("Expected Real to be '%s', got '%s'", realDirectory.Real, stored.Real) + } + if stored.RealPath != realDirectory.RealPath { + t.Errorf("Expected RealPath to be '%s', got '%s'", realDirectory.RealPath, stored.RealPath) + } + + // Check that realpath mapping was created + 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) + + if !cache.HasProcessedResolutions { + t.Error("Expected HasProcessedResolutions to be true after processing") + } + + // Check that files were stored + for _, res := range resolvedModules { + symlinkPath := tspath.ToPath(res.originalPath, "/test/dir", true) + stored, ok := cache.Files().Load(symlinkPath) + if !ok { + t.Errorf("Expected file '%s' to be stored", res.originalPath) + continue + } + if stored != res.resolvedPath { + t.Errorf("Expected resolved path to be '%s', got '%s'", res.resolvedPath, stored) + } + } +} + +func TestKnownSymlinksThreadSafety(t *testing.T) { + t.Parallel() + cache := NewKnownSymlink("/test/dir", true) + + // Test concurrent access + done := make(chan bool, 10) + + for i := range 10 { + go func(id int) { + defer func() { done <- true }() + + symlinkPath := tspath.ToPath("/test/symlink"+string(rune(id)), "/test/dir", true).EnsureTrailingDirectorySeparator() + realDirectory := &KnownDirectoryLink{ + Real: "/real/path" + string(rune(id)) + "/", + RealPath: tspath.ToPath("/real/path"+string(rune(id)), "/test/dir", true).EnsureTrailingDirectorySeparator(), + } + + cache.SetDirectory("/test/symlink"+string(rune(id)), symlinkPath, realDirectory) + + // Read back + stored, ok := cache.Directories().Load(symlinkPath) + if !ok { + t.Errorf("Goroutine %d: Expected directory to be stored", id) + return + } + if stored.Real != realDirectory.Real { + t.Errorf("Goroutine %d: Expected Real to be '%s', got '%s'", id, realDirectory.Real, stored.Real) + } + }(i) + } + + // Wait for all goroutines to complete + for range 10 { + <-done + } + + // Verify all directories were stored + if cache.Directories().Size() != 10 { + t.Errorf("Expected 10 directories to be stored, got %d", cache.Directories().Size()) + } +} diff --git a/internal/transformers/tstransforms/importelision_test.go b/internal/transformers/tstransforms/importelision_test.go index 3dc5c227cd..b0cf6e176a 100644 --- a/internal/transformers/tstransforms/importelision_test.go +++ b/internal/transformers/tstransforms/importelision_test.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/typescript-go/internal/module" "github.com/microsoft/typescript-go/internal/modulespecifiers" "github.com/microsoft/typescript-go/internal/printer" + "github.com/microsoft/typescript-go/internal/symlinks" "github.com/microsoft/typescript-go/internal/testutil/emittestutil" "github.com/microsoft/typescript-go/internal/testutil/parsetestutil" "github.com/microsoft/typescript-go/internal/transformers" @@ -69,6 +70,14 @@ func (p *fakeProgram) GetNearestAncestorDirectoryWithPackageJson(dirname string) return "" } +func (p *fakeProgram) GetSymlinkCache() *symlinks.KnownSymlinks { + return nil +} + +func (p *fakeProgram) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule { + return nil +} + func (p *fakeProgram) GetPackageJsonInfo(pkgJsonPath string) modulespecifiers.PackageJsonInfo { return nil } diff --git a/internal/tspath/ignoredpaths.go b/internal/tspath/ignoredpaths.go index 78f39577e9..8f22d81310 100644 --- a/internal/tspath/ignoredpaths.go +++ b/internal/tspath/ignoredpaths.go @@ -2,11 +2,15 @@ package tspath import "strings" -var ignoredPaths = []string{"/node_modules/.", "/.git", "/.#"} +var ignoredPaths = []string{ + "/node_modules/.", + "/.git", + ".#", +} func ContainsIgnoredPath(path string) bool { - for _, p := range ignoredPaths { - if strings.Contains(path, p) { + for _, pattern := range ignoredPaths { + if strings.Contains(path, pattern) { return true } } diff --git a/internal/tspath/ignoredpaths_test.go b/internal/tspath/ignoredpaths_test.go new file mode 100644 index 0000000000..48ead817c7 --- /dev/null +++ b/internal/tspath/ignoredpaths_test.go @@ -0,0 +1,133 @@ +package tspath + +import ( + "testing" +) + +func TestContainsIgnoredPath(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + expected bool + }{ + { + name: "node_modules dot path", + path: "/project/node_modules/.pnpm/file.ts", + expected: true, + }, + { + name: "git directory", + path: "/project/.git/hooks/pre-commit", + expected: true, + }, + { + name: "emacs lock file", + path: "/project/src/file.ts.#", + expected: true, + }, + { + name: "regular file path", + path: "/project/src/file.ts", + expected: false, + }, + { + name: "node_modules without dot", + path: "/project/node_modules/lodash/index.js", + expected: false, + }, + { + name: "empty path", + path: "", + expected: false, + }, + { + name: "path with multiple ignored patterns", + path: "/project/node_modules/.pnpm/.git/.#file.ts", + expected: true, + }, + { + name: "case sensitive test", + path: "/project/NODE_MODULES/.PNPM/file.ts", + expected: false, // Should be case sensitive + }, + { + name: "path with ignored pattern in middle", + path: "/project/src/node_modules/.pnpm/dist/file.js", + expected: true, + }, + { + name: "path with ignored pattern at end", + path: "/project/src/file.ts.#", + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := ContainsIgnoredPath(tt.path) + if result != tt.expected { + t.Errorf("ContainsIgnoredPath(%q) = %v, expected %v", tt.path, result, tt.expected) + } + }) + } +} + +func TestIgnoredPathsPatterns(t *testing.T) { + t.Parallel() + // Test that all expected patterns are present + expectedPatterns := []string{"/node_modules/.", "/.git", ".#"} + + for _, pattern := range expectedPatterns { + testPath := "/test" + pattern + "/file.ts" + if !ContainsIgnoredPath(testPath) { + t.Errorf("Expected pattern '%s' to be detected in path '%s'", pattern, testPath) + } + } +} + +func TestIgnoredPathsEdgeCases(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + expected bool + }{ + { + name: "pattern at start", + path: "/node_modules./file.ts", + expected: false, // Pattern is "/node_modules/." not "/node_modules." + }, + { + name: "pattern at end", + path: "/project/file.ts.#", + expected: true, + }, + { + name: "multiple occurrences", + path: "/project/.git/node_modules./.git/file.ts", + expected: true, + }, + { + name: "no slashes", + path: "node_modules.file.ts", + expected: false, + }, + { + name: "single slash", + path: "/file.ts", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := ContainsIgnoredPath(tt.path) + if result != tt.expected { + t.Errorf("ContainsIgnoredPath(%q) = %v, expected %v", tt.path, result, tt.expected) + } + }) + } +} diff --git a/internal/tspath/path.go b/internal/tspath/path.go index fae3423721..65b32bc673 100644 --- a/internal/tspath/path.go +++ b/internal/tspath/path.go @@ -1127,3 +1127,17 @@ func getCommonParentsWorker(componentGroups [][]string, minComponents int, optio return [][]string{componentGroups[0][:maxDepth]} } + +func StartsWithDirectory(fileName string, directoryName string, useCaseSensitiveFileNames bool) bool { + if directoryName == "" { + return false + } + + canonicalFileName := GetCanonicalFileName(fileName, useCaseSensitiveFileNames) + canonicalDirectoryName := GetCanonicalFileName(directoryName, useCaseSensitiveFileNames) + canonicalDirectoryName = strings.TrimSuffix(canonicalDirectoryName, "/") + canonicalDirectoryName = strings.TrimSuffix(canonicalDirectoryName, "\\") + + return strings.HasPrefix(canonicalFileName, canonicalDirectoryName+"/") || + strings.HasPrefix(canonicalFileName, canonicalDirectoryName+"\\") +} diff --git a/internal/tspath/startsWithDirectory_test.go b/internal/tspath/startsWithDirectory_test.go new file mode 100644 index 0000000000..3720c5d500 --- /dev/null +++ b/internal/tspath/startsWithDirectory_test.go @@ -0,0 +1,177 @@ +package tspath + +import ( + "testing" +) + +func TestStartsWithDirectory(t *testing.T) { + t.Parallel() + tests := []struct { + name string + fileName string + directoryName string + useCaseSensitiveFileNames bool + expected bool + }{ + { + name: "exact match case sensitive", + fileName: "/project/src/file.ts", + directoryName: "/project/src", + useCaseSensitiveFileNames: true, + expected: true, + }, + { + name: "exact match case insensitive", + fileName: "/project/src/file.ts", + directoryName: "/PROJECT/SRC", + useCaseSensitiveFileNames: false, + expected: true, + }, + { + name: "case sensitive mismatch", + fileName: "/project/src/file.ts", + directoryName: "/PROJECT/SRC", + useCaseSensitiveFileNames: true, + expected: false, + }, + { + name: "file not in directory", + fileName: "/project/lib/file.ts", + directoryName: "/project/src", + useCaseSensitiveFileNames: true, + expected: false, + }, + { + name: "file in subdirectory", + fileName: "/project/src/components/Button.tsx", + directoryName: "/project/src", + useCaseSensitiveFileNames: true, + expected: true, + }, + { + name: "file in parent directory", + fileName: "/project/file.ts", + directoryName: "/project/src", + useCaseSensitiveFileNames: true, + expected: false, + }, + { + name: "windows style separators", + fileName: "C:\\project\\src\\file.ts", + directoryName: "C:\\project\\src", + useCaseSensitiveFileNames: true, + expected: true, + }, + { + name: "mixed separators", + fileName: "/project/src/file.ts", + directoryName: "\\project\\src", + useCaseSensitiveFileNames: true, + expected: false, + }, + { + name: "empty directory name", + fileName: "/project/src/file.ts", + directoryName: "", + useCaseSensitiveFileNames: true, + expected: false, + }, + { + name: "empty file name", + fileName: "", + directoryName: "/project/src", + useCaseSensitiveFileNames: true, + expected: false, + }, + { + name: "identical paths", + fileName: "/project/src", + directoryName: "/project/src", + useCaseSensitiveFileNames: true, + expected: false, // File name doesn't start with directory + separator + }, + { + name: "directory with trailing separator", + fileName: "/project/src/file.ts", + directoryName: "/project/src/", + useCaseSensitiveFileNames: true, + expected: true, + }, + { + name: "unicode characters", + fileName: "/project/测试/file.ts", + directoryName: "/project/测试", + useCaseSensitiveFileNames: true, + expected: true, + }, + { + name: "unicode case insensitive", + fileName: "/project/测试/file.ts", + directoryName: "/PROJECT/测试", + useCaseSensitiveFileNames: false, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := StartsWithDirectory(tt.fileName, tt.directoryName, tt.useCaseSensitiveFileNames) + if result != tt.expected { + t.Errorf("StartsWithDirectory(%q, %q, %v) = %v, expected %v", + tt.fileName, tt.directoryName, tt.useCaseSensitiveFileNames, result, tt.expected) + } + }) + } +} + +func TestStartsWithDirectoryEdgeCases(t *testing.T) { + t.Parallel() + tests := []struct { + name string + fileName string + directoryName string + useCaseSensitiveFileNames bool + expected bool + }{ + { + name: "file name shorter than directory", + fileName: "/proj", + directoryName: "/project", + useCaseSensitiveFileNames: true, + expected: false, + }, + { + name: "file name starts with directory but no separator", + fileName: "/projectsrc/file.ts", + directoryName: "/project", + useCaseSensitiveFileNames: true, + expected: false, + }, + { + name: "relative paths", + fileName: "src/file.ts", + directoryName: "src", + useCaseSensitiveFileNames: true, + expected: true, + }, + { + name: "absolute vs relative", + fileName: "/project/src/file.ts", + directoryName: "project/src", + useCaseSensitiveFileNames: true, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := StartsWithDirectory(tt.fileName, tt.directoryName, tt.useCaseSensitiveFileNames) + if result != tt.expected { + t.Errorf("StartsWithDirectory(%q, %q, %v) = %v, expected %v", + tt.fileName, tt.directoryName, tt.useCaseSensitiveFileNames, result, tt.expected) + } + }) + } +} diff --git a/testdata/baselines/reference/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 index e4c91af8f8..02949766f8 100644 --- a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference2.js.diff +++ b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference2.js.diff @@ -8,12 +8,4 @@ +const pkg2_1 = require("@raymondfeng/pkg2"); exports.ADMIN = pkg2_1.MetadataAccessor.create('1'); //// [index.js] - "use strict"; -@@= skipped -24, +24 lines =@@ - - //// [keys.d.ts] - import { MetadataAccessor } from "@raymondfeng/pkg2"; --export declare const ADMIN: MetadataAccessor; -+export declare const ADMIN: MetadataAccessor; - //// [index.d.ts] - export * from './keys'; \ No newline at end of file + "use strict"; \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.errors.txt b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.errors.txt new file mode 100644 index 0000000000..be730da192 --- /dev/null +++ b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.errors.txt @@ -0,0 +1,60 @@ +monorepo/pkg3/src/keys.ts(3,14): error TS2742: The inferred type of 'ADMIN' cannot be named without a reference to '../../pkg2/node_modules/@raymondfeng/pkg1/dist'. This is likely not portable. A type annotation is necessary. + + +==== monorepo/pkg3/tsconfig.json (0 errors) ==== + { + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "target": "es5", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "declaration": true + } + } + +==== monorepo/pkg3/src/index.ts (0 errors) ==== + export * from './keys'; +==== monorepo/pkg3/src/keys.ts (1 errors) ==== + import {MetadataAccessor} from "@raymondfeng/pkg2"; + + export const ADMIN = MetadataAccessor.create('1'); + ~~~~~ +!!! error TS2742: The inferred type of 'ADMIN' cannot be named without a reference to '../../pkg2/node_modules/@raymondfeng/pkg1/dist'. This is likely not portable. A type annotation is necessary. +==== monorepo/pkg1/dist/index.d.ts (0 errors) ==== + export * from './types'; +==== monorepo/pkg1/dist/types.d.ts (0 errors) ==== + export declare type A = { + id: string; + }; + export declare type B = { + id: number; + }; + export declare type IdType = A | B; + export declare class MetadataAccessor { + readonly key: string; + private constructor(); + toString(): string; + static create(key: string): MetadataAccessor; + } +==== monorepo/pkg1/package.json (0 errors) ==== + { + "name": "@raymondfeng/pkg1", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "typings": "dist/index.d.ts" + } +==== monorepo/pkg2/dist/index.d.ts (0 errors) ==== + export * from './types'; +==== monorepo/pkg2/dist/types.d.ts (0 errors) ==== + export {MetadataAccessor} from '@raymondfeng/pkg1'; +==== monorepo/pkg2/package.json (0 errors) ==== + { + "name": "@raymondfeng/pkg2", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "typings": "dist/index.d.ts" + } \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.errors.txt.diff b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.errors.txt.diff deleted file mode 100644 index b3cfc6c939..0000000000 --- a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.errors.txt.diff +++ /dev/null @@ -1,64 +0,0 @@ ---- old.declarationEmitReexportedSymlinkReference3.errors.txt -+++ new.declarationEmitReexportedSymlinkReference3.errors.txt -@@= skipped -0, +0 lines =@@ --monorepo/pkg3/src/keys.ts(3,14): error TS2742: The inferred type of 'ADMIN' cannot be named without a reference to '../../pkg2/node_modules/@raymondfeng/pkg1/dist'. This is likely not portable. A type annotation is necessary. -- -- --==== monorepo/pkg3/tsconfig.json (0 errors) ==== -- { -- "compilerOptions": { -- "outDir": "dist", -- "rootDir": "src", -- "target": "es5", -- "module": "commonjs", -- "strict": true, -- "esModuleInterop": true, -- "declaration": true -- } -- } -- --==== monorepo/pkg3/src/index.ts (0 errors) ==== -- export * from './keys'; --==== monorepo/pkg3/src/keys.ts (1 errors) ==== -- import {MetadataAccessor} from "@raymondfeng/pkg2"; -- -- export const ADMIN = MetadataAccessor.create('1'); -- ~~~~~ --!!! error TS2742: The inferred type of 'ADMIN' cannot be named without a reference to '../../pkg2/node_modules/@raymondfeng/pkg1/dist'. This is likely not portable. A type annotation is necessary. --==== monorepo/pkg1/dist/index.d.ts (0 errors) ==== -- export * from './types'; --==== monorepo/pkg1/dist/types.d.ts (0 errors) ==== -- export declare type A = { -- id: string; -- }; -- export declare type B = { -- id: number; -- }; -- export declare type IdType = A | B; -- export declare class MetadataAccessor { -- readonly key: string; -- private constructor(); -- toString(): string; -- static create(key: string): MetadataAccessor; -- } --==== monorepo/pkg1/package.json (0 errors) ==== -- { -- "name": "@raymondfeng/pkg1", -- "version": "1.0.0", -- "description": "", -- "main": "dist/index.js", -- "typings": "dist/index.d.ts" -- } --==== monorepo/pkg2/dist/index.d.ts (0 errors) ==== -- export * from './types'; --==== monorepo/pkg2/dist/types.d.ts (0 errors) ==== -- export {MetadataAccessor} from '@raymondfeng/pkg1'; --==== monorepo/pkg2/package.json (0 errors) ==== -- { -- "name": "@raymondfeng/pkg2", -- "version": "1.0.0", -- "description": "", -- "main": "dist/index.js", -- "typings": "dist/index.d.ts" -- } -+ \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.js b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.js index ca11f0526b..b581d79632 100644 --- a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.js +++ b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.js @@ -71,6 +71,6 @@ __exportStar(require("./keys"), exports); //// [keys.d.ts] import { MetadataAccessor } from "@raymondfeng/pkg2"; -export declare const ADMIN: MetadataAccessor; +export declare const ADMIN: any; //// [index.d.ts] export * from './keys'; diff --git a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.js.diff b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.js.diff index 63337a5943..af346cb8f5 100644 --- a/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.js.diff +++ b/testdata/baselines/reference/submodule/compiler/declarationEmitReexportedSymlinkReference3.js.diff @@ -15,6 +15,6 @@ +//// [keys.d.ts] +import { MetadataAccessor } from "@raymondfeng/pkg2"; -+export declare const ADMIN: MetadataAccessor; ++export declare const ADMIN: any; //// [index.d.ts] export * from './keys'; \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.js b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.js index 6be21eb01b..551c470855 100644 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.js +++ b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.js @@ -81,4 +81,4 @@ exports.a = pkg.invoke(); //// [index.d.ts] -export declare const a: import("../packageA/foo").Foo; +export declare const a: import("package-a/cls").Foo; diff --git a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.js.diff b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.js.diff deleted file mode 100644 index d5d42c3f80..0000000000 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.js.diff +++ /dev/null @@ -1,8 +0,0 @@ ---- old.symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.js -+++ new.symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.js -@@= skipped -80, +80 lines =@@ - - - //// [index.d.ts] --export declare const a: import("package-a/cls").Foo; -+export declare const a: import("../packageA/foo").Foo; \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js index 59acfb4178..747221c6c4 100644 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js +++ b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js @@ -36,4 +36,4 @@ exports.a = pkg.invoke(); //// [index.d.ts] -export declare const a: import("../packageA").Foo; +export declare const a: import("package-a").Foo; diff --git a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js.diff b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js.diff index ef5b9496f8..2bb82e39ba 100644 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js.diff +++ b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.js.diff @@ -8,7 +8,3 @@ +const pkg = require("package-b"); exports.a = pkg.invoke(); - - //// [index.d.ts] --export declare const a: import("package-a").Foo; -+export declare const a: import("../packageA").Foo; \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js index fe24bc8f2a..17052246c0 100644 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js +++ b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js @@ -38,4 +38,4 @@ exports.a = pkg.invoke(); //// [index.d.ts] -export declare const a: import("../packageA").Foo; +export declare const a: import("package-a").Foo; diff --git a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js.diff b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js.diff index 32ff6fc95c..ba36af39d2 100644 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js.diff +++ b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.js.diff @@ -8,7 +8,3 @@ +const pkg = require("package-b"); exports.a = pkg.invoke(); - - //// [index.d.ts] --export declare const a: import("package-a").Foo; -+export declare const a: import("../packageA").Foo; \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js index f1715cc27c..6d5548d955 100644 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js +++ b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js @@ -38,4 +38,4 @@ exports.a = pkg.invoke(); //// [index.d.ts] -export declare const a: import("../packageA").Foo; +export declare const a: import("package-a").Foo; diff --git a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js.diff b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js.diff index be48d6757d..fd46ac1a28 100644 --- a/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js.diff +++ b/testdata/baselines/reference/submodule/compiler/symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.js.diff @@ -8,7 +8,3 @@ +const pkg = require("package-b"); exports.a = pkg.invoke(); - - //// [index.d.ts] --export declare const a: import("package-a").Foo; -+export declare const a: import("../packageA").Foo; \ No newline at end of file diff --git a/testdata/baselines/reference/tsbuild/moduleSpecifiers/synthesized-module-specifiers-across-projects-resolve-correctly.js b/testdata/baselines/reference/tsbuild/moduleSpecifiers/synthesized-module-specifiers-across-projects-resolve-correctly.js index 26f491e365..3c15b28a44 100644 --- a/testdata/baselines/reference/tsbuild/moduleSpecifiers/synthesized-module-specifiers-across-projects-resolve-correctly.js +++ b/testdata/baselines/reference/tsbuild/moduleSpecifiers/synthesized-module-specifiers-across-projects-resolve-correctly.js @@ -159,7 +159,7 @@ export const LASSIE_CONFIG = { name: 'Lassie' }; //// [/home/src/workspaces/packages/src-dogs/lassie/lassiedog.d.ts] *new* import { Dog } from '../dog.js'; export declare class LassieDog extends Dog { - protected static getDogConfig: () => import("../index.js").DogConfig; + protected static getDogConfig: () => import("src-types").DogConfig; } //// [/home/src/workspaces/packages/src-dogs/lassie/lassiedog.js] *new* @@ -170,7 +170,7 @@ export class LassieDog extends Dog { } //// [/home/src/workspaces/packages/src-dogs/tsconfig.tsbuildinfo] *new* -{"version":"FakeTSVersion","root":[[4,8]],"fileNames":["lib.es2022.full.d.ts","../src-types/dogconfig.d.ts","../src-types/index.d.ts","./dogconfig.ts","./dog.ts","./lassie/lassieconfig.ts","./lassie/lassiedog.ts","./index.ts"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},{"version":"a71e22ebb89c8c5bea7cef8d090ace25-export interface DogConfig {\n name: string;\n}\n","impliedNodeFormat":99},{"version":"3c21c50da3a1aea8b6fafa5aa595f160-export * from './dogconfig.js';\n","impliedNodeFormat":99},{"version":"a8c9e5169f1e05ea3fd4da563dc779b7-import { DogConfig } from 'src-types';\n\nexport const DOG_CONFIG: DogConfig = {\n name: 'Default dog',\n};","signature":"55c35bfb192d26f7ab56e9447864b637-import { DogConfig } from 'src-types';\nexport declare const DOG_CONFIG: DogConfig;\n","impliedNodeFormat":99},{"version":"4ef4eb6072aff36903b09b7e1fa75eea-import { DogConfig } from 'src-types';\nimport { DOG_CONFIG } from './dogconfig.js';\n\nexport abstract class Dog {\n\n public static getCapabilities(): DogConfig {\n return DOG_CONFIG;\n }\n}","signature":"1130c09f22ac69e13b25f0c42f3a9379-import { DogConfig } from 'src-types';\nexport declare abstract class Dog {\n static getCapabilities(): DogConfig;\n}\n","impliedNodeFormat":99},{"version":"37fa5afea0e398a9cc485818c902b71c-import { DogConfig } from 'src-types';\n\nexport const LASSIE_CONFIG: DogConfig = { name: 'Lassie' };","signature":"2ef44fffbc07bb77765462af9f6df2a2-import { DogConfig } from 'src-types';\nexport declare const LASSIE_CONFIG: DogConfig;\n","impliedNodeFormat":99},{"version":"16f2a31a47590452f19f34bb56d0345f-import { Dog } from '../dog.js';\nimport { LASSIE_CONFIG } from './lassieconfig.js';\n\nexport class LassieDog extends Dog {\n protected static getDogConfig = () => LASSIE_CONFIG;\n}","signature":"4e9a2f5bdce32a44b15cca0af7254c50-import { Dog } from '../dog.js';\nexport declare class LassieDog extends Dog {\n protected static getDogConfig: () => import(\"../index.js\").DogConfig;\n}\n","impliedNodeFormat":99},{"version":"099983d5c3c8b20233df02ca964ad12f-export * from 'src-types';\nexport * from './lassie/lassiedog.js';","signature":"0fb03f7b5b8061b0e2cd78a4131e3df7-export * from 'src-types';\nexport * from './lassie/lassiedog.js';\n","impliedNodeFormat":99}],"fileIdsList":[[3,4],[3],[3,7],[5,6],[2]],"options":{"composite":true,"declaration":true,"module":100},"referencedMap":[[5,1],[4,2],[8,3],[6,2],[7,4],[3,5]],"latestChangedDtsFile":"./index.d.ts"} +{"version":"FakeTSVersion","root":[[4,8]],"fileNames":["lib.es2022.full.d.ts","../src-types/dogconfig.d.ts","../src-types/index.d.ts","./dogconfig.ts","./dog.ts","./lassie/lassieconfig.ts","./lassie/lassiedog.ts","./index.ts"],"fileInfos":[{"version":"8859c12c614ce56ba9a18e58384a198f-/// \ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ninterface SymbolConstructor {\n (desc?: string | number): symbol;\n for(name: string): symbol;\n readonly toStringTag: symbol;\n}\ndeclare var Symbol: SymbolConstructor;\ninterface Symbol {\n readonly [Symbol.toStringTag]: string;\n}\ndeclare const console: { log(msg: any): void; };","affectsGlobalScope":true,"impliedNodeFormat":1},{"version":"a71e22ebb89c8c5bea7cef8d090ace25-export interface DogConfig {\n name: string;\n}\n","impliedNodeFormat":99},{"version":"3c21c50da3a1aea8b6fafa5aa595f160-export * from './dogconfig.js';\n","impliedNodeFormat":99},{"version":"a8c9e5169f1e05ea3fd4da563dc779b7-import { DogConfig } from 'src-types';\n\nexport const DOG_CONFIG: DogConfig = {\n name: 'Default dog',\n};","signature":"55c35bfb192d26f7ab56e9447864b637-import { DogConfig } from 'src-types';\nexport declare const DOG_CONFIG: DogConfig;\n","impliedNodeFormat":99},{"version":"4ef4eb6072aff36903b09b7e1fa75eea-import { DogConfig } from 'src-types';\nimport { DOG_CONFIG } from './dogconfig.js';\n\nexport abstract class Dog {\n\n public static getCapabilities(): DogConfig {\n return DOG_CONFIG;\n }\n}","signature":"1130c09f22ac69e13b25f0c42f3a9379-import { DogConfig } from 'src-types';\nexport declare abstract class Dog {\n static getCapabilities(): DogConfig;\n}\n","impliedNodeFormat":99},{"version":"37fa5afea0e398a9cc485818c902b71c-import { DogConfig } from 'src-types';\n\nexport const LASSIE_CONFIG: DogConfig = { name: 'Lassie' };","signature":"2ef44fffbc07bb77765462af9f6df2a2-import { DogConfig } from 'src-types';\nexport declare const LASSIE_CONFIG: DogConfig;\n","impliedNodeFormat":99},{"version":"16f2a31a47590452f19f34bb56d0345f-import { Dog } from '../dog.js';\nimport { LASSIE_CONFIG } from './lassieconfig.js';\n\nexport class LassieDog extends Dog {\n protected static getDogConfig = () => LASSIE_CONFIG;\n}","signature":"e1943411d89cafd8c6f5a028539f5775-import { Dog } from '../dog.js';\nexport declare class LassieDog extends Dog {\n protected static getDogConfig: () => import(\"src-types\").DogConfig;\n}\n","impliedNodeFormat":99},{"version":"099983d5c3c8b20233df02ca964ad12f-export * from 'src-types';\nexport * from './lassie/lassiedog.js';","signature":"0fb03f7b5b8061b0e2cd78a4131e3df7-export * from 'src-types';\nexport * from './lassie/lassiedog.js';\n","impliedNodeFormat":99}],"fileIdsList":[[3,4],[3],[3,7],[5,6],[2]],"options":{"composite":true,"declaration":true,"module":100},"referencedMap":[[5,1],[4,2],[8,3],[6,2],[7,4],[3,5]],"latestChangedDtsFile":"./index.d.ts"} //// [/home/src/workspaces/packages/src-dogs/tsconfig.tsbuildinfo.readable.baseline.txt] *new* { "version": "FakeTSVersion", @@ -268,11 +268,11 @@ export class LassieDog extends Dog { { "fileName": "./lassie/lassiedog.ts", "version": "16f2a31a47590452f19f34bb56d0345f-import { Dog } from '../dog.js';\nimport { LASSIE_CONFIG } from './lassieconfig.js';\n\nexport class LassieDog extends Dog {\n protected static getDogConfig = () => LASSIE_CONFIG;\n}", - "signature": "4e9a2f5bdce32a44b15cca0af7254c50-import { Dog } from '../dog.js';\nexport declare class LassieDog extends Dog {\n protected static getDogConfig: () => import(\"../index.js\").DogConfig;\n}\n", + "signature": "e1943411d89cafd8c6f5a028539f5775-import { Dog } from '../dog.js';\nexport declare class LassieDog extends Dog {\n protected static getDogConfig: () => import(\"src-types\").DogConfig;\n}\n", "impliedNodeFormat": "ESNext", "original": { "version": "16f2a31a47590452f19f34bb56d0345f-import { Dog } from '../dog.js';\nimport { LASSIE_CONFIG } from './lassieconfig.js';\n\nexport class LassieDog extends Dog {\n protected static getDogConfig = () => LASSIE_CONFIG;\n}", - "signature": "4e9a2f5bdce32a44b15cca0af7254c50-import { Dog } from '../dog.js';\nexport declare class LassieDog extends Dog {\n protected static getDogConfig: () => import(\"../index.js\").DogConfig;\n}\n", + "signature": "e1943411d89cafd8c6f5a028539f5775-import { Dog } from '../dog.js';\nexport declare class LassieDog extends Dog {\n protected static getDogConfig: () => import(\"src-types\").DogConfig;\n}\n", "impliedNodeFormat": 99 } }, @@ -337,7 +337,7 @@ export class LassieDog extends Dog { ] }, "latestChangedDtsFile": "./index.d.ts", - "size": 3218 + "size": 3216 } //// [/home/src/workspaces/packages/src-types/dogconfig.d.ts] *new* export interface DogConfig { diff --git a/testdata/baselines/reference/tsc/declarationEmit/when-pkg-references-sibling-package-through-indirect-symlink.js b/testdata/baselines/reference/tsc/declarationEmit/when-pkg-references-sibling-package-through-indirect-symlink.js index 75149f241c..710a6e7b26 100644 --- a/testdata/baselines/reference/tsc/declarationEmit/when-pkg-references-sibling-package-through-indirect-symlink.js +++ b/testdata/baselines/reference/tsc/declarationEmit/when-pkg-references-sibling-package-through-indirect-symlink.js @@ -56,8 +56,13 @@ export const ADMIN = MetadataAccessor.create('1'); } tsgo -p pkg3 --explainFiles -ExitStatus:: Success +ExitStatus:: DiagnosticsPresent_OutputsGenerated Output:: +pkg3/src/keys.ts:2:14 - error TS2742: The inferred type of 'ADMIN' cannot be named without a reference to '../../pkg2/node_modules/@raymondfeng/pkg1/dist'. This is likely not portable. A type annotation is necessary. + +2 export const ADMIN = MetadataAccessor.create('1'); +   ~~~~~ + ../../../../home/src/tslibs/TS/Lib/lib.d.ts Default library for target 'ES5' pkg1/dist/types.d.ts @@ -73,6 +78,9 @@ pkg3/src/keys.ts Matched by default include pattern '**/*' pkg3/src/index.ts Matched by default include pattern '**/*' + +Found 1 error in pkg3/src/keys.ts:2 + //// [/home/src/tslibs/TS/Lib/lib.d.ts] *Lib* /// interface Boolean {} @@ -120,7 +128,7 @@ __exportStar(require("./keys"), exports); //// [/user/username/projects/myproject/pkg3/dist/keys.d.ts] *new* import { MetadataAccessor } from "@raymondfeng/pkg2"; -export declare const ADMIN: MetadataAccessor; +export declare const ADMIN: any; //// [/user/username/projects/myproject/pkg3/dist/keys.js] *new* "use strict"; 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 +