diff --git a/internal/execute/tsc.go b/internal/execute/tsc.go index d4e56103bc..d2184891c8 100644 --- a/internal/execute/tsc.go +++ b/internal/execute/tsc.go @@ -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() { diff --git a/internal/execute/tsc/init.go b/internal/execute/tsc/init.go new file mode 100644 index 0000000000..0ec2f2118c --- /dev/null +++ b/internal/execute/tsc/init.go @@ -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") +} diff --git a/internal/execute/tsctests/tsc_test.go b/internal/execute/tsctests/tsc_test.go index df7e4e39b1..1a142e6ffe 100644 --- a/internal/execute/tsctests/tsc_test.go +++ b/internal/execute/tsctests/tsc_test.go @@ -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"}, diff --git a/testdata/baselines/reference/tsc/commandLine/init-with---lib-esnext.js b/testdata/baselines/reference/tsc/commandLine/init-with---lib-esnext.js new file mode 100644 index 0000000000..ab844b0100 --- /dev/null +++ b/testdata/baselines/reference/tsc/commandLine/init-with---lib-esnext.js @@ -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, + } +} + + diff --git a/testdata/baselines/reference/tsc/commandLine/init-with-tsconfig.json.js b/testdata/baselines/reference/tsc/commandLine/init-with-tsconfig.json.js new file mode 100644 index 0000000000..543a4d4988 --- /dev/null +++ b/testdata/baselines/reference/tsc/commandLine/init-with-tsconfig.json.js @@ -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'. + diff --git a/testdata/baselines/reference/tsc/commandLine/init.js b/testdata/baselines/reference/tsc/commandLine/init.js new file mode 100644 index 0000000000..76ed0bc973 --- /dev/null +++ b/testdata/baselines/reference/tsc/commandLine/init.js @@ -0,0 +1,58 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: + +tsgo --init +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": [], + // 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, + } +} + +