Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions internal/checker/nodebuilderimpl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions internal/compiler/emitHost.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
53 changes: 0 additions & 53 deletions internal/compiler/knownsymlinks.go

This file was deleted.

54 changes: 54 additions & 0 deletions internal/compiler/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition looks to be impossible as-written, but I think lazy initialization is a good idea. However, you need to guard the field initialization with a sync.Once like the other lazy computed caches.

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.
Comment on lines +1649 to +1650
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t recall exactly how this happened in Strada, but I don’t think this comment applies in Corsa.

Copy link
Author

@chase chase Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This occurs regularly in pnpm workspaces when running tsgo --build --emitDeclarationsOnly, if I recall correctly.

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(),
Expand Down
44 changes: 44 additions & 0 deletions internal/compiler/program_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
9 changes: 5 additions & 4 deletions internal/compiler/projectreferencedtsfakinghost.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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,
})
Expand Down Expand Up @@ -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
Expand Down
Loading