Skip to content
Open
Show file tree
Hide file tree
Changes from all 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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you do all of the tests listed in _submodules/TypeScript/src/testRunner/unittests/config/initializeTSConfig.ts?

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