Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion internal/execute/tsc.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ func tscCompilation(sys tsc.System, commandLine *tsoptions.ParsedCommandLine, te
}

if commandLine.CompilerOptions().Init.IsTrue() {
return tsc.CommandLineResult{Status: tsc.ExitStatusNotImplemented}
tsc.WriteConfigFile(sys, reportDiagnostic, commandLine.CompilerOptions())
return tsc.CommandLineResult{Status: tsc.ExitStatusSuccess}
}

if commandLine.CompilerOptions().Version.IsTrue() {
Expand Down
238 changes: 238 additions & 0 deletions internal/execute/tsc/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package tsc

import (
"fmt"
"reflect"
"slices"
"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/diagnostics"
"github.com/microsoft/typescript-go/internal/jsonutil"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
)

func WriteConfigFile(sys System, reportDiagnostic DiagnosticReporter, options *core.CompilerOptions) {
getCurrentDirectory := sys.GetCurrentDirectory()
file := tspath.NormalizePath(tspath.CombinePaths(getCurrentDirectory, "tsconfig.json"))
if sys.FS().FileExists(file) {
reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.A_tsconfig_json_file_is_already_defined_at_Colon_0, file))
} else {
_ = sys.FS().WriteFile(file, generateTSConfig(options), false)
output := []string{"\n"}
output = append(output, getHeader(sys, "Created a new tsconfig.json")...)
output = append(output, "You can learn more at https://aka.ms/tsconfig", "\n")
fmt.Fprint(sys.Writer(), strings.Join(output, ""))
}
}

func convertOptionsToMap(options *core.CompilerOptions) *collections.OrderedMap[string, any] {
val := reflect.ValueOf(options).Elem()
typ := val.Type()

result := collections.NewOrderedMapWithSizeHint[string, any](val.NumField())

for i := range val.NumField() {
field := typ.Field(i)
fieldValue := val.Field(i)

if fieldValue.IsZero() {
continue
}

// Get the field name, considering 'json' tag if present
fieldName := field.Name
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
fieldName, _, _ = strings.Cut(jsonTag, ",")
}

if fieldName != "" && fieldName != "init" && fieldName != "help" && fieldName != "watch" {
result.Set(fieldName, fieldValue.Interface())
}
}
return result
}

func generateTSConfig(options *core.CompilerOptions) string {
const tab = " "
var result []string

optionsMap := convertOptionsToMap(options)
allSetOptions := slices.Collect(optionsMap.Keys())

// !!! locale getLocaleSpecificMessage
emitHeader := func(header *diagnostics.Message) {
result = append(result, tab+tab+"// "+header.Format())
}
newline := func() {
result = append(result, "")
}
push := func(args ...string) {
result = append(result, args...)
}

formatSingleValue := func(value any, enumMap *collections.OrderedMap[string, any]) string {
if enumMap != nil {
var found bool
for k, v := range enumMap.Entries() {
if value == v {
value = k
found = true
break
}
}
if !found {
panic(fmt.Sprintf("No matching value of %v", value))
}
}

b, err := jsonutil.MarshalIndent(value, "", "")
if err != nil {
panic(fmt.Sprintf("should not happen: %v", err))
}
return string(b)
}

formatValueOrArray := func(settingName string, value any) string {
var option *tsoptions.CommandLineOption
for _, decl := range tsoptions.OptionsDeclarations {
if decl.Name == settingName {
option = decl
}
}
if option == nil {
panic(`No option named ` + settingName)
}

rval := reflect.ValueOf(value)
if rval.Kind() == reflect.Slice {
var enumMap *collections.OrderedMap[string, any]
if elemOption := option.Elements(); elemOption != nil {
enumMap = elemOption.EnumMap()
}

var elems []string
for i := range rval.Len() {
elems = append(elems, formatSingleValue(rval.Index(i).Interface(), enumMap))
}
return `[` + strings.Join(elems, ", ") + `]`
} else {
return formatSingleValue(value, option.EnumMap())
}
}

// commentedNever': Never comment this out
// commentedAlways': Always comment this out, even if it's on commandline
// commentedOptional': Comment out unless it's on commandline
const (
commentedNever = 0
commentedAlways = 1
commentedOptional = 2
)
emitOption := func(setting string, defaultValue any, commented int) {
if commented > 2 {
panic("should not happen: invalid `commented`, must be a bug.")
}

existingOptionIndex := slices.Index(allSetOptions, setting)
if existingOptionIndex >= 0 {
allSetOptions = slices.Delete(allSetOptions, existingOptionIndex, existingOptionIndex+1)
}

var comment bool
switch commented {
case commentedAlways:
comment = true
case commentedNever:
comment = false
default:
comment = !optionsMap.Has(setting)
}

value, ok := optionsMap.Get(setting)
if !ok {
value = defaultValue
}

if comment {
push(tab + tab + `// "` + setting + `": ` + formatValueOrArray(setting, value) + `,`)
} else {
push(tab + tab + `"` + setting + `": ` + formatValueOrArray(setting, value) + `,`)
}
}

push("{")
// !!! locale getLocaleSpecificMessage
push(tab + `// ` + diagnostics.Visit_https_Colon_Slash_Slashaka_ms_Slashtsconfig_to_read_more_about_this_file.Format())
push(tab + `"compilerOptions": {`)

emitHeader(diagnostics.File_Layout)
emitOption("rootDir", "./src", commentedOptional)
emitOption("outDir", "./dist", commentedOptional)

newline()

emitHeader(diagnostics.Environment_Settings)
emitHeader(diagnostics.See_also_https_Colon_Slash_Slashaka_ms_Slashtsconfig_Slashmodule)
emitOption("module", core.ModuleKindNodeNext, commentedNever)
emitOption("target", core.ScriptTargetESNext, commentedNever)
emitOption("types", []any{}, commentedNever)
if len(options.Lib) != 0 {
emitOption("lib", options.Lib, commentedNever)
}
emitHeader(diagnostics.For_nodejs_Colon)
push(tab + tab + `// "lib": ["esnext"],`)
push(tab + tab + `// "types": ["node"],`)
emitHeader(diagnostics.X_and_npm_install_D_types_Slashnode)

newline()

emitHeader(diagnostics.Other_Outputs)
emitOption("sourceMap" /*defaultValue*/, true, commentedNever)
emitOption("declaration" /*defaultValue*/, true, commentedNever)
emitOption("declarationMap" /*defaultValue*/, true, commentedNever)

newline()

emitHeader(diagnostics.Stricter_Typechecking_Options)
emitOption("noUncheckedIndexedAccess" /*defaultValue*/, true, commentedNever)
emitOption("exactOptionalPropertyTypes" /*defaultValue*/, true, commentedNever)

newline()

emitHeader(diagnostics.Style_Options)
emitOption("noImplicitReturns" /*defaultValue*/, true, commentedOptional)
emitOption("noImplicitOverride" /*defaultValue*/, true, commentedOptional)
emitOption("noUnusedLocals" /*defaultValue*/, true, commentedOptional)
emitOption("noUnusedParameters" /*defaultValue*/, true, commentedOptional)
emitOption("noFallthroughCasesInSwitch" /*defaultValue*/, true, commentedOptional)
emitOption("noPropertyAccessFromIndexSignature" /*defaultValue*/, true, commentedOptional)

newline()

emitHeader(diagnostics.Recommended_Options)
emitOption("strict" /*defaultValue*/, true, commentedNever)
emitOption("jsx", core.JsxEmitReactJSX, commentedNever)
emitOption("verbatimModuleSyntax" /*defaultValue*/, true, commentedNever)
emitOption("isolatedModules" /*defaultValue*/, true, commentedNever)
emitOption("noUncheckedSideEffectImports" /*defaultValue*/, true, commentedNever)
emitOption("moduleDetection", core.ModuleDetectionKindForce, commentedNever)
emitOption("skipLibCheck" /*defaultValue*/, true, commentedNever)

// Write any user-provided options we haven't already
if len(allSetOptions) > 0 {
newline()
for len(allSetOptions) > 0 {
emitOption(allSetOptions[0], optionsMap.GetOrZero(allSetOptions[0]), commentedNever)
}
}

push(tab + "}")
push(`}`)
push(``)

return strings.Join(result, "\n")
}
22 changes: 22 additions & 0 deletions internal/execute/tsctests/tsc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,28 @@ func TestTscCommandline(t *testing.T) {
subScenario: "when build not first argument",
commandLineArgs: []string{"--verbose", "--build"},
},
{
subScenario: "init",
commandLineArgs: []string{"--init"},
},
{
subScenario: "init with --lib esnext",
commandLineArgs: []string{"--init", "--lib", "esnext"},
},
{
subScenario: "init with tsconfig.json",
commandLineArgs: []string{"--init"},
files: FileMap{
"/home/src/workspaces/project/first.ts": `export const a = 1`,
"/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(`
{
"compilerOptions": {
"strict": true,
"noEmit": true
}
}`),
},
},
{
subScenario: "help",
commandLineArgs: []string{"--help"},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
currentDirectory::/home/src/workspaces/project
useCaseSensitiveFileNames::true
Input::

tsgo --init --lib esnext
ExitStatus:: Success
Output::

Created a new tsconfig.json

You can learn more at https://aka.ms/tsconfig
//// [/home/src/workspaces/project/tsconfig.json] *new*
{
// Visit https://aka.ms/tsconfig to read more about this file
"compilerOptions": {
// File Layout
// "rootDir": "./src",
// "outDir": "./dist",

// Environment Settings
// See also https://aka.ms/tsconfig/module
"module": "nodenext",
"target": "esnext",
"types": [],
"lib": ["esnext"],
// For nodejs:
// "lib": ["esnext"],
// "types": ["node"],
// and npm install -D @types/node

// Other Outputs
"sourceMap": true,
"declaration": true,
"declarationMap": true,

// Stricter Typechecking Options
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,

// Style Options
// "noImplicitReturns": true,
// "noImplicitOverride": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "noFallthroughCasesInSwitch": true,
// "noPropertyAccessFromIndexSignature": true,

// Recommended Options
"strict": true,
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true,
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
currentDirectory::/home/src/workspaces/project
useCaseSensitiveFileNames::true
Input::
//// [/home/src/workspaces/project/first.ts] *new*
export const a = 1
//// [/home/src/workspaces/project/tsconfig.json] *new*
{
"compilerOptions": {
"strict": true,
"noEmit": true
}
}

tsgo --init
ExitStatus:: Success
Output::
error TS5054: A 'tsconfig.json' file is already defined at: '/home/src/workspaces/project/tsconfig.json'.

Loading