Skip to content
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.

I was like "oh no, hope nobody calls this", but no, it's all callers from 5+ years ago (https://github.com/search?q=%2Fts%28+as+%5B%5E%28%5D%2B%5C%29%29%3F%5C.generateTSConfig%5C%28%2F+-path%3Atestrunner&type=code)

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