390 lines
14 KiB
Go
390 lines
14 KiB
Go
package tsoptions
|
|
|
|
import (
|
|
"reflect"
|
|
|
|
"github.com/microsoft/typescript-go/internal/collections"
|
|
"github.com/microsoft/typescript-go/internal/core"
|
|
"github.com/microsoft/typescript-go/internal/debug"
|
|
"github.com/microsoft/typescript-go/internal/diagnostics"
|
|
"github.com/microsoft/typescript-go/internal/tspath"
|
|
)
|
|
|
|
// computeFn wraps a typed getter method so it can be stored in an impliedOption's
|
|
// compute field (which has type func(*core.CompilerOptions) any).
|
|
func computeFn[T any](fn func(*core.CompilerOptions) T) func(*core.CompilerOptions) any {
|
|
return func(opts *core.CompilerOptions) any {
|
|
return fn(opts)
|
|
}
|
|
}
|
|
|
|
// impliedOption describes a compiler option whose effective value can be derived from
|
|
// other options. This mirrors TypeScript's computedOptions concept used in convertToTSConfig.
|
|
type impliedOption struct {
|
|
// name is the Go struct field name of the CompilerOptions field (e.g., "Module").
|
|
name string
|
|
// dependencies lists the Go struct field names that this option depends on.
|
|
dependencies []string
|
|
// compute returns the effective value of this option given compiler options.
|
|
compute func(opts *core.CompilerOptions) any
|
|
}
|
|
|
|
// impliedOptions lists the compiler options that may be implied by other options,
|
|
// mirroring TypeScript's computedOptions used in convertToTSConfig.
|
|
// Each compute function delegates directly to an existing core.CompilerOptions getter.
|
|
var impliedOptions = []impliedOption{
|
|
{name: "Module", dependencies: []string{"Target"}, compute: computeFn((*core.CompilerOptions).GetEmitModuleKind)},
|
|
{name: "ModuleResolution", dependencies: []string{"Module", "Target"}, compute: computeFn((*core.CompilerOptions).GetModuleResolutionKind)},
|
|
{name: "ModuleDetection", dependencies: []string{"Module", "Target"}, compute: computeFn((*core.CompilerOptions).GetEmitModuleDetectionKind)},
|
|
{name: "IsolatedModules", dependencies: []string{"VerbatimModuleSyntax"}, compute: computeFn((*core.CompilerOptions).GetIsolatedModules)},
|
|
{name: "PreserveConstEnums", dependencies: []string{"IsolatedModules", "VerbatimModuleSyntax"}, compute: computeFn((*core.CompilerOptions).ShouldPreserveConstEnums)},
|
|
{name: "Declaration", dependencies: []string{"Composite"}, compute: computeFn((*core.CompilerOptions).GetEmitDeclarations)},
|
|
{name: "DeclarationMap", dependencies: []string{"Declaration", "Composite"}, compute: computeFn((*core.CompilerOptions).GetAreDeclarationMapsEnabled)},
|
|
{name: "Incremental", dependencies: []string{"Composite"}, compute: computeFn((*core.CompilerOptions).IsIncremental)},
|
|
{name: "UseDefineForClassFields", dependencies: []string{"Target", "Module"}, compute: computeFn((*core.CompilerOptions).GetUseDefineForClassFields)},
|
|
{name: "ResolvePackageJsonExports", dependencies: []string{"ModuleResolution", "Module", "Target"}, compute: computeFn((*core.CompilerOptions).GetResolvePackageJsonExports)},
|
|
{name: "ResolvePackageJsonImports", dependencies: []string{"ModuleResolution", "ResolvePackageJsonExports", "Module", "Target"}, compute: computeFn((*core.CompilerOptions).GetResolvePackageJsonImports)},
|
|
{name: "ResolveJsonModule", dependencies: []string{"ModuleResolution", "Module", "Target"}, compute: computeFn((*core.CompilerOptions).GetResolveJsonModule)},
|
|
{name: "AllowJs", dependencies: []string{"CheckJs"}, compute: computeFn((*core.CompilerOptions).GetAllowJS)},
|
|
{name: "AllowImportingTsExtensions", dependencies: []string{"RewriteRelativeImportExtensions"}, compute: computeFn((*core.CompilerOptions).GetAllowImportingTsExtensions)},
|
|
}
|
|
|
|
// TSConfig represents the output structure for --showConfig
|
|
type TSConfig struct {
|
|
CompilerOptions *collections.OrderedMap[string, any] `json:"compilerOptions"`
|
|
References []any `json:"references,omitzero"`
|
|
Files []string `json:"files,omitzero"`
|
|
Include []string `json:"include,omitzero"`
|
|
Exclude []string `json:"exclude,omitzero"`
|
|
CompileOnSave *bool `json:"compileOnSave,omitzero"`
|
|
}
|
|
|
|
// ConvertToTSConfig generates a complete tsconfig representation for --showConfig output,
|
|
// matching the behavior of TypeScript's convertToTSConfig function.
|
|
func ConvertToTSConfig(configParseResult *ParsedCommandLine, configFileName string) *TSConfig {
|
|
if configFileName == "" {
|
|
configFileName = "tsconfig.json"
|
|
}
|
|
normalizedConfigPath := tspath.GetNormalizedAbsolutePath(configFileName, configParseResult.GetCurrentDirectory())
|
|
comparePathsOptions := tspath.ComparePathsOptions{
|
|
CurrentDirectory: configParseResult.GetCurrentDirectory(),
|
|
UseCaseSensitiveFileNames: configParseResult.UseCaseSensitiveFileNames(),
|
|
}
|
|
|
|
// Build the list of all resolved files as relative paths from the config file.
|
|
var files []string
|
|
for _, f := range configParseResult.FileNames() {
|
|
normalizedFilePath := tspath.GetNormalizedAbsolutePath(f, configParseResult.GetCurrentDirectory())
|
|
relativePath := tspath.GetRelativePathFromFile(normalizedConfigPath, normalizedFilePath, comparePathsOptions)
|
|
files = append(files, relativePath)
|
|
}
|
|
|
|
// Serialize compiler options
|
|
optionMap := serializeCompilerOptions(configParseResult.CompilerOptions(), normalizedConfigPath, comparePathsOptions)
|
|
|
|
// Remove command-line-only options from the output
|
|
for _, name := range []string{
|
|
"showConfig", "configFile", "configFilePath", "help", "init",
|
|
"listFilesOnly", "listEmittedFiles", "project", "build", "version",
|
|
} {
|
|
optionMap.Delete(name)
|
|
}
|
|
|
|
// Add implied compiler options (options that are derived from explicitly set options,
|
|
// such as moduleResolution implied by module, or useDefineForClassFields implied by target).
|
|
// This mirrors TypeScript's convertToTSConfig computedOptions logic.
|
|
addImpliedOptions(optionMap, configParseResult.CompilerOptions(), normalizedConfigPath, comparePathsOptions)
|
|
|
|
config := &TSConfig{
|
|
CompilerOptions: optionMap,
|
|
}
|
|
|
|
// Add references
|
|
if refs := configParseResult.ProjectReferences(); len(refs) > 0 {
|
|
var references []any
|
|
for _, r := range refs {
|
|
ref := &collections.OrderedMap[string, any]{}
|
|
ref.Set("path", r.OriginalPath)
|
|
if r.Circular {
|
|
ref.Set("circular", true)
|
|
}
|
|
references = append(references, ref)
|
|
}
|
|
config.References = references
|
|
}
|
|
|
|
// Add files
|
|
if len(files) > 0 {
|
|
config.Files = files
|
|
}
|
|
|
|
// Add include/exclude from configFileSpecs
|
|
if configParseResult.ConfigFile != nil && configParseResult.ConfigFile.configFileSpecs != nil {
|
|
specs := configParseResult.ConfigFile.configFileSpecs
|
|
include := filterSameAsDefaultInclude(specs.validatedIncludeSpecs)
|
|
if len(include) > 0 {
|
|
config.Include = include
|
|
}
|
|
config.Exclude = specs.validatedExcludeSpecs
|
|
}
|
|
|
|
// Add compileOnSave
|
|
if configParseResult.CompileOnSave != nil && *configParseResult.CompileOnSave {
|
|
t := true
|
|
config.CompileOnSave = &t
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
// filterSameAsDefaultInclude returns nil if specs is the default include spec ["**/*"]
|
|
func filterSameAsDefaultInclude(specs []string) []string {
|
|
if len(specs) == 0 {
|
|
return nil
|
|
}
|
|
if len(specs) == 1 && specs[0] == defaultIncludeSpec {
|
|
return nil
|
|
}
|
|
return specs
|
|
}
|
|
|
|
// getNameOfCompilerOptionValue returns the string key for a given enum value by
|
|
// searching the option's enum map.
|
|
func getNameOfCompilerOptionValue(value any, enumMap *collections.OrderedMap[string, any]) string {
|
|
for k, v := range enumMap.Entries() {
|
|
if v == value {
|
|
return k
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// serializeCompilerOptions converts CompilerOptions to an ordered map with
|
|
// string names as keys and serialized values (enums as strings, paths as
|
|
// relative paths, etc.) matching the output of tsc --showConfig.
|
|
func serializeCompilerOptions(options *core.CompilerOptions, configFilePath string, comparePathsOptions tspath.ComparePathsOptions) *collections.OrderedMap[string, any] {
|
|
result := collections.NewOrderedMapWithSizeHint[string, any](32)
|
|
configDir := tspath.GetDirectoryPath(configFilePath)
|
|
|
|
optionsValue := reflect.ValueOf(options).Elem()
|
|
optionsTypeInfo := reflect.TypeFor[core.CompilerOptions]()
|
|
|
|
for i := range optionsValue.NumField() {
|
|
field := optionsTypeInfo.Field(i)
|
|
if !field.IsExported() {
|
|
continue
|
|
}
|
|
|
|
optionDecl := CommandLineCompilerOptionsMap.Get(field.Name)
|
|
if optionDecl == nil {
|
|
continue
|
|
}
|
|
|
|
// Skip command-line-only and output formatting options
|
|
if optionDecl.Category == diagnostics.Command_line_Options || optionDecl.Category == diagnostics.Output_Formatting {
|
|
continue
|
|
}
|
|
|
|
fieldValue := optionsValue.Field(i)
|
|
|
|
// Skip zero values (unset options)
|
|
if fieldValue.IsZero() {
|
|
continue
|
|
}
|
|
|
|
name := optionDecl.Name
|
|
value := fieldValue.Interface()
|
|
|
|
enumMap := optionDecl.EnumMap()
|
|
if enumMap != nil {
|
|
// Enum option - convert numeric value to string name
|
|
serialized := serializeEnumValue(value, enumMap)
|
|
if serialized != "" {
|
|
result.Set(name, serialized)
|
|
}
|
|
continue
|
|
}
|
|
|
|
switch optionDecl.Kind {
|
|
case CommandLineOptionTypeListOrElement:
|
|
debug.Assert(false, "listOrElement option should not reach serialization")
|
|
case CommandLineOptionTypeList:
|
|
elem := optionDecl.Elements()
|
|
if elem != nil && elem.IsFilePath {
|
|
// List of file paths - make relative
|
|
if strs, ok := value.([]string); ok {
|
|
relPaths := make([]string, len(strs))
|
|
for j, s := range strs {
|
|
absPath := tspath.GetNormalizedAbsolutePath(s, configDir)
|
|
relPaths[j] = tspath.GetRelativePathFromFile(configFilePath, absPath, comparePathsOptions)
|
|
}
|
|
result.Set(name, relPaths)
|
|
continue
|
|
}
|
|
}
|
|
if elem != nil && elem.EnumMap() != nil {
|
|
// List of enum values (e.g., lib)
|
|
elemMap := elem.EnumMap()
|
|
if strs, ok := value.([]string); ok {
|
|
serialized := make([]string, 0, len(strs))
|
|
for _, s := range strs {
|
|
// lib values are already stored as the d.ts filename, need to find original key
|
|
found := getNameOfCompilerOptionValue(s, elemMap)
|
|
if found != "" {
|
|
serialized = append(serialized, found)
|
|
} else {
|
|
serialized = append(serialized, s)
|
|
}
|
|
}
|
|
result.Set(name, serialized)
|
|
continue
|
|
}
|
|
}
|
|
result.Set(name, value)
|
|
|
|
case CommandLineOptionTypeString:
|
|
if optionDecl.IsFilePath {
|
|
// File path option - make relative to config
|
|
if s, ok := value.(string); ok && s != "" {
|
|
absPath := tspath.GetNormalizedAbsolutePath(s, configDir)
|
|
result.Set(name, tspath.GetRelativePathFromFile(configFilePath, absPath, comparePathsOptions))
|
|
continue
|
|
}
|
|
}
|
|
result.Set(name, value)
|
|
|
|
case CommandLineOptionTypeBoolean:
|
|
if t, ok := value.(core.Tristate); ok {
|
|
if t.IsTrue() {
|
|
result.Set(name, true)
|
|
} else if t.IsFalse() {
|
|
result.Set(name, false)
|
|
}
|
|
} else {
|
|
result.Set(name, value)
|
|
}
|
|
|
|
case CommandLineOptionTypeNumber:
|
|
result.Set(name, value)
|
|
|
|
default:
|
|
result.Set(name, value)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// serializeEnumValue converts an enum field value to its corresponding string key
|
|
// using the option's enum map. It handles int32-based enum types.
|
|
func serializeEnumValue(value any, enumMap *collections.OrderedMap[string, any]) string {
|
|
// The enum maps store values as core.ModuleKind, core.ScriptTarget, etc.
|
|
// But those are all int32 underneath. We need to compare by the underlying int32 value.
|
|
rv := reflect.ValueOf(value)
|
|
if rv.CanInt() {
|
|
intVal := rv.Int()
|
|
for k, v := range enumMap.Entries() {
|
|
ev := reflect.ValueOf(v)
|
|
if ev.CanInt() && ev.Int() == intVal {
|
|
return k
|
|
}
|
|
}
|
|
}
|
|
// Fallback: direct comparison
|
|
return getNameOfCompilerOptionValue(value, enumMap)
|
|
}
|
|
|
|
// addImpliedOptions adds compiler options that are implied by other explicitly-set options,
|
|
// mirroring TypeScript's convertToTSConfig behavior for computedOptions.
|
|
// For example, when module: nodenext is set, moduleResolution: nodenext is implied.
|
|
func addImpliedOptions(
|
|
optionMap *collections.OrderedMap[string, any],
|
|
options *core.CompilerOptions,
|
|
_ string,
|
|
_ tspath.ComparePathsOptions,
|
|
) {
|
|
// Build the set of explicitly provided option JSON names (e.g., "module", "target").
|
|
provided := make(map[string]bool, optionMap.Size())
|
|
for k := range optionMap.Keys() {
|
|
provided[k] = true
|
|
}
|
|
|
|
defaultOpts := &core.CompilerOptions{}
|
|
|
|
for _, entry := range impliedOptions {
|
|
// Get the option declaration for this implied option (using case-insensitive lookup).
|
|
optionDecl := CommandLineCompilerOptionsMap.Get(entry.name)
|
|
if optionDecl == nil {
|
|
continue
|
|
}
|
|
|
|
// Skip if this option is already explicitly provided.
|
|
if provided[optionDecl.Name] {
|
|
continue
|
|
}
|
|
|
|
// Check if any direct dependency is in the provided set.
|
|
// This mirrors TypeScript's optionDependsOn check.
|
|
if !anyDependencyProvided(entry.dependencies, provided) {
|
|
continue
|
|
}
|
|
|
|
// Compute the effective value with current options and the default value with empty options.
|
|
implied := entry.compute(options)
|
|
defaultVal := entry.compute(defaultOpts)
|
|
|
|
// If the implied value equals the default, this option doesn't add useful information.
|
|
if reflect.DeepEqual(implied, defaultVal) {
|
|
continue
|
|
}
|
|
|
|
// Serialize the implied value and add it to the option map.
|
|
serialized := serializeImpliedOptionValue(optionDecl, implied)
|
|
if serialized == nil {
|
|
continue
|
|
}
|
|
optionMap.Set(optionDecl.Name, serialized)
|
|
}
|
|
}
|
|
|
|
// anyDependencyProvided returns true if any of the given dependency names
|
|
// (using Go field names like "Target") corresponds to an option in the provided set.
|
|
func anyDependencyProvided(dependencies []string, provided map[string]bool) bool {
|
|
for _, dep := range dependencies {
|
|
depDecl := CommandLineCompilerOptionsMap.Get(dep)
|
|
if depDecl != nil && provided[depDecl.Name] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// serializeImpliedOptionValue converts a computed implied option value to its serializable form.
|
|
// For enum options, it converts numeric values to their string names.
|
|
// For boolean options, it returns the bool directly.
|
|
func serializeImpliedOptionValue(optionDecl *CommandLineOption, value any) any {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
enumMap := optionDecl.EnumMap()
|
|
if enumMap != nil {
|
|
s := serializeEnumValue(value, enumMap)
|
|
if s != "" {
|
|
return s
|
|
}
|
|
return nil
|
|
}
|
|
switch v := value.(type) {
|
|
case bool:
|
|
return v
|
|
case core.Tristate:
|
|
if v.IsTrue() {
|
|
return true
|
|
} else if v.IsFalse() {
|
|
return false
|
|
}
|
|
return nil
|
|
}
|
|
return value
|
|
}
|