-
Notifications
You must be signed in to change notification settings - Fork 734
Fix #1034: Improve symlink resolution in module specifier generation #1902
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4269eff
07a2207
b7b39d5
0b0dcbf
1b8d265
bab6338
24985b7
80b7c37
54d5ddd
e8086c4
29a4113
618cef6
9de9df2
fe86f69
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,8 +23,10 @@ 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" | ||
| "github.com/microsoft/typescript-go/internal/vfs" | ||
| ) | ||
|
|
||
| type ProgramOptions struct { | ||
|
|
@@ -66,6 +68,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 +213,11 @@ 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) | ||
| } | ||
| p.populateSymlinkCacheFromResolutions() | ||
| return p | ||
| } | ||
|
|
||
|
|
@@ -240,6 +248,11 @@ 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) | ||
| } | ||
| p.populateSymlinkCacheFromResolutions() | ||
| return result, true | ||
| } | ||
|
|
||
|
|
@@ -1635,6 +1648,86 @@ 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 { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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
+1657
to
+1658
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This occurs regularly in pnpm workspaces when running |
||
| if len(p.resolvedModules) > 0 || len(p.typeResolutionsInFile) > 0 { | ||
| p.knownSymlinks.SetSymlinksFromResolutions(p.ForEachResolvedModule, p.ForEachResolvedTypeReferenceDirective) | ||
| } | ||
| p.populateSymlinkCacheFromResolutions() | ||
| } | ||
| return p.knownSymlinks | ||
| } | ||
|
|
||
| func (p *Program) populateSymlinkCacheFromResolutions() { | ||
| p.Host().FS().WalkDir( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don’t think we should do a WalkDir here. Basically, I think we should find the package.json for each non-declaration input file, and then resolve the dependencies for each unique package.json. Finding the package.json could be moved to file loading to avoid a separate loop over all the files (and in fact, this already happens in order to determine the module kind under certain settings/situations; with this we could make it unconditional). |
||
| p.GetCurrentDirectory(), | ||
| func(path string, d vfs.DirEntry, e error) error { | ||
| if e != nil { | ||
| return e | ||
| } | ||
| if !d.IsDir() { | ||
| return nil | ||
| } | ||
|
|
||
| packageJsonDir := p.GetNearestAncestorDirectoryWithPackageJson(tspath.GetDirectoryPath(path)) | ||
| if packageJsonDir == "" { | ||
| return nil | ||
| } | ||
|
|
||
| packageJsonPath := tspath.CombinePaths(packageJsonDir, "package.json") | ||
| // Check if we've already populated symlinks for this package.json | ||
| if p.knownSymlinks.IsPackagePopulated(packageJsonPath) { | ||
| return nil | ||
| } | ||
| pkgJsonInfo := p.GetPackageJsonInfo(packageJsonPath) | ||
| if pkgJsonInfo == nil { | ||
| return nil | ||
| } | ||
| pkgJson := pkgJsonInfo.GetContents() | ||
| if pkgJson == nil { | ||
| return nil | ||
| } | ||
| p.knownSymlinks.PopulateFromResolutions(packageJsonPath, pkgJson, p.ResolveModuleName) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are a two ways to save some work here:
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also think the naming of this (both the function we're in and the function being called here) should say something about dependencies |
||
| return nil | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| 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(), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should just copy the symlink cache on program clone. I think it's intended to be immutable after initialization, so by copy, I mean the new program can share the same reference. (If the cache were mutable, we would have to add a clone method on it.) This function needs to be dirt cheap, and calling it means you're asserting that the only relevant change that's happened in the Program's whole world is to the file you're passing in.