Skip to content

Commit beeea27

Browse files
authored
Port tsc --init (#2033)
1 parent 336dda7 commit beeea27

15 files changed

+778
-2
lines changed

internal/execute/tsc.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77

88
"github.com/microsoft/typescript-go/internal/ast"
9+
"github.com/microsoft/typescript-go/internal/collections"
910
"github.com/microsoft/typescript-go/internal/compiler"
1011
"github.com/microsoft/typescript-go/internal/core"
1112
"github.com/microsoft/typescript-go/internal/diagnostics"
@@ -113,7 +114,8 @@ func tscCompilation(sys tsc.System, commandLine *tsoptions.ParsedCommandLine, te
113114
}
114115

115116
if commandLine.CompilerOptions().Init.IsTrue() {
116-
return tsc.CommandLineResult{Status: tsc.ExitStatusNotImplemented}
117+
tsc.WriteConfigFile(sys, reportDiagnostic, commandLine.Raw.(*collections.OrderedMap[string, any]))
118+
return tsc.CommandLineResult{Status: tsc.ExitStatusSuccess}
117119
}
118120

119121
if commandLine.CompilerOptions().Version.IsTrue() {

internal/execute/tsc/init.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
package tsc
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"slices"
7+
"strings"
8+
9+
"github.com/microsoft/typescript-go/internal/ast"
10+
"github.com/microsoft/typescript-go/internal/collections"
11+
"github.com/microsoft/typescript-go/internal/core"
12+
"github.com/microsoft/typescript-go/internal/diagnostics"
13+
"github.com/microsoft/typescript-go/internal/jsonutil"
14+
"github.com/microsoft/typescript-go/internal/tsoptions"
15+
"github.com/microsoft/typescript-go/internal/tspath"
16+
)
17+
18+
func WriteConfigFile(sys System, reportDiagnostic DiagnosticReporter, options *collections.OrderedMap[string, any]) {
19+
getCurrentDirectory := sys.GetCurrentDirectory()
20+
file := tspath.NormalizePath(tspath.CombinePaths(getCurrentDirectory, "tsconfig.json"))
21+
if sys.FS().FileExists(file) {
22+
reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.A_tsconfig_json_file_is_already_defined_at_Colon_0, file))
23+
} else {
24+
_ = sys.FS().WriteFile(file, generateTSConfig(options), false)
25+
output := []string{"\n"}
26+
output = append(output, getHeader(sys, "Created a new tsconfig.json")...)
27+
output = append(output, "You can learn more at https://aka.ms/tsconfig", "\n")
28+
fmt.Fprint(sys.Writer(), strings.Join(output, ""))
29+
}
30+
}
31+
32+
func generateTSConfig(options *collections.OrderedMap[string, any]) string {
33+
const tab = " "
34+
var result []string
35+
36+
allSetOptions := make([]string, 0, options.Size())
37+
for k := range options.Keys() {
38+
if k != "init" && k != "help" && k != "watch" {
39+
allSetOptions = append(allSetOptions, k)
40+
}
41+
}
42+
43+
// !!! locale getLocaleSpecificMessage
44+
emitHeader := func(header *diagnostics.Message) {
45+
result = append(result, tab+tab+"// "+header.Format())
46+
}
47+
newline := func() {
48+
result = append(result, "")
49+
}
50+
push := func(args ...string) {
51+
result = append(result, args...)
52+
}
53+
54+
formatSingleValue := func(value any, enumMap *collections.OrderedMap[string, any]) string {
55+
if enumMap != nil {
56+
var found bool
57+
for k, v := range enumMap.Entries() {
58+
if value == v {
59+
value = k
60+
found = true
61+
break
62+
}
63+
}
64+
if !found {
65+
panic(fmt.Sprintf("No matching value of %v", value))
66+
}
67+
}
68+
69+
b, err := jsonutil.MarshalIndent(value, "", "")
70+
if err != nil {
71+
panic(fmt.Sprintf("should not happen: %v", err))
72+
}
73+
return string(b)
74+
}
75+
76+
formatValueOrArray := func(settingName string, value any) string {
77+
var option *tsoptions.CommandLineOption
78+
for _, decl := range tsoptions.OptionsDeclarations {
79+
if decl.Name == settingName {
80+
option = decl
81+
}
82+
}
83+
if option == nil {
84+
panic(`No option named ` + settingName)
85+
}
86+
87+
rval := reflect.ValueOf(value)
88+
if rval.Kind() == reflect.Slice {
89+
var enumMap *collections.OrderedMap[string, any]
90+
if elemOption := option.Elements(); elemOption != nil {
91+
enumMap = elemOption.EnumMap()
92+
}
93+
94+
var elems []string
95+
for i := range rval.Len() {
96+
elems = append(elems, formatSingleValue(rval.Index(i).Interface(), enumMap))
97+
}
98+
return `[` + strings.Join(elems, ", ") + `]`
99+
} else {
100+
return formatSingleValue(value, option.EnumMap())
101+
}
102+
}
103+
104+
// commentedNever': Never comment this out
105+
// commentedAlways': Always comment this out, even if it's on commandline
106+
// commentedOptional': Comment out unless it's on commandline
107+
type commented int
108+
const (
109+
commentedNever commented = iota
110+
commentedAlways
111+
commentedOptional
112+
)
113+
emitOption := func(setting string, defaultValue any, commented commented) {
114+
if commented > 2 {
115+
panic("should not happen: invalid `commented`, must be a bug.")
116+
}
117+
118+
existingOptionIndex := slices.Index(allSetOptions, setting)
119+
if existingOptionIndex >= 0 {
120+
allSetOptions = slices.Delete(allSetOptions, existingOptionIndex, existingOptionIndex+1)
121+
}
122+
123+
var comment bool
124+
switch commented {
125+
case commentedAlways:
126+
comment = true
127+
case commentedNever:
128+
comment = false
129+
default:
130+
comment = !options.Has(setting)
131+
}
132+
133+
value, ok := options.Get(setting)
134+
if !ok {
135+
value = defaultValue
136+
}
137+
138+
if comment {
139+
push(tab + tab + `// "` + setting + `": ` + formatValueOrArray(setting, value) + `,`)
140+
} else {
141+
push(tab + tab + `"` + setting + `": ` + formatValueOrArray(setting, value) + `,`)
142+
}
143+
}
144+
145+
push("{")
146+
// !!! locale getLocaleSpecificMessage
147+
push(tab + `// ` + diagnostics.Visit_https_Colon_Slash_Slashaka_ms_Slashtsconfig_to_read_more_about_this_file.Format())
148+
push(tab + `"compilerOptions": {`)
149+
150+
emitHeader(diagnostics.File_Layout)
151+
emitOption("rootDir", "./src", commentedOptional)
152+
emitOption("outDir", "./dist", commentedOptional)
153+
154+
newline()
155+
156+
emitHeader(diagnostics.Environment_Settings)
157+
emitHeader(diagnostics.See_also_https_Colon_Slash_Slashaka_ms_Slashtsconfig_Slashmodule)
158+
emitOption("module", core.ModuleKindNodeNext, commentedNever)
159+
emitOption("target", core.ScriptTargetESNext, commentedNever)
160+
emitOption("types", []any{}, commentedNever)
161+
if lib, ok := options.Get("lib"); ok {
162+
emitOption("lib", lib, commentedNever)
163+
}
164+
emitHeader(diagnostics.For_nodejs_Colon)
165+
push(tab + tab + `// "lib": ["esnext"],`)
166+
push(tab + tab + `// "types": ["node"],`)
167+
emitHeader(diagnostics.X_and_npm_install_D_types_Slashnode)
168+
169+
newline()
170+
171+
emitHeader(diagnostics.Other_Outputs)
172+
emitOption("sourceMap" /*defaultValue*/, true, commentedNever)
173+
emitOption("declaration" /*defaultValue*/, true, commentedNever)
174+
emitOption("declarationMap" /*defaultValue*/, true, commentedNever)
175+
176+
newline()
177+
178+
emitHeader(diagnostics.Stricter_Typechecking_Options)
179+
emitOption("noUncheckedIndexedAccess" /*defaultValue*/, true, commentedNever)
180+
emitOption("exactOptionalPropertyTypes" /*defaultValue*/, true, commentedNever)
181+
182+
newline()
183+
184+
emitHeader(diagnostics.Style_Options)
185+
emitOption("noImplicitReturns" /*defaultValue*/, true, commentedOptional)
186+
emitOption("noImplicitOverride" /*defaultValue*/, true, commentedOptional)
187+
emitOption("noUnusedLocals" /*defaultValue*/, true, commentedOptional)
188+
emitOption("noUnusedParameters" /*defaultValue*/, true, commentedOptional)
189+
emitOption("noFallthroughCasesInSwitch" /*defaultValue*/, true, commentedOptional)
190+
emitOption("noPropertyAccessFromIndexSignature" /*defaultValue*/, true, commentedOptional)
191+
192+
newline()
193+
194+
emitHeader(diagnostics.Recommended_Options)
195+
emitOption("strict" /*defaultValue*/, true, commentedNever)
196+
emitOption("jsx", core.JsxEmitReactJSX, commentedNever)
197+
emitOption("verbatimModuleSyntax" /*defaultValue*/, true, commentedNever)
198+
emitOption("isolatedModules" /*defaultValue*/, true, commentedNever)
199+
emitOption("noUncheckedSideEffectImports" /*defaultValue*/, true, commentedNever)
200+
emitOption("moduleDetection", core.ModuleDetectionKindForce, commentedNever)
201+
emitOption("skipLibCheck" /*defaultValue*/, true, commentedNever)
202+
203+
// Write any user-provided options we haven't already
204+
if len(allSetOptions) > 0 {
205+
newline()
206+
for len(allSetOptions) > 0 {
207+
emitOption(allSetOptions[0], options.GetOrZero(allSetOptions[0]), commentedNever)
208+
}
209+
}
210+
211+
push(tab + "}")
212+
push(`}`)
213+
push(``)
214+
215+
return strings.Join(result, "\n")
216+
}

internal/execute/tsctests/tsc_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,60 @@ func TestTscCommandline(t *testing.T) {
3636
subScenario: "when build not first argument",
3737
commandLineArgs: []string{"--verbose", "--build"},
3838
},
39+
{
40+
subScenario: "Initialized TSConfig with files options",
41+
commandLineArgs: []string{"--init", "file0.st", "file1.ts", "file2.ts"},
42+
},
43+
{
44+
subScenario: "Initialized TSConfig with boolean value compiler options",
45+
commandLineArgs: []string{"--init", "--noUnusedLocals"},
46+
},
47+
{
48+
subScenario: "Initialized TSConfig with enum value compiler options",
49+
commandLineArgs: []string{"--init", "--target", "es5", "--jsx", "react"},
50+
},
51+
{
52+
subScenario: "Initialized TSConfig with list compiler options",
53+
commandLineArgs: []string{"--init", "--types", "jquery,mocha"},
54+
},
55+
{
56+
subScenario: "Initialized TSConfig with list compiler options with enum value",
57+
commandLineArgs: []string{"--init", "--lib", "es5,es2015.core"},
58+
},
59+
{
60+
subScenario: "Initialized TSConfig with incorrect compiler option",
61+
commandLineArgs: []string{"--init", "--someNonExistOption"},
62+
},
63+
{
64+
subScenario: "Initialized TSConfig with incorrect compiler option value",
65+
commandLineArgs: []string{"--init", "--lib", "nonExistLib,es5,es2015.promise"},
66+
},
67+
{
68+
subScenario: "Initialized TSConfig with advanced options",
69+
commandLineArgs: []string{"--init", "--declaration", "--declarationDir", "lib", "--skipLibCheck", "--noErrorTruncation"},
70+
},
71+
{
72+
subScenario: "Initialized TSConfig with --help",
73+
commandLineArgs: []string{"--init", "--help"},
74+
},
75+
{
76+
subScenario: "Initialized TSConfig with --watch",
77+
commandLineArgs: []string{"--init", "--watch"},
78+
},
79+
{
80+
subScenario: "Initialized TSConfig with tsconfig.json",
81+
commandLineArgs: []string{"--init"},
82+
files: FileMap{
83+
"/home/src/workspaces/project/first.ts": `export const a = 1`,
84+
"/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(`
85+
{
86+
"compilerOptions": {
87+
"strict": true,
88+
"noEmit": true
89+
}
90+
}`),
91+
},
92+
},
3993
{
4094
subScenario: "help",
4195
commandLineArgs: []string{"--help"},

internal/tsoptions/commandlineparser.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func ParseCommandLine(
4646
commandLine = []string{}
4747
}
4848
parser := parseCommandLineWorker(CompilerOptionsDidYouMeanDiagnostics, commandLine, host.FS())
49-
optionsWithAbsolutePaths := convertToOptionsWithAbsolutePaths(parser.options, CommandLineCompilerOptionsMap, host.GetCurrentDirectory())
49+
optionsWithAbsolutePaths := convertToOptionsWithAbsolutePaths(parser.options.Clone(), CommandLineCompilerOptionsMap, host.GetCurrentDirectory())
5050
compilerOptions := convertMapToOptions(optionsWithAbsolutePaths, &compilerOptionsParser{&core.CompilerOptions{}}).CompilerOptions
5151
watchOptions := convertMapToOptions(optionsWithAbsolutePaths, &watchOptionsParser{&core.WatchOptions{}}).WatchOptions
5252
result := NewParsedCommandLine(compilerOptions, parser.fileNames, tspath.ComparePathsOptions{
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
currentDirectory::/home/src/workspaces/project
2+
useCaseSensitiveFileNames::true
3+
Input::
4+
5+
tsgo --init --help
6+
ExitStatus:: Success
7+
Output::
8+
9+
Created a new tsconfig.json
10+
11+
You can learn more at https://aka.ms/tsconfig
12+
//// [/home/src/workspaces/project/tsconfig.json] *new*
13+
{
14+
// Visit https://aka.ms/tsconfig to read more about this file
15+
"compilerOptions": {
16+
// File Layout
17+
// "rootDir": "./src",
18+
// "outDir": "./dist",
19+
20+
// Environment Settings
21+
// See also https://aka.ms/tsconfig/module
22+
"module": "nodenext",
23+
"target": "esnext",
24+
"types": [],
25+
// For nodejs:
26+
// "lib": ["esnext"],
27+
// "types": ["node"],
28+
// and npm install -D @types/node
29+
30+
// Other Outputs
31+
"sourceMap": true,
32+
"declaration": true,
33+
"declarationMap": true,
34+
35+
// Stricter Typechecking Options
36+
"noUncheckedIndexedAccess": true,
37+
"exactOptionalPropertyTypes": true,
38+
39+
// Style Options
40+
// "noImplicitReturns": true,
41+
// "noImplicitOverride": true,
42+
// "noUnusedLocals": true,
43+
// "noUnusedParameters": true,
44+
// "noFallthroughCasesInSwitch": true,
45+
// "noPropertyAccessFromIndexSignature": true,
46+
47+
// Recommended Options
48+
"strict": true,
49+
"jsx": "react-jsx",
50+
"verbatimModuleSyntax": true,
51+
"isolatedModules": true,
52+
"noUncheckedSideEffectImports": true,
53+
"moduleDetection": "force",
54+
"skipLibCheck": true,
55+
}
56+
}
57+
58+

0 commit comments

Comments
 (0)