diff --git a/internal/api/api.go b/internal/api/api.go index 95e38fda6e..2f3c0ec9f0 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -29,9 +29,10 @@ type API struct { host APIHost options APIOptions - documentRegistry *project.DocumentRegistry - scriptInfosMu sync.RWMutex - scriptInfos map[tspath.Path]*project.ScriptInfo + documentRegistry *project.DocumentRegistry + scriptInfosMu sync.RWMutex + scriptInfos map[tspath.Path]*project.ScriptInfo + configFileRegistry *project.ConfigFileRegistry projects handleMap[project.Project] filesMu sync.Mutex @@ -65,6 +66,9 @@ func NewAPI(host APIHost, options APIOptions) *API { }, }, } + api.configFileRegistry = &project.ConfigFileRegistry{ + Host: api, + } return api } @@ -83,6 +87,11 @@ func (api *API) DocumentRegistry() *project.DocumentRegistry { return api.documentRegistry } +// ConfigFileRegistry implements ProjectHost. +func (api *API) ConfigFileRegistry() *project.ConfigFileRegistry { + return api.configFileRegistry +} + // FS implements ProjectHost. func (api *API) FS() vfs.FS { return api.host.FS() diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index 5915491a43..03535d8cdd 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -3579,3 +3579,23 @@ func IsTrivia(token Kind) bool { func HasDecorators(node *Node) bool { return HasSyntacticModifier(node, ModifierFlagsDecorator) } + +type hasFileNameImpl struct { + fileName string + path tspath.Path +} + +func NewHasFileName(fileName string, path tspath.Path) HasFileName { + return &hasFileNameImpl{ + fileName: fileName, + path: path, + } +} + +func (h *hasFileNameImpl) FileName() string { + return h.fileName +} + +func (h *hasFileNameImpl) Path() tspath.Path { + return h.path +} diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 872055650e..47610ff765 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -25,6 +25,7 @@ import ( "github.com/microsoft/typescript-go/internal/printer" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/stringutil" + "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -527,6 +528,7 @@ type Program interface { BindSourceFiles() FileExists(fileName string) bool GetSourceFile(fileName string) *ast.SourceFile + GetSourceFileForResolvedModule(fileName string) *ast.SourceFile GetEmitModuleFormatOfFile(sourceFile ast.HasFileName) core.ModuleKind GetEmitSyntaxForUsageLocation(sourceFile ast.HasFileName, usageLocation *ast.StringLiteralLike) core.ResolutionMode GetImpliedNodeFormatForEmit(sourceFile ast.HasFileName) core.ModuleKind @@ -535,6 +537,8 @@ type Program interface { GetSourceFileMetaData(path tspath.Path) *ast.SourceFileMetaData GetJSXRuntimeImportSpecifier(path tspath.Path) (moduleReference string, specifier *ast.Node) GetImportHelpersImportSpecifier(path tspath.Path) *ast.Node + IsSourceFromProjectReference(path tspath.Path) bool + GetSourceAndProjectReference(path tspath.Path) *tsoptions.SourceAndProjectReference } type Host interface { @@ -2086,7 +2090,7 @@ func (c *Checker) getSymbol(symbols ast.SymbolTable, name string, meaning ast.Sy } func (c *Checker) CheckSourceFile(ctx context.Context, sourceFile *ast.SourceFile) { - if SkipTypeChecking(sourceFile, c.compilerOptions) { + if SkipTypeChecking(sourceFile, c.compilerOptions, c.program) { return } c.checkSourceFile(ctx, sourceFile) @@ -6462,15 +6466,13 @@ func (c *Checker) checkAliasSymbol(node *ast.Node) { // in files that are unambiguously CommonJS in this mode. c.error(node, diagnostics.ESM_syntax_is_not_allowed_in_a_CommonJS_module_when_module_is_set_to_preserve) } - // !!! - - // if c.compilerOptions.VerbatimModuleSyntax.IsTrue() && !ast.IsTypeOnlyImportOrExportDeclaration(node) && node.Flags&ast.NodeFlagsAmbient == 0 && targetFlags&ast.SymbolFlagsConstEnum != 0 { - // constEnumDeclaration := target.ValueDeclaration - // redirect := host.getRedirectReferenceForResolutionFromSourceOfProject(ast.GetSourceFileOfNode(constEnumDeclaration).ResolvedPath) - // if constEnumDeclaration.Flags&ast.NodeFlagsAmbient != 0 && (redirect == nil || !shouldPreserveConstEnums(redirect.commandLine.options)) { - // c.error(node, diagnostics.Cannot_access_ambient_const_enums_when_0_is_enabled, c.getIsolatedModulesLikeFlagName()) - // } - // } + if c.compilerOptions.VerbatimModuleSyntax.IsTrue() && !ast.IsTypeOnlyImportOrExportDeclaration(node) && node.Flags&ast.NodeFlagsAmbient == 0 && targetFlags&ast.SymbolFlagsConstEnum != 0 { + constEnumDeclaration := target.ValueDeclaration + redirect := c.program.GetSourceAndProjectReference(ast.GetSourceFileOfNode(constEnumDeclaration).Path()) + if constEnumDeclaration.Flags&ast.NodeFlagsAmbient != 0 && (redirect == nil || !redirect.Resolved.CompilerOptions().ShouldPreserveConstEnums()) { + c.error(node, diagnostics.Cannot_access_ambient_const_enums_when_0_is_enabled, c.getIsolatedModulesLikeFlagName()) + } + } } if ast.IsImportSpecifier(node) { targetSymbol := c.resolveAliasWithDeprecationCheck(symbol, node) @@ -7184,15 +7186,14 @@ func (c *Checker) checkConstEnumAccess(node *ast.Node, t *Type) { // --verbatimModuleSyntax only gets checked here when the enum usage does not // resolve to an import, because imports of ambient const enums get checked // separately in `checkAliasSymbol`. - // !!! - // if c.compilerOptions.IsolatedModules.IsTrue() || c.compilerOptions.VerbatimModuleSyntax.IsTrue() && ok && c.resolveName(node, getFirstIdentifier(node).Text(), ast.SymbolFlagsAlias, nil, false, true) == nil { - // // Debug.assert(t.symbol.Flags&ast.SymbolFlagsConstEnum != 0) - // constEnumDeclaration := t.symbol.ValueDeclaration - // redirect := host.getRedirectReferenceForResolutionFromSourceOfProject(ast.GetSourceFileOfNode(constEnumDeclaration).ResolvedPath) - // if constEnumDeclaration.Flags&ast.NodeFlagsAmbient != 0 && !isValidTypeOnlyAliasUseSite(node) && (redirect == nil || !shouldPreserveConstEnums(redirect.commandLine.options)) { - // c.error(node, diagnostics.Cannot_access_ambient_const_enums_when_0_is_enabled, c.getIsolatedModulesLikeFlagName()) - // } - // } + if c.compilerOptions.IsolatedModules.IsTrue() || c.compilerOptions.VerbatimModuleSyntax.IsTrue() && ok && c.resolveName(node, ast.GetFirstIdentifier(node).Text(), ast.SymbolFlagsAlias, nil, false, true) == nil { + // Debug.assert(t.symbol.Flags&ast.SymbolFlagsConstEnum != 0) + constEnumDeclaration := t.symbol.ValueDeclaration + redirect := c.program.GetSourceAndProjectReference(ast.GetSourceFileOfNode(constEnumDeclaration).Path()) + if constEnumDeclaration.Flags&ast.NodeFlagsAmbient != 0 && !ast.IsValidTypeOnlyAliasUseSite(node) && (redirect == nil || !redirect.Resolved.CompilerOptions().ShouldPreserveConstEnums()) { + c.error(node, diagnostics.Cannot_access_ambient_const_enums_when_0_is_enabled, c.getIsolatedModulesLikeFlagName()) + } + } } func (c *Checker) instantiateTypeWithSingleGenericCallSignature(node *ast.Node, t *Type, checkMode CheckMode) *Type { @@ -14467,7 +14468,7 @@ func (c *Checker) resolveExternalModule(location *ast.Node, moduleReference stri var sourceFile *ast.SourceFile resolvedModule := c.program.GetResolvedModule(importingSourceFile, moduleReference, mode) if resolvedModule.IsResolved() { - sourceFile = c.program.GetSourceFile(resolvedModule.ResolvedFileName) + sourceFile = c.program.GetSourceFileForResolvedModule(resolvedModule.ResolvedFileName) } if sourceFile != nil { @@ -14573,6 +14574,21 @@ func (c *Checker) resolveExternalModule(location *ast.Node, moduleReference stri } if moduleNotFoundError != nil { + + // See if this was possibly a projectReference redirect + if resolvedModule.IsResolved() { + redirect := c.program.GetOutputAndProjectReference(tspath.ToPath(resolvedModule.ResolvedFileName, c.program.GetCurrentDirectory(), c.program.UseCaseSensitiveFileNames())) + if redirect != nil && redirect.OutputDts != "" { + c.error( + errorNode, + diagnostics.Output_file_0_has_not_been_built_from_source_file_1, + redirect.OutputDts, + resolvedModule.ResolvedFileName, + ) + return nil + } + } + // !!! isExtensionlessRelativePathImport := tspath.PathIsRelative(moduleReference) && !tspath.HasExtension(moduleReference) resolutionIsNode16OrNext := c.moduleResolutionKind == core.ModuleResolutionKindNode16 || c.moduleResolutionKind == core.ModuleResolutionKindNodeNext diff --git a/internal/checker/checker_test.go b/internal/checker/checker_test.go index 31db5a9ee6..161be261eb 100644 --- a/internal/checker/checker_test.go +++ b/internal/checker/checker_test.go @@ -36,7 +36,7 @@ foo.bar;` fs = bundled.WrapFS(fs) cd := "/" - host := compiler.NewCompilerHost(nil, cd, fs, bundled.LibPath()) + host := compiler.NewCompilerHost(nil, cd, fs, bundled.LibPath(), nil) parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, host, nil) assert.Equal(t, len(errors), 0, "Expected no errors in parsed command line") @@ -70,7 +70,7 @@ func TestCheckSrcCompiler(t *testing.T) { rootPath := tspath.CombinePaths(tspath.NormalizeSlashes(repo.TypeScriptSubmodulePath), "src", "compiler") - host := compiler.NewCompilerHost(nil, rootPath, fs, bundled.LibPath()) + host := compiler.NewCompilerHost(nil, rootPath, fs, bundled.LibPath(), nil) parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(tspath.CombinePaths(rootPath, "tsconfig.json"), &core.CompilerOptions{}, host, nil) assert.Equal(t, len(errors), 0, "Expected no errors in parsed command line") p := compiler.NewProgram(compiler.ProgramOptions{ @@ -87,7 +87,7 @@ func BenchmarkNewChecker(b *testing.B) { rootPath := tspath.CombinePaths(tspath.NormalizeSlashes(repo.TypeScriptSubmodulePath), "src", "compiler") - host := compiler.NewCompilerHost(nil, rootPath, fs, bundled.LibPath()) + host := compiler.NewCompilerHost(nil, rootPath, fs, bundled.LibPath(), nil) parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(tspath.CombinePaths(rootPath, "tsconfig.json"), &core.CompilerOptions{}, host, nil) assert.Equal(b, len(errors), 0, "Expected no errors in parsed command line") p := compiler.NewProgram(compiler.ProgramOptions{ diff --git a/internal/checker/utilities.go b/internal/checker/utilities.go index fa959157e7..3ccb0687b3 100644 --- a/internal/checker/utilities.go +++ b/internal/checker/utilities.go @@ -1471,10 +1471,11 @@ func forEachYieldExpression(body *ast.Node, visitor func(expr *ast.Node)) { traverse(body) } -func SkipTypeChecking(sourceFile *ast.SourceFile, options *core.CompilerOptions) bool { +func SkipTypeChecking(sourceFile *ast.SourceFile, options *core.CompilerOptions, host Program) bool { return options.NoCheck.IsTrue() || options.SkipLibCheck.IsTrue() && sourceFile.IsDeclarationFile || options.SkipDefaultLibCheck.IsTrue() && sourceFile.HasNoDefaultLib || + host.IsSourceFromProjectReference(sourceFile.Path()) || !canIncludeBindAndCheckDiagnostics(sourceFile, options) } diff --git a/internal/compiler/emitHost.go b/internal/compiler/emitHost.go index bceaae4ac5..3fa146826c 100644 --- a/internal/compiler/emitHost.go +++ b/internal/compiler/emitHost.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/typescript-go/internal/outputpaths" "github.com/microsoft/typescript-go/internal/printer" "github.com/microsoft/typescript-go/internal/transformers/declarations" + "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -73,18 +74,14 @@ func (host *emitHost) GetPackageJsonInfo(pkgJsonPath string) modulespecifiers.Pa return host.program.GetPackageJsonInfo(pkgJsonPath) } -func (host *emitHost) GetProjectReferenceRedirect(path string) string { - return host.program.GetProjectReferenceRedirect(path) +func (host *emitHost) GetOutputAndProjectReference(path tspath.Path) *tsoptions.OutputDtsAndProjectReference { + return host.program.GetOutputAndProjectReference(path) } func (host *emitHost) GetRedirectTargets(path tspath.Path) []string { return host.program.GetRedirectTargets(path) } -func (host *emitHost) IsSourceOfProjectReferenceRedirect(path string) bool { - return host.program.IsSourceOfProjectReferenceRedirect(path) -} - func (host *emitHost) GetEffectiveDeclarationFlags(node *ast.Node, flags ast.ModifierFlags) ast.ModifierFlags { return host.GetEmitResolver(ast.GetSourceFileOfNode(node), true).GetEffectiveDeclarationFlags(node, flags) } diff --git a/internal/compiler/emitter.go b/internal/compiler/emitter.go index 1eb27b621c..b8a91a6609 100644 --- a/internal/compiler/emitter.go +++ b/internal/compiler/emitter.go @@ -317,7 +317,10 @@ func sourceFileMayBeEmitted(sourceFile *ast.SourceFile, host printer.EmitHost, f return true } - // !!! Source files from referenced projects are not emitted + // Source files from referenced projects are not emitted + if host.GetOutputAndProjectReference(sourceFile.Path()) != nil { + return false + } // Any non json file should be emitted if !ast.IsJsonSourceFile(sourceFile) { diff --git a/internal/compiler/fileloader.go b/internal/compiler/fileloader.go index b8fd8da493..075d9f0660 100644 --- a/internal/compiler/fileloader.go +++ b/internal/compiler/fileloader.go @@ -2,7 +2,6 @@ package compiler import ( "cmp" - "iter" "slices" "strings" "sync" @@ -21,26 +20,35 @@ type fileLoader struct { resolver *module.Resolver defaultLibraryPath string comparePathsOptions tspath.ComparePathsOptions - wg core.WorkGroup supportedExtensions []string - tasksByFileName collections.SyncMap[string, *parseTask] - rootTasks []*parseTask + parseTasks *fileLoaderWorker[*parseTask] + projectReferenceParseTasks *fileLoaderWorker[*projectReferenceParseTask] + rootTasks []*parseTask totalFileCount atomic.Int32 libFileCount atomic.Int32 factoryMu sync.Mutex factory ast.NodeFactory + + projectReferenceFileMapper *projectReferenceFileMapper + dtsDirectories collections.Set[tspath.Path] } type processedFiles struct { + resolver *module.Resolver files []*ast.SourceFile + filesByPath map[tspath.Path]*ast.SourceFile + projectReferenceFileMapper *projectReferenceFileMapper missingFiles []string resolvedModules map[tspath.Path]module.ModeAwareCache[*module.ResolvedModule] + typeResolutionsInFile map[tspath.Path]module.ModeAwareCache[*module.ResolvedTypeReferenceDirective] sourceFileMetaDatas map[tspath.Path]*ast.SourceFileMetaData jsxRuntimeImportSpecifiers map[tspath.Path]*jsxRuntimeImportSpecifier importHelpersImportSpecifiers map[tspath.Path]*ast.Node + // List of present unsupported extensions + unsupportedExtensions []string } type jsxRuntimeImportSpecifier struct { @@ -50,7 +58,6 @@ type jsxRuntimeImportSpecifier struct { func processAllProgramFiles( opts ProgramOptions, - resolver *module.Resolver, libs []string, singleThreaded bool, ) processedFiles { @@ -59,24 +66,32 @@ func processAllProgramFiles( supportedExtensions := tsoptions.GetSupportedExtensions(compilerOptions, nil /*extraFileExtensions*/) loader := fileLoader{ opts: opts, - resolver: resolver, defaultLibraryPath: tspath.GetNormalizedAbsolutePath(opts.Host.DefaultLibraryPath(), opts.Host.GetCurrentDirectory()), comparePathsOptions: tspath.ComparePathsOptions{ UseCaseSensitiveFileNames: opts.Host.FS().UseCaseSensitiveFileNames(), CurrentDirectory: opts.Host.GetCurrentDirectory(), }, - wg: core.NewWorkGroup(singleThreaded), + parseTasks: &fileLoaderWorker[*parseTask]{ + wg: core.NewWorkGroup(singleThreaded), + getSubTasks: getSubTasksOfParseTask, + }, + projectReferenceParseTasks: &fileLoaderWorker[*projectReferenceParseTask]{ + wg: core.NewWorkGroup(singleThreaded), + getSubTasks: getSubTasksOfProjectReferenceParseTask, + }, rootTasks: make([]*parseTask, 0, len(rootFiles)+len(libs)), supportedExtensions: core.Flatten(tsoptions.GetSupportedExtensionsWithJsonIfResolveJsonModule(compilerOptions, supportedExtensions)), } - + loader.addProjectReferenceTasks() + loader.resolver = module.NewResolver(loader.projectReferenceFileMapper.host, compilerOptions, opts.TypingsLocation, opts.ProjectName) loader.addRootTasks(rootFiles, false) loader.addRootTasks(libs, true) loader.addAutomaticTypeDirectiveTasks() - loader.startTasks(loader.rootTasks) - - loader.wg.RunAndWait() + loader.parseTasks.runAndWait(&loader, loader.rootTasks) + // Clear out loader and host to ensure its not used post program creation + loader.projectReferenceFileMapper.loader = nil + loader.projectReferenceFileMapper.host = nil totalFileCount := int(loader.totalFileCount.Load()) libFileCount := int(loader.libFileCount.Load()) @@ -85,16 +100,22 @@ func processAllProgramFiles( files := make([]*ast.SourceFile, 0, totalFileCount-libFileCount) libFiles := make([]*ast.SourceFile, 0, totalFileCount) // totalFileCount here since we append files to it later to construct the final list + filesByPath := make(map[tspath.Path]*ast.SourceFile, totalFileCount) resolvedModules := make(map[tspath.Path]module.ModeAwareCache[*module.ResolvedModule], totalFileCount) + typeResolutionsInFile := make(map[tspath.Path]module.ModeAwareCache[*module.ResolvedTypeReferenceDirective], totalFileCount) sourceFileMetaDatas := make(map[tspath.Path]*ast.SourceFileMetaData, totalFileCount) var jsxRuntimeImportSpecifiers map[tspath.Path]*jsxRuntimeImportSpecifier var importHelpersImportSpecifiers map[tspath.Path]*ast.Node + var unsupportedExtensions []string - for task := range loader.collectTasks(loader.rootTasks) { + loader.parseTasks.collect(&loader, loader.rootTasks, func(task *parseTask, _ []tspath.Path) { file := task.file + if task.isRedirected { + return + } if file == nil { missingFiles = append(missingFiles, task.normalizedFilePath) - continue + return } if task.isLib { libFiles = append(libFiles, file) @@ -102,7 +123,10 @@ func processAllProgramFiles( files = append(files, file) } path := file.Path() + + filesByPath[path] = file resolvedModules[path] = task.resolutionsInFile + typeResolutionsInFile[path] = task.typeResolutionsInFile sourceFileMetaDatas[path] = task.metadata if task.jsxRuntimeImportSpecifier != nil { if jsxRuntimeImportSpecifiers == nil { @@ -116,20 +140,33 @@ func processAllProgramFiles( } importHelpersImportSpecifiers[path] = task.importHelpersImportSpecifier } - } + extension := tspath.TryGetExtensionFromPath(file.FileName()) + if slices.Contains(tspath.SupportedJSExtensionsFlat, extension) { + unsupportedExtensions = core.AppendIfUnique(unsupportedExtensions, extension) + } + }) loader.sortLibs(libFiles) allFiles := append(libFiles, files...) return processedFiles{ + resolver: loader.resolver, files: allFiles, + filesByPath: filesByPath, + projectReferenceFileMapper: loader.projectReferenceFileMapper, resolvedModules: resolvedModules, + typeResolutionsInFile: typeResolutionsInFile, sourceFileMetaDatas: sourceFileMetaDatas, jsxRuntimeImportSpecifiers: jsxRuntimeImportSpecifiers, importHelpersImportSpecifiers: importHelpersImportSpecifiers, + unsupportedExtensions: unsupportedExtensions, } } +func (p *fileLoader) toPath(file string) tspath.Path { + return tspath.ToPath(file, p.opts.Host.GetCurrentDirectory(), p.opts.Host.FS().UseCaseSensitiveFileNames()) +} + func (p *fileLoader) addRootTasks(files []string, isLib bool) { for _, fileName := range files { absPath := tspath.GetNormalizedAbsolutePath(fileName, p.opts.Host.GetCurrentDirectory()) @@ -158,45 +195,42 @@ func (p *fileLoader) addAutomaticTypeDirectiveTasks() { } } -func (p *fileLoader) startTasks(tasks []*parseTask) { - if len(tasks) > 0 { - for i, task := range tasks { - loadedTask, loaded := p.tasksByFileName.LoadOrStore(task.normalizedFilePath, task) - if loaded { - // dedup tasks to ensure correct file order, regardless of which task would be started first - tasks[i] = loadedTask - } else { - loadedTask.start(p) - } - } +func (p *fileLoader) addProjectReferenceTasks() { + p.projectReferenceFileMapper = &projectReferenceFileMapper{ + opts: p.opts, + host: p.opts.Host, } -} - -func (p *fileLoader) collectTasks(tasks []*parseTask) iter.Seq[*parseTask] { - return func(yield func(*parseTask) bool) { - p.collectTasksWorker(tasks, collections.Set[*parseTask]{}, yield) + projectReferences := p.opts.Config.ProjectReferences() + if len(projectReferences) == 0 { + return } -} -func (p *fileLoader) collectTasksWorker(tasks []*parseTask, seen collections.Set[*parseTask], yield func(*parseTask) bool) bool { - for _, task := range tasks { - // ensure we only walk each task once - if seen.Has(task) { - continue - } - seen.Add(task) - - if len(task.subTasks) > 0 { - if !p.collectTasksWorker(task.subTasks, seen, yield) { - return false + rootTasks := createProjectReferenceParseTasks(projectReferences) + p.projectReferenceParseTasks.runAndWait(p, rootTasks) + p.projectReferenceFileMapper.init(p, rootTasks) + + // Add files from project references as root if the module kind is 'none'. + // This ensures that files from project references are included in the root tasks + // when no module system is specified, allowing including all files for global symbol merging + // !!! sheetal Do we really need it? + if len(p.opts.Config.FileNames()) != 0 { + for _, resolved := range p.projectReferenceFileMapper.getResolvedProjectReferences() { + if resolved == nil || resolved.CompilerOptions().GetEmitModuleKind() != core.ModuleKindNone { + continue + } + if p.opts.canUseProjectReferenceSource() { + for _, fileName := range resolved.FileNames() { + p.rootTasks = append(p.rootTasks, &parseTask{normalizedFilePath: fileName, isLib: false}) + } + } else { + for outputDts := range resolved.GetOutputDeclarationFileNames() { + if outputDts != "" { + p.rootTasks = append(p.rootTasks, &parseTask{normalizedFilePath: outputDts, isLib: false}) + } + } } - } - - if !yield(task) { - return false } } - return true } func (p *fileLoader) sortLibs(libFiles []*ast.SourceFile) { @@ -225,73 +259,6 @@ func (p *fileLoader) getDefaultLibFilePriority(a *ast.SourceFile) int { return len(tsoptions.Libs) + 2 } -type parseTask struct { - normalizedFilePath string - file *ast.SourceFile - isLib bool - subTasks []*parseTask - - metadata *ast.SourceFileMetaData - resolutionsInFile module.ModeAwareCache[*module.ResolvedModule] - importHelpersImportSpecifier *ast.Node - jsxRuntimeImportSpecifier *jsxRuntimeImportSpecifier -} - -func (t *parseTask) start(loader *fileLoader) { - loader.totalFileCount.Add(1) - if t.isLib { - loader.libFileCount.Add(1) - } - - loader.wg.Queue(func() { - t.metadata = loader.loadSourceFileMetaData(t.normalizedFilePath) - file := loader.parseSourceFile(t.normalizedFilePath, t.metadata) - if file == nil { - return - } - - t.file = file - - // !!! if noResolve, skip all of this - t.subTasks = make([]*parseTask, 0, len(file.ReferencedFiles)+len(file.Imports())+len(file.ModuleAugmentations)) - - for _, ref := range file.ReferencedFiles { - resolvedPath := loader.resolveTripleslashPathReference(ref.FileName, file.FileName()) - t.addSubTask(resolvedPath, false) - } - - compilerOptions := loader.opts.Config.CompilerOptions() - for _, ref := range file.TypeReferenceDirectives { - resolutionMode := getModeForTypeReferenceDirectiveInFile(ref, file, t.metadata, compilerOptions) - resolved := loader.resolver.ResolveTypeReferenceDirective(ref.FileName, file.FileName(), resolutionMode, nil) - if resolved.IsResolved() { - t.addSubTask(resolved.ResolvedFileName, false) - } - } - - if compilerOptions.NoLib != core.TSTrue { - for _, lib := range file.LibReferenceDirectives { - name, ok := tsoptions.GetLibFileName(lib.FileName) - if !ok { - continue - } - t.addSubTask(tspath.CombinePaths(loader.defaultLibraryPath, name), true) - } - } - - toParse, resolutionsInFile, importHelpersImportSpecifier, jsxRuntimeImportSpecifier := loader.resolveImportsAndModuleAugmentations(file, t.metadata) - for _, imp := range toParse { - t.addSubTask(imp, false) - } - - t.resolutionsInFile = resolutionsInFile - t.importHelpersImportSpecifier = importHelpersImportSpecifier - t.jsxRuntimeImportSpecifier = jsxRuntimeImportSpecifier - - loader.startTasks(t.subTasks) - }) -} - func (p *fileLoader) loadSourceFileMetaData(fileName string) *ast.SourceFileMetaData { packageJsonScope := p.resolver.GetPackageJsonScopeIfApplicable(fileName) var packageJsonType, packageJsonDirectory string @@ -309,17 +276,12 @@ func (p *fileLoader) loadSourceFileMetaData(fileName string) *ast.SourceFileMeta } } -func (p *fileLoader) parseSourceFile(fileName string, metadata *ast.SourceFileMetaData) *ast.SourceFile { - path := tspath.ToPath(fileName, p.opts.Host.GetCurrentDirectory(), p.opts.Host.FS().UseCaseSensitiveFileNames()) - sourceFile := p.opts.Host.GetSourceFile(fileName, path, p.opts.Config.CompilerOptions().SourceFileAffecting(), metadata) // TODO(jakebailey): cache :( +func (p *fileLoader) parseSourceFile(t *parseTask) *ast.SourceFile { + path := p.toPath(t.normalizedFilePath) + sourceFile := p.opts.Host.GetSourceFile(t.normalizedFilePath, path, p.projectReferenceFileMapper.getCompilerOptionsForFile(t).SourceFileAffecting(), t.metadata) // TODO(jakebailey): cache :( return sourceFile } -func (t *parseTask) addSubTask(fileName string, isLib bool) { - normalizedFilePath := tspath.NormalizePath(fileName) - t.subTasks = append(t.subTasks, &parseTask{normalizedFilePath: normalizedFilePath, isLib: isLib}) -} - func (p *fileLoader) resolveTripleslashPathReference(moduleName string, containingFile string) string { basePath := tspath.GetDirectoryPath(containingFile) referencedFileName := moduleName @@ -330,6 +292,26 @@ func (p *fileLoader) resolveTripleslashPathReference(moduleName string, containi return tspath.NormalizePath(referencedFileName) } +func (p *fileLoader) resolveTypeReferenceDirectives(file *ast.SourceFile, meta *ast.SourceFileMetaData) ( + toParse []string, + typeResolutionsInFile module.ModeAwareCache[*module.ResolvedTypeReferenceDirective], +) { + if len(file.TypeReferenceDirectives) != 0 { + toParse = make([]string, 0, len(file.TypeReferenceDirectives)) + typeResolutionsInFile = make(module.ModeAwareCache[*module.ResolvedTypeReferenceDirective], len(file.TypeReferenceDirectives)) + for _, ref := range file.TypeReferenceDirectives { + redirect := p.projectReferenceFileMapper.getRedirectForResolution(file) + resolutionMode := getModeForTypeReferenceDirectiveInFile(ref, file, meta, module.GetCompilerOptionsWithRedirect(p.opts.Config.CompilerOptions(), redirect)) + resolved := p.resolver.ResolveTypeReferenceDirective(ref.FileName, file.FileName(), resolutionMode, redirect) + typeResolutionsInFile[module.ModeAwareCacheKey{Name: ref.FileName, Mode: resolutionMode}] = resolved + if resolved.IsResolved() { + toParse = append(toParse, resolved.ResolvedFileName) + } + } + } + return toParse, typeResolutionsInFile +} + const externalHelpersModuleNameText = "tslib" // TODO(jakebailey): dedupe func (p *fileLoader) resolveImportsAndModuleAugmentations(file *ast.SourceFile, meta *ast.SourceFileMetaData) ( @@ -350,15 +332,16 @@ func (p *fileLoader) resolveImportsAndModuleAugmentations(file *ast.SourceFile, isJavaScriptFile := ast.IsSourceFileJS(file) isExternalModuleFile := ast.IsExternalModule(file) - compilerOptions := p.opts.Config.CompilerOptions() - if isJavaScriptFile || (!file.IsDeclarationFile && (compilerOptions.GetIsolatedModules() || isExternalModuleFile)) { - if compilerOptions.ImportHelpers.IsTrue() { + redirect := p.projectReferenceFileMapper.getRedirectForResolution(file) + optionsForFile := module.GetCompilerOptionsWithRedirect(p.opts.Config.CompilerOptions(), redirect) + if isJavaScriptFile || (!file.IsDeclarationFile && (optionsForFile.GetIsolatedModules() || isExternalModuleFile)) { + if optionsForFile.ImportHelpers.IsTrue() { specifier := p.createSyntheticImport(externalHelpersModuleNameText, file) moduleNames = append(moduleNames, specifier) importHelpersImportSpecifier = specifier } - jsxImport := ast.GetJSXRuntimeImport(ast.GetJSXImplicitImportBase(compilerOptions, file), compilerOptions) + jsxImport := ast.GetJSXRuntimeImport(ast.GetJSXImplicitImportBase(optionsForFile, file), optionsForFile) if jsxImport != "" { specifier := p.createSyntheticImport(jsxImport, file) moduleNames = append(moduleNames, specifier) @@ -372,8 +355,7 @@ func (p *fileLoader) resolveImportsAndModuleAugmentations(file *ast.SourceFile, if len(moduleNames) != 0 { toParse = make([]string, 0, len(moduleNames)) - resolutions := p.resolveModuleNames(moduleNames, file, meta) - optionsForFile := p.getCompilerOptionsForFile(file) + resolutions := p.resolveModuleNames(moduleNames, file, meta, redirect) resolutionsInFile = make(module.ModeAwareCache[*module.ResolvedModule], len(resolutions)) @@ -395,9 +377,9 @@ func (p *fileLoader) resolveImportsAndModuleAugmentations(file *ast.SourceFile, // Don't add the file if it has a bad extension (e.g. 'tsx' if we don't have '--allowJs') // This may still end up being an untyped module -- the file won't be included but imports will be allowed. hasAllowedExtension := false - if compilerOptions.GetResolveJsonModule() { + if optionsForFile.GetResolveJsonModule() { hasAllowedExtension = tspath.FileExtensionIsOneOf(resolvedFileName, tspath.SupportedTSExtensionsWithJsonFlat) - } else if compilerOptions.AllowJs.IsTrue() { + } else if optionsForFile.AllowJs.IsTrue() { hasAllowedExtension = tspath.FileExtensionIsOneOf(resolvedFileName, tspath.SupportedJSExtensionsFlat) || tspath.FileExtensionIsOneOf(resolvedFileName, tspath.SupportedTSExtensionsFlat) } else { hasAllowedExtension = tspath.FileExtensionIsOneOf(resolvedFileName, tspath.SupportedTSExtensionsFlat) @@ -415,7 +397,7 @@ func (p *fileLoader) resolveImportsAndModuleAugmentations(file *ast.SourceFile, return toParse, resolutionsInFile, importHelpersImportSpecifier, jsxRuntimeImportSpecifier_ } -func (p *fileLoader) resolveModuleNames(entries []*ast.Node, file *ast.SourceFile, meta *ast.SourceFileMetaData) []*resolution { +func (p *fileLoader) resolveModuleNames(entries []*ast.Node, file *ast.SourceFile, meta *ast.SourceFileMetaData, redirect *tsoptions.ParsedCommandLine) []*resolution { if len(entries) == 0 { return nil } @@ -427,7 +409,7 @@ func (p *fileLoader) resolveModuleNames(entries []*ast.Node, file *ast.SourceFil if moduleName == "" { continue } - resolvedModule := p.resolver.ResolveModuleName(moduleName, file.FileName(), getModeForUsageLocation(file.FileName(), meta, entry, p.opts.Config.CompilerOptions()), nil) + resolvedModule := p.resolver.ResolveModuleName(moduleName, file.FileName(), getModeForUsageLocation(file.FileName(), meta, entry, module.GetCompilerOptionsWithRedirect(p.opts.Config.CompilerOptions(), redirect)), redirect) resolvedModules = append(resolvedModules, &resolution{node: entry, resolvedModule: resolvedModule}) } @@ -452,11 +434,6 @@ type resolution struct { resolvedModule *module.ResolvedModule } -func (p *fileLoader) getCompilerOptionsForFile(file *ast.SourceFile) *core.CompilerOptions { - // !!! return getRedirectReferenceForResolution(file)?.commandLine.options || options; - return p.opts.Config.CompilerOptions() -} - func getModeForTypeReferenceDirectiveInFile(ref *ast.FileReference, file *ast.SourceFile, meta *ast.SourceFileMetaData, options *core.CompilerOptions) core.ResolutionMode { if ref.ResolutionMode != core.ResolutionModeNone { return ref.ResolutionMode diff --git a/internal/compiler/fileloadertask.go b/internal/compiler/fileloadertask.go new file mode 100644 index 0000000000..9ad77d7f32 --- /dev/null +++ b/internal/compiler/fileloadertask.go @@ -0,0 +1,64 @@ +package compiler + +import ( + "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/tspath" +) + +type fileLoaderWorkerTask interface { + comparable + FileName() string + start(loader *fileLoader) +} + +type fileLoaderWorker[K fileLoaderWorkerTask] struct { + wg core.WorkGroup + tasksByFileName collections.SyncMap[string, K] + getSubTasks func(t K) []K +} + +func (w *fileLoaderWorker[K]) runAndWait(loader *fileLoader, tasks []K) { + w.start(loader, tasks) + w.wg.RunAndWait() +} + +func (w *fileLoaderWorker[K]) start(loader *fileLoader, tasks []K) { + if len(tasks) > 0 { + for i, task := range tasks { + loadedTask, loaded := w.tasksByFileName.LoadOrStore(task.FileName(), task) + if loaded { + // dedup tasks to ensure correct file order, regardless of which task would be started first + tasks[i] = loadedTask + } else { + w.wg.Queue(func() { + task.start(loader) + subTasks := w.getSubTasks(task) + w.start(loader, subTasks) + }) + } + } + } +} + +func (w *fileLoaderWorker[K]) collect(loader *fileLoader, tasks []K, iterate func(K, []tspath.Path)) []tspath.Path { + return w.collectWorker(loader, tasks, iterate, collections.Set[K]{}) +} + +func (w *fileLoaderWorker[K]) collectWorker(loader *fileLoader, tasks []K, iterate func(K, []tspath.Path), seen collections.Set[K]) []tspath.Path { + var results []tspath.Path + for _, task := range tasks { + // ensure we only walk each task once + if seen.Has(task) { + continue + } + seen.Add(task) + var subResults []tspath.Path + if subTasks := w.getSubTasks(task); len(subTasks) > 0 { + subResults = w.collectWorker(loader, subTasks, iterate, seen) + } + iterate(task, subResults) + results = append(results, loader.toPath(task.FileName())) + } + return results +} diff --git a/internal/compiler/host.go b/internal/compiler/host.go index b2079fe275..fcaef38c6f 100644 --- a/internal/compiler/host.go +++ b/internal/compiler/host.go @@ -2,9 +2,11 @@ package compiler import ( "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/parser" "github.com/microsoft/typescript-go/internal/scanner" + "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/cachedvfs" @@ -17,6 +19,7 @@ type CompilerHost interface { NewLine() string Trace(msg string) GetSourceFile(fileName string, path tspath.Path, options *core.SourceFileAffectingCompilerOptions, metadata *ast.SourceFileMetaData) *ast.SourceFile + GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine } type FileInfo struct { @@ -27,23 +30,37 @@ type FileInfo struct { var _ CompilerHost = (*compilerHost)(nil) type compilerHost struct { - options *core.CompilerOptions - currentDirectory string - fs vfs.FS - defaultLibraryPath string + options *core.CompilerOptions + currentDirectory string + fs vfs.FS + defaultLibraryPath string + extendedConfigCache *collections.SyncMap[tspath.Path, *tsoptions.ExtendedConfigCacheEntry] } -func NewCachedFSCompilerHost(options *core.CompilerOptions, currentDirectory string, fs vfs.FS, defaultLibraryPath string) CompilerHost { - return NewCompilerHost(options, currentDirectory, cachedvfs.From(fs), defaultLibraryPath) +func NewCachedFSCompilerHost( + options *core.CompilerOptions, + currentDirectory string, + fs vfs.FS, + defaultLibraryPath string, + extendedConfigCache *collections.SyncMap[tspath.Path, *tsoptions.ExtendedConfigCacheEntry], +) CompilerHost { + return NewCompilerHost(options, currentDirectory, cachedvfs.From(fs), defaultLibraryPath, extendedConfigCache) } -func NewCompilerHost(options *core.CompilerOptions, currentDirectory string, fs vfs.FS, defaultLibraryPath string) CompilerHost { - h := &compilerHost{} - h.options = options - h.currentDirectory = currentDirectory - h.fs = fs - h.defaultLibraryPath = defaultLibraryPath - return h +func NewCompilerHost( + options *core.CompilerOptions, + currentDirectory string, + fs vfs.FS, + defaultLibraryPath string, + extendedConfigCache *collections.SyncMap[tspath.Path, *tsoptions.ExtendedConfigCacheEntry], +) CompilerHost { + return &compilerHost{ + options: options, + currentDirectory: currentDirectory, + fs: fs, + defaultLibraryPath: defaultLibraryPath, + extendedConfigCache: extendedConfigCache, + } } func (h *compilerHost) FS() vfs.FS { @@ -83,3 +100,8 @@ func (h *compilerHost) GetSourceFile(fileName string, path tspath.Path, options } return parser.ParseSourceFile(fileName, path, text, options, metadata, scanner.JSDocParsingModeParseForTypeErrors) } + +func (h *compilerHost) GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine { + commandLine, _ := tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, nil, h, nil) + return commandLine +} diff --git a/internal/compiler/knownsymlinks.go b/internal/compiler/knownsymlinks.go new file mode 100644 index 0000000000..246a641127 --- /dev/null +++ b/internal/compiler/knownsymlinks.go @@ -0,0 +1,53 @@ +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/parsetask.go b/internal/compiler/parsetask.go new file mode 100644 index 0000000000..5c4e506918 --- /dev/null +++ b/internal/compiler/parsetask.go @@ -0,0 +1,102 @@ +package compiler + +import ( + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/tsoptions" + "github.com/microsoft/typescript-go/internal/tspath" +) + +type parseTask struct { + normalizedFilePath string + path tspath.Path + file *ast.SourceFile + isLib bool + isRedirected bool + subTasks []*parseTask + + metadata *ast.SourceFileMetaData + resolutionsInFile module.ModeAwareCache[*module.ResolvedModule] + typeResolutionsInFile module.ModeAwareCache[*module.ResolvedTypeReferenceDirective] + importHelpersImportSpecifier *ast.Node + jsxRuntimeImportSpecifier *jsxRuntimeImportSpecifier +} + +func (t *parseTask) FileName() string { + return t.normalizedFilePath +} + +func (t *parseTask) Path() tspath.Path { + return t.path +} + +func (t *parseTask) start(loader *fileLoader) { + t.path = loader.toPath(t.normalizedFilePath) + redirect := loader.projectReferenceFileMapper.getParseFileRedirect(t) + if redirect != "" { + t.redirect(loader, redirect) + return + } + + loader.totalFileCount.Add(1) + if t.isLib { + loader.libFileCount.Add(1) + } + + t.metadata = loader.loadSourceFileMetaData(t.normalizedFilePath) + file := loader.parseSourceFile(t) + if file == nil { + return + } + + t.file = file + + // !!! if noResolve, skip all of this + t.subTasks = make([]*parseTask, 0, len(file.ReferencedFiles)+len(file.Imports())+len(file.ModuleAugmentations)) + + for _, ref := range file.ReferencedFiles { + resolvedPath := loader.resolveTripleslashPathReference(ref.FileName, file.FileName()) + t.addSubTask(resolvedPath, false) + } + + compilerOptions := loader.opts.Config.CompilerOptions() + toParseTypeRefs, typeResolutionsInFile := loader.resolveTypeReferenceDirectives(file, t.metadata) + t.typeResolutionsInFile = typeResolutionsInFile + for _, typeResolution := range toParseTypeRefs { + t.addSubTask(typeResolution, false) + } + + if compilerOptions.NoLib != core.TSTrue { + for _, lib := range file.LibReferenceDirectives { + name, ok := tsoptions.GetLibFileName(lib.FileName) + if !ok { + continue + } + t.addSubTask(tspath.CombinePaths(loader.defaultLibraryPath, name), true) + } + } + + toParse, resolutionsInFile, importHelpersImportSpecifier, jsxRuntimeImportSpecifier := loader.resolveImportsAndModuleAugmentations(file, t.metadata) + for _, imp := range toParse { + t.addSubTask(imp, false) + } + + t.resolutionsInFile = resolutionsInFile + t.importHelpersImportSpecifier = importHelpersImportSpecifier + t.jsxRuntimeImportSpecifier = jsxRuntimeImportSpecifier +} + +func (t *parseTask) redirect(loader *fileLoader, fileName string) { + t.isRedirected = true + t.subTasks = []*parseTask{{normalizedFilePath: tspath.NormalizePath(fileName), isLib: t.isLib}} +} + +func (t *parseTask) addSubTask(fileName string, isLib bool) { + normalizedFilePath := tspath.NormalizePath(fileName) + t.subTasks = append(t.subTasks, &parseTask{normalizedFilePath: normalizedFilePath, isLib: isLib}) +} + +func getSubTasksOfParseTask(t *parseTask) []*parseTask { + return t.subTasks +} diff --git a/internal/compiler/program.go b/internal/compiler/program.go index bf94b73df8..aa66e69f13 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -23,33 +23,31 @@ import ( ) type ProgramOptions struct { - Host CompilerHost - Config *tsoptions.ParsedCommandLine - SingleThreaded core.Tristate - CreateCheckerPool func(*Program) CheckerPool - TypingsLocation string - ProjectName string + Host CompilerHost + Config *tsoptions.ParsedCommandLine + UseSourceOfProjectReference bool + SingleThreaded core.Tristate + CreateCheckerPool func(*Program) CheckerPool + TypingsLocation string + ProjectName string +} + +func (p *ProgramOptions) canUseProjectReferenceSource() bool { + return p.UseSourceOfProjectReference && !p.Config.CompilerOptions().DisableSourceOfProjectReferenceRedirect.IsTrue() } type Program struct { - opts ProgramOptions - nodeModules map[string]*ast.SourceFile - checkerPool CheckerPool - currentDirectory string + opts ProgramOptions + nodeModules map[string]*ast.SourceFile + checkerPool CheckerPool sourceAffectingCompilerOptionsOnce sync.Once sourceAffectingCompilerOptions *core.SourceFileAffectingCompilerOptions - resolver *module.Resolver - - comparePathsOptions tspath.ComparePathsOptions - supportedExtensions [][]string - supportedExtensionsWithJsonIfResolveJsonModule []string + comparePathsOptions tspath.ComparePathsOptions processedFiles - filesByPath map[tspath.Path]*ast.SourceFile - // The below settings are to track if a .js file should be add to the program if loaded via searching under node_modules. // This works as imported modules are discovered recursively in a depth first manner, specifically: // - For each root file, findSourceFile is called. @@ -65,9 +63,6 @@ type Program struct { commonSourceDirectory string commonSourceDirectoryOnce sync.Once - // List of present unsupported extensions - unsupportedExtensions []string - declarationDiagnosticCache collections.SyncMap[*ast.SourceFile, []*ast.Diagnostic] } @@ -104,19 +99,33 @@ func (p *Program) GetPackageJsonInfo(pkgJsonPath string) modulespecifiers.Packag return nil } -// GetProjectReferenceRedirect implements checker.Program. -func (p *Program) GetProjectReferenceRedirect(path string) string { - return "" // !!! TODO: project references support -} - // GetRedirectTargets implements checker.Program. func (p *Program) GetRedirectTargets(path tspath.Path) []string { return nil // !!! TODO: project references support } -// IsSourceOfProjectReferenceRedirect implements checker.Program. -func (p *Program) IsSourceOfProjectReferenceRedirect(path string) bool { - return false // !!! TODO: project references support +// GetOutputAndProjectReference implements checker.Program. +func (p *Program) GetOutputAndProjectReference(path tspath.Path) *tsoptions.OutputDtsAndProjectReference { + return p.projectReferenceFileMapper.getOutputAndProjectReference(path) +} + +// IsSourceFromProjectReference implements checker.Program. +func (p *Program) IsSourceFromProjectReference(path tspath.Path) bool { + return p.projectReferenceFileMapper.isSourceFromProjectReference(path) +} + +func (p *Program) GetSourceAndProjectReference(path tspath.Path) *tsoptions.SourceAndProjectReference { + return p.projectReferenceFileMapper.getSourceAndProjectReference(path) +} + +func (p *Program) GetResolvedProjectReferenceFor(path tspath.Path) (*tsoptions.ParsedCommandLine, bool) { + return p.projectReferenceFileMapper.getResolvedReferenceFor(path) +} + +func (p *Program) ForEachResolvedProjectReference( + fn func(path tspath.Path, config *tsoptions.ParsedCommandLine) bool, +) { + p.projectReferenceFileMapper.forEachResolvedProjectReference(fn) } // UseCaseSensitiveFileNames implements checker.Program. @@ -189,8 +198,6 @@ func NewProgram(opts ProgramOptions) *Program { // tracing?.push(tracing.Phase.Program, "createProgram", { configFilePath: options.configFilePath, rootDir: options.rootDir }, /*separateBeginAndEnd*/ true); // performance.mark("beforeProgram"); - p.resolver = module.NewResolver(p.Host(), compilerOptions, p.opts.TypingsLocation, p.opts.ProjectName) - var libs []string if compilerOptions.NoLib != core.TSTrue { @@ -208,18 +215,7 @@ func NewProgram(opts ProgramOptions) *Program { } } - p.processedFiles = processAllProgramFiles(p.opts, p.resolver, libs, p.singleThreaded()) - p.filesByPath = make(map[tspath.Path]*ast.SourceFile, len(p.files)) - for _, file := range p.files { - p.filesByPath[file.Path()] = file - } - - for _, file := range p.files { - extension := tspath.TryGetExtensionFromPath(file.FileName()) - if slices.Contains(tspath.SupportedJSExtensionsFlat, extension) { - p.unsupportedExtensions = core.AppendIfUnique(p.unsupportedExtensions, extension) - } - } + p.processedFiles = processAllProgramFiles(p.opts, libs, p.singleThreaded()) return p } @@ -237,14 +233,10 @@ func (p *Program) UpdateProgram(changedFilePath tspath.Path) (*Program, bool) { result := &Program{ opts: p.opts, nodeModules: p.nodeModules, - currentDirectory: p.currentDirectory, - resolver: p.resolver, comparePathsOptions: p.comparePathsOptions, processedFiles: p.processedFiles, - filesByPath: p.filesByPath, currentNodeModulesDepth: p.currentNodeModulesDepth, usesUriStyleNodeCoreModules: p.usesUriStyleNodeCoreModules, - unsupportedExtensions: p.unsupportedExtensions, } result.initCheckerPool() index := core.FindIndex(result.files, func(file *ast.SourceFile) bool { return file.Path() == newFile.Path() }) @@ -381,11 +373,6 @@ func (p *Program) GetResolvedModules() map[tspath.Path]module.ModeAwareCache[*mo return p.resolvedModules } -func (p *Program) findSourceFile(candidate string, reason FileIncludeReason) *ast.SourceFile { - path := tspath.ToPath(candidate, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) - return p.filesByPath[path] -} - func (p *Program) GetSyntacticDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { return p.getDiagnosticsHelper(ctx, sourceFile, false /*ensureBound*/, false /*ensureChecked*/, p.getSyntacticDiagnosticsForFile) } @@ -444,7 +431,7 @@ func (p *Program) getBindDiagnosticsForFile(ctx context.Context, sourceFile *ast func (p *Program) getSemanticDiagnosticsForFile(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { compilerOptions := p.Options() - if checker.SkipTypeChecking(sourceFile, compilerOptions) { + if checker.SkipTypeChecking(sourceFile, compilerOptions, p) { return nil } @@ -535,7 +522,7 @@ func (p *Program) getDeclarationDiagnosticsForFile(_ctx context.Context, sourceF } func (p *Program) getSuggestionDiagnosticsForFile(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic { - if checker.SkipTypeChecking(sourceFile, p.Options()) { + if checker.SkipTypeChecking(sourceFile, p.Options(), p) { return nil } @@ -692,37 +679,39 @@ func (p *Program) GetSourceFileMetaData(path tspath.Path) *ast.SourceFileMetaDat } func (p *Program) GetEmitModuleFormatOfFile(sourceFile ast.HasFileName) core.ModuleKind { - return ast.GetEmitModuleFormatOfFileWorker(sourceFile.FileName(), p.Options(), p.GetSourceFileMetaData(sourceFile.Path())) + return ast.GetEmitModuleFormatOfFileWorker(sourceFile.FileName(), p.projectReferenceFileMapper.getCompilerOptionsForFile(sourceFile), p.GetSourceFileMetaData(sourceFile.Path())) } func (p *Program) GetEmitSyntaxForUsageLocation(sourceFile ast.HasFileName, location *ast.StringLiteralLike) core.ResolutionMode { - return getEmitSyntaxForUsageLocationWorker(sourceFile.FileName(), p.sourceFileMetaDatas[sourceFile.Path()], location, p.Options()) + return getEmitSyntaxForUsageLocationWorker(sourceFile.FileName(), p.sourceFileMetaDatas[sourceFile.Path()], location, p.projectReferenceFileMapper.getCompilerOptionsForFile(sourceFile)) } func (p *Program) GetImpliedNodeFormatForEmit(sourceFile ast.HasFileName) core.ResolutionMode { - return ast.GetImpliedNodeFormatForEmitWorker(sourceFile.FileName(), p.Options().GetEmitModuleKind(), p.GetSourceFileMetaData(sourceFile.Path())) + return ast.GetImpliedNodeFormatForEmitWorker(sourceFile.FileName(), p.projectReferenceFileMapper.getCompilerOptionsForFile(sourceFile).GetEmitModuleKind(), p.GetSourceFileMetaData(sourceFile.Path())) } func (p *Program) GetModeForUsageLocation(sourceFile ast.HasFileName, location *ast.StringLiteralLike) core.ResolutionMode { - return getModeForUsageLocation(sourceFile.FileName(), p.sourceFileMetaDatas[sourceFile.Path()], location, p.Options()) + return getModeForUsageLocation(sourceFile.FileName(), p.sourceFileMetaDatas[sourceFile.Path()], location, p.projectReferenceFileMapper.getCompilerOptionsForFile(sourceFile)) } func (p *Program) GetDefaultResolutionModeForFile(sourceFile ast.HasFileName) core.ResolutionMode { - return getDefaultResolutionModeForFile(sourceFile.FileName(), p.sourceFileMetaDatas[sourceFile.Path()], p.Options()) + return getDefaultResolutionModeForFile(sourceFile.FileName(), p.sourceFileMetaDatas[sourceFile.Path()], p.projectReferenceFileMapper.getCompilerOptionsForFile(sourceFile)) } func (p *Program) CommonSourceDirectory() string { p.commonSourceDirectoryOnce.Do(func() { - var files []string - host := &emitHost{program: p} - for _, file := range p.files { - if sourceFileMayBeEmitted(file, host, false /*forceDtsEmit*/) { - files = append(files, file.FileName()) - } - } - p.commonSourceDirectory = getCommonSourceDirectory( + p.commonSourceDirectory = outputpaths.GetCommonSourceDirectory( p.Options(), - files, + func() []string { + var files []string + host := &emitHost{program: p} + for _, file := range p.files { + if sourceFileMayBeEmitted(file, host, false /*forceDtsEmit*/) { + files = append(files, file.FileName()) + } + } + return files + }, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames(), ) @@ -730,72 +719,6 @@ func (p *Program) CommonSourceDirectory() string { return p.commonSourceDirectory } -func computeCommonSourceDirectoryOfFilenames(fileNames []string, currentDirectory string, useCaseSensitiveFileNames bool) string { - var commonPathComponents []string - for _, sourceFile := range fileNames { - // Each file contributes into common source file path - sourcePathComponents := tspath.GetNormalizedPathComponents(sourceFile, currentDirectory) - - // The base file name is not part of the common directory path - sourcePathComponents = sourcePathComponents[:len(sourcePathComponents)-1] - - if commonPathComponents == nil { - // first file - commonPathComponents = sourcePathComponents - continue - } - - n := min(len(commonPathComponents), len(sourcePathComponents)) - for i := range n { - if tspath.GetCanonicalFileName(commonPathComponents[i], useCaseSensitiveFileNames) != tspath.GetCanonicalFileName(sourcePathComponents[i], useCaseSensitiveFileNames) { - if i == 0 { - // Failed to find any common path component - return "" - } - - // New common path found that is 0 -> i-1 - commonPathComponents = commonPathComponents[:i] - break - } - } - - // If the sourcePathComponents was shorter than the commonPathComponents, truncate to the sourcePathComponents - if len(sourcePathComponents) < len(commonPathComponents) { - commonPathComponents = commonPathComponents[:len(sourcePathComponents)] - } - } - - if len(commonPathComponents) == 0 { - // Can happen when all input files are .d.ts files - return currentDirectory - } - - return tspath.GetPathFromPathComponents(commonPathComponents) -} - -func getCommonSourceDirectory(options *core.CompilerOptions, files []string, currentDirectory string, useCaseSensitiveFileNames bool) string { - var commonSourceDirectory string - if options.RootDir != "" { - // If a rootDir is specified use it as the commonSourceDirectory - commonSourceDirectory = options.RootDir - } else if options.Composite.IsTrue() && options.ConfigFilePath != "" { - // If the rootDir is not specified, but the project is composite, then the common source directory - // is the directory of the config file. - commonSourceDirectory = tspath.GetDirectoryPath(options.ConfigFilePath) - } else { - commonSourceDirectory = computeCommonSourceDirectoryOfFilenames(files, currentDirectory, useCaseSensitiveFileNames) - } - - if len(commonSourceDirectory) > 0 { - // Make sure directory path ends with directory separator so this string can directly - // used to replace with "" to get the relative path of the source file and the relative path doesn't - // start with / making it rooted path - commonSourceDirectory = tspath.EnsureTrailingDirectorySeparator(commonSourceDirectory) - } - - return commonSourceDirectory -} - type EmitOptions struct { TargetSourceFile *ast.SourceFile // Single file to emit. If `nil`, emits all files forceDtsEmit bool @@ -879,6 +802,17 @@ func (p *Program) GetSourceFile(filename string) *ast.SourceFile { return p.GetSourceFileByPath(path) } +func (p *Program) GetSourceFileForResolvedModule(fileName string) *ast.SourceFile { + file := p.GetSourceFile(fileName) + if file == nil { + filename := p.projectReferenceFileMapper.getParseFileRedirect(ast.NewHasFileName(fileName, tspath.ToPath(fileName, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()))) + if filename != "" { + return p.GetSourceFile(filename) + } + } + return file +} + func (p *Program) GetSourceFileByPath(path tspath.Path) *ast.SourceFile { return p.filesByPath[path] } @@ -899,12 +833,12 @@ func (p *Program) GetLibFileFromReference(ref *ast.FileReference) *ast.SourceFil } func (p *Program) GetResolvedTypeReferenceDirectiveFromTypeReferenceDirective(typeRef *ast.FileReference, sourceFile *ast.SourceFile) *module.ResolvedTypeReferenceDirective { - return p.resolver.ResolveTypeReferenceDirective( - typeRef.FileName, - sourceFile.FileName(), - p.getModeForTypeReferenceDirectiveInFile(typeRef, sourceFile), - nil, - ) + if resolutions, ok := p.typeResolutionsInFile[sourceFile.Path()]; ok { + if resolved, ok := resolutions[module.ModeAwareCacheKey{Name: typeRef.FileName, Mode: p.getModeForTypeReferenceDirectiveInFile(typeRef, sourceFile)}]; ok { + return resolved + } + } + return nil } func (p *Program) getModeForTypeReferenceDirectiveInFile(ref *ast.FileReference, sourceFile *ast.SourceFile) core.ResolutionMode { diff --git a/internal/compiler/program_test.go b/internal/compiler/program_test.go index 803e1ed11c..0043848cef 100644 --- a/internal/compiler/program_test.go +++ b/internal/compiler/program_test.go @@ -240,7 +240,7 @@ func TestProgram(t *testing.T) { CompilerOptions: &opts, }, }, - Host: NewCompilerHost(&opts, "c:/dev/src", fs, bundled.LibPath()), + Host: NewCompilerHost(&opts, "c:/dev/src", fs, bundled.LibPath(), nil), }) actualFiles := []string{} @@ -277,7 +277,7 @@ func BenchmarkNewProgram(b *testing.B) { CompilerOptions: &opts, }, }, - Host: NewCompilerHost(&opts, "c:/dev/src", fs, bundled.LibPath()), + Host: NewCompilerHost(&opts, "c:/dev/src", fs, bundled.LibPath(), nil), } for b.Loop() { @@ -294,9 +294,9 @@ func BenchmarkNewProgram(b *testing.B) { fs := osvfs.FS() fs = bundled.WrapFS(fs) - host := NewCompilerHost(nil, rootPath, fs, bundled.LibPath()) + host := NewCompilerHost(nil, rootPath, fs, bundled.LibPath(), nil) - parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(tspath.CombinePaths(rootPath, "tsconfig.json"), &core.CompilerOptions{}, host, nil) + parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(tspath.CombinePaths(rootPath, "tsconfig.json"), nil, host, nil) assert.Equal(b, len(errors), 0, "Expected no errors in parsed command line") opts := ProgramOptions{ diff --git a/internal/compiler/projectreferencedtsfakinghost.go b/internal/compiler/projectreferencedtsfakinghost.go new file mode 100644 index 0000000000..9c03e8414f --- /dev/null +++ b/internal/compiler/projectreferencedtsfakinghost.go @@ -0,0 +1,225 @@ +package compiler + +import ( + "strings" + + "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/tspath" + "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/cachedvfs" +) + +type projectReferenceDtsFakingHost struct { + host CompilerHost + fs *cachedvfs.FS +} + +var _ module.ResolutionHost = (*projectReferenceDtsFakingHost)(nil) + +func newProjectReferenceDtsFakingHost(loader *fileLoader) module.ResolutionHost { + // Create a new host that will fake the dts files + host := &projectReferenceDtsFakingHost{ + host: loader.opts.Host, + fs: cachedvfs.From(&projectReferenceDtsFakingVfs{ + projectReferenceFileMapper: loader.projectReferenceFileMapper, + dtsDirectories: loader.dtsDirectories, + knownSymlinks: knownSymlinks{}, + }), + } + return host +} + +// FS implements module.ResolutionHost. +func (h *projectReferenceDtsFakingHost) FS() vfs.FS { + return h.fs +} + +// GetCurrentDirectory implements module.ResolutionHost. +func (h *projectReferenceDtsFakingHost) GetCurrentDirectory() string { + return h.host.GetCurrentDirectory() +} + +// Trace implements module.ResolutionHost. +func (h *projectReferenceDtsFakingHost) Trace(msg string) { + h.host.Trace(msg) +} + +type projectReferenceDtsFakingVfs struct { + projectReferenceFileMapper *projectReferenceFileMapper + dtsDirectories collections.Set[tspath.Path] + knownSymlinks knownSymlinks +} + +var _ vfs.FS = (*projectReferenceDtsFakingVfs)(nil) + +// UseCaseSensitiveFileNames implements vfs.FS. +func (fs *projectReferenceDtsFakingVfs) UseCaseSensitiveFileNames() bool { + return fs.projectReferenceFileMapper.opts.Host.FS().UseCaseSensitiveFileNames() +} + +// FileExists implements vfs.FS. +func (fs *projectReferenceDtsFakingVfs) FileExists(path string) bool { + if fs.projectReferenceFileMapper.opts.Host.FS().FileExists(path) { + return true + } + if !tspath.IsDeclarationFileName(path) { + return false + } + // Project references go to source file instead of .d.ts file + return fs.fileOrDirectoryExistsUsingSource(path /*isFile*/, true) +} + +// ReadFile implements vfs.FS. +func (fs *projectReferenceDtsFakingVfs) ReadFile(path string) (contents string, ok bool) { + // Dont need to override as we cannot mimick read file + return fs.projectReferenceFileMapper.opts.Host.FS().ReadFile(path) +} + +// WriteFile implements vfs.FS. +func (fs *projectReferenceDtsFakingVfs) WriteFile(path string, data string, writeByteOrderMark bool) error { + panic("should not be called by resolver") +} + +// Remove implements vfs.FS. +func (fs *projectReferenceDtsFakingVfs) Remove(path string) error { + panic("should not be called by resolver") +} + +// DirectoryExists implements vfs.FS. +func (fs *projectReferenceDtsFakingVfs) DirectoryExists(path string) bool { + if fs.projectReferenceFileMapper.opts.Host.FS().DirectoryExists(path) { + fs.handleDirectoryCouldBeSymlink(path) + return true + } + return fs.fileOrDirectoryExistsUsingSource(path /*isFile*/, false) +} + +// GetAccessibleEntries implements vfs.FS. +func (fs *projectReferenceDtsFakingVfs) GetAccessibleEntries(path string) vfs.Entries { + panic("should not be called by resolver") +} + +// Stat implements vfs.FS. +func (fs *projectReferenceDtsFakingVfs) Stat(path string) vfs.FileInfo { + panic("should not be called by resolver") +} + +// WalkDir implements vfs.FS. +func (fs *projectReferenceDtsFakingVfs) WalkDir(root string, walkFn vfs.WalkDirFunc) error { + panic("should not be called by resolver") +} + +// Realpath implements vfs.FS. +func (fs *projectReferenceDtsFakingVfs) Realpath(path string) string { + result, ok := fs.knownSymlinks.Files().Load(fs.toPath(path)) + if ok { + return result + } + return fs.projectReferenceFileMapper.opts.Host.FS().Realpath(path) +} + +func (fs *projectReferenceDtsFakingVfs) toPath(path string) tspath.Path { + return tspath.ToPath(path, fs.projectReferenceFileMapper.opts.Host.GetCurrentDirectory(), fs.UseCaseSensitiveFileNames()) +} + +func (fs *projectReferenceDtsFakingVfs) handleDirectoryCouldBeSymlink(directory string) { + if tspath.ContainsIgnoredPath(directory) { + return + } + + // Because we already watch node_modules, handle symlinks in there + if !strings.Contains(directory, "/node_modules/") { + return + } + + directoryPath := tspath.Path(tspath.EnsureTrailingDirectorySeparator(string(fs.toPath(directory)))) + if _, ok := fs.knownSymlinks.Directories().Load(directoryPath); ok { + return + } + + realDirectory := fs.Realpath(directory) + var realPath tspath.Path + if realDirectory == directory { + // not symlinked + return + } + if realPath = tspath.Path(tspath.EnsureTrailingDirectorySeparator(string(fs.toPath(realDirectory)))); realPath == directoryPath { + // not symlinked + return + } + fs.knownSymlinks.SetDirectory(directory, directoryPath, &knownDirectoryLink{ + Real: tspath.EnsureTrailingDirectorySeparator(realDirectory), + RealPath: realPath, + }) +} + +func (fs *projectReferenceDtsFakingVfs) fileOrDirectoryExistsUsingSource(fileOrDirectory string, isFile bool) bool { + fileOrDirectoryExistsUsingSource := core.IfElse(isFile, fs.fileExistsIfProjectReferenceDts, fs.directoryExistsIfProjectReferenceDeclDir) + // Check current directory or file + result := fileOrDirectoryExistsUsingSource(fileOrDirectory) + if result != core.TSUnknown { + return result == core.TSTrue + } + + knownDirectoryLinks := fs.knownSymlinks.Directories() + if knownDirectoryLinks.Size() == 0 { + return false + } + fileOrDirectoryPath := fs.toPath(fileOrDirectory) + if !strings.Contains(string(fileOrDirectoryPath), "/node_modules/") { + return false + } + if isFile { + _, ok := fs.knownSymlinks.Files().Load(fileOrDirectoryPath) + if ok { + return true + } + } + + // 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 { + relative, hasPrefix := strings.CutPrefix(string(fileOrDirectoryPath), string(directoryPath)) + if !hasPrefix { + return true + } + if exists = fileOrDirectoryExistsUsingSource(string(knownDirectoryLink.RealPath) + relative).IsTrue(); exists { + if isFile { + // Store the real path for the file + absolutePath := tspath.GetNormalizedAbsolutePath(fileOrDirectory, fs.projectReferenceFileMapper.opts.Host.GetCurrentDirectory()) + fs.knownSymlinks.SetFile( + fileOrDirectoryPath, + knownDirectoryLink.Real+absolutePath[len(directoryPath):], + ) + } + return false + } + return true + }) + return exists +} + +func (fs *projectReferenceDtsFakingVfs) fileExistsIfProjectReferenceDts(file string) core.Tristate { + source := fs.projectReferenceFileMapper.getSourceAndProjectReference(fs.toPath(file)) + if source != nil { + return core.IfElse(fs.projectReferenceFileMapper.opts.Host.FS().FileExists(source.Source), core.TSTrue, core.TSFalse) + } + return core.TSUnknown +} + +func (fs *projectReferenceDtsFakingVfs) directoryExistsIfProjectReferenceDeclDir(dir string) core.Tristate { + dirPath := fs.toPath(dir) + dirPathWithTrailingDirectorySeparator := dirPath + "/" + for declDirPath := range fs.dtsDirectories.Keys() { + if dirPath == declDirPath || + // Any parent directory of declaration dir + strings.HasPrefix(string(declDirPath), string(dirPathWithTrailingDirectorySeparator)) || + // Any directory inside declaration dir + strings.HasPrefix(string(dirPath), string(declDirPath)+"/") { + return core.TSTrue + } + } + return core.TSUnknown +} diff --git a/internal/compiler/projectreferencefilemapper.go b/internal/compiler/projectreferencefilemapper.go new file mode 100644 index 0000000000..d1c1457752 --- /dev/null +++ b/internal/compiler/projectreferencefilemapper.go @@ -0,0 +1,193 @@ +package compiler + +import ( + "strings" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/tsoptions" + "github.com/microsoft/typescript-go/internal/tspath" +) + +type projectReferenceFileMapper struct { + opts ProgramOptions + host module.ResolutionHost + loader *fileLoader // Only present during populating the mapper and parsing, released after that + + configToProjectReference map[tspath.Path]*tsoptions.ParsedCommandLine // All the resolved references needed + referencesInConfigFile map[tspath.Path][]tspath.Path // Map of config file to its references + sourceToOutput map[tspath.Path]*tsoptions.OutputDtsAndProjectReference + outputDtsToSource map[tspath.Path]*tsoptions.SourceAndProjectReference + + // Store all the realpath from dts in node_modules to source file from project reference needed during parsing so it can be used later + realpathDtsToSource collections.SyncMap[tspath.Path, *tsoptions.SourceAndProjectReference] +} + +func (mapper *projectReferenceFileMapper) init(loader *fileLoader, rootTasks []*projectReferenceParseTask) { + totalReferences := loader.projectReferenceParseTasks.tasksByFileName.Size() + 1 + mapper.loader = loader + mapper.configToProjectReference = make(map[tspath.Path]*tsoptions.ParsedCommandLine, totalReferences) + mapper.referencesInConfigFile = make(map[tspath.Path][]tspath.Path, totalReferences) + mapper.sourceToOutput = make(map[tspath.Path]*tsoptions.OutputDtsAndProjectReference) + mapper.outputDtsToSource = make(map[tspath.Path]*tsoptions.SourceAndProjectReference) + mapper.referencesInConfigFile[mapper.opts.Config.ConfigFile.SourceFile.Path()] = loader.projectReferenceParseTasks.collect( + loader, + rootTasks, + func(task *projectReferenceParseTask, referencesInConfig []tspath.Path) { + path := loader.toPath(task.configName) + mapper.configToProjectReference[path] = task.resolved + if task.resolved == nil || mapper.opts.Config.ConfigFile == task.resolved.ConfigFile { + return + } + mapper.referencesInConfigFile[path] = referencesInConfig + for key, value := range task.resolved.SourceToOutput() { + mapper.sourceToOutput[key] = value + } + for key, value := range task.resolved.OutputDtsToSource() { + mapper.outputDtsToSource[key] = value + } + if mapper.opts.canUseProjectReferenceSource() { + declDir := task.resolved.CompilerOptions().DeclarationDir + if declDir == "" { + declDir = task.resolved.CompilerOptions().OutDir + } + if declDir != "" { + loader.dtsDirectories.Add(loader.toPath(declDir)) + } + } + }) + if mapper.opts.canUseProjectReferenceSource() && len(loader.projectReferenceFileMapper.outputDtsToSource) != 0 { + mapper.host = newProjectReferenceDtsFakingHost(loader) + } +} + +func (mapper *projectReferenceFileMapper) getParseFileRedirect(file ast.HasFileName) string { + if mapper.opts.canUseProjectReferenceSource() { + // Map to source file from project reference + source := mapper.getSourceAndProjectReference(file.Path()) + if source == nil { + source = mapper.getSourceToDtsIfSymlink(file) + } + if source != nil { + return source.Source + } + } else { + // Map to dts file from project reference + output := mapper.getOutputAndProjectReference(file.Path()) + if output != nil && output.OutputDts != "" { + return output.OutputDts + } + } + return "" +} + +func (mapper *projectReferenceFileMapper) getResolvedProjectReferences() []*tsoptions.ParsedCommandLine { + refs, ok := mapper.referencesInConfigFile[mapper.opts.Config.ConfigFile.SourceFile.Path()] + var result []*tsoptions.ParsedCommandLine + if ok { + result = make([]*tsoptions.ParsedCommandLine, 0, len(refs)) + for _, refPath := range refs { + refConfig, _ := mapper.configToProjectReference[refPath] + result = append(result, refConfig) + } + } + return result +} + +func (mapper *projectReferenceFileMapper) getOutputAndProjectReference(path tspath.Path) *tsoptions.OutputDtsAndProjectReference { + return mapper.sourceToOutput[path] +} + +func (mapper *projectReferenceFileMapper) getSourceAndProjectReference(path tspath.Path) *tsoptions.SourceAndProjectReference { + return mapper.outputDtsToSource[path] +} + +func (mapper *projectReferenceFileMapper) isSourceFromProjectReference(path tspath.Path) bool { + return mapper.opts.canUseProjectReferenceSource() && mapper.getOutputAndProjectReference(path) != nil +} + +func (mapper *projectReferenceFileMapper) getCompilerOptionsForFile(file ast.HasFileName) *core.CompilerOptions { + redirect := mapper.getRedirectForResolution(file) + return module.GetCompilerOptionsWithRedirect(mapper.opts.Config.CompilerOptions(), redirect) +} + +func (mapper *projectReferenceFileMapper) getRedirectForResolution(file ast.HasFileName) *tsoptions.ParsedCommandLine { + path := file.Path() + // Check if outputdts of source file from project reference + output := mapper.getOutputAndProjectReference(path) + if output != nil { + return output.Resolved + } + + // Source file from project reference + resultFromDts := mapper.getSourceAndProjectReference(path) + if resultFromDts != nil { + return resultFromDts.Resolved + } + + realpathDtsToSource := mapper.getSourceToDtsIfSymlink(file) + if realpathDtsToSource != nil { + return realpathDtsToSource.Resolved + } + return nil +} + +func (mapper *projectReferenceFileMapper) getResolvedReferenceFor(path tspath.Path) (*tsoptions.ParsedCommandLine, bool) { + config, ok := mapper.configToProjectReference[path] + return config, ok +} + +func (mapper *projectReferenceFileMapper) forEachResolvedProjectReference( + fn func(path tspath.Path, config *tsoptions.ParsedCommandLine) bool, +) { + if mapper.opts.Config.ConfigFile == nil { + return + } + refs := mapper.referencesInConfigFile[mapper.opts.Config.ConfigFile.SourceFile.Path()] + mapper.forEachResolvedReferenceWorker(refs, fn) +} + +func (mapper *projectReferenceFileMapper) forEachResolvedReferenceWorker( + referenes []tspath.Path, + fn func(path tspath.Path, config *tsoptions.ParsedCommandLine) bool, +) { + for _, path := range referenes { + config, _ := mapper.configToProjectReference[path] + if !fn(path, config) { + return + } + } +} + +func (mapper *projectReferenceFileMapper) getSourceToDtsIfSymlink(file ast.HasFileName) *tsoptions.SourceAndProjectReference { + // If preserveSymlinks is true, module resolution wont jump the symlink + // but the resolved real path may be the .d.ts from project reference + // Note:: Currently we try the real path only if the + // file is from node_modules to avoid having to run real path on all file paths + path := file.Path() + realpathDtsToSource, ok := mapper.realpathDtsToSource.Load(path) + if ok { + return realpathDtsToSource + } + if mapper.loader != nil && mapper.opts.Config.CompilerOptions().PreserveSymlinks == core.TSTrue { + fileName := file.FileName() + if !strings.Contains(fileName, "/node_modules/") { + mapper.realpathDtsToSource.Store(path, nil) + } else { + realDeclarationPath := mapper.loader.toPath(mapper.host.FS().Realpath(fileName)) + if realDeclarationPath == path { + mapper.realpathDtsToSource.Store(path, nil) + } else { + realpathDtsToSource := mapper.getSourceAndProjectReference(realDeclarationPath) + if realpathDtsToSource != nil { + mapper.realpathDtsToSource.Store(path, realpathDtsToSource) + return realpathDtsToSource + } + mapper.realpathDtsToSource.Store(path, nil) + } + } + } + return nil +} diff --git a/internal/compiler/projectreferenceparsetask.go b/internal/compiler/projectreferenceparsetask.go new file mode 100644 index 0000000000..0ea1e21ad8 --- /dev/null +++ b/internal/compiler/projectreferenceparsetask.go @@ -0,0 +1,48 @@ +package compiler + +import ( + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/tsoptions" +) + +type projectReferenceParseTask struct { + configName string + resolved *tsoptions.ParsedCommandLine + subTasks []*projectReferenceParseTask +} + +func (t *projectReferenceParseTask) FileName() string { + return t.configName +} + +func (t *projectReferenceParseTask) start(loader *fileLoader) { + t.resolved = loader.opts.Host.GetResolvedProjectReference(t.configName, loader.toPath(t.configName)) + if t.resolved == nil { + return + } + if t.resolved.SourceToOutput() == nil { + loader.projectReferenceParseTasks.wg.Queue(func() { + t.resolved.ParseInputOutputNames() + }) + } + subReferences := t.resolved.ProjectReferences() + if len(subReferences) == 0 { + return + } + t.subTasks = createProjectReferenceParseTasks(subReferences) +} + +func getSubTasksOfProjectReferenceParseTask(t *projectReferenceParseTask) []*projectReferenceParseTask { + return t.subTasks +} + +func createProjectReferenceParseTasks(projectReferences []*core.ProjectReference) []*projectReferenceParseTask { + tasks := make([]*projectReferenceParseTask, 0, len(projectReferences)) + for _, reference := range projectReferences { + configName := core.ResolveProjectReferencePath(reference) + tasks = append(tasks, &projectReferenceParseTask{ + configName: configName, + }) + } + return tasks +} diff --git a/internal/core/parsedoptions.go b/internal/core/parsedoptions.go index b999e44a74..3aba067df0 100644 --- a/internal/core/parsedoptions.go +++ b/internal/core/parsedoptions.go @@ -5,6 +5,6 @@ type ParsedOptions struct { WatchOptions *WatchOptions `json:"watchOptions"` TypeAcquisition *TypeAcquisition `json:"typeAcquisition"` - FileNames []string `json:"fileNames"` - ProjectReferences []ProjectReference `json:"projectReferences"` + FileNames []string `json:"fileNames"` + ProjectReferences []*ProjectReference `json:"projectReferences"` } diff --git a/internal/core/projectreference.go b/internal/core/projectreference.go index 2644bdbc21..ded897f4b6 100644 --- a/internal/core/projectreference.go +++ b/internal/core/projectreference.go @@ -1,7 +1,20 @@ package core +import "github.com/microsoft/typescript-go/internal/tspath" + type ProjectReference struct { Path string OriginalPath string Circular bool } + +func ResolveProjectReferencePath(ref *ProjectReference) string { + return resolveConfigFileNameOfProjectReference(ref.Path) +} + +func resolveConfigFileNameOfProjectReference(path string) string { + if tspath.FileExtensionIs(path, tspath.ExtensionJson) { + return path + } + return tspath.CombinePaths(path, "tsconfig.json") +} diff --git a/internal/execute/tsc.go b/internal/execute/tsc.go index bf694fd235..0a55c0b0b1 100644 --- a/internal/execute/tsc.go +++ b/internal/execute/tsc.go @@ -10,6 +10,7 @@ import ( "time" "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/diagnostics" @@ -115,8 +116,8 @@ func executeCommandLineWorker(sys System, cb cbType, commandLine *tsoptions.Pars if configFileName != "" { configStart := sys.Now() - extendedConfigCache := map[tspath.Path]*tsoptions.ExtendedConfigCacheEntry{} - configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFile(configFileName, compilerOptionsFromCommandLine, sys, extendedConfigCache) + extendedConfigCache := collections.SyncMap[tspath.Path, *tsoptions.ExtendedConfigCacheEntry]{} + configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFile(configFileName, compilerOptionsFromCommandLine, sys, &extendedConfigCache) configTime := sys.Now().Sub(configStart) if len(errors) != 0 { // these are unrecoverable errors--exit to report them as diagnostics @@ -139,6 +140,7 @@ func executeCommandLineWorker(sys System, cb cbType, commandLine *tsoptions.Pars cb, configParseResult, reportDiagnostic, + &extendedConfigCache, configTime, ), nil } else { @@ -158,6 +160,7 @@ func executeCommandLineWorker(sys System, cb cbType, commandLine *tsoptions.Pars cb, commandLine, reportDiagnostic, + nil, 0, /*configTime*/ ), nil } @@ -176,8 +179,15 @@ func findConfigFile(searchPath string, fileExists func(string) bool, configName return result } -func performCompilation(sys System, cb cbType, config *tsoptions.ParsedCommandLine, reportDiagnostic diagnosticReporter, configTime time.Duration) ExitStatus { - host := compiler.NewCachedFSCompilerHost(config.CompilerOptions(), sys.GetCurrentDirectory(), sys.FS(), sys.DefaultLibraryPath()) +func performCompilation( + sys System, + cb cbType, + config *tsoptions.ParsedCommandLine, + reportDiagnostic diagnosticReporter, + extendedConfigCache *collections.SyncMap[tspath.Path, *tsoptions.ExtendedConfigCacheEntry], + configTime time.Duration, +) ExitStatus { + host := compiler.NewCachedFSCompilerHost(config.CompilerOptions(), sys.GetCurrentDirectory(), sys.FS(), sys.DefaultLibraryPath(), extendedConfigCache) // todo: cache, statistics, tracing parseStart := sys.Now() program := compiler.NewProgram(compiler.ProgramOptions{ diff --git a/internal/execute/tsc_test.go b/internal/execute/tsc_test.go index a5ab773ba7..4a4126b418 100644 --- a/internal/execute/tsc_test.go +++ b/internal/execute/tsc_test.go @@ -145,37 +145,6 @@ func TestNoEmit(t *testing.T) { }).verify(t, "noEmit") } -func TestProjectReferences(t *testing.T) { - t.Parallel() - if !bundled.Embedded { - // Without embedding, we'd need to read all of the lib files out from disk into the MapFS. - // Just skip this for now. - t.Skip("bundled files are not embedded") - } - - (&tscInput{ - subScenario: "when project references composite project with noEmit", - sys: newTestSys(FileMap{ - "/home/src/workspaces/solution/src/utils/index.ts": "export const x = 10;", - "/home/src/workspaces/solution/src/utils/tsconfig.json": `{ - "compilerOptions": { - "composite": true, - "noEmit": true, - }, -}`, - "/home/src/workspaces/solution/project/index.ts": `import { x } from "../utils";`, - "/home/src/workspaces/solution/project/tsconfig.json": `{ - "references": [ - { "path": "../utils" }, - ], -}`, - }, - "/home/src/workspaces/solution", - ), - commandLineArgs: []string{"--p", "project"}, - }).verify(t, "projectReferences") -} - func TestExtends(t *testing.T) { t.Parallel() if !bundled.Embedded { diff --git a/internal/execute/tscprojectreferences_test.go b/internal/execute/tscprojectreferences_test.go new file mode 100644 index 0000000000..f655027458 --- /dev/null +++ b/internal/execute/tscprojectreferences_test.go @@ -0,0 +1,200 @@ +package execute_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/bundled" +) + +func TestProjectReferences(t *testing.T) { + t.Parallel() + if !bundled.Embedded { + // Without embedding, we'd need to read all of the lib files out from disk into the MapFS. + // Just skip this for now. + t.Skip("bundled files are not embedded") + } + + cases := []tscInput{ + // !!! sheetal todo verifyCompilerOptions - check for noEmit + { + subScenario: "when project references composite project with noEmit", + sys: newTestSys(FileMap{ + "/home/src/workspaces/solution/utils/index.ts": "export const x = 10;", + "/home/src/workspaces/solution/utils/tsconfig.json": `{ + "compilerOptions": { + "composite": true, + "noEmit": true, + }, + }`, + "/home/src/workspaces/solution/project/index.ts": `import { x } from "../utils";`, + "/home/src/workspaces/solution/project/tsconfig.json": `{ + "references": [ + { "path": "../utils" }, + ], + }`, + }, + "/home/src/workspaces/solution", + ), + commandLineArgs: []string{"--p", "project"}, + }, + { + subScenario: "when project references composite", + sys: newTestSys(FileMap{ + "/home/src/workspaces/solution/utils/index.ts": "export const x = 10;", + "/home/src/workspaces/solution/utils/index.d.ts": "export declare const x = 10;", + "/home/src/workspaces/solution/utils/tsconfig.json": `{ + "compilerOptions": { + "composite": true, + }, +}`, + "/home/src/workspaces/solution/project/index.ts": `import { x } from "../utils";`, + "/home/src/workspaces/solution/project/tsconfig.json": `{ + "references": [ + { "path": "../utils" }, + ], +}`, + }, "/home/src/workspaces/solution"), + commandLineArgs: []string{"--p", "project"}, + }, + { + subScenario: "when project reference is not built", + sys: newTestSys(FileMap{ + "/home/src/workspaces/solution/utils/index.ts": "export const x = 10;", + "/home/src/workspaces/solution/utils/tsconfig.json": `{ + "compilerOptions": { + "composite": true, + }, +}`, + "/home/src/workspaces/solution/project/index.ts": `import { x } from "../utils";`, + "/home/src/workspaces/solution/project/tsconfig.json": `{ + "references": [ + { "path": "../utils" }, + ], +}`, + }, "/home/src/workspaces/solution"), + commandLineArgs: []string{"--p", "project"}, + }, + { + // !!! sheetal verifyProjectReferences - checks this + subScenario: "when project contains invalid project reference", + sys: newTestSys(FileMap{ + "/home/src/workspaces/solution/project/index.ts": `export const x = 10;`, + "/home/src/workspaces/solution/project/tsconfig.json": `{ + "references": [ + { "path": "../utils" }, + ], +}`, + }, "/home/src/workspaces/solution"), + commandLineArgs: []string{"--p", "project"}, + }, + { + subScenario: "default import interop uses referenced project settings", + sys: newTestSys(FileMap{ + "/home/src/workspaces/project/node_modules/ambiguous-package/package.json": `{ "name": "ambiguous-package" }`, + "/home/src/workspaces/project/node_modules/ambiguous-package/index.d.ts": "export declare const ambiguous: number;", + "/home/src/workspaces/project/node_modules/esm-package/package.json": `{ "name": "esm-package", "type": "module" }`, + "/home/src/workspaces/project/node_modules/esm-package/index.d.ts": "export declare const esm: number;", + "/home/src/workspaces/project/lib/tsconfig.json": `{ + "compilerOptions": { + "composite": true, + "declaration": true, + "rootDir": "src", + "outDir": "dist", + "module": "esnext", + "moduleResolution": "bundler", + }, + "include": ["src"], + }`, + "/home/src/workspaces/project/lib/src/a.ts": "export const a = 0;", + "/home/src/workspaces/project/lib/dist/a.d.ts": "export declare const a = 0;", + "/home/src/workspaces/project/app/tsconfig.json": `{ + "compilerOptions": { + "module": "esnext", + "moduleResolution": "bundler", + "rootDir": "src", + "outDir": "dist", + }, + "include": ["src"], + "references": [ + { "path": "../lib" }, + ], + }`, + "/home/src/workspaces/project/app/src/local.ts": "export const local = 0;", + "/home/src/workspaces/project/app/src/index.ts": ` + import local from "./local"; // Error + import esm from "esm-package"; // Error + import referencedSource from "../../lib/src/a"; // Error + import referencedDeclaration from "../../lib/dist/a"; // Error + import ambiguous from "ambiguous-package"; // Ok`, + }, "/home/src/workspaces/project"), + commandLineArgs: []string{"--p", "app", "--pretty", "false"}, + }, + { + subScenario: "referencing ambient const enum from referenced project with preserveConstEnums", + sys: newTestSys(FileMap{ + "/home/src/workspaces/solution/utils/index.ts": "export const enum E { A = 1 }", + "/home/src/workspaces/solution/utils/index.d.ts": "export declare const enum E { A = 1 }", + "/home/src/workspaces/solution/utils/tsconfig.json": `{ + "compilerOptions": { + "composite": true, + "declaration": true, + "preserveConstEnums": true, + }, + }`, + "/home/src/workspaces/solution/project/index.ts": `import { E } from "../utils"; E.A;`, + "/home/src/workspaces/solution/project/tsconfig.json": `{ + "compilerOptions": { + "isolatedModules": true, + }, + "references": [ + { "path": "../utils" }, + ], + }`, + }, "/home/src/workspaces/solution"), + commandLineArgs: []string{"--p", "project"}, + }, + { + subScenario: "importing const enum from referenced project with preserveConstEnums and verbatimModuleSyntax", + sys: newTestSys(FileMap{ + "/home/src/workspaces/solution/preserve/index.ts": "export const enum E { A = 1 }", + "/home/src/workspaces/solution/preserve/index.d.ts": "export declare const enum E { A = 1 }", + "/home/src/workspaces/solution/preserve/tsconfig.json": `{ + "compilerOptions": { + "composite": true, + "declaration": true, + "preserveConstEnums": true, + }, + }`, + "/home/src/workspaces/solution/no-preserve/index.ts": "export const enum E { A = 1 }", + "/home/src/workspaces/solution/no-preserve/index.d.ts": "export declare const enum F { A = 1 }", + "/home/src/workspaces/solution/no-preserve/tsconfig.json": `{ + "compilerOptions": { + "composite": true, + "declaration": true, + "preserveConstEnums": false, + }, + }`, + "/home/src/workspaces/solution/project/index.ts": ` + import { E } from "../preserve"; + import { F } from "../no-preserve"; + E.A; + F.A;`, + "/home/src/workspaces/solution/project/tsconfig.json": `{ + "compilerOptions": { + "module": "preserve", + "verbatimModuleSyntax": true, + }, + "references": [ + { "path": "../preserve" }, + { "path": "../no-preserve" }, + ], + }`, + }, "/home/src/workspaces/solution"), + commandLineArgs: []string{"--p", "project", "--pretty", "false"}, + }, + } + + for _, c := range cases { + c.verify(t, "projectReferences") + } +} diff --git a/internal/execute/watch.go b/internal/execute/watch.go index d02689680e..84e49c78a4 100644 --- a/internal/execute/watch.go +++ b/internal/execute/watch.go @@ -23,7 +23,7 @@ func start(w *watcher) ExitStatus { func (w *watcher) initialize() { // if this function is updated, make sure to update `StartForTest` in export_test.go as needed if w.configFileName == "" { - w.host = compiler.NewCompilerHost(w.options.CompilerOptions(), w.sys.GetCurrentDirectory(), w.sys.FS(), w.sys.DefaultLibraryPath()) + w.host = compiler.NewCompilerHost(w.options.CompilerOptions(), w.sys.GetCurrentDirectory(), w.sys.FS(), w.sys.DefaultLibraryPath(), nil) } } diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index a9c225334b..827bed8b11 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -4,6 +4,7 @@ import ( "reflect" "time" + "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/tsoptions" @@ -44,9 +45,9 @@ func (w *watcher) compileAndEmit() { func (w *watcher) hasErrorsInTsConfig() bool { // only need to check and reparse tsconfig options/update host if we are watching a config file if w.configFileName != "" { - extendedConfigCache := map[tspath.Path]*tsoptions.ExtendedConfigCacheEntry{} + extendedConfigCache := collections.SyncMap[tspath.Path, *tsoptions.ExtendedConfigCacheEntry]{} // !!! need to check that this merges compileroptions correctly. This differs from non-watch, since we allow overriding of previous options - configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFile(w.configFileName, &core.CompilerOptions{}, w.sys, extendedConfigCache) + configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFile(w.configFileName, &core.CompilerOptions{}, w.sys, &extendedConfigCache) if len(errors) > 0 { for _, e := range errors { w.reportDiagnostic(e) @@ -58,7 +59,7 @@ func (w *watcher) hasErrorsInTsConfig() bool { w.configModified = true } w.options = configParseResult - w.host = compiler.NewCompilerHost(w.options.CompilerOptions(), w.sys.GetCurrentDirectory(), w.sys.FS(), w.sys.DefaultLibraryPath()) + w.host = compiler.NewCompilerHost(w.options.CompilerOptions(), w.sys.GetCurrentDirectory(), w.sys.FS(), w.sys.DefaultLibraryPath(), &extendedConfigCache) } return false } diff --git a/internal/module/resolver.go b/internal/module/resolver.go index 344900fb67..6aceb4238f 100644 --- a/internal/module/resolver.go +++ b/internal/module/resolver.go @@ -63,20 +63,16 @@ func newResolutionState( isTypeReferenceDirective bool, resolutionMode core.ResolutionMode, compilerOptions *core.CompilerOptions, - redirectedReference *ResolvedProjectReference, + redirectedReference ResolvedProjectReference, resolver *Resolver, ) *resolutionState { state := &resolutionState{ name: name, containingDirectory: containingDirectory, - compilerOptions: compilerOptions, + compilerOptions: GetCompilerOptionsWithRedirect(compilerOptions, redirectedReference), resolver: resolver, } - if redirectedReference != nil { - state.compilerOptions = redirectedReference.CommandLine.CompilerOptions - } - if isTypeReferenceDirective { state.extensions = extensionsDeclaration } else if compilerOptions.NoDtsResolution == core.TSTrue { @@ -105,6 +101,16 @@ func newResolutionState( return state } +func GetCompilerOptionsWithRedirect(compilerOptions *core.CompilerOptions, redirectedReference ResolvedProjectReference) *core.CompilerOptions { + if redirectedReference == nil { + return compilerOptions + } + if optionsFromRedirect := redirectedReference.CompilerOptions(); optionsFromRedirect != nil { + return optionsFromRedirect + } + return compilerOptions +} + type Resolver struct { caches host ResolutionHost @@ -150,22 +156,27 @@ func (r *Resolver) GetPackageJsonScopeIfApplicable(path string) *packagejson.Inf return nil } -func (r *Resolver) ResolveTypeReferenceDirective(typeReferenceDirectiveName string, containingFile string, resolutionMode core.ResolutionMode, redirectedReference *ResolvedProjectReference) *ResolvedTypeReferenceDirective { - traceEnabled := r.traceEnabled() - - compilerOptions := r.compilerOptions - if redirectedReference != nil { - compilerOptions = redirectedReference.CommandLine.CompilerOptions +func (r *Resolver) traceResolutionUsingProjectReference(redirectedReference ResolvedProjectReference) { + if redirectedReference != nil && redirectedReference.CompilerOptions() != nil { + r.host.Trace(diagnostics.Using_compiler_options_of_project_reference_redirect_0.Format(redirectedReference.ConfigName())) } +} +func (r *Resolver) ResolveTypeReferenceDirective( + typeReferenceDirectiveName string, + containingFile string, + resolutionMode core.ResolutionMode, + redirectedReference ResolvedProjectReference, +) *ResolvedTypeReferenceDirective { + traceEnabled := r.traceEnabled() + + compilerOptions := GetCompilerOptionsWithRedirect(r.compilerOptions, redirectedReference) containingDirectory := tspath.GetDirectoryPath(containingFile) typeRoots, fromConfig := compilerOptions.GetEffectiveTypeRoots(r.host.GetCurrentDirectory()) if traceEnabled { r.host.Trace(diagnostics.Resolving_type_reference_directive_0_containing_file_1_root_directory_2.Format(typeReferenceDirectiveName, containingFile, strings.Join(typeRoots, ","))) - if redirectedReference != nil { - r.host.Trace(diagnostics.Using_compiler_options_of_project_reference_redirect_0.Format(redirectedReference.SourceFile.FileName())) - } + r.traceResolutionUsingProjectReference(redirectedReference) } state := newResolutionState(typeReferenceDirectiveName, containingDirectory, true /*isTypeReferenceDirective*/, resolutionMode, compilerOptions, redirectedReference, r) @@ -177,19 +188,12 @@ func (r *Resolver) ResolveTypeReferenceDirective(typeReferenceDirectiveName stri return result } -func (r *Resolver) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode, redirectedReference *ResolvedProjectReference) *ResolvedModule { +func (r *Resolver) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) *ResolvedModule { traceEnabled := r.traceEnabled() - - compilerOptions := r.compilerOptions - if redirectedReference != nil { - compilerOptions = redirectedReference.CommandLine.CompilerOptions - } - + compilerOptions := GetCompilerOptionsWithRedirect(r.compilerOptions, redirectedReference) if traceEnabled { r.host.Trace(diagnostics.Resolving_module_0_from_1.Format(moduleName, containingFile)) - if redirectedReference != nil { - r.host.Trace(diagnostics.Using_compiler_options_of_project_reference_redirect_0.Format(redirectedReference.SourceFile.FileName())) - } + r.traceResolutionUsingProjectReference(redirectedReference) } containingDirectory := tspath.GetDirectoryPath(containingFile) diff --git a/internal/module/resolver_test.go b/internal/module/resolver_test.go index 6274422887..b7375b18f5 100644 --- a/internal/module/resolver_test.go +++ b/internal/module/resolver_test.go @@ -10,7 +10,6 @@ import ( "sync" "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/repo" @@ -230,16 +229,29 @@ func sanitizeTraceOutput(trace string) string { return typesVersionsMessageRegex.ReplaceAllString(trace, "that matches compiler version '3.1.0-dev'") } +type RedirectRef struct { + fileName string + options *core.CompilerOptions +} + +func (r *RedirectRef) ConfigName() string { + return r.fileName +} + +func (r *RedirectRef) CompilerOptions() *core.CompilerOptions { + return r.options +} + +var _ module.ResolvedProjectReference = (*RedirectRef)(nil) + func doCall(t *testing.T, resolver *module.Resolver, call functionCall, skipLocations bool) { switch call.call { case "resolveModuleName", "resolveTypeReferenceDirective": - var redirectedReference *module.ResolvedProjectReference + var redirectedReference module.ResolvedProjectReference if call.args.RedirectedRef != nil { - redirectedReference = &module.ResolvedProjectReference{ - SourceFile: (&ast.NodeFactory{}).NewSourceFile("", call.args.RedirectedRef.SourceFile.FileName, tspath.Path(call.args.RedirectedRef.SourceFile.FileName), nil).AsSourceFile(), - CommandLine: core.ParsedOptions{ - CompilerOptions: call.args.RedirectedRef.CommandLine.CompilerOptions, - }, + redirectedReference = &RedirectRef{ + fileName: call.args.RedirectedRef.SourceFile.FileName, + options: call.args.RedirectedRef.CommandLine.CompilerOptions, } } diff --git a/internal/module/types.go b/internal/module/types.go index 073bf592f3..5077d6e568 100644 --- a/internal/module/types.go +++ b/internal/module/types.go @@ -22,10 +22,9 @@ type ModeAwareCacheKey struct { Mode core.ResolutionMode } -type ResolvedProjectReference struct { - CommandLine core.ParsedOptions - SourceFile *ast.SourceFile - References []*ResolvedProjectReference +type ResolvedProjectReference interface { + ConfigName() string + CompilerOptions() *core.CompilerOptions } type NodeResolutionFeatures int32 diff --git a/internal/modulespecifiers/specifiers.go b/internal/modulespecifiers/specifiers.go index 04f35504cc..fa05bf937c 100644 --- a/internal/modulespecifiers/specifiers.go +++ b/internal/modulespecifiers/specifiers.go @@ -200,12 +200,13 @@ func getEachFileNameOfModule( preferSymlinks bool, ) []ModulePath { cwd := host.GetCurrentDirectory() + importedPath := tspath.ToPath(importedFileName, cwd, host.UseCaseSensitiveFileNames()) var referenceRedirect string - if host.IsSourceOfProjectReferenceRedirect(importedFileName) { - referenceRedirect = host.GetProjectReferenceRedirect(importedFileName) + outputAndReference := host.GetOutputAndProjectReference(importedPath) + if outputAndReference != nil && outputAndReference.OutputDts != "" { + referenceRedirect = outputAndReference.OutputDts } - importedPath := tspath.ToPath(importedFileName, cwd, host.UseCaseSensitiveFileNames()) redirects := host.GetRedirectTargets(importedPath) importedFileNames := make([]string, 0, 2+len(redirects)) if len(referenceRedirect) > 0 { diff --git a/internal/modulespecifiers/types.go b/internal/modulespecifiers/types.go index 14c26f1cdf..c6727aa022 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/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -52,8 +53,7 @@ type ModuleSpecifierGenerationHost interface { UseCaseSensitiveFileNames() bool GetCurrentDirectory() string - IsSourceOfProjectReferenceRedirect(path string) bool - GetProjectReferenceRedirect(path string) string + GetOutputAndProjectReference(path tspath.Path) *tsoptions.OutputDtsAndProjectReference GetRedirectTargets(path tspath.Path) []string FileExists(path string) bool diff --git a/internal/outputpaths/commonsourcedirectory.go b/internal/outputpaths/commonsourcedirectory.go new file mode 100644 index 0000000000..efd7a937fc --- /dev/null +++ b/internal/outputpaths/commonsourcedirectory.go @@ -0,0 +1,72 @@ +package outputpaths + +import ( + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/tspath" +) + +func computeCommonSourceDirectoryOfFilenames(fileNames []string, currentDirectory string, useCaseSensitiveFileNames bool) string { + var commonPathComponents []string + for _, sourceFile := range fileNames { + // Each file contributes into common source file path + sourcePathComponents := tspath.GetNormalizedPathComponents(sourceFile, currentDirectory) + + // The base file name is not part of the common directory path + sourcePathComponents = sourcePathComponents[:len(sourcePathComponents)-1] + + if commonPathComponents == nil { + // first file + commonPathComponents = sourcePathComponents + continue + } + + n := min(len(commonPathComponents), len(sourcePathComponents)) + for i := range n { + if tspath.GetCanonicalFileName(commonPathComponents[i], useCaseSensitiveFileNames) != tspath.GetCanonicalFileName(sourcePathComponents[i], useCaseSensitiveFileNames) { + if i == 0 { + // Failed to find any common path component + return "" + } + + // New common path found that is 0 -> i-1 + commonPathComponents = commonPathComponents[:i] + break + } + } + + // If the sourcePathComponents was shorter than the commonPathComponents, truncate to the sourcePathComponents + if len(sourcePathComponents) < len(commonPathComponents) { + commonPathComponents = commonPathComponents[:len(sourcePathComponents)] + } + } + + if len(commonPathComponents) == 0 { + // Can happen when all input files are .d.ts files + return currentDirectory + } + + return tspath.GetPathFromPathComponents(commonPathComponents) +} + +func GetCommonSourceDirectory(options *core.CompilerOptions, files func() []string, currentDirectory string, useCaseSensitiveFileNames bool) string { + var commonSourceDirectory string + if options.RootDir != "" { + // If a rootDir is specified use it as the commonSourceDirectory + commonSourceDirectory = options.RootDir + } else if options.Composite.IsTrue() && options.ConfigFilePath != "" { + // If the rootDir is not specified, but the project is composite, then the common source directory + // is the directory of the config file. + commonSourceDirectory = tspath.GetDirectoryPath(options.ConfigFilePath) + } else { + commonSourceDirectory = computeCommonSourceDirectoryOfFilenames(files(), currentDirectory, useCaseSensitiveFileNames) + } + + if len(commonSourceDirectory) > 0 { + // Make sure directory path ends with directory separator so this string can directly + // used to replace with "" to get the relative path of the source file and the relative path doesn't + // start with / making it rooted path + commonSourceDirectory = tspath.EnsureTrailingDirectorySeparator(commonSourceDirectory) + } + + return commonSourceDirectory +} diff --git a/internal/printer/emithost.go b/internal/printer/emithost.go index 6b0757472d..dc5710171d 100644 --- a/internal/printer/emithost.go +++ b/internal/printer/emithost.go @@ -3,6 +3,8 @@ package printer import ( "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/tsoptions" + "github.com/microsoft/typescript-go/internal/tspath" ) type WriteFileData struct { @@ -24,4 +26,5 @@ type EmitHost interface { WriteFile(fileName string, text string, writeByteOrderMark bool, relatedSourceFiles []*ast.SourceFile, data *WriteFileData) error GetEmitModuleFormatOfFile(file ast.HasFileName) core.ModuleKind GetEmitResolver(file *ast.SourceFile, skipDiagnostics bool) EmitResolver + GetOutputAndProjectReference(path tspath.Path) *tsoptions.OutputDtsAndProjectReference } diff --git a/internal/project/configfileregistry.go b/internal/project/configfileregistry.go new file mode 100644 index 0000000000..4c27f9ee9d --- /dev/null +++ b/internal/project/configfileregistry.go @@ -0,0 +1,226 @@ +package project + +import ( + "context" + "fmt" + "slices" + "sync" + + "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/tsoptions" + "github.com/microsoft/typescript-go/internal/tspath" +) + +type ConfigFileEntry struct { + mu sync.Mutex + commandLine *tsoptions.ParsedCommandLine + projects collections.Set[*Project] + pendingReload PendingReload + rootFilesWatch *watchedFiles[[]string] +} + +type ExtendedConfigFileEntry struct { + mu sync.Mutex + configFiles collections.Set[tspath.Path] +} + +type ConfigFileRegistry struct { + Host ProjectHost + ConfigFiles collections.SyncMap[tspath.Path, *ConfigFileEntry] + ExtendedConfigCache collections.SyncMap[tspath.Path, *tsoptions.ExtendedConfigCacheEntry] + ExtendedConfigsUsedBy collections.SyncMap[tspath.Path, *ExtendedConfigFileEntry] +} + +func (e *ConfigFileEntry) SetPendingReload(level PendingReload) bool { + if e.pendingReload < level { + e.pendingReload = level + return true + } + return false +} + +var _ watchFileHost = (*configFileWatchHost)(nil) + +type configFileWatchHost struct { + fileName string + host ProjectHost +} + +func (h *configFileWatchHost) Name() string { + return h.fileName +} + +func (c *configFileWatchHost) Client() Client { + return c.host.Client() +} + +func (c *configFileWatchHost) Log(message string) { + c.host.Log(message) +} + +func (c *ConfigFileRegistry) ReleaseConfig(path tspath.Path, project *Project) { + entry, ok := c.ConfigFiles.Load(path) + if !ok { + return + } + entry.mu.Lock() + defer entry.mu.Unlock() + entry.projects.Delete(project) + if entry.projects.Len() == 0 { + c.ConfigFiles.Delete(path) + commandLine := entry.commandLine + entry.commandLine = nil + c.updateExtendedConfigsUsedBy(path, entry, commandLine) + if entry.rootFilesWatch != nil { + entry.rootFilesWatch.update(context.Background(), nil) + } + } +} + +func (c *ConfigFileRegistry) AcquireConfig(fileName string, path tspath.Path, project *Project) *tsoptions.ParsedCommandLine { + entry, ok := c.ConfigFiles.Load(path) + if !ok { + // Create parsed command line + config, _ := tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, nil, c.Host, &c.ExtendedConfigCache) + var rootFilesWatch *watchedFiles[[]string] + client := c.Host.Client() + if c.Host.IsWatchEnabled() && client != nil { + rootFilesWatch = newWatchedFiles(&configFileWatchHost{fileName: fileName, host: c.Host}, lsproto.WatchKindChange|lsproto.WatchKindCreate|lsproto.WatchKindDelete, core.Identity, "root files") + } + entry, _ = c.ConfigFiles.LoadOrStore(path, &ConfigFileEntry{ + commandLine: config, + pendingReload: PendingReloadFull, + rootFilesWatch: rootFilesWatch, + }) + } + entry.mu.Lock() + defer entry.mu.Unlock() + entry.projects.Add(project) + if entry.pendingReload == PendingReloadNone { + return entry.commandLine + } + switch entry.pendingReload { + case PendingReloadFileNames: + entry.commandLine = tsoptions.ReloadFileNamesOfParsedCommandLine(entry.commandLine, c.Host.FS()) + case PendingReloadFull: + oldCommandLine := entry.commandLine + entry.commandLine, _ = tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, nil, c.Host, &c.ExtendedConfigCache) + c.updateExtendedConfigsUsedBy(path, entry, oldCommandLine) + c.updateRootFilesWatch(fileName, entry) + } + entry.pendingReload = PendingReloadNone + return entry.commandLine +} + +func (c *ConfigFileRegistry) updateRootFilesWatch(fileName string, entry *ConfigFileEntry) { + if entry.rootFilesWatch == nil { + return + } + + wildcardGlobs := entry.commandLine.WildcardDirectories() + rootFileGlobs := make([]string, 0, len(wildcardGlobs)+1+len(entry.commandLine.ExtendedSourceFiles())) + rootFileGlobs = append(rootFileGlobs, fileName) + for _, extendedConfig := range entry.commandLine.ExtendedSourceFiles() { + rootFileGlobs = append(rootFileGlobs, extendedConfig) + } + for dir, recursive := range wildcardGlobs { + rootFileGlobs = append(rootFileGlobs, fmt.Sprintf("%s/%s", tspath.NormalizePath(dir), core.IfElse(recursive, recursiveFileGlobPattern, fileGlobPattern))) + } + for _, fileName := range entry.commandLine.LiteralFileNames() { + rootFileGlobs = append(rootFileGlobs, fileName) + } + entry.rootFilesWatch.update(context.Background(), rootFileGlobs) +} + +func (c *ConfigFileRegistry) updateExtendedConfigsUsedBy(path tspath.Path, entry *ConfigFileEntry, oldCommandLine *tsoptions.ParsedCommandLine) { + extendedConfigs := entry.commandLine.ExtendedSourceFiles() + newConfigs := make([]tspath.Path, 0, len(extendedConfigs)) + for _, extendedConfig := range extendedConfigs { + extendedPath := tspath.ToPath(extendedConfig, c.Host.GetCurrentDirectory(), c.Host.FS().UseCaseSensitiveFileNames()) + newConfigs = append(newConfigs, extendedPath) + extendedEntry, _ := c.ExtendedConfigsUsedBy.LoadOrStore(extendedPath, &ExtendedConfigFileEntry{ + mu: sync.Mutex{}, + }) + extendedEntry.mu.Lock() + extendedEntry.configFiles.Add(path) + extendedEntry.mu.Unlock() + } + for _, extendedConfig := range oldCommandLine.ExtendedSourceFiles() { + extendedPath := tspath.ToPath(extendedConfig, c.Host.GetCurrentDirectory(), c.Host.FS().UseCaseSensitiveFileNames()) + if !slices.Contains(newConfigs, extendedPath) { + extendedEntry, _ := c.ExtendedConfigsUsedBy.Load(extendedPath) + extendedEntry.mu.Lock() + extendedEntry.configFiles.Delete(path) + if extendedEntry.configFiles.Len() == 0 { + c.ExtendedConfigsUsedBy.Delete(extendedPath) + c.ExtendedConfigCache.Delete(extendedPath) + } + extendedEntry.mu.Unlock() + } + } +} + +func (c *ConfigFileRegistry) onWatchedFilesChanged(path tspath.Path, changeKind lsproto.FileChangeType) (err error, handled bool) { + if c.onConfigChange(path, changeKind) { + handled = true + } + + if entry, loaded := c.ExtendedConfigsUsedBy.Load(path); loaded { + entry.mu.Lock() + for configFilePath := range entry.configFiles.Keys() { + if c.onConfigChange(configFilePath, changeKind) { + handled = true + } + } + entry.mu.Unlock() + } + return err, handled +} + +func (c *ConfigFileRegistry) onConfigChange(path tspath.Path, changeKind lsproto.FileChangeType) bool { + entry, ok := c.ConfigFiles.Load(path) + if !ok { + return false + } + entry.mu.Lock() + defer entry.mu.Unlock() + if entry.SetPendingReload(PendingReloadFull) { + for project := range entry.projects.Keys() { + if project.configFilePath == path { + switch changeKind { + case lsproto.FileChangeTypeCreated: + fallthrough + case lsproto.FileChangeTypeChanged: + project.deferredClose = false + project.SetPendingReload(PendingReloadFull) + case lsproto.FileChangeTypeDeleted: + project.deferredClose = true + } + } else { + project.markAsDirty() + } + } + } + return false +} + +func (c *ConfigFileRegistry) tryInvokeWildCardDirectories(fileName string, path tspath.Path) { + configFiles := c.ConfigFiles.ToMap() + for configPath, entry := range configFiles { + entry.mu.Lock() + if entry.commandLine != nil && entry.commandLine.MatchesFileName(fileName) { + if entry.SetPendingReload(PendingReloadFileNames) { + for project := range entry.projects.Keys() { + if project.configFilePath == configPath { + project.SetPendingReload(PendingReloadFileNames) + } else { + project.markAsDirty() + } + } + } + } + entry.mu.Unlock() + } +} diff --git a/internal/project/project.go b/internal/project/project.go index edbcdf9c0c..83a53993ac 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -81,6 +81,7 @@ type ProjectHost interface { DefaultLibraryPath() string TypingsInstaller() *TypingsInstaller DocumentRegistry() *DocumentRegistry + ConfigFileRegistry() *ConfigFileRegistry GetScriptInfoByPath(path tspath.Path) *ScriptInfo GetOrCreateScriptInfoForFile(fileName string, path tspath.Path, scriptKind core.ScriptKind) *ScriptInfo OnDiscoveredSymlink(info *ScriptInfo) @@ -122,7 +123,10 @@ func typeAcquisitionChanged(opt1 *core.TypeAcquisition, opt2 *core.TypeAcquisiti opt1.DisableFilenameBasedTypeAcquisition.IsTrue() != opt2.DisableFilenameBasedTypeAcquisition.IsTrue()) } -var _ compiler.CompilerHost = (*Project)(nil) +var ( + _ compiler.CompilerHost = (*Project)(nil) + _ watchFileHost = (*Project)(nil) +) type Project struct { host *projectHostWithCachedFS @@ -163,7 +167,6 @@ type Project struct { typingFiles []string // Watchers - rootFilesWatch *watchedFiles[[]string] failedLookupsWatch *watchedFiles[map[tspath.Path]string] affectingLocationsWatch *watchedFiles[map[tspath.Path]string] typingsFilesWatch *watchedFiles[map[tspath.Path]string] @@ -181,10 +184,6 @@ func NewConfiguredProject( project.configFilePath = configFilePath project.initialLoadPending = true project.pendingReload = PendingReloadFull - client := host.Client() - if host.IsWatchEnabled() && client != nil { - project.rootFilesWatch = newWatchedFiles(project, lsproto.WatchKindChange|lsproto.WatchKindCreate|lsproto.WatchKindDelete, core.Identity, "root files") - } return project } @@ -216,7 +215,7 @@ func NewProject(name string, kind Kind, currentDirectory string, host ProjectHos CurrentDirectory: currentDirectory, UseCaseSensitiveFileNames: project.host.FS().UseCaseSensitiveFileNames(), } - client := project.host.Client() + client := project.Client() if project.host.IsWatchEnabled() && client != nil { globMapper := createResolutionLookupGlobMapper(project.host) project.failedLookupsWatch = newWatchedFiles(project, lsproto.WatchKindCreate, globMapper, "failed lookup") @@ -246,6 +245,10 @@ func (p *projectHostWithCachedFS) FS() vfs.FS { return p.fs } +func (p *Project) Client() Client { + return p.host.Client() +} + // FS implements compiler.CompilerHost. func (p *Project) FS() vfs.FS { return p.host.FS() @@ -288,6 +291,11 @@ func (p *Project) GetSourceFile(fileName string, path tspath.Path, options *core return nil } +// GetResolvedProjectReference implements compiler.CompilerHost. +func (p *Project) GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine { + return p.host.ConfigFileRegistry().AcquireConfig(fileName, path, p) +} + // Updates the program if needed. func (p *Project) GetProgram() *compiler.Program { program, _ := p.updateGraph() @@ -348,22 +356,6 @@ func (p *Project) GetLanguageServiceForRequest(ctx context.Context) (*ls.Languag return languageService, cleanup } -func (p *Project) getRootFileWatchGlobs() []string { - if p.kind == KindConfigured { - globs := p.parsedCommandLine.WildcardDirectories() - result := make([]string, 0, len(globs)+1) - result = append(result, p.configFileName) - for dir, recursive := range globs { - result = append(result, fmt.Sprintf("%s/%s", tspath.NormalizePath(dir), core.IfElse(recursive, recursiveFileGlobPattern, fileGlobPattern))) - } - for _, fileName := range p.parsedCommandLine.LiteralFileNames() { - result = append(result, fileName) - } - return result - } - return nil -} - func (p *Project) getModuleResolutionWatchGlobs() (failedLookups map[tspath.Path]string, affectingLocaions map[tspath.Path]string) { failedLookups = make(map[tspath.Path]string) affectingLocaions = make(map[tspath.Path]string) @@ -387,32 +379,16 @@ func (p *Project) getModuleResolutionWatchGlobs() (failedLookups map[tspath.Path } func (p *Project) updateWatchers(ctx context.Context) { - client := p.host.Client() + client := p.Client() if !p.host.IsWatchEnabled() || client == nil { return } - rootFileGlobs := p.getRootFileWatchGlobs() failedLookupGlobs, affectingLocationGlobs := p.getModuleResolutionWatchGlobs() - - if rootFileGlobs != nil { - p.rootFilesWatch.update(ctx, rootFileGlobs) - } - p.failedLookupsWatch.update(ctx, failedLookupGlobs) p.affectingLocationsWatch.update(ctx, affectingLocationGlobs) } -func (p *Project) tryInvokeWildCardDirectories(fileName string, path tspath.Path) bool { - if p.kind == KindConfigured { - if p.rootFileNames.Has(path) || p.parsedCommandLine.MatchesFileName(fileName) { - p.SetPendingReload(PendingReloadFileNames) - return true - } - } - return false -} - // onWatchEventForNilScriptInfo is fired for watch events that are not the // project tsconfig, and do not have a ScriptInfo for the associated file. // This could be a case of one of the following: @@ -422,9 +398,6 @@ func (p *Project) tryInvokeWildCardDirectories(fileName string, path tspath.Path // part of the project, e.g., a .js file in a project without --allowJs. func (p *Project) onWatchEventForNilScriptInfo(fileName string) { path := p.toPath(fileName) - if p.tryInvokeWildCardDirectories(fileName, path) { - return - } if _, ok := p.failedLookupsWatch.data[path]; ok { p.markAsDirty() } else if _, ok := p.affectingLocationsWatch.data[path]; ok { @@ -527,7 +500,7 @@ func (p *Project) updateGraph() (*compiler.Program, bool) { if p.kind == KindConfigured && p.pendingReload != PendingReloadNone { switch p.pendingReload { case PendingReloadFileNames: - p.parsedCommandLine = tsoptions.ReloadFileNamesOfParsedCommandLine(p.parsedCommandLine, p.host.FS()) + p.parsedCommandLine = p.GetResolvedProjectReference(p.configFileName, p.configFilePath) p.setRootFiles(p.parsedCommandLine.FileNames()) p.programConfig = nil p.pendingReload = PendingReloadNone @@ -538,7 +511,6 @@ func (p *Project) updateGraph() (*compiler.Program, bool) { } } } - oldProgramReused := p.updateProgram() hasAddedOrRemovedFiles := p.hasAddedorRemovedFiles.Load() p.hasAddedorRemovedFiles.Store(false) @@ -557,6 +529,13 @@ func (p *Project) updateGraph() (*compiler.Program, bool) { p.detachScriptInfoIfNotInferredRoot(oldSourceFile.Path()) } } + + oldProgram.ForEachResolvedProjectReference(func(path tspath.Path, ref *tsoptions.ParsedCommandLine) bool { + if _, ok := p.program.GetResolvedProjectReferenceFor(path); !ok { + p.host.ConfigFileRegistry().ReleaseConfig(path, p) + } + return true + }) } p.enqueueInstallTypingsForProject(oldProgram, hasAddedOrRemovedFiles) // TODO: this is currently always synchronously called by some kind of updating request, @@ -605,9 +584,10 @@ func (p *Project) updateProgram() bool { typingsLocation = p.host.TypingsInstaller().TypingsLocation } p.program = compiler.NewProgram(compiler.ProgramOptions{ - Config: p.programConfig, - Host: p, - TypingsLocation: typingsLocation, + Config: p.programConfig, + Host: p, + UseSourceOfProjectReference: true, + TypingsLocation: typingsLocation, CreateCheckerPool: func(program *compiler.Program) compiler.CheckerPool { p.checkerPool = newCheckerPool(4, program, p.Log) return p.checkerPool @@ -785,7 +765,7 @@ func (p *Project) UpdateTypingFiles(typingsInfo *TypingsInfo, typingFiles []stri // this.resolutionCache.setFilesWithInvalidatedNonRelativeUnresolvedImports(this.cachedUnresolvedImportsPerFile); p.markAsDirtyLocked() - client := p.host.Client() + client := p.Client() if client != nil { err := client.RefreshDiagnostics(context.Background()) if err != nil { @@ -802,7 +782,7 @@ func (p *Project) WatchTypingLocations(files []string) { return } - client := p.host.Client() + client := p.Client() if !p.host.IsWatchEnabled() || client == nil { return } @@ -915,33 +895,20 @@ func (p *Project) LoadConfig() error { p.programConfig = nil p.pendingReload = PendingReloadNone - if configFileContent, ok := p.host.FS().ReadFile(p.configFileName); ok { - configDir := tspath.GetDirectoryPath(p.configFileName) - tsConfigSourceFile := tsoptions.NewTsconfigSourceFileFromFilePath(p.configFileName, p.configFilePath, configFileContent) - parsedCommandLine := tsoptions.ParseJsonSourceFileConfigFileContent( - tsConfigSourceFile, - p.host, - configDir, - nil, /*existingOptions*/ - p.configFileName, - nil, /*resolutionStack*/ - nil, /*extraFileExtensions*/ - nil, /*extendedConfigCache*/ - ) - + p.parsedCommandLine = p.GetResolvedProjectReference(p.configFileName, p.configFilePath) + if p.parsedCommandLine != nil { p.Logf("Config: %s : %s", p.configFileName, core.Must(core.StringifyJson(map[string]any{ - "rootNames": parsedCommandLine.FileNames(), - "options": parsedCommandLine.CompilerOptions(), - "projectReferences": parsedCommandLine.ProjectReferences(), + "rootNames": p.parsedCommandLine.FileNames(), + "options": p.parsedCommandLine.CompilerOptions(), + "projectReferences": p.parsedCommandLine.ProjectReferences(), }, " ", " ")), ) - p.parsedCommandLine = parsedCommandLine - p.compilerOptions = parsedCommandLine.CompilerOptions() - p.typeAcquisition = parsedCommandLine.TypeAcquisition() - p.setRootFiles(parsedCommandLine.FileNames()) + p.compilerOptions = p.parsedCommandLine.CompilerOptions() + p.typeAcquisition = p.parsedCommandLine.TypeAcquisition() + p.setRootFiles(p.parsedCommandLine.FileNames()) } else { p.compilerOptions = &core.CompilerOptions{} p.typeAcquisition = nil @@ -1074,6 +1041,13 @@ func (p *Project) Close() { // Detach script info if its not root or is root of non inferred project p.detachScriptInfoIfNotInferredRoot(sourceFile.Path()) } + p.program.ForEachResolvedProjectReference(func(path tspath.Path, ref *tsoptions.ParsedCommandLine) bool { + p.host.ConfigFileRegistry().ReleaseConfig(path, p) + return true + }) + if p.kind == KindConfigured { + p.host.ConfigFileRegistry().ReleaseConfig(p.configFilePath, p) + } p.program = nil } @@ -1095,13 +1069,9 @@ func (p *Project) Close() { p.typingFiles = nil // Clean up file watchers waiting for missing files - client := p.host.Client() + client := p.Client() if p.host.IsWatchEnabled() && client != nil { ctx := context.Background() - if p.rootFilesWatch != nil { - p.rootFilesWatch.update(ctx, nil) - } - p.failedLookupsWatch.update(ctx, nil) p.affectingLocationsWatch.update(ctx, nil) p.typingsFilesWatch.update(ctx, nil) diff --git a/internal/project/projectlifetime_test.go b/internal/project/projectlifetime_test.go index 60994c9170..b4ba9c91bf 100644 --- a/internal/project/projectlifetime_test.go +++ b/internal/project/projectlifetime_test.go @@ -5,11 +5,17 @@ import ( "github.com/microsoft/typescript-go/internal/bundled" "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/project" "github.com/microsoft/typescript-go/internal/testutil/projecttestutil" "github.com/microsoft/typescript-go/internal/tspath" "gotest.tools/v3/assert" ) +func configFileExists(t *testing.T, service *project.Service, path tspath.Path, exists bool) { + _, loaded := service.ConfigFileRegistry().ConfigFiles.Load(path) + assert.Equal(t, loaded, exists, "config file %s should exist: %v", path, exists) +} + func TestProjectLifetime(t *testing.T) { t.Parallel() if !bundled.Embedded { @@ -60,6 +66,8 @@ func TestProjectLifetime(t *testing.T) { assert.Assert(t, service.ConfiguredProject(tspath.ToPath("/home/projects/TS/p1/tsconfig.json", host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames())) != nil) assert.Assert(t, service.ConfiguredProject(tspath.ToPath("/home/projects/TS/p2/tsconfig.json", host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames())) != nil) assert.Equal(t, len(host.ClientMock.WatchFilesCalls()), 2) + configFileExists(t, service, tspath.ToPath("/home/projects/TS/p1/tsconfig.json", host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames()), true) + configFileExists(t, service, tspath.ToPath("/home/projects/TS/p2/tsconfig.json", host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames()), true) service.CloseFile("/home/projects/TS/p1/src/index.ts") service.OpenFile("/home/projects/TS/p3/src/index.ts", files["/home/projects/TS/p3/src/index.ts"].(string), core.ScriptKindTS, "") @@ -71,6 +79,9 @@ func TestProjectLifetime(t *testing.T) { assert.Assert(t, service.GetScriptInfoByPath(tspath.ToPath("/home/projects/TS/p1/src/x.ts", host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames())) == nil) assert.Equal(t, len(host.ClientMock.WatchFilesCalls()), 3) assert.Equal(t, len(host.ClientMock.UnwatchFilesCalls()), 1) + configFileExists(t, service, tspath.ToPath("/home/projects/TS/p1/tsconfig.json", host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames()), false) + configFileExists(t, service, tspath.ToPath("/home/projects/TS/p2/tsconfig.json", host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames()), true) + configFileExists(t, service, tspath.ToPath("/home/projects/TS/p3/tsconfig.json", host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames()), true) service.CloseFile("/home/projects/TS/p2/src/index.ts") service.CloseFile("/home/projects/TS/p3/src/index.ts") @@ -84,6 +95,9 @@ func TestProjectLifetime(t *testing.T) { assert.Assert(t, service.GetScriptInfoByPath(tspath.ToPath("/home/projects/TS/p3/src/x.ts", host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames())) == nil) assert.Equal(t, len(host.ClientMock.WatchFilesCalls()), 4) assert.Equal(t, len(host.ClientMock.UnwatchFilesCalls()), 3) + configFileExists(t, service, tspath.ToPath("/home/projects/TS/p1/tsconfig.json", host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames()), true) + configFileExists(t, service, tspath.ToPath("/home/projects/TS/p2/tsconfig.json", host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames()), false) + configFileExists(t, service, tspath.ToPath("/home/projects/TS/p3/tsconfig.json", host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames()), false) }) t.Run("inferred projects", func(t *testing.T) { diff --git a/internal/project/projectreferencesprogram_test.go b/internal/project/projectreferencesprogram_test.go new file mode 100644 index 0000000000..5be5928f41 --- /dev/null +++ b/internal/project/projectreferencesprogram_test.go @@ -0,0 +1,324 @@ +package project_test + +import ( + "fmt" + "testing" + + "github.com/microsoft/typescript-go/internal/bundled" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/project" + "github.com/microsoft/typescript-go/internal/testutil/projecttestutil" + "github.com/microsoft/typescript-go/internal/tspath" + "github.com/microsoft/typescript-go/internal/vfs/vfstest" + "gotest.tools/v3/assert" +) + +func TestProjectReferencesProgram(t *testing.T) { + t.Parallel() + + if !bundled.Embedded { + t.Skip("bundled files are not embedded") + } + + t.Run("program for referenced project", func(t *testing.T) { + t.Parallel() + files := filesForReferencedProjectProgram(false) + service, _ := projecttestutil.Setup(files, nil) + assert.Equal(t, len(service.Projects()), 0) + service.OpenFile("/user/username/projects/myproject/main/main.ts", files["/user/username/projects/myproject/main/main.ts"].(string), core.ScriptKindTS, "/user/username/projects/myproject") + assert.Equal(t, len(service.Projects()), 1) + p := service.Projects()[0] + assert.Equal(t, p.Kind(), project.KindConfigured) + scriptInfo := service.GetScriptInfo("/user/username/projects/myproject/dependency/fns.ts") + assert.Assert(t, scriptInfo != nil) + dtsScriptInfo := service.GetScriptInfo("/user/username/projects/myproject/decls/fns.d.ts") + assert.Assert(t, dtsScriptInfo == nil) + file := p.CurrentProgram().GetSourceFileByPath(tspath.Path("/user/username/projects/myproject/dependency/fns.ts")) + assert.Assert(t, file != nil) + dtsFile := p.CurrentProgram().GetSourceFileByPath(tspath.Path("/user/username/projects/myproject/decls/fns.d.ts")) + assert.Assert(t, dtsFile == nil) + }) + + t.Run("program with disableSourceOfProjectReferenceRedirect", func(t *testing.T) { + t.Parallel() + files := filesForReferencedProjectProgram(true) + files["/user/username/projects/myproject/decls/fns.d.ts"] = ` + export declare function fn1(): void; + export declare function fn2(): void; + export declare function fn3(): void; + export declare function fn4(): void; + export declare function fn5(): void; + ` + service, _ := projecttestutil.Setup(files, nil) + assert.Equal(t, len(service.Projects()), 0) + service.OpenFile("/user/username/projects/myproject/main/main.ts", files["/user/username/projects/myproject/main/main.ts"].(string), core.ScriptKindTS, "/user/username/projects/myproject") + assert.Equal(t, len(service.Projects()), 1) + p := service.Projects()[0] + assert.Equal(t, p.Kind(), project.KindConfigured) + scriptInfo := service.GetScriptInfo("/user/username/projects/myproject/dependency/fns.ts") + assert.Assert(t, scriptInfo == nil) + dtsScriptInfo := service.GetScriptInfo("/user/username/projects/myproject/decls/fns.d.ts") + assert.Assert(t, dtsScriptInfo != nil) + file := p.CurrentProgram().GetSourceFileByPath(tspath.Path("/user/username/projects/myproject/dependency/fns.ts")) + assert.Assert(t, file == nil) + dtsFile := p.CurrentProgram().GetSourceFileByPath(tspath.Path("/user/username/projects/myproject/decls/fns.d.ts")) + assert.Assert(t, dtsFile != nil) + }) + + t.Run("references through symlink with index and typings", func(t *testing.T) { + t.Parallel() + files, aTest, bFoo, bBar := filesForSymlinkReferences(false, "") + service, _ := projecttestutil.Setup(files, nil) + assert.Equal(t, len(service.Projects()), 0) + service.OpenFile(aTest, files[aTest].(string), core.ScriptKindTS, "") + assert.Equal(t, len(service.Projects()), 1) + p := service.Projects()[0] + assert.Equal(t, p.Kind(), project.KindConfigured) + fooInfo := service.GetScriptInfo(bFoo) + assert.Assert(t, fooInfo != nil) + barInfo := service.GetScriptInfo(bBar) + assert.Assert(t, barInfo != nil) + fooFile := p.CurrentProgram().GetSourceFile(bFoo) + assert.Assert(t, fooFile != nil) + barFile := p.CurrentProgram().GetSourceFile(bBar) + assert.Assert(t, barFile != nil) + }) + + t.Run("references through symlink with index and typings with preserveSymlinks", func(t *testing.T) { + t.Parallel() + files, aTest, bFoo, bBar := filesForSymlinkReferences(true, "") + service, _ := projecttestutil.Setup(files, nil) + assert.Equal(t, len(service.Projects()), 0) + service.OpenFile(aTest, files[aTest].(string), core.ScriptKindTS, "") + assert.Equal(t, len(service.Projects()), 1) + p := service.Projects()[0] + assert.Equal(t, p.Kind(), project.KindConfigured) + fooInfo := service.GetScriptInfo(bFoo) + assert.Assert(t, fooInfo != nil) + barInfo := service.GetScriptInfo(bBar) + assert.Assert(t, barInfo != nil) + fooFile := p.CurrentProgram().GetSourceFile(bFoo) + assert.Assert(t, fooFile != nil) + barFile := p.CurrentProgram().GetSourceFile(bBar) + assert.Assert(t, barFile != nil) + }) + + t.Run("references through symlink with index and typings scoped package", func(t *testing.T) { + t.Parallel() + files, aTest, bFoo, bBar := filesForSymlinkReferences(false, "@issue/") + service, _ := projecttestutil.Setup(files, nil) + assert.Equal(t, len(service.Projects()), 0) + service.OpenFile(aTest, files[aTest].(string), core.ScriptKindTS, "") + assert.Equal(t, len(service.Projects()), 1) + p := service.Projects()[0] + assert.Equal(t, p.Kind(), project.KindConfigured) + fooInfo := service.GetScriptInfo(bFoo) + assert.Assert(t, fooInfo != nil) + barInfo := service.GetScriptInfo(bBar) + assert.Assert(t, barInfo != nil) + fooFile := p.CurrentProgram().GetSourceFile(bFoo) + assert.Assert(t, fooFile != nil) + barFile := p.CurrentProgram().GetSourceFile(bBar) + assert.Assert(t, barFile != nil) + }) + + t.Run("references through symlink with index and typings with scoped package preserveSymlinks", func(t *testing.T) { + t.Parallel() + files, aTest, bFoo, bBar := filesForSymlinkReferences(true, "@issue/") + service, _ := projecttestutil.Setup(files, nil) + assert.Equal(t, len(service.Projects()), 0) + service.OpenFile(aTest, files[aTest].(string), core.ScriptKindTS, "") + assert.Equal(t, len(service.Projects()), 1) + p := service.Projects()[0] + assert.Equal(t, p.Kind(), project.KindConfigured) + fooInfo := service.GetScriptInfo(bFoo) + assert.Assert(t, fooInfo != nil) + barInfo := service.GetScriptInfo(bBar) + assert.Assert(t, barInfo != nil) + fooFile := p.CurrentProgram().GetSourceFile(bFoo) + assert.Assert(t, fooFile != nil) + barFile := p.CurrentProgram().GetSourceFile(bBar) + assert.Assert(t, barFile != nil) + }) + + t.Run("references through symlink referencing from subFolder", func(t *testing.T) { + t.Parallel() + files, aTest, bFoo, bBar := filesForSymlinkReferencesInSubfolder(false, "") + service, _ := projecttestutil.Setup(files, nil) + assert.Equal(t, len(service.Projects()), 0) + service.OpenFile(aTest, files[aTest].(string), core.ScriptKindTS, "") + assert.Equal(t, len(service.Projects()), 1) + p := service.Projects()[0] + assert.Equal(t, p.Kind(), project.KindConfigured) + fooInfo := service.GetScriptInfo(bFoo) + assert.Assert(t, fooInfo != nil) + barInfo := service.GetScriptInfo(bBar) + assert.Assert(t, barInfo != nil) + fooFile := p.CurrentProgram().GetSourceFile(bFoo) + assert.Assert(t, fooFile != nil) + barFile := p.CurrentProgram().GetSourceFile(bBar) + assert.Assert(t, barFile != nil) + }) + + t.Run("references through symlink referencing from subFolder with preserveSymlinks", func(t *testing.T) { + t.Parallel() + files, aTest, bFoo, bBar := filesForSymlinkReferencesInSubfolder(true, "") + service, _ := projecttestutil.Setup(files, nil) + assert.Equal(t, len(service.Projects()), 0) + service.OpenFile(aTest, files[aTest].(string), core.ScriptKindTS, "") + assert.Equal(t, len(service.Projects()), 1) + p := service.Projects()[0] + assert.Equal(t, p.Kind(), project.KindConfigured) + fooInfo := service.GetScriptInfo(bFoo) + assert.Assert(t, fooInfo != nil) + barInfo := service.GetScriptInfo(bBar) + assert.Assert(t, barInfo != nil) + fooFile := p.CurrentProgram().GetSourceFile(bFoo) + assert.Assert(t, fooFile != nil) + barFile := p.CurrentProgram().GetSourceFile(bBar) + assert.Assert(t, barFile != nil) + }) + + t.Run("references through symlink referencing from subFolder scoped package", func(t *testing.T) { + t.Parallel() + files, aTest, bFoo, bBar := filesForSymlinkReferencesInSubfolder(false, "@issue/") + service, _ := projecttestutil.Setup(files, nil) + assert.Equal(t, len(service.Projects()), 0) + service.OpenFile(aTest, files[aTest].(string), core.ScriptKindTS, "") + assert.Equal(t, len(service.Projects()), 1) + p := service.Projects()[0] + assert.Equal(t, p.Kind(), project.KindConfigured) + fooInfo := service.GetScriptInfo(bFoo) + assert.Assert(t, fooInfo != nil) + barInfo := service.GetScriptInfo(bBar) + assert.Assert(t, barInfo != nil) + fooFile := p.CurrentProgram().GetSourceFile(bFoo) + assert.Assert(t, fooFile != nil) + barFile := p.CurrentProgram().GetSourceFile(bBar) + assert.Assert(t, barFile != nil) + }) + + t.Run("references through symlink referencing from subFolder with scoped package preserveSymlinks", func(t *testing.T) { + t.Parallel() + files, aTest, bFoo, bBar := filesForSymlinkReferencesInSubfolder(true, "@issue/") + service, _ := projecttestutil.Setup(files, nil) + assert.Equal(t, len(service.Projects()), 0) + service.OpenFile(aTest, files[aTest].(string), core.ScriptKindTS, "") + assert.Equal(t, len(service.Projects()), 1) + p := service.Projects()[0] + assert.Equal(t, p.Kind(), project.KindConfigured) + fooInfo := service.GetScriptInfo(bFoo) + assert.Assert(t, fooInfo != nil) + barInfo := service.GetScriptInfo(bBar) + assert.Assert(t, barInfo != nil) + fooFile := p.CurrentProgram().GetSourceFile(bFoo) + assert.Assert(t, fooFile != nil) + barFile := p.CurrentProgram().GetSourceFile(bBar) + assert.Assert(t, barFile != nil) + }) +} + +func filesForReferencedProjectProgram(disableSourceOfProjectReferenceRedirect bool) map[string]any { + return map[string]any{ + "/user/username/projects/myproject/main/tsconfig.json": fmt.Sprintf(`{ + "compilerOptions": { + "composite": true%s + }, + "references": [{ "path": "../dependency" }] + }`, core.IfElse(disableSourceOfProjectReferenceRedirect, `, "disableSourceOfProjectReferenceRedirect": true`, "")), + "/user/username/projects/myproject/main/main.ts": ` + import { + fn1, + fn2, + fn3, + fn4, + fn5 + } from '../decls/fns' + fn1(); + fn2(); + fn3(); + fn4(); + fn5(); + `, + "/user/username/projects/myproject/dependency/tsconfig.json": `{ + "compilerOptions": { + "composite": true, + "declarationDir": "../decls" + }, + }`, + "/user/username/projects/myproject/dependency/fns.ts": ` + export function fn1() { } + export function fn2() { } + export function fn3() { } + export function fn4() { } + export function fn5() { } + `, + } +} + +func filesForSymlinkReferences(preserveSymlinks bool, scope string) (files map[string]any, aTest string, bFoo string, bBar string) { + aTest = "/user/username/projects/myproject/packages/A/src/index.ts" + bFoo = "/user/username/projects/myproject/packages/B/src/index.ts" + bBar = "/user/username/projects/myproject/packages/B/src/bar.ts" + files = map[string]any{ + "/user/username/projects/myproject/packages/B/package.json": `{ + "main": "lib/index.js", + "types": "lib/index.d.ts", + }`, + aTest: fmt.Sprintf(` + import { foo } from '%sb'; + import { bar } from '%sb/lib/bar'; + foo(); + bar(); + `, scope, scope), + bFoo: `export function foo() { }`, + bBar: `export function bar() { }`, + fmt.Sprintf(`/user/username/projects/myproject/node_modules/%sb`, scope): vfstest.Symlink("/user/username/projects/myproject/packages/B"), + } + addConfigForPackage(files, "A", preserveSymlinks, []string{"../B"}) + addConfigForPackage(files, "B", preserveSymlinks, nil) + return files, aTest, bFoo, bBar +} + +func filesForSymlinkReferencesInSubfolder(preserveSymlinks bool, scope string) (files map[string]any, aTest string, bFoo string, bBar string) { + aTest = "/user/username/projects/myproject/packages/A/src/test.ts" + bFoo = "/user/username/projects/myproject/packages/B/src/foo.ts" + bBar = "/user/username/projects/myproject/packages/B/src/bar/foo.ts" + files = map[string]any{ + "/user/username/projects/myproject/packages/B/package.json": `{}`, + "/user/username/projects/myproject/packages/A/src/test.ts": fmt.Sprintf(` + import { foo } from '%sb/lib/foo'; + import { bar } from '%sb/lib/bar/foo'; + foo(); + bar(); + `, scope, scope), + bFoo: `export function foo() { }`, + bBar: `export function bar() { }`, + fmt.Sprintf(`/user/username/projects/myproject/node_modules/%sb`, scope): vfstest.Symlink("/user/username/projects/myproject/packages/B"), + } + addConfigForPackage(files, "A", preserveSymlinks, []string{"../B"}) + addConfigForPackage(files, "B", preserveSymlinks, nil) + return files, aTest, bFoo, bBar +} + +func addConfigForPackage(files map[string]any, packageName string, preserveSymlinks bool, references []string) { + compilerOptions := map[string]any{ + "outDir": "lib", + "rootDir": "src", + "composite": true, + } + if preserveSymlinks { + compilerOptions["preserveSymlinks"] = true + } + var referencesToAdd []map[string]any + for _, ref := range references { + referencesToAdd = append(referencesToAdd, map[string]any{ + "path": ref, + }) + } + files[fmt.Sprintf("/user/username/projects/myproject/packages/%s/tsconfig.json", packageName)] = core.Must(core.StringifyJson(map[string]any{ + "compilerOptions": compilerOptions, + "include": []string{"src"}, + "references": referencesToAdd, + }, " ", " ")) +} diff --git a/internal/project/service.go b/internal/project/service.go index 1bde0ae22e..9c6c6556c8 100644 --- a/internal/project/service.go +++ b/internal/project/service.go @@ -53,6 +53,7 @@ type Service struct { scriptInfos map[tspath.Path]*ScriptInfo openFiles map[tspath.Path]string // values are projectRootPath, if provided configFileForOpenFiles map[tspath.Path]string // default config project for open files !!! todo solution and project reference handling + configFileRegistry *ConfigFileRegistry // Contains all the deleted script info's version information so that // it does not reset when creating script info again @@ -93,7 +94,9 @@ func NewService(host ServiceHost, options ServiceOptions) *Service { filenameToScriptInfoVersion: make(map[tspath.Path]int), realpathToScriptInfos: make(map[tspath.Path]map[*ScriptInfo]struct{}), } - + service.configFileRegistry = &ConfigFileRegistry{ + Host: service, + } service.converters = ls.NewConverters(options.PositionEncoding, func(fileName string) *ls.LineMap { return service.GetScriptInfo(fileName).LineMap() }) @@ -149,6 +152,11 @@ func (s *Service) DocumentRegistry() *DocumentRegistry { return s.documentRegistry } +// ConfigFileRegistry implements ProjectHost. +func (s *Service) ConfigFileRegistry() *ConfigFileRegistry { + return s.configFileRegistry +} + // FS implements ProjectHost. func (s *Service) FS() vfs.FS { return s.host.FS() @@ -229,11 +237,7 @@ func (s *Service) OpenFile(fileName string, fileContent string, scriptKind core. info := s.getOrCreateOpenScriptInfo(fileName, path, fileContent, scriptKind, projectRootPath) if existing == nil && info != nil && !info.isDynamic { // Invoke wild card directory watcher to ensure that the file presence is reflected - s.projectsMu.RLock() - for _, project := range s.configuredProjects { - project.tryInvokeWildCardDirectories(fileName, info.path) - } - s.projectsMu.RUnlock() + s.configFileRegistry.tryInvokeWildCardDirectories(fileName, info.path) } result := s.assignProjectToOpenedScriptInfo(info) s.cleanupProjectsAndScriptInfos(info, result) @@ -321,9 +325,8 @@ func (s *Service) OnWatchedFilesChanged(ctx context.Context, changes []*lsproto. for _, change := range changes { fileName := ls.DocumentURIToFileName(change.Uri) path := s.toPath(fileName) - if project, ok := s.configuredProjects[path]; ok { - // tsconfig of project - if err := s.onConfigFileChanged(project, change.Type); err != nil { + if err, ok := s.configFileRegistry.onWatchedFilesChanged(path, change.Type); ok { + if err != nil { return fmt.Errorf("error handling config file change: %w", err) } } else if _, ok := s.openFiles[path]; ok { @@ -346,6 +349,7 @@ func (s *Service) OnWatchedFilesChanged(ctx context.Context, changes []*lsproto. for _, project := range s.inferredProjects { project.onWatchEventForNilScriptInfo(fileName) } + s.configFileRegistry.tryInvokeWildCardDirectories(fileName, path) } } @@ -357,23 +361,6 @@ func (s *Service) OnWatchedFilesChanged(ctx context.Context, changes []*lsproto. return nil } -func (s *Service) onConfigFileChanged(project *Project, changeKind lsproto.FileChangeType) error { - wasDeferredClose := project.deferredClose - switch changeKind { - case lsproto.FileChangeTypeCreated: - if wasDeferredClose { - project.deferredClose = false - } - case lsproto.FileChangeTypeDeleted: - project.deferredClose = true - } - - if !project.deferredClose { - project.SetPendingReload(PendingReloadFull) - } - return nil -} - func (s *Service) ensureProjectStructureUpToDate() { var hasChanges bool s.projectsMu.RLock() diff --git a/internal/project/watch.go b/internal/project/watch.go index 6aa87f68bd..be463cec9e 100644 --- a/internal/project/watch.go +++ b/internal/project/watch.go @@ -18,8 +18,14 @@ const ( recursiveFileGlobPattern = "**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,json}" ) +type watchFileHost interface { + Name() string + Client() Client + Log(message string) +} + type watchedFiles[T any] struct { - p *Project + p watchFileHost getGlobs func(data T) []string watchKind lsproto.WatchKind @@ -30,7 +36,7 @@ type watchedFiles[T any] struct { } func newWatchedFiles[T any]( - p *Project, + p watchFileHost, watchKind lsproto.WatchKind, getGlobs func(data T) []string, watchType string, @@ -55,11 +61,11 @@ func (w *watchedFiles[T]) update(ctx context.Context, newData T) { w.globs = newGlobs if w.watcherID != "" { - if err := w.p.host.Client().UnwatchFiles(ctx, w.watcherID); err != nil { - w.p.Log(fmt.Sprintf("%s:: Failed to unwatch %s watch: %s, err: %v newGlobs that are not updated: \n%s", w.p.name, w.watchType, w.watcherID, err, formatFileList(w.globs, "\t", hr))) + if err := w.p.Client().UnwatchFiles(ctx, w.watcherID); err != nil { + w.p.Log(fmt.Sprintf("%s:: Failed to unwatch %s watch: %s, err: %v newGlobs that are not updated: \n%s", w.p.Name(), w.watchType, w.watcherID, err, formatFileList(w.globs, "\t", hr))) return } - w.p.Logf("%s:: %s watches unwatch %s", w.p.name, w.watchType, w.watcherID) + w.p.Log(fmt.Sprintf("%s:: %s watches unwatch %s", w.p.Name(), w.watchType, w.watcherID)) } w.watcherID = "" @@ -76,13 +82,13 @@ func (w *watchedFiles[T]) update(ctx context.Context, newData T) { Kind: &w.watchKind, }) } - watcherID, err := w.p.host.Client().WatchFiles(ctx, watchers) + watcherID, err := w.p.Client().WatchFiles(ctx, watchers) if err != nil { - w.p.Log(fmt.Sprintf("%s:: Failed to update %s watch: %v\n%s", w.p.name, w.watchType, err, formatFileList(w.globs, "\t", hr))) + w.p.Log(fmt.Sprintf("%s:: Failed to update %s watch: %v\n%s", w.p.Name(), w.watchType, err, formatFileList(w.globs, "\t", hr))) return } w.watcherID = watcherID - w.p.Logf("%s:: %s watches updated %s:\n%s", w.p.name, w.watchType, w.watcherID, formatFileList(w.globs, "\t", hr)) + w.p.Log(fmt.Sprintf("%s:: %s watches updated %s:\n%s", w.p.Name(), w.watchType, w.watcherID, formatFileList(w.globs, "\t", hr))) return } diff --git a/internal/testutil/harnessutil/harnessutil.go b/internal/testutil/harnessutil/harnessutil.go index 20efa9baaa..f816bde694 100644 --- a/internal/testutil/harnessutil/harnessutil.go +++ b/internal/testutil/harnessutil/harnessutil.go @@ -529,7 +529,7 @@ func (h *cachedCompilerHost) GetSourceFile(fileName string, path tspath.Path, op func createCompilerHost(fs vfs.FS, defaultLibraryPath string, options *core.CompilerOptions, currentDirectory string) compiler.CompilerHost { return &cachedCompilerHost{ - CompilerHost: compiler.NewCompilerHost(options, currentDirectory, fs, defaultLibraryPath), + CompilerHost: compiler.NewCompilerHost(options, currentDirectory, fs, defaultLibraryPath, nil), options: options, } } diff --git a/internal/transformers/importelision_test.go b/internal/transformers/importelision_test.go index 63b12b8c89..3d3ac64a3f 100644 --- a/internal/transformers/importelision_test.go +++ b/internal/transformers/importelision_test.go @@ -12,17 +12,19 @@ import ( "github.com/microsoft/typescript-go/internal/printer" "github.com/microsoft/typescript-go/internal/testutil/emittestutil" "github.com/microsoft/typescript-go/internal/testutil/parsetestutil" + "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" ) type fakeProgram struct { - singleThreaded bool - compilerOptions *core.CompilerOptions - files []*ast.SourceFile - getEmitModuleFormatOfFile func(sourceFile ast.HasFileName) core.ModuleKind - getImpliedNodeFormatForEmit func(sourceFile ast.HasFileName) core.ModuleKind - getResolvedModule func(currentSourceFile ast.HasFileName, moduleReference string, mode core.ResolutionMode) *module.ResolvedModule - getSourceFile func(FileName string) *ast.SourceFile + singleThreaded bool + compilerOptions *core.CompilerOptions + files []*ast.SourceFile + getEmitModuleFormatOfFile func(sourceFile ast.HasFileName) core.ModuleKind + getImpliedNodeFormatForEmit func(sourceFile ast.HasFileName) core.ModuleKind + getResolvedModule func(currentSourceFile ast.HasFileName, moduleReference string, mode core.ResolutionMode) *module.ResolvedModule + getSourceFile func(FileName string) *ast.SourceFile + getSourceFileForResolvedModule func(FileName string) *ast.SourceFile } // GetEmitSyntaxForUsageLocation implements checker.Program. @@ -59,18 +61,22 @@ func (p *fakeProgram) GetPackageJsonInfo(pkgJsonPath string) modulespecifiers.Pa return nil } -func (p *fakeProgram) GetProjectReferenceRedirect(path string) string { - return "" +func (p *fakeProgram) GetRedirectTargets(path tspath.Path) []string { + return nil } -func (p *fakeProgram) GetRedirectTargets(path tspath.Path) []string { +func (p *fakeProgram) GetOutputAndProjectReference(path tspath.Path) *tsoptions.OutputDtsAndProjectReference { return nil } -func (p *fakeProgram) IsSourceOfProjectReferenceRedirect(path string) bool { +func (p *fakeProgram) IsSourceFromProjectReference(path tspath.Path) bool { return false } +func (p *fakeProgram) GetSourceAndProjectReference(path tspath.Path) *tsoptions.SourceAndProjectReference { + return nil +} + func (p *fakeProgram) UseCaseSensitiveFileNames() bool { return true } @@ -119,6 +125,10 @@ func (p *fakeProgram) GetSourceFile(FileName string) *ast.SourceFile { return p.getSourceFile(FileName) } +func (p *fakeProgram) GetSourceFileForResolvedModule(FileName string) *ast.SourceFile { + return p.getSourceFileForResolvedModule(FileName) +} + func (p *fakeProgram) GetSourceFileMetaData(path tspath.Path) *ast.SourceFileMetaData { return nil } @@ -199,6 +209,12 @@ func TestImportElision(t *testing.T) { } return nil }, + getSourceFileForResolvedModule: func(fileName string) *ast.SourceFile { + if fileName == "other.ts" { + return other + } + return nil + }, getResolvedModule: func(currentSourceFile ast.HasFileName, moduleReference string, mode core.ResolutionMode) *module.ResolvedModule { if currentSourceFile == file && moduleReference == "other" { return &module.ResolvedModule{ diff --git a/internal/tsoptions/parsedcommandline.go b/internal/tsoptions/parsedcommandline.go index 9991a03237..926ae73706 100644 --- a/internal/tsoptions/parsedcommandline.go +++ b/internal/tsoptions/parsedcommandline.go @@ -1,12 +1,15 @@ package tsoptions import ( + "iter" "slices" "sync" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/module" + "github.com/microsoft/typescript-go/internal/outputpaths" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" ) @@ -23,10 +26,118 @@ type ParsedCommandLine struct { wildcardDirectoriesOnce sync.Once wildcardDirectories map[string]bool extraFileExtensions []FileExtensionInfo + + sourceAndOutputMapsOnce sync.Once + sourceToOutput map[tspath.Path]*OutputDtsAndProjectReference + outputDtsToSource map[tspath.Path]*SourceAndProjectReference + + commonSourceDirectory string + commonSourceDirectoryOnce sync.Once +} + +type SourceAndProjectReference struct { + Source string + Resolved *ParsedCommandLine +} + +type OutputDtsAndProjectReference struct { + OutputDts string + Resolved *ParsedCommandLine +} + +var ( + _ module.ResolvedProjectReference = (*ParsedCommandLine)(nil) + _ outputpaths.OutputPathsHost = (*ParsedCommandLine)(nil) +) + +func (p *ParsedCommandLine) ConfigName() string { + if p == nil { + return "" + } + return p.ConfigFile.SourceFile.FileName() +} + +func (p *ParsedCommandLine) SourceToOutput() map[tspath.Path]*OutputDtsAndProjectReference { + return p.sourceToOutput +} + +func (p *ParsedCommandLine) OutputDtsToSource() map[tspath.Path]*SourceAndProjectReference { + return p.outputDtsToSource +} + +func (p *ParsedCommandLine) ParseInputOutputNames() { + p.sourceAndOutputMapsOnce.Do(func() { + sourceToOutput := map[tspath.Path]*OutputDtsAndProjectReference{} + outputDtsToSource := map[tspath.Path]*SourceAndProjectReference{} + + for outputDts, source := range p.GetOutputDeclarationFileNames() { + path := tspath.ToPath(source, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) + if outputDts != "" { + outputDtsToSource[tspath.ToPath(outputDts, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames())] = &SourceAndProjectReference{ + Source: source, + Resolved: p, + } + } + sourceToOutput[path] = &OutputDtsAndProjectReference{ + OutputDts: outputDts, + Resolved: p, + } + } + p.outputDtsToSource = outputDtsToSource + p.sourceToOutput = sourceToOutput + }) +} + +func (p *ParsedCommandLine) CommonSourceDirectory() string { + p.commonSourceDirectoryOnce.Do(func() { + p.commonSourceDirectory = outputpaths.GetCommonSourceDirectory( + p.ParsedConfig.CompilerOptions, + func() []string { + return core.Filter( + p.ParsedConfig.FileNames, + func(file string) bool { + return !(p.ParsedConfig.CompilerOptions.NoEmitForJsFiles.IsTrue() && tspath.HasJSFileExtension(file)) && + !tspath.IsDeclarationFileName(file) + }) + }, + p.GetCurrentDirectory(), + p.UseCaseSensitiveFileNames(), + ) + }) + return p.commonSourceDirectory +} + +func (p *ParsedCommandLine) GetCurrentDirectory() string { + return p.comparePathsOptions.CurrentDirectory +} + +func (p *ParsedCommandLine) UseCaseSensitiveFileNames() bool { + return p.comparePathsOptions.UseCaseSensitiveFileNames +} + +func (p *ParsedCommandLine) GetOutputDeclarationFileNames() iter.Seq2[string, string] { + return func(yield func(dtsName string, inputName string) bool) { + for _, fileName := range p.ParsedConfig.FileNames { + if tspath.IsDeclarationFileName(fileName) { + continue + } + var outputDts string + if !tspath.FileExtensionIs(fileName, tspath.ExtensionJson) { + outputDts = outputpaths.GetOutputDeclarationFileNameWorker(fileName, p.CompilerOptions(), p) + } + if !yield(outputDts, fileName) { + return + } + } + } } // WildcardDirectories returns the cached wildcard directories, initializing them if needed func (p *ParsedCommandLine) WildcardDirectories() map[string]bool { + if p == nil { + return nil + } + if p.wildcardDirectories != nil { return p.wildcardDirectories } @@ -59,6 +170,9 @@ func (p *ParsedCommandLine) SetCompilerOptions(o *core.CompilerOptions) { } func (p *ParsedCommandLine) CompilerOptions() *core.CompilerOptions { + if p == nil { + return nil + } return p.ParsedConfig.CompilerOptions } @@ -75,10 +189,17 @@ func (p *ParsedCommandLine) FileNames() []string { return p.ParsedConfig.FileNames } -func (p *ParsedCommandLine) ProjectReferences() []core.ProjectReference { +func (p *ParsedCommandLine) ProjectReferences() []*core.ProjectReference { return p.ParsedConfig.ProjectReferences } +func (p *ParsedCommandLine) ExtendedSourceFiles() []string { + if p == nil || p.ConfigFile == nil { + return nil + } + return p.ConfigFile.ExtendedSourceFiles +} + func (p *ParsedCommandLine) GetConfigFileParsingDiagnostics() []*ast.Diagnostic { if p.ConfigFile != nil { // todo: !!! should be ConfigFile.ParseDiagnostics, check if they are the same @@ -89,9 +210,9 @@ func (p *ParsedCommandLine) GetConfigFileParsingDiagnostics() []*ast.Diagnostic // Porting reference: ProjectService.isMatchedByConfig func (p *ParsedCommandLine) MatchesFileName(fileName string) bool { - path := tspath.ToPath(fileName, p.comparePathsOptions.CurrentDirectory, p.comparePathsOptions.UseCaseSensitiveFileNames) + path := tspath.ToPath(fileName, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) if slices.ContainsFunc(p.FileNames(), func(f string) bool { - return path == tspath.ToPath(f, p.comparePathsOptions.CurrentDirectory, p.comparePathsOptions.UseCaseSensitiveFileNames) + return path == tspath.ToPath(f, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) }) { return true } @@ -119,7 +240,7 @@ func (p *ParsedCommandLine) MatchesFileName(fileName string) bool { var allFileNames collections.Set[tspath.Path] for _, fileName := range p.FileNames() { - allFileNames.Add(tspath.ToPath(fileName, p.comparePathsOptions.CurrentDirectory, p.comparePathsOptions.UseCaseSensitiveFileNames)) + allFileNames.Add(tspath.ToPath(fileName, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames())) } if hasFileWithHigherPriorityExtension(string(path), supportedExtensions, func(fileName string) bool { @@ -135,7 +256,7 @@ func ReloadFileNamesOfParsedCommandLine(p *ParsedCommandLine, fs vfs.FS) *Parsed parsedConfig := *p.ParsedConfig parsedConfig.FileNames = getFileNamesFromConfigSpecs( *p.ConfigFile.configFileSpecs, - p.comparePathsOptions.CurrentDirectory, + p.GetCurrentDirectory(), p.CompilerOptions(), fs, p.extraFileExtensions, diff --git a/internal/tsoptions/parsinghelpers.go b/internal/tsoptions/parsinghelpers.go index f63cf68a7d..e13280b1a5 100644 --- a/internal/tsoptions/parsinghelpers.go +++ b/internal/tsoptions/parsinghelpers.go @@ -65,20 +65,17 @@ func parseNumber(value any) *int { return nil } -func parseProjectReference(json any) []core.ProjectReference { - var result []core.ProjectReference +func parseProjectReference(json any) []*core.ProjectReference { + var result []*core.ProjectReference if v, ok := json.(*collections.OrderedMap[string, any]); ok { var reference core.ProjectReference if v, ok := v.Get("path"); ok { reference.Path = v.(string) } - if v, ok := v.Get("originalPath"); ok { - reference.OriginalPath = v.(string) - } if v, ok := v.Get("circular"); ok { reference.Circular = v.(bool) } - result = append(result, reference) + result = append(result, &reference) } return result } diff --git a/internal/tsoptions/tsconfigparsing.go b/internal/tsoptions/tsconfigparsing.go index 879d4e2d75..9cb90e150f 100644 --- a/internal/tsoptions/tsconfigparsing.go +++ b/internal/tsoptions/tsconfigparsing.go @@ -239,7 +239,7 @@ func parseOwnConfigOfJsonSourceFile( } type TsConfigSourceFile struct { - extendedSourceFiles []string + ExtendedSourceFiles []string configFileSpecs *configFileSpecs SourceFile *ast.SourceFile } @@ -528,7 +528,7 @@ func getExtendsConfigPath( type tsConfigOptions struct { prop map[string][]string - references []core.ProjectReference + references []*core.ProjectReference notDefined string } @@ -643,7 +643,16 @@ type resolverHost struct { func (r *resolverHost) Trace(msg string) {} -func ParseJsonSourceFileConfigFileContent(sourceFile *TsConfigSourceFile, host ParseConfigHost, basePath string, existingOptions *core.CompilerOptions, configFileName string, resolutionStack []tspath.Path, extraFileExtensions []FileExtensionInfo, extendedConfigCache map[tspath.Path]*ExtendedConfigCacheEntry) *ParsedCommandLine { +func ParseJsonSourceFileConfigFileContent( + sourceFile *TsConfigSourceFile, + host ParseConfigHost, + basePath string, + existingOptions *core.CompilerOptions, + configFileName string, + resolutionStack []tspath.Path, + extraFileExtensions []FileExtensionInfo, + extendedConfigCache *collections.SyncMap[tspath.Path, *ExtendedConfigCacheEntry], +) *ParsedCommandLine { // tracing?.push(tracing.Phase.Parse, "parseJsonSourceFileConfigFileContent", { path: sourceFile.fileName }); result := parseJsonConfigFileContentWorker(nil /*json*/, sourceFile, host, basePath, existingOptions, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache) // tracing?.pop(); @@ -782,7 +791,7 @@ func convertPropertyValueToJson(sourceFile *ast.SourceFile, valueExpression *ast // jsonNode: The contents of the config file to parse // host: Instance of ParseConfigHost used to enumerate files in folder. // basePath: A root directory to resolve relative path entries in the config file to. e.g. outDir -func ParseJsonConfigFileContent(json any, host ParseConfigHost, basePath string, existingOptions *core.CompilerOptions, configFileName string, resolutionStack []tspath.Path, extraFileExtensions []FileExtensionInfo, extendedConfigCache map[tspath.Path]*ExtendedConfigCacheEntry) *ParsedCommandLine { +func ParseJsonConfigFileContent(json any, host ParseConfigHost, basePath string, existingOptions *core.CompilerOptions, configFileName string, resolutionStack []tspath.Path, extraFileExtensions []FileExtensionInfo, extendedConfigCache *collections.SyncMap[tspath.Path, *ExtendedConfigCacheEntry]) *ParsedCommandLine { result := parseJsonConfigFileContentWorker(parseJsonToStringKey(json), nil /*sourceFile*/, host, basePath, existingOptions, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache) return result } @@ -883,18 +892,23 @@ func getExtendedConfig( extendedConfigPath string, host ParseConfigHost, resolutionStack []string, - extendedConfigCache map[tspath.Path]*ExtendedConfigCacheEntry, + extendedConfigCache *collections.SyncMap[tspath.Path, *ExtendedConfigCacheEntry], result *extendsResult, ) (*parsedTsconfig, []*ast.Diagnostic) { path := tspath.ToPath(extendedConfigPath, host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames()) var extendedResult *TsConfigSourceFile var extendedConfig *parsedTsconfig var errors []*ast.Diagnostic - value := extendedConfigCache[path] - if extendedConfigCache != nil && value != nil { - extendedResult = value.extendedResult - extendedConfig = value.extendedConfig - } else { + var cacheEntry *ExtendedConfigCacheEntry + if extendedConfigCache != nil { + entry, ok := extendedConfigCache.Load(path) + if ok && entry != nil { + cacheEntry = entry + extendedResult = cacheEntry.extendedResult + extendedConfig = cacheEntry.extendedConfig + } + } + if cacheEntry == nil { var err []*ast.Diagnostic extendedResult, err = readJsonConfigFile(extendedConfigPath, path, host.FS().ReadFile) errors = append(errors, err...) @@ -903,18 +917,21 @@ func getExtendedConfig( errors = append(errors, err...) } if extendedConfigCache != nil { - extendedConfigCache[path] = &ExtendedConfigCacheEntry{ + entry, loaded := extendedConfigCache.LoadOrStore(path, &ExtendedConfigCacheEntry{ extendedResult: extendedResult, extendedConfig: extendedConfig, + }) + if loaded { + // If we loaded an entry, we can use the cached result + extendedResult = entry.extendedResult + extendedConfig = entry.extendedConfig } } } if sourceFile != nil { result.extendedSourceFiles.Add(extendedResult.SourceFile.FileName()) - if len(extendedResult.extendedSourceFiles) != 0 { - for _, extenedSourceFile := range extendedResult.extendedSourceFiles { - result.extendedSourceFiles.Add(extenedSourceFile) - } + for _, extendedSourceFile := range extendedResult.ExtendedSourceFiles { + result.extendedSourceFiles.Add(extendedSourceFile) } } if len(extendedResult.SourceFile.Diagnostics()) != 0 { @@ -933,7 +950,7 @@ func parseConfig( basePath string, configFileName string, resolutionStack []string, - extendedConfigCache map[tspath.Path]*ExtendedConfigCacheEntry, + extendedConfigCache *collections.SyncMap[tspath.Path, *ExtendedConfigCacheEntry], ) (*parsedTsconfig, []*ast.Diagnostic) { basePath = tspath.NormalizeSlashes(basePath) resolvedPath := tspath.GetNormalizedAbsolutePath(configFileName, basePath) @@ -1045,7 +1062,7 @@ func parseConfig( } if sourceFile != nil { for extendedSourceFile := range result.extendedSourceFiles.Keys() { - sourceFile.extendedSourceFiles = append(sourceFile.extendedSourceFiles, extendedSourceFile) + sourceFile.ExtendedSourceFiles = append(sourceFile.ExtendedSourceFiles, extendedSourceFile) } } ownConfig.options = mergeCompilerOptions(result.options, ownConfig.options) @@ -1078,7 +1095,7 @@ func parseJsonConfigFileContentWorker( configFileName string, resolutionStack []tspath.Path, extraFileExtensions []FileExtensionInfo, - extendedConfigCache map[tspath.Path]*ExtendedConfigCacheEntry, + extendedConfigCache *collections.SyncMap[tspath.Path, *ExtendedConfigCacheEntry], ) *ParsedCommandLine { // Debug.assert((json === undefined && sourceFile !== undefined) || (json !== undefined && sourceFile === undefined)); @@ -1219,8 +1236,8 @@ func parseJsonConfigFileContentWorker( return fileNames } - getProjectReferences := func(basePath string) []core.ProjectReference { - var projectReferences []core.ProjectReference = []core.ProjectReference{} + getProjectReferences := func(basePath string) []*core.ProjectReference { + var projectReferences []*core.ProjectReference = []*core.ProjectReference{} newReferencesOfRaw := getPropFromRaw("references", func(element any) bool { return reflect.TypeOf(element) == orderedMapType }, "object") if newReferencesOfRaw.sliceValue != nil { for _, reference := range newReferencesOfRaw.sliceValue { @@ -1230,7 +1247,7 @@ func parseJsonConfigFileContentWorker( errors = append(errors, ast.NewCompilerDiagnostic(diagnostics.Compiler_option_0_requires_a_value_of_type_1, "reference.path", "string")) } } else { - projectReferences = append(projectReferences, core.ProjectReference{ + projectReferences = append(projectReferences, &core.ProjectReference{ Path: tspath.GetNormalizedAbsolutePath(ref.Path, basePath), OriginalPath: ref.Path, Circular: ref.Circular, @@ -1635,7 +1652,23 @@ func GetSupportedExtensionsWithJsonIfResolveJsonModule(compilerOptions *core.Com } // Reads the config file and reports errors. -func GetParsedCommandLineOfConfigFile(configFileName string, options *core.CompilerOptions, sys ParseConfigHost, extendedConfigCache map[tspath.Path]*ExtendedConfigCacheEntry) (*ParsedCommandLine, []*ast.Diagnostic) { +func GetParsedCommandLineOfConfigFile( + configFileName string, + options *core.CompilerOptions, + sys ParseConfigHost, + extendedConfigCache *collections.SyncMap[tspath.Path, *ExtendedConfigCacheEntry], +) (*ParsedCommandLine, []*ast.Diagnostic) { + configFileName = tspath.GetNormalizedAbsolutePath(configFileName, sys.GetCurrentDirectory()) + return GetParsedCommandLineOfConfigFilePath(configFileName, tspath.ToPath(configFileName, sys.GetCurrentDirectory(), sys.FS().UseCaseSensitiveFileNames()), options, sys, extendedConfigCache) +} + +func GetParsedCommandLineOfConfigFilePath( + configFileName string, + path tspath.Path, + options *core.CompilerOptions, + sys ParseConfigHost, + extendedConfigCache *collections.SyncMap[tspath.Path, *ExtendedConfigCacheEntry], +) (*ParsedCommandLine, []*ast.Diagnostic) { errors := []*ast.Diagnostic{} configFileText, errors := tryReadFile(configFileName, sys.FS().ReadFile, errors) if len(errors) > 0 { @@ -1643,16 +1676,15 @@ func GetParsedCommandLineOfConfigFile(configFileName string, options *core.Compi return nil, errors } - cwd := sys.GetCurrentDirectory() - tsConfigSourceFile := NewTsconfigSourceFileFromFilePath(configFileName, tspath.ToPath(configFileName, cwd, sys.FS().UseCaseSensitiveFileNames()), configFileText) + tsConfigSourceFile := NewTsconfigSourceFileFromFilePath(configFileName, path, configFileText) // tsConfigSourceFile.resolvedPath = tsConfigSourceFile.FileName() // tsConfigSourceFile.originalFileName = tsConfigSourceFile.FileName() return ParseJsonSourceFileConfigFileContent( tsConfigSourceFile, sys, - tspath.GetNormalizedAbsolutePath(tspath.GetDirectoryPath(configFileName), cwd), + tspath.GetDirectoryPath(configFileName), options, - tspath.GetNormalizedAbsolutePath(configFileName, cwd), + configFileName, nil, nil, extendedConfigCache, diff --git a/internal/tspath/ignoredpaths.go b/internal/tspath/ignoredpaths.go new file mode 100644 index 0000000000..78f39577e9 --- /dev/null +++ b/internal/tspath/ignoredpaths.go @@ -0,0 +1,14 @@ +package tspath + +import "strings" + +var ignoredPaths = []string{"/node_modules/.", "/.git", "/.#"} + +func ContainsIgnoredPath(path string) bool { + for _, p := range ignoredPaths { + if strings.Contains(path, p) { + return true + } + } + return false +} diff --git a/testdata/baselines/reference/submodule/compiler/isolatedModulesAmbientConstEnum.errors.txt b/testdata/baselines/reference/submodule/compiler/isolatedModulesAmbientConstEnum.errors.txt new file mode 100644 index 0000000000..75d30e7f17 --- /dev/null +++ b/testdata/baselines/reference/submodule/compiler/isolatedModulesAmbientConstEnum.errors.txt @@ -0,0 +1,9 @@ +file1.ts(2,16): error TS2748: Cannot access ambient const enums when 'isolatedModules' is enabled. + + +==== file1.ts (1 errors) ==== + declare const enum E { X = 1} + export var y = E.X; + ~ +!!! error TS2748: Cannot access ambient const enums when 'isolatedModules' is enabled. + \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/compiler/isolatedModulesAmbientConstEnum.errors.txt.diff b/testdata/baselines/reference/submodule/compiler/isolatedModulesAmbientConstEnum.errors.txt.diff deleted file mode 100644 index 99de075694..0000000000 --- a/testdata/baselines/reference/submodule/compiler/isolatedModulesAmbientConstEnum.errors.txt.diff +++ /dev/null @@ -1,13 +0,0 @@ ---- old.isolatedModulesAmbientConstEnum.errors.txt -+++ new.isolatedModulesAmbientConstEnum.errors.txt -@@= skipped -0, +0 lines =@@ --file1.ts(2,16): error TS2748: Cannot access ambient const enums when 'isolatedModules' is enabled. -- -- --==== file1.ts (1 errors) ==== -- declare const enum E { X = 1} -- export var y = E.X; -- ~ --!!! error TS2748: Cannot access ambient const enums when 'isolatedModules' is enabled. -- -+ \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/verbatimModuleSyntaxAmbientConstEnum.errors.txt b/testdata/baselines/reference/submodule/conformance/verbatimModuleSyntaxAmbientConstEnum.errors.txt new file mode 100644 index 0000000000..e405f0bed9 --- /dev/null +++ b/testdata/baselines/reference/submodule/conformance/verbatimModuleSyntaxAmbientConstEnum.errors.txt @@ -0,0 +1,27 @@ +/a.ts(1,10): error TS2748: Cannot access ambient const enums when 'verbatimModuleSyntax' is enabled. +/a.ts(4,1): error TS2748: Cannot access ambient const enums when 'verbatimModuleSyntax' is enabled. +/b.ts(1,10): error TS2748: Cannot access ambient const enums when 'verbatimModuleSyntax' is enabled. + + +==== /node_modules/pkg/index.d.ts (0 errors) ==== + export declare const enum E { A, B, C } + declare global { + const enum F { A, B, C } + } + +==== /a.ts (2 errors) ==== + import { E } from "pkg"; // Error + ~ +!!! error TS2748: Cannot access ambient const enums when 'verbatimModuleSyntax' is enabled. + import type { E as _E } from "pkg"; // Ok + console.log(E.A); // Ok + F.A; // Error + ~ +!!! error TS2748: Cannot access ambient const enums when 'verbatimModuleSyntax' is enabled. + +==== /b.ts (1 errors) ==== + export { E } from "pkg"; // Error + ~ +!!! error TS2748: Cannot access ambient const enums when 'verbatimModuleSyntax' is enabled. + export type { E as _E } from "pkg"; // Ok + \ No newline at end of file diff --git a/testdata/baselines/reference/submodule/conformance/verbatimModuleSyntaxAmbientConstEnum.errors.txt.diff b/testdata/baselines/reference/submodule/conformance/verbatimModuleSyntaxAmbientConstEnum.errors.txt.diff deleted file mode 100644 index b8ab741d9f..0000000000 --- a/testdata/baselines/reference/submodule/conformance/verbatimModuleSyntaxAmbientConstEnum.errors.txt.diff +++ /dev/null @@ -1,31 +0,0 @@ ---- old.verbatimModuleSyntaxAmbientConstEnum.errors.txt -+++ new.verbatimModuleSyntaxAmbientConstEnum.errors.txt -@@= skipped -0, +0 lines =@@ --/a.ts(1,10): error TS2748: Cannot access ambient const enums when 'verbatimModuleSyntax' is enabled. --/a.ts(4,1): error TS2748: Cannot access ambient const enums when 'verbatimModuleSyntax' is enabled. --/b.ts(1,10): error TS2748: Cannot access ambient const enums when 'verbatimModuleSyntax' is enabled. -- -- --==== /node_modules/pkg/index.d.ts (0 errors) ==== -- export declare const enum E { A, B, C } -- declare global { -- const enum F { A, B, C } -- } -- --==== /a.ts (2 errors) ==== -- import { E } from "pkg"; // Error -- ~ --!!! error TS2748: Cannot access ambient const enums when 'verbatimModuleSyntax' is enabled. -- import type { E as _E } from "pkg"; // Ok -- console.log(E.A); // Ok -- F.A; // Error -- ~ --!!! error TS2748: Cannot access ambient const enums when 'verbatimModuleSyntax' is enabled. -- --==== /b.ts (1 errors) ==== -- export { E } from "pkg"; // Error -- ~ --!!! error TS2748: Cannot access ambient const enums when 'verbatimModuleSyntax' is enabled. -- export type { E as _E } from "pkg"; // Ok -- -+ \ No newline at end of file diff --git a/testdata/baselines/reference/tsc/projectReferences/default-import-interop-uses-referenced-project-settings.js b/testdata/baselines/reference/tsc/projectReferences/default-import-interop-uses-referenced-project-settings.js new file mode 100644 index 0000000000..48869ca558 --- /dev/null +++ b/testdata/baselines/reference/tsc/projectReferences/default-import-interop-uses-referenced-project-settings.js @@ -0,0 +1,78 @@ + +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input::--p app --pretty false +//// [/home/src/workspaces/project/app/src/index.ts] new file + + import local from "./local"; // Error + import esm from "esm-package"; // Error + import referencedSource from "../../lib/src/a"; // Error + import referencedDeclaration from "../../lib/dist/a"; // Error + import ambiguous from "ambiguous-package"; // Ok +//// [/home/src/workspaces/project/app/src/local.ts] new file +export const local = 0; +//// [/home/src/workspaces/project/app/tsconfig.json] new file +{ + "compilerOptions": { + "module": "esnext", + "moduleResolution": "bundler", + "rootDir": "src", + "outDir": "dist", + }, + "include": ["src"], + "references": [ + { "path": "../lib" }, + ], + } +//// [/home/src/workspaces/project/lib/dist/a.d.ts] new file +export declare const a = 0; +//// [/home/src/workspaces/project/lib/src/a.ts] new file +export const a = 0; +//// [/home/src/workspaces/project/lib/tsconfig.json] new file +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "rootDir": "src", + "outDir": "dist", + "module": "esnext", + "moduleResolution": "bundler", + }, + "include": ["src"], + } +//// [/home/src/workspaces/project/node_modules/ambiguous-package/index.d.ts] new file +export declare const ambiguous: number; +//// [/home/src/workspaces/project/node_modules/ambiguous-package/package.json] new file +{ "name": "ambiguous-package" } +//// [/home/src/workspaces/project/node_modules/esm-package/index.d.ts] new file +export declare const esm: number; +//// [/home/src/workspaces/project/node_modules/esm-package/package.json] new file +{ "name": "esm-package", "type": "module" } + +ExitStatus:: 2 + +CompilerOptions::{ + "project": "/home/src/workspaces/project/app", + "pretty": false +} +Output:: +app/src/index.ts(2,13): error TS2613: Module '"/home/src/workspaces/project/app/src/local"' has no default export. Did you mean to use 'import { local } from "/home/src/workspaces/project/app/src/local"' instead? + +app/src/index.ts(3,13): error TS2613: Module '"/home/src/workspaces/project/node_modules/esm-package/index"' has no default export. Did you mean to use 'import { esm } from "/home/src/workspaces/project/node_modules/esm-package/index"' instead? +//// [/home/src/workspaces/project/app/dist/index.js] new file +export {}; + +//// [/home/src/workspaces/project/app/dist/local.js] new file +export const local = 0; + +//// [/home/src/workspaces/project/app/src/index.ts] no change +//// [/home/src/workspaces/project/app/src/local.ts] no change +//// [/home/src/workspaces/project/app/tsconfig.json] no change +//// [/home/src/workspaces/project/lib/dist/a.d.ts] no change +//// [/home/src/workspaces/project/lib/src/a.ts] no change +//// [/home/src/workspaces/project/lib/tsconfig.json] no change +//// [/home/src/workspaces/project/node_modules/ambiguous-package/index.d.ts] no change +//// [/home/src/workspaces/project/node_modules/ambiguous-package/package.json] no change +//// [/home/src/workspaces/project/node_modules/esm-package/index.d.ts] no change +//// [/home/src/workspaces/project/node_modules/esm-package/package.json] no change + diff --git a/testdata/baselines/reference/tsc/projectReferences/importing-const-enum-from-referenced-project-with-preserveConstEnums-and-verbatimModuleSyntax.js b/testdata/baselines/reference/tsc/projectReferences/importing-const-enum-from-referenced-project-with-preserveConstEnums-and-verbatimModuleSyntax.js new file mode 100644 index 0000000000..7086c37045 --- /dev/null +++ b/testdata/baselines/reference/tsc/projectReferences/importing-const-enum-from-referenced-project-with-preserveConstEnums-and-verbatimModuleSyntax.js @@ -0,0 +1,69 @@ + +currentDirectory::/home/src/workspaces/solution +useCaseSensitiveFileNames::true +Input::--p project --pretty false +//// [/home/src/workspaces/solution/no-preserve/index.d.ts] new file +export declare const enum F { A = 1 } +//// [/home/src/workspaces/solution/no-preserve/index.ts] new file +export const enum E { A = 1 } +//// [/home/src/workspaces/solution/no-preserve/tsconfig.json] new file +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "preserveConstEnums": false, + }, + } +//// [/home/src/workspaces/solution/preserve/index.d.ts] new file +export declare const enum E { A = 1 } +//// [/home/src/workspaces/solution/preserve/index.ts] new file +export const enum E { A = 1 } +//// [/home/src/workspaces/solution/preserve/tsconfig.json] new file +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "preserveConstEnums": true, + }, + } +//// [/home/src/workspaces/solution/project/index.ts] new file + + import { E } from "../preserve"; + import { F } from "../no-preserve"; + E.A; + F.A; +//// [/home/src/workspaces/solution/project/tsconfig.json] new file +{ + "compilerOptions": { + "module": "preserve", + "verbatimModuleSyntax": true, + }, + "references": [ + { "path": "../preserve" }, + { "path": "../no-preserve" }, + ], + } + +ExitStatus:: 2 + +CompilerOptions::{ + "project": "/home/src/workspaces/solution/project", + "pretty": false +} +Output:: +project/index.ts(3,14): error TS2748: Cannot access ambient const enums when 'verbatimModuleSyntax' is enabled. +//// [/home/src/workspaces/solution/no-preserve/index.d.ts] no change +//// [/home/src/workspaces/solution/no-preserve/index.ts] no change +//// [/home/src/workspaces/solution/no-preserve/tsconfig.json] no change +//// [/home/src/workspaces/solution/preserve/index.d.ts] no change +//// [/home/src/workspaces/solution/preserve/index.ts] no change +//// [/home/src/workspaces/solution/preserve/tsconfig.json] no change +//// [/home/src/workspaces/solution/project/index.js] new file +import { E } from "../preserve"; +import { F } from "../no-preserve"; +E.A; +F.A; + +//// [/home/src/workspaces/solution/project/index.ts] no change +//// [/home/src/workspaces/solution/project/tsconfig.json] no change + diff --git a/testdata/baselines/reference/tsc/projectReferences/referencing-ambient-const-enum-from-referenced-project-with-preserveConstEnums.js b/testdata/baselines/reference/tsc/projectReferences/referencing-ambient-const-enum-from-referenced-project-with-preserveConstEnums.js new file mode 100644 index 0000000000..bd7c02a633 --- /dev/null +++ b/testdata/baselines/reference/tsc/projectReferences/referencing-ambient-const-enum-from-referenced-project-with-preserveConstEnums.js @@ -0,0 +1,46 @@ + +currentDirectory::/home/src/workspaces/solution +useCaseSensitiveFileNames::true +Input::--p project +//// [/home/src/workspaces/solution/project/index.ts] new file +import { E } from "../utils"; E.A; +//// [/home/src/workspaces/solution/project/tsconfig.json] new file +{ + "compilerOptions": { + "isolatedModules": true, + }, + "references": [ + { "path": "../utils" }, + ], + } +//// [/home/src/workspaces/solution/utils/index.d.ts] new file +export declare const enum E { A = 1 } +//// [/home/src/workspaces/solution/utils/index.ts] new file +export const enum E { A = 1 } +//// [/home/src/workspaces/solution/utils/tsconfig.json] new file +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "preserveConstEnums": true, + }, + } + +ExitStatus:: 0 + +CompilerOptions::{ + "project": "/home/src/workspaces/solution/project" +} +Output:: +//// [/home/src/workspaces/solution/project/index.js] new file +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../utils"); +utils_1.E.A; + +//// [/home/src/workspaces/solution/project/index.ts] no change +//// [/home/src/workspaces/solution/project/tsconfig.json] no change +//// [/home/src/workspaces/solution/utils/index.d.ts] no change +//// [/home/src/workspaces/solution/utils/index.ts] no change +//// [/home/src/workspaces/solution/utils/tsconfig.json] no change + diff --git a/testdata/baselines/reference/tsc/projectReferences/when-project-contains-invalid-project-reference.js b/testdata/baselines/reference/tsc/projectReferences/when-project-contains-invalid-project-reference.js new file mode 100644 index 0000000000..6e080dcb55 --- /dev/null +++ b/testdata/baselines/reference/tsc/projectReferences/when-project-contains-invalid-project-reference.js @@ -0,0 +1,28 @@ + +currentDirectory::/home/src/workspaces/solution +useCaseSensitiveFileNames::true +Input::--p project +//// [/home/src/workspaces/solution/project/index.ts] new file +export const x = 10; +//// [/home/src/workspaces/solution/project/tsconfig.json] new file +{ + "references": [ + { "path": "../utils" }, + ], +} + +ExitStatus:: 0 + +CompilerOptions::{ + "project": "/home/src/workspaces/solution/project" +} +Output:: +//// [/home/src/workspaces/solution/project/index.js] new file +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.x = void 0; +exports.x = 10; + +//// [/home/src/workspaces/solution/project/index.ts] no change +//// [/home/src/workspaces/solution/project/tsconfig.json] no change + diff --git a/testdata/baselines/reference/tsc/projectReferences/when-project-reference-is-not-built.js b/testdata/baselines/reference/tsc/projectReferences/when-project-reference-is-not-built.js new file mode 100644 index 0000000000..193090cc44 --- /dev/null +++ b/testdata/baselines/reference/tsc/projectReferences/when-project-reference-is-not-built.js @@ -0,0 +1,44 @@ + +currentDirectory::/home/src/workspaces/solution +useCaseSensitiveFileNames::true +Input::--p project +//// [/home/src/workspaces/solution/project/index.ts] new file +import { x } from "../utils"; +//// [/home/src/workspaces/solution/project/tsconfig.json] new file +{ + "references": [ + { "path": "../utils" }, + ], +} +//// [/home/src/workspaces/solution/utils/index.ts] new file +export const x = 10; +//// [/home/src/workspaces/solution/utils/tsconfig.json] new file +{ + "compilerOptions": { + "composite": true, + }, +} + +ExitStatus:: 2 + +CompilerOptions::{ + "project": "/home/src/workspaces/solution/project" +} +Output:: +project/index.ts:1:19 - error TS6305: Output file '/home/src/workspaces/solution/utils/index.d.ts' has not been built from source file '/home/src/workspaces/solution/utils/index.ts'. + +1 import { x } from "../utils"; +   ~~~~~~~~~~ + + +Found 1 error in project/index.ts:1 + +//// [/home/src/workspaces/solution/project/index.js] new file +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); + +//// [/home/src/workspaces/solution/project/index.ts] no change +//// [/home/src/workspaces/solution/project/tsconfig.json] no change +//// [/home/src/workspaces/solution/utils/index.ts] no change +//// [/home/src/workspaces/solution/utils/tsconfig.json] no change + diff --git a/testdata/baselines/reference/tsc/projectReferences/when-project-references-composite-project-with-noEmit.js b/testdata/baselines/reference/tsc/projectReferences/when-project-references-composite-project-with-noEmit.js index 015789404f..efb89c0c40 100644 --- a/testdata/baselines/reference/tsc/projectReferences/when-project-references-composite-project-with-noEmit.js +++ b/testdata/baselines/reference/tsc/projectReferences/when-project-references-composite-project-with-noEmit.js @@ -6,19 +6,19 @@ Input::--p project import { x } from "../utils"; //// [/home/src/workspaces/solution/project/tsconfig.json] new file { - "references": [ - { "path": "../utils" }, - ], -} -//// [/home/src/workspaces/solution/src/utils/index.ts] new file + "references": [ + { "path": "../utils" }, + ], + } +//// [/home/src/workspaces/solution/utils/index.ts] new file export const x = 10; -//// [/home/src/workspaces/solution/src/utils/tsconfig.json] new file +//// [/home/src/workspaces/solution/utils/tsconfig.json] new file { - "compilerOptions": { - "composite": true, - "noEmit": true, - }, -} + "compilerOptions": { + "composite": true, + "noEmit": true, + }, + } ExitStatus:: 2 @@ -26,7 +26,7 @@ CompilerOptions::{ "project": "/home/src/workspaces/solution/project" } Output:: -project/index.ts:1:19 - error TS2307: Cannot find module '../utils' or its corresponding type declarations. +project/index.ts:1:19 - error TS6305: Output file '/home/src/workspaces/solution/utils/index.d.ts' has not been built from source file '/home/src/workspaces/solution/utils/index.ts'. 1 import { x } from "../utils";    ~~~~~~~~~~ @@ -40,6 +40,6 @@ Object.defineProperty(exports, "__esModule", { value: true }); //// [/home/src/workspaces/solution/project/index.ts] no change //// [/home/src/workspaces/solution/project/tsconfig.json] no change -//// [/home/src/workspaces/solution/src/utils/index.ts] no change -//// [/home/src/workspaces/solution/src/utils/tsconfig.json] no change +//// [/home/src/workspaces/solution/utils/index.ts] no change +//// [/home/src/workspaces/solution/utils/tsconfig.json] no change diff --git a/testdata/baselines/reference/tsc/projectReferences/when-project-references-composite.js b/testdata/baselines/reference/tsc/projectReferences/when-project-references-composite.js new file mode 100644 index 0000000000..d0d15de884 --- /dev/null +++ b/testdata/baselines/reference/tsc/projectReferences/when-project-references-composite.js @@ -0,0 +1,39 @@ + +currentDirectory::/home/src/workspaces/solution +useCaseSensitiveFileNames::true +Input::--p project +//// [/home/src/workspaces/solution/project/index.ts] new file +import { x } from "../utils"; +//// [/home/src/workspaces/solution/project/tsconfig.json] new file +{ + "references": [ + { "path": "../utils" }, + ], +} +//// [/home/src/workspaces/solution/utils/index.d.ts] new file +export declare const x = 10; +//// [/home/src/workspaces/solution/utils/index.ts] new file +export const x = 10; +//// [/home/src/workspaces/solution/utils/tsconfig.json] new file +{ + "compilerOptions": { + "composite": true, + }, +} + +ExitStatus:: 0 + +CompilerOptions::{ + "project": "/home/src/workspaces/solution/project" +} +Output:: +//// [/home/src/workspaces/solution/project/index.js] new file +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); + +//// [/home/src/workspaces/solution/project/index.ts] no change +//// [/home/src/workspaces/solution/project/tsconfig.json] no change +//// [/home/src/workspaces/solution/utils/index.d.ts] no change +//// [/home/src/workspaces/solution/utils/index.ts] no change +//// [/home/src/workspaces/solution/utils/tsconfig.json] no change +