Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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.
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
Contributor 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