Skip to content

tsc --init update #61813

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jun 6, 2025
261 changes: 122 additions & 139 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ import {
filterMutate,
find,
findIndex,
firstOrUndefinedIterator,
flatten,
forEach,
forEachEntry,
Expand Down Expand Up @@ -2789,140 +2788,146 @@ function serializeOptionBaseObject(
return result;
}

/**
* Generate a list of the compiler options whose value is not the default.
* @param options compilerOptions to be evaluated.
/** @internal */
export function getCompilerOptionsDiffValue(options: CompilerOptions, newLine: string): string {
const compilerOptionsMap = getSerializedCompilerOption(options);
return getOverwrittenDefaultOptions();

function makePadding(paddingLength: number): string {
return Array(paddingLength + 1).join(" ");
}

function getOverwrittenDefaultOptions() {
const result: string[] = [];
const tab = makePadding(2);
commandOptionsWithoutBuild.forEach(cmd => {
if (!compilerOptionsMap.has(cmd.name)) {
return;
}

const newValue = compilerOptionsMap.get(cmd.name);
const defaultValue = getDefaultValueForOption(cmd);
if (newValue !== defaultValue) {
result.push(`${tab}${cmd.name}: ${newValue}`);
}
else if (hasProperty(defaultInitCompilerOptions, cmd.name)) {
result.push(`${tab}${cmd.name}: ${defaultValue}`);
}
});
return result.join(newLine) + newLine;
}
}

/**
* Get the compiler options to be written into the tsconfig.json.
* @param options commandlineOptions to be included in the compileOptions.
*/
function getSerializedCompilerOption(options: CompilerOptions): Map<string, CompilerOptionsValue> {
const compilerOptions = extend(options, defaultInitCompilerOptions);
return serializeCompilerOptions(compilerOptions);
}
/**
* Generate tsconfig configuration when running command line "--init"
* @param options commandlineOptions to be generated into tsconfig.json
* @param fileNames array of filenames to be generated into tsconfig.json
*
* @internal
*/
export function generateTSConfig(options: CompilerOptions, fileNames: readonly string[], newLine: string): string {
const compilerOptionsMap = getSerializedCompilerOption(options);
return writeConfigurations();
export function generateTSConfig(options: CompilerOptions, newLine: string): string {
Copy link
Member

Choose a reason for hiding this comment

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

type PresetValue = string | number | boolean | (string | number | boolean)[];

const tab = " ";
const result: string[] = [];
const allSetOptions = Object.keys(options).filter(k => k !== "init" && k !== "help" && k !== "watch");

result.push(`{`);
result.push(`${tab}// ${getLocaleSpecificMessage(Diagnostics.Visit_https_Colon_Slash_Slashaka_ms_Slashtsconfig_to_read_more_about_this_file)}`);
result.push(`${tab}"compilerOptions": {`);

emitHeader(Diagnostics.File_Layout);
emitOption("rootDir", "./src", "optional");
emitOption("outDir", "./dist", "optional");

newline();

emitHeader(Diagnostics.Environment_Settings);
emitHeader(Diagnostics.See_also_https_Colon_Slash_Slashaka_ms_Slashtsconfig_Slashmodule);
emitOption("module", ModuleKind.NodeNext);
emitOption("target", ScriptTarget.ESNext);
emitOption("types", []);
if (options.lib) {
emitOption("lib", options.lib);
}
emitHeader(Diagnostics.For_nodejs_Colon);
result.push(`${tab}${tab}// "lib": ["esnext"],`);
result.push(`${tab}${tab}// "types": ["node"],`);
emitHeader(Diagnostics.and_npm_install_D_types_Slashnode);

newline();

emitHeader(Diagnostics.Other_Outputs);
emitOption("sourceMap", /*defaultValue*/ true);
emitOption("declaration", /*defaultValue*/ true);
emitOption("declarationMap", /*defaultValue*/ true);

newline();

emitHeader(Diagnostics.Stricter_Typechecking_Options);
emitOption("noUncheckedIndexedAccess", /*defaultValue*/ true);
emitOption("exactOptionalPropertyTypes", /*defaultValue*/ true);

newline();

emitHeader(Diagnostics.Style_Options);
emitOption("noImplicitReturns", /*defaultValue*/ true, "optional");
emitOption("noImplicitOverride", /*defaultValue*/ true, "optional");
emitOption("noUnusedLocals", /*defaultValue*/ true, "optional");
emitOption("noUnusedParameters", /*defaultValue*/ true, "optional");
emitOption("noFallthroughCasesInSwitch", /*defaultValue*/ true, "optional");
emitOption("noPropertyAccessFromIndexSignature", /*defaultValue*/ true, "optional");

newline();

emitHeader(Diagnostics.Recommended_Options);
emitOption("strict", /*defaultValue*/ true);
emitOption("jsx", JsxEmit.ReactJSX);
emitOption("verbatimModuleSyntax", /*defaultValue*/ true);
emitOption("isolatedModules", /*defaultValue*/ true);
emitOption("noUncheckedSideEffectImports", /*defaultValue*/ true);
emitOption("moduleDetection", ModuleDetectionKind.Force);
emitOption("skipLibCheck", /*defaultValue*/ true);

// Write any user-provided options we haven't already
if (allSetOptions.length > 0) {
newline();
while (allSetOptions.length > 0) {
emitOption(allSetOptions[0], options[allSetOptions[0]]);
}
}

function makePadding(paddingLength: number): string {
return Array(paddingLength + 1).join(" ");
function newline() {
result.push("");
}

function isAllowedOptionForOutput({ category, name, isCommandLineOnly }: CommandLineOption): boolean {
// Skip options which do not have a category or have categories which are more niche
const categoriesToSkip = [Diagnostics.Command_line_Options, Diagnostics.Editor_Support, Diagnostics.Compiler_Diagnostics, Diagnostics.Backwards_Compatibility, Diagnostics.Watch_and_Build_Modes, Diagnostics.Output_Formatting];
return !isCommandLineOnly && category !== undefined && (!categoriesToSkip.includes(category) || compilerOptionsMap.has(name));
function emitHeader(header: DiagnosticMessage) {
result.push(`${tab}${tab}// ${getLocaleSpecificMessage(header)}`);
}

function writeConfigurations() {
// Filter applicable options to place in the file
const categorizedOptions = new Map<DiagnosticMessage, CommandLineOption[]>();
// Set allowed categories in order
categorizedOptions.set(Diagnostics.Projects, []);
categorizedOptions.set(Diagnostics.Language_and_Environment, []);
categorizedOptions.set(Diagnostics.Modules, []);
categorizedOptions.set(Diagnostics.JavaScript_Support, []);
categorizedOptions.set(Diagnostics.Emit, []);
categorizedOptions.set(Diagnostics.Interop_Constraints, []);
categorizedOptions.set(Diagnostics.Type_Checking, []);
categorizedOptions.set(Diagnostics.Completeness, []);
for (const option of optionDeclarations) {
if (isAllowedOptionForOutput(option)) {
let listForCategory = categorizedOptions.get(option.category!);
if (!listForCategory) categorizedOptions.set(option.category!, listForCategory = []);
listForCategory.push(option);
}
// commented = 'always': Always comment this out, even if it's on commandline
// commented = 'optional': Comment out unless it's on commandline
// commented = 'never': Never comment this out
function emitOption<K extends string & keyof CompilerOptions>(setting: K, defaultValue: CompilerOptions[K], commented: "always" | "optional" | "never" = "never") {
const existingOptionIndex = allSetOptions.indexOf(setting);
if (existingOptionIndex >= 0) {
allSetOptions.splice(existingOptionIndex, 1);
}

// Serialize all options and their descriptions
let marginLength = 0;
let seenKnownKeys = 0;
const entries: { value: string; description?: string; }[] = [];
categorizedOptions.forEach((options, category) => {
if (entries.length !== 0) {
entries.push({ value: "" });
}
entries.push({ value: `/* ${getLocaleSpecificMessage(category)} */` });
for (const option of options) {
let optionName;
if (compilerOptionsMap.has(option.name)) {
optionName = `"${option.name}": ${JSON.stringify(compilerOptionsMap.get(option.name))}${(seenKnownKeys += 1) === compilerOptionsMap.size ? "" : ","}`;
}
else {
optionName = `// "${option.name}": ${JSON.stringify(getDefaultValueForOption(option))},`;
}
entries.push({
value: optionName,
description: `/* ${option.description && getLocaleSpecificMessage(option.description) || option.name} */`,
});
marginLength = Math.max(optionName.length, marginLength);
}
});

// Write the output
const tab = makePadding(2);
const result: string[] = [];
result.push(`{`);
result.push(`${tab}"compilerOptions": {`);
result.push(`${tab}${tab}/* ${getLocaleSpecificMessage(Diagnostics.Visit_https_Colon_Slash_Slashaka_ms_Slashtsconfig_to_read_more_about_this_file)} */`);
result.push("");
// Print out each row, aligning all the descriptions on the same column.
for (const entry of entries) {
const { value, description = "" } = entry;
result.push(value && `${tab}${tab}${value}${description && (makePadding(marginLength - value.length + 2) + description)}`);
let comment: boolean;
if (commented === "always") {
comment = true;
}
if (fileNames.length) {
result.push(`${tab}},`);
result.push(`${tab}"files": [`);
for (let i = 0; i < fileNames.length; i++) {
result.push(`${tab}${tab}${JSON.stringify(fileNames[i])}${i === fileNames.length - 1 ? "" : ","}`);
}
result.push(`${tab}]`);
else if (commented === "never") {
comment = false;
}
else {
result.push(`${tab}}`);
comment = !hasProperty(options, setting);
}
result.push(`}`);

return result.join(newLine) + newLine;
const value = (options[setting] ?? defaultValue) as PresetValue;
if (comment) {
result.push(`${tab}${tab}// "${setting}": ${formatValueOrArray(setting, value)},`);
}
else {
result.push(`${tab}${tab}"${setting}": ${formatValueOrArray(setting, value)},`);
}
}

function formatValueOrArray(settingName: string, value: PresetValue): string {
const option = optionDeclarations.filter(c => c.name === settingName)[0];
if (!option) Debug.fail(`No option named ${settingName}?`);
const map = (option.type instanceof Map) ? option.type : undefined;
if (isArray(value)) {
// eslint-disable-next-line local/no-in-operator
const map = ("element" in option && (option.element.type instanceof Map)) ? option.element.type : undefined;
return `[${value.map(v => formatSingleValue(v, map)).join(", ")}]`;
}
else {
return formatSingleValue(value, map);
}
}

function formatSingleValue(value: PresetValue, map: Map<string, string | number> | undefined) {
if (map) {
value = getNameOfCompilerOptionValue(value as string | number, map) ?? Debug.fail(`No matching value of ${value}`);
}
return JSON.stringify(value);
}

result.push(`${tab}}`);
result.push(`}`);
result.push(``);

return result.join(newLine);
}

/** @internal */
Expand Down Expand Up @@ -4256,25 +4261,3 @@ function getOptionValueWithEmptyStrings(value: any, option: CommandLineOption):
});
}
}

function getDefaultValueForOption(option: CommandLineOption): {} {
switch (option.type) {
case "number":
return 1;
case "boolean":
return true;
case "string":
const defaultValue = option.defaultValueDescription;
return option.isFilePath ? `./${defaultValue && typeof defaultValue === "string" ? defaultValue : ""}` : "";
case "list":
return [];
case "listOrElement":
return getDefaultValueForOption(option.element);
case "object":
return {};
default:
const value = firstOrUndefinedIterator(option.type.keys());
if (value !== undefined) return value;
return Debug.fail("Expected 'option.type' to have entries.");
}
}
36 changes: 36 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -5702,6 +5702,42 @@
"category": "Message",
"code": 6283
},
"File Layout": {
"category": "Message",
"code": 6284
},
"Environment Settings": {
"category": "Message",
"code": 6285
},
"See also https://aka.ms/tsconfig/module": {
"category": "Message",
"code": 6286
},
"For nodejs:": {
"category": "Message",
"code": 6287
},
"and npm install -D @types/node": {
"category": "Message",
"code": 6290
},
"Other Outputs": {
"category": "Message",
"code": 6291
},
"Stricter Typechecking Options": {
"category": "Message",
"code": 6292
},
"Style Options": {
"category": "Message",
"code": 6293
},
"Recommended Options": {
"category": "Message",
"code": 6294
},

"Enable project compilation": {
"category": "Message",
Expand Down
9 changes: 3 additions & 6 deletions src/compiler/executeCommandLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ import {
formatMessage,
generateTSConfig,
getBuildOrderFromAnyBuildOrder,
getCompilerOptionsDiffValue,
getConfigFileParsingDiagnostics,
getDiagnosticText,
getErrorSummaryText,
Expand Down Expand Up @@ -574,7 +573,7 @@ function executeCommandLineWorker(
}

if (commandLine.options.init) {
writeConfigFile(sys, reportDiagnostic, commandLine.options, commandLine.fileNames);
writeConfigFile(sys, reportDiagnostic, commandLine.options);
return sys.exit(ExitStatus.Success);
}

Expand Down Expand Up @@ -1277,17 +1276,15 @@ function writeConfigFile(
sys: System,
reportDiagnostic: DiagnosticReporter,
options: CompilerOptions,
fileNames: string[],
) {
const currentDirectory = sys.getCurrentDirectory();
const file = normalizePath(combinePaths(currentDirectory, "tsconfig.json"));
if (sys.fileExists(file)) {
reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_tsconfig_json_file_is_already_defined_at_Colon_0, file));
}
else {
sys.writeFile(file, generateTSConfig(options, fileNames, sys.newLine));
const output: string[] = [sys.newLine, ...getHeader(sys, "Created a new tsconfig.json with:")];
output.push(getCompilerOptionsDiffValue(options, sys.newLine) + sys.newLine + sys.newLine);
sys.writeFile(file, generateTSConfig(options, sys.newLine));
const output: string[] = [sys.newLine, ...getHeader(sys, "Created a new tsconfig.json")];
output.push(`You can learn more at https://aka.ms/tsconfig` + sys.newLine);
for (const line of output) {
sys.write(line);
Expand Down
2 changes: 1 addition & 1 deletion src/testRunner/unittests/config/initializeTSConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe("unittests:: config:: initTSConfig", () => {
function initTSConfigCorrectly(name: string, commandLinesArgs: string[]) {
describe(name, () => {
const commandLine = ts.parseCommandLine(commandLinesArgs);
const initResult = ts.generateTSConfig(commandLine.options, commandLine.fileNames, "\n");
const initResult = ts.generateTSConfig(commandLine.options, "\n");
const outputFileName = `config/initTSConfig/${name.replace(/[^a-z0-9\-. ]/gi, "")}/tsconfig.json`;

it(`Correct output for ${outputFileName}`, () => {
Expand Down
Loading