kittenipc/kitcom/internal/tsgo/tsoptions/parsedcommandline.go
2025-10-15 10:12:44 +03:00

382 lines
12 KiB
Go

package tsoptions
import (
"fmt"
"iter"
"slices"
"strings"
"sync"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/glob"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/module"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/outputpaths"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs"
)
const (
fileGlobPattern = "*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,json}"
recursiveFileGlobPattern = "**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,json}"
)
type ParsedCommandLine struct {
ParsedConfig *core.ParsedOptions `json:"parsedConfig"`
ConfigFile *TsConfigSourceFile `json:"configFile"` // TsConfigSourceFile, used in Program and ExecuteCommandLine
Errors []*ast.Diagnostic `json:"errors"`
Raw any `json:"raw"`
CompileOnSave *bool `json:"compileOnSave"`
comparePathsOptions tspath.ComparePathsOptions
wildcardDirectoriesOnce sync.Once
wildcardDirectories map[string]bool
includeGlobsOnce sync.Once
includeGlobs []*glob.Glob
extraFileExtensions []FileExtensionInfo
sourceAndOutputMapsOnce sync.Once
sourceToProjectReference map[tspath.Path]*SourceOutputAndProjectReference
outputDtsToProjectReference map[tspath.Path]*SourceOutputAndProjectReference
commonSourceDirectory string
commonSourceDirectoryOnce sync.Once
resolvedProjectReferencePaths []string
resolvedProjectReferencePathsOnce sync.Once
literalFileNamesLen int
fileNamesByPath map[tspath.Path]string // maps file names to their paths, used for quick lookups
fileNamesByPathOnce sync.Once
}
func NewParsedCommandLine(
compilerOptions *core.CompilerOptions,
rootFileNames []string,
comparePathsOptions tspath.ComparePathsOptions,
) *ParsedCommandLine {
return &ParsedCommandLine{
ParsedConfig: &core.ParsedOptions{
CompilerOptions: compilerOptions,
FileNames: rootFileNames,
},
comparePathsOptions: comparePathsOptions,
}
}
type SourceOutputAndProjectReference struct {
Source string
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) SourceToProjectReference() map[tspath.Path]*SourceOutputAndProjectReference {
return p.sourceToProjectReference
}
func (p *ParsedCommandLine) OutputDtsToProjectReference() map[tspath.Path]*SourceOutputAndProjectReference {
return p.outputDtsToProjectReference
}
func (p *ParsedCommandLine) ParseInputOutputNames() {
p.sourceAndOutputMapsOnce.Do(func() {
sourceToOutput := map[tspath.Path]*SourceOutputAndProjectReference{}
outputDtsToSource := map[tspath.Path]*SourceOutputAndProjectReference{}
for outputDts, source := range p.GetOutputDeclarationAndSourceFileNames() {
path := tspath.ToPath(source, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames())
projectReference := &SourceOutputAndProjectReference{
Source: source,
OutputDts: outputDts,
Resolved: p,
}
if outputDts != "" {
outputDtsToSource[tspath.ToPath(outputDts, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames())] = projectReference
}
sourceToOutput[path] = projectReference
}
p.outputDtsToProjectReference = outputDtsToSource
p.sourceToProjectReference = 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) GetOutputDeclarationAndSourceFileNames() iter.Seq2[string, string] {
return func(yield func(dtsName string, inputName string) bool) {
for _, fileName := range p.ParsedConfig.FileNames {
var outputDts string
if !tspath.IsDeclarationFileName(fileName) && !tspath.FileExtensionIs(fileName, tspath.ExtensionJson) {
outputDts = outputpaths.GetOutputDeclarationFileNameWorker(fileName, p.CompilerOptions(), p)
}
if !yield(outputDts, fileName) {
return
}
}
}
}
func (p *ParsedCommandLine) GetOutputFileNames() iter.Seq[string] {
return func(yield func(outputName string) bool) {
for _, fileName := range p.ParsedConfig.FileNames {
if tspath.IsDeclarationFileName(fileName) {
continue
}
jsFileName := outputpaths.GetOutputJSFileName(fileName, p.CompilerOptions(), p)
isJson := tspath.FileExtensionIs(fileName, tspath.ExtensionJson)
if jsFileName != "" {
if !yield(jsFileName) {
return
}
if !isJson {
sourceMap := outputpaths.GetSourceMapFilePath(jsFileName, p.CompilerOptions())
if sourceMap != "" {
if !yield(sourceMap) {
return
}
}
}
}
if isJson {
continue
}
if p.CompilerOptions().GetEmitDeclarations() {
dtsFileName := outputpaths.GetOutputDeclarationFileNameWorker(fileName, p.CompilerOptions(), p)
if dtsFileName != "" {
if !yield(dtsFileName) {
return
}
if p.CompilerOptions().GetAreDeclarationMapsEnabled() {
declarationMap := dtsFileName + ".map"
if !yield(declarationMap) {
return
}
}
}
}
}
}
}
func (p *ParsedCommandLine) GetBuildInfoFileName() string {
return outputpaths.GetBuildInfoFileName(p.CompilerOptions(), p.comparePathsOptions)
}
// WildcardDirectories returns the cached wildcard directories, initializing them if needed
func (p *ParsedCommandLine) WildcardDirectories() map[string]bool {
if p == nil {
return nil
}
p.wildcardDirectoriesOnce.Do(func() {
if p.wildcardDirectories == nil {
p.wildcardDirectories = getWildcardDirectories(
p.ConfigFile.configFileSpecs.validatedIncludeSpecs,
p.ConfigFile.configFileSpecs.validatedExcludeSpecs,
p.comparePathsOptions,
)
}
})
return p.wildcardDirectories
}
func (p *ParsedCommandLine) WildcardDirectoryGlobs() []*glob.Glob {
wildcardDirectories := p.WildcardDirectories()
if wildcardDirectories == nil {
return nil
}
p.includeGlobsOnce.Do(func() {
if p.includeGlobs == nil {
globs := make([]*glob.Glob, 0, len(wildcardDirectories))
for dir, recursive := range wildcardDirectories {
if parsed, err := glob.Parse(fmt.Sprintf("%s/%s", tspath.NormalizePath(dir), core.IfElse(recursive, recursiveFileGlobPattern, fileGlobPattern))); err == nil {
globs = append(globs, parsed)
}
}
p.includeGlobs = globs
}
})
return p.includeGlobs
}
// Normalized file names explicitly specified in `files`
func (p *ParsedCommandLine) LiteralFileNames() []string {
if p != nil && p.ConfigFile != nil {
return p.FileNames()[0:p.literalFileNamesLen]
}
return nil
}
func (p *ParsedCommandLine) SetParsedOptions(o *core.ParsedOptions) {
p.ParsedConfig = o
}
func (p *ParsedCommandLine) SetCompilerOptions(o *core.CompilerOptions) {
p.ParsedConfig.CompilerOptions = o
}
func (p *ParsedCommandLine) CompilerOptions() *core.CompilerOptions {
if p == nil {
return nil
}
return p.ParsedConfig.CompilerOptions
}
func (p *ParsedCommandLine) SetTypeAcquisition(o *core.TypeAcquisition) {
p.ParsedConfig.TypeAcquisition = o
}
func (p *ParsedCommandLine) TypeAcquisition() *core.TypeAcquisition {
return p.ParsedConfig.TypeAcquisition
}
// All file names matched by files, include, and exclude patterns
func (p *ParsedCommandLine) FileNames() []string {
return p.ParsedConfig.FileNames
}
func (p *ParsedCommandLine) FileNamesByPath() map[tspath.Path]string {
p.fileNamesByPathOnce.Do(func() {
p.fileNamesByPath = make(map[tspath.Path]string, len(p.ParsedConfig.FileNames))
for _, fileName := range p.ParsedConfig.FileNames {
path := tspath.ToPath(fileName, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames())
p.fileNamesByPath[path] = fileName
}
})
return p.fileNamesByPath
}
func (p *ParsedCommandLine) ProjectReferences() []*core.ProjectReference {
return p.ParsedConfig.ProjectReferences
}
func (p *ParsedCommandLine) ResolvedProjectReferencePaths() []string {
p.resolvedProjectReferencePathsOnce.Do(func() {
p.resolvedProjectReferencePaths = core.Map(p.ParsedConfig.ProjectReferences, core.ResolveProjectReferencePath)
})
return p.resolvedProjectReferencePaths
}
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
return slices.Concat(p.ConfigFile.SourceFile.Diagnostics(), p.Errors)
}
return p.Errors
}
// PossiblyMatchesFileName is a fast check to see if a file is currently included by a config
// or would be included if the file were to be created. It may return false positives.
func (p *ParsedCommandLine) PossiblyMatchesFileName(fileName string) bool {
path := tspath.ToPath(fileName, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames())
if _, ok := p.FileNamesByPath()[path]; ok {
return true
}
for _, include := range p.ConfigFile.configFileSpecs.validatedIncludeSpecs {
if !strings.ContainsAny(include, "*?") && !vfs.IsImplicitGlob(include) {
includePath := tspath.ToPath(include, p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames())
if includePath == path {
return true
}
}
}
if wildcardDirectoryGlobs := p.WildcardDirectoryGlobs(); len(wildcardDirectoryGlobs) > 0 {
for _, glob := range wildcardDirectoryGlobs {
if glob.Match(fileName) {
return true
}
}
}
return false
}
func (p *ParsedCommandLine) GetMatchedFileSpec(fileName string) string {
return p.ConfigFile.configFileSpecs.getMatchedFileSpec(fileName, p.comparePathsOptions)
}
func (p *ParsedCommandLine) GetMatchedIncludeSpec(fileName string) (string, bool) {
if len(p.ConfigFile.configFileSpecs.validatedIncludeSpecs) == 0 {
return "", false
}
if p.ConfigFile.configFileSpecs.isDefaultIncludeSpec {
return p.ConfigFile.configFileSpecs.validatedIncludeSpecs[0], true
}
return p.ConfigFile.configFileSpecs.getMatchedIncludeSpec(fileName, p.comparePathsOptions), false
}
func (p *ParsedCommandLine) ReloadFileNamesOfParsedCommandLine(fs vfs.FS) *ParsedCommandLine {
parsedConfig := *p.ParsedConfig
fileNames, literalFileNamesLen := getFileNamesFromConfigSpecs(
*p.ConfigFile.configFileSpecs,
p.GetCurrentDirectory(),
p.CompilerOptions(),
fs,
p.extraFileExtensions,
)
parsedConfig.FileNames = fileNames
parsedCommandLine := ParsedCommandLine{
ParsedConfig: &parsedConfig,
ConfigFile: p.ConfigFile,
Errors: p.Errors,
Raw: p.Raw,
CompileOnSave: p.CompileOnSave,
comparePathsOptions: p.comparePathsOptions,
wildcardDirectories: p.wildcardDirectories,
includeGlobs: p.includeGlobs,
extraFileExtensions: p.extraFileExtensions,
literalFileNamesLen: literalFileNamesLen,
}
return &parsedCommandLine
}