New command line help developer experience. (#44157)

* refactor help CLI output.

* fix typo and omit false condition boolean output.

* fix typo

* add line break

* add --plugins option to basic option category.

* add ts icon and remove old print help function.

* better command description for --jsx.

* Fix test

* fix lint

* Improvements to the UX

* Fix tests

* fix test.

* add missing help out.

* use environemnt variable to control test case.

* fix merge issues.

* No `No Category`

* make getWidthOfTerminal optional.

* fix test

* fix review.

* fix review.

* Update baselines

Co-authored-by: Orta <git@orta.io>
This commit is contained in:
Song Gao
2021-06-16 21:18:00 +08:00
committed by GitHub
parent 6bb1f0792b
commit 971133d5d0
13 changed files with 888 additions and 220 deletions

View File

@@ -24,7 +24,7 @@ namespace ts {
}
function getCountsMap() {
const counts = createMap<number>();
const counts = new Map<string, number>();
counts.set("Library", 0);
counts.set("Definitions", 0);
counts.set("TypeScript", 0);
@@ -89,111 +89,331 @@ namespace ts {
sys.write(getDiagnosticText(Diagnostics.Version_0, version) + sys.newLine);
}
function printHelp(sys: System, optionsList: readonly CommandLineOption[], syntaxPrefix = "") {
const output: string[] = [];
function createColors(sys: System) {
const showColors = defaultIsPretty(sys);
if (!showColors) {
return {
bold: (str: string) => str,
blue: (str: string) => str,
blueBackground: (str: string) => str,
white: (str: string) => str
};
}
// We want to align our "syntax" and "examples" commands to a certain margin.
const syntaxLength = getDiagnosticText(Diagnostics.Syntax_Colon_0, "").length;
const examplesLength = getDiagnosticText(Diagnostics.Examples_Colon_0, "").length;
let marginLength = Math.max(syntaxLength, examplesLength);
function bold(str: string) {
return `\x1b[1m${str}\x1b[22m`;
}
function blue(str: string) {
return `\x1b[34m${str}\x1b[39m`;
}
function blueBackground(str: string) {
return `\x1b[44m${str}\x1b[49m`;
}
function white(str: string) {
return `\x1b[37m${str}\x1b[39m`;
}
return {
bold,
blue,
white,
blueBackground
};
}
// Build up the syntactic skeleton.
let syntax = makePadding(marginLength - syntaxLength);
syntax += `tsc ${syntaxPrefix}[${getDiagnosticText(Diagnostics.options)}] [${getDiagnosticText(Diagnostics.file)}...]`;
function getDisplayNameTextOfOption(option: CommandLineOption) {
return `--${option.name}${option.shortName ? `, -${option.shortName}` : ""}`;
}
output.push(getDiagnosticText(Diagnostics.Syntax_Colon_0, syntax));
output.push(sys.newLine + sys.newLine);
function generateOptionOutput(sys: System, option: CommandLineOption, rightAlignOfLeft: number, leftAlignOfRight: number) {
interface ValueCandidate {
// "one or more" or "any of"
valueType: string;
possibleValues: string;
}
// Build up the list of examples.
const padding = makePadding(marginLength);
output.push(getDiagnosticText(Diagnostics.Examples_Colon_0, makePadding(marginLength - examplesLength) + "tsc hello.ts") + sys.newLine);
output.push(padding + "tsc --outFile file.js file.ts" + sys.newLine);
output.push(padding + "tsc @args.txt" + sys.newLine);
output.push(padding + "tsc --build tsconfig.json" + sys.newLine);
output.push(sys.newLine);
const text: string[] = [];
const colors = createColors(sys);
output.push(getDiagnosticText(Diagnostics.Options_Colon) + sys.newLine);
// name and description
const name = getDisplayNameTextOfOption(option);
// We want our descriptions to align at the same column in our output,
// so we keep track of the longest option usage string.
marginLength = 0;
const usageColumn: string[] = []; // Things like "-d, --declaration" go in here.
const descriptionColumn: string[] = [];
// value type and possible value
const valueCandidates = getValueCandidate(option);
const defaultValueDescription = typeof option.defaultValueDescription === "object" ? getDiagnosticText(option.defaultValueDescription) : option.defaultValueDescription;
const terminalWidth = sys.getWidthOfTerminal?.() ?? 0;
const optionsDescriptionMap = new Map<string, string[]>(); // Map between option.description and list of option.type if it is a kind
// Note: child_process might return `terminalWidth` as undefined.
if (terminalWidth >= 60) {
let description = "";
if (option.description) {
description = getDiagnosticText(option.description);
}
text.push(...getPrettyOutput(name, description, rightAlignOfLeft, leftAlignOfRight, terminalWidth, /*colorLeft*/ true), sys.newLine);
if (showAdditionalInfoOutput(valueCandidates, option)) {
if (valueCandidates) {
text.push(...getPrettyOutput(valueCandidates.valueType, valueCandidates.possibleValues, rightAlignOfLeft, leftAlignOfRight, terminalWidth, /*colorLeft*/ false), sys.newLine);
}
if (defaultValueDescription) {
text.push(...getPrettyOutput(getDiagnosticText(Diagnostics.default_Colon), defaultValueDescription, rightAlignOfLeft, leftAlignOfRight, terminalWidth, /*colorLeft*/ false), sys.newLine);
}
}
text.push(sys.newLine);
}
else {
text.push(colors.blue(name), sys.newLine);
if (option.description) {
const description = getDiagnosticText(option.description);
text.push(description);
}
text.push(sys.newLine);
if (showAdditionalInfoOutput(valueCandidates, option)) {
if (valueCandidates) {
text.push(`${valueCandidates.valueType} ${valueCandidates.possibleValues}`);
}
if (defaultValueDescription) {
if (valueCandidates) text.push(sys.newLine);
const diagType = getDiagnosticText(Diagnostics.default_Colon);
text.push(`${diagType} ${defaultValueDescription}`);
}
text.push(sys.newLine);
}
text.push(sys.newLine);
}
return text;
function showAdditionalInfoOutput(valueCandidates: ValueCandidate | undefined, option: CommandLineOption): boolean {
const ignoreValues = ["string"];
const ignoredDescriptions = [undefined, "false", "n/a"];
const defaultValueDescription = option.defaultValueDescription;
if (option.category === Diagnostics.Command_line_Options) return false;
if (contains(ignoreValues, valueCandidates?.possibleValues) && contains(ignoredDescriptions, defaultValueDescription)) {
return false;
}
return true;
}
function getPrettyOutput(left: string, right: string, rightAlignOfLeft: number, leftAlignOfRight: number, terminalWidth: number, colorLeft: boolean) {
const res = [];
let isFirstLine = true;
let remainRight = right;
const rightCharacterNumber = terminalWidth - leftAlignOfRight;
while (remainRight.length > 0) {
let curLeft = "";
if (isFirstLine) {
curLeft = padLeft(left, rightAlignOfLeft);
curLeft = padRight(curLeft, leftAlignOfRight);
curLeft = colorLeft ? colors.blue(curLeft) : curLeft;
}
else {
curLeft = padLeft("", leftAlignOfRight);
}
const curRight = remainRight.substr(0, rightCharacterNumber);
remainRight = remainRight.slice(rightCharacterNumber);
res.push(`${curLeft}${curRight}`);
isFirstLine = false;
}
return res;
}
function getValueCandidate(option: CommandLineOption): ValueCandidate | undefined {
// option.type might be "string" | "number" | "boolean" | "object" | "list" | ESMap<string, number | string>
// string -- any of: string
// number -- any of: number
// boolean -- any of: boolean
// object -- null
// list -- one or more: , content depends on `option.element.type`, the same as others
// ESMap<string, number | string> -- any of: key1, key2, ....
if (option.type === "object") {
return undefined;
}
return {
valueType: getValueType(option),
possibleValues: getPossibleValues(option)
};
function getValueType(option: CommandLineOption) {
switch (option.type) {
case "string":
case "number":
case "boolean":
return getDiagnosticText(Diagnostics.type_Colon);
case "list":
return getDiagnosticText(Diagnostics.one_or_more_Colon);
default:
return getDiagnosticText(Diagnostics.one_of_Colon);
}
}
function getPossibleValues(option: CommandLineOption) {
let possibleValues: string;
switch (option.type) {
case "string":
case "number":
case "boolean":
possibleValues = option.type;
break;
case "list":
// TODO: check infinite loop
possibleValues = getPossibleValues(option.element);
break;
case "object":
possibleValues = "";
break;
default:
// ESMap<string, number | string>
const keys = arrayFrom(option.type.keys());
possibleValues = keys.join(", ");
}
return possibleValues;
}
}
}
function generateGroupOptionOutput(sys: System, optionsList: readonly CommandLineOption[]) {
let maxLength = 0;
for (const option of optionsList) {
// If an option lacks a description,
// it is not officially supported.
if (!option.description) {
const curLength = getDisplayNameTextOfOption(option).length;
maxLength = maxLength > curLength ? maxLength : curLength;
}
// left part should be right align, right part should be left align
// assume 2 space between left margin and left part.
const rightAlignOfLeftPart = maxLength + 2;
// assume 2 space between left and right part
const leftAlignOfRightPart = rightAlignOfLeftPart + 2;
let lines: string[] = [];
for (const option of optionsList) {
const tmp = generateOptionOutput(sys, option, rightAlignOfLeftPart, leftAlignOfRightPart);
lines = [...lines, ...tmp];
}
// make sure always a blank line in the end.
if (lines[lines.length - 2] !== sys.newLine) {
lines.push(sys.newLine);
}
return lines;
}
function generateSectionOptionsOutput(sys: System, sectionName: string, options: readonly CommandLineOption[], subCategory: boolean, beforeOptionsDescription?: string, afterOptionsDescription?: string) {
let res: string[] = [];
res.push(createColors(sys).bold(sectionName) + sys.newLine + sys.newLine);
if (beforeOptionsDescription) {
res.push(beforeOptionsDescription + sys.newLine + sys.newLine);
}
if (!subCategory) {
res = [...res, ...generateGroupOptionOutput(sys, options)];
if (afterOptionsDescription) {
res.push(afterOptionsDescription + sys.newLine + sys.newLine);
}
return res;
}
const categoryMap = new Map<string, CommandLineOption[]>();
for (const option of options) {
if (!option.category) {
continue;
}
let usageText = " ";
if (option.shortName) {
usageText += "-" + option.shortName;
usageText += getParamType(option);
usageText += ", ";
}
usageText += "--" + option.name;
usageText += getParamType(option);
usageColumn.push(usageText);
let description: string;
if (option.name === "lib") {
description = getDiagnosticText(option.description);
const element = (option as CommandLineOptionOfListType).element;
const typeMap = element.type as ESMap<string, number | string>;
optionsDescriptionMap.set(description, arrayFrom(typeMap.keys()).map(key => `'${key}'`));
}
else {
description = getDiagnosticText(option.description);
}
descriptionColumn.push(description);
// Set the new margin for the description column if necessary.
marginLength = Math.max(usageText.length, marginLength);
const curCategory = getDiagnosticText(option.category);
const optionsOfCurCategory = categoryMap.get(curCategory) ?? [];
optionsOfCurCategory.push(option);
categoryMap.set(curCategory, optionsOfCurCategory);
}
// Special case that can't fit in the loop.
const usageText = " @<" + getDiagnosticText(Diagnostics.file) + ">";
usageColumn.push(usageText);
descriptionColumn.push(getDiagnosticText(Diagnostics.Insert_command_line_options_and_files_from_a_file));
marginLength = Math.max(usageText.length, marginLength);
// Print out each row, aligning all the descriptions on the same column.
for (let i = 0; i < usageColumn.length; i++) {
const usage = usageColumn[i];
const description = descriptionColumn[i];
const kindsList = optionsDescriptionMap.get(description);
output.push(usage + makePadding(marginLength - usage.length + 2) + description + sys.newLine);
if (kindsList) {
output.push(makePadding(marginLength + 4));
for (const kind of kindsList) {
output.push(kind + " ");
}
output.push(sys.newLine);
}
categoryMap.forEach((value, key) => {
res.push(`### ${key}${sys.newLine}${sys.newLine}`);
res = [...res, ...generateGroupOptionOutput(sys, value)];
});
if (afterOptionsDescription) {
res.push(afterOptionsDescription + sys.newLine + sys.newLine);
}
return res;
}
function printEasyHelp(sys: System, simpleOptions: readonly CommandLineOption[]) {
const colors = createColors(sys);
let output: string[] = [...getHelpHeader(sys)];
output.push(colors.bold(getDiagnosticText(Diagnostics.COMMON_COMMANDS)) + sys.newLine + sys.newLine);
example("tsc", Diagnostics.Compiles_the_current_project_tsconfig_json_in_the_working_directory);
example("tsc app.ts util.ts", Diagnostics.Ignoring_tsconfig_json_compiles_the_specified_files_with_default_compiler_options);
example("tsc -b", Diagnostics.Build_a_composite_project_in_the_working_directory);
example("tsc --init", Diagnostics.Creates_a_tsconfig_json_with_the_recommended_settings_in_the_working_directory);
example("tsc -p .path/to/tsconfig.json", Diagnostics.Compiles_the_TypeScript_project_located_at_the_specified_path);
example("tsc --help --all", Diagnostics.An_expanded_version_of_this_information_showing_all_possible_compiler_options);
example(["tsc --noEmit", "tsc --target esnext"], Diagnostics.Compiles_the_current_project_with_additional_settings);
const cliCommands = simpleOptions.filter(opt => opt.isCommandLineOnly || opt.category === Diagnostics.Command_line_Options);
const configOpts = simpleOptions.filter(opt => !contains(cliCommands, opt));
output = [
...output,
...generateSectionOptionsOutput(sys, getDiagnosticText(Diagnostics.COMMAND_LINE_FLAGS), cliCommands, /*subCategory*/ false, /* beforeOptionsDescription */ undefined, /* afterOptionsDescription*/ undefined),
...generateSectionOptionsOutput(sys, getDiagnosticText(Diagnostics.COMMON_COMPILER_OPTIONS), configOpts, /*subCategory*/ false, /* beforeOptionsDescription */ undefined, formatMessage(/*_dummy*/ undefined, Diagnostics.You_can_learn_about_all_of_the_compiler_options_at_0, "https://aka.ms/tsconfig-reference"))
];
for (const line of output) {
sys.write(line);
}
return;
function getParamType(option: CommandLineOption) {
if (option.paramType !== undefined) {
return " " + getDiagnosticText(option.paramType);
function example(ex: string | string[], desc: DiagnosticMessage) {
const examples = typeof ex === "string" ? [ex] : ex;
for (const example of examples) {
output.push(" " + colors.blue(example) + sys.newLine);
}
return "";
output.push(" " + getDiagnosticText(desc) + sys.newLine + sys.newLine);
}
}
function makePadding(paddingLength: number): string {
return Array(paddingLength + 1).join(" ");
function printAllHelp(sys: System, compilerOptions: readonly CommandLineOption[], buildOptions: readonly CommandLineOption[], watchOptions: readonly CommandLineOption[]) {
let output: string[] = [...getHelpHeader(sys)];
output = [...output, ...generateSectionOptionsOutput(sys, getDiagnosticText(Diagnostics.ALL_COMPILER_OPTIONS), compilerOptions, /*subCategory*/ true, /* beforeOptionsDescription */ undefined, formatMessage(/*_dummy*/ undefined, Diagnostics.You_can_learn_about_all_of_the_compiler_options_at_0, "https://aka.ms/tsconfig-reference"))];
output = [...output, ...generateSectionOptionsOutput(sys, getDiagnosticText(Diagnostics.WATCH_OPTIONS), watchOptions, /*subCategory*/ false, getDiagnosticText(Diagnostics.Including_watch_w_will_start_watching_the_current_project_for_the_file_changes_Once_set_you_can_config_watch_mode_with_Colon))];
output = [...output, ...generateSectionOptionsOutput(sys, getDiagnosticText(Diagnostics.BUILD_OPTIONS), buildOptions, /*subCategory*/ false, formatMessage(/*_dummy*/ undefined, Diagnostics.Using_build_b_will_make_tsc_behave_more_like_a_build_orchestrator_than_a_compiler_This_is_used_to_trigger_building_composite_projects_which_you_can_learn_more_about_at_0, "https://aka.ms/tsc-composite-builds"))];
for (const line of output) {
sys.write(line);
}
}
function printBuildHelp(sys: System, buildOptions: readonly CommandLineOption[]) {
let output: string[] = [...getHelpHeader(sys)];
output = [...output, ...generateSectionOptionsOutput(sys, getDiagnosticText(Diagnostics.BUILD_OPTIONS), buildOptions, /*subCategory*/ false, formatMessage(/*_dummy*/ undefined, Diagnostics.Using_build_b_will_make_tsc_behave_more_like_a_build_orchestrator_than_a_compiler_This_is_used_to_trigger_building_composite_projects_which_you_can_learn_more_about_at_0, "https://aka.ms/tsc-composite-builds"))];
for (const line of output) {
sys.write(line);
}
}
function getHelpHeader(sys: System) {
const colors = createColors(sys);
const header: string[] = [];
const tscExplanation = `${getDiagnosticText(Diagnostics.tsc_Colon_The_TypeScript_Compiler)} - ${getDiagnosticText(Diagnostics.Version_0, version)}`;
const terminalWidth = sys.getWidthOfTerminal?.() ?? 0;;
const tsIconLength = 5;
const tsIconFirstLine = colors.blueBackground(padLeft("", tsIconLength));
const tsIconSecondLine = colors.blueBackground(colors.white(padLeft("TS ", tsIconLength)));
// If we have enough space, print TS icon.
if (terminalWidth >= tscExplanation.length + tsIconLength) {
// right align of the icon is 120 at most.
const rightAlign = terminalWidth > 120 ? 120 : terminalWidth;
const leftAlign = rightAlign - tsIconLength;
header.push(padRight(tscExplanation, leftAlign) + tsIconFirstLine + sys.newLine);
header.push(padLeft("", leftAlign) + tsIconSecondLine + sys.newLine);
}
else {
header.push(tscExplanation + sys.newLine);
header.push(sys.newLine);
}
return header;
}
function printHelp(sys: System, commandLine: ParsedCommandLine) {
if (!commandLine.options.all) {
printEasyHelp(sys, getOptionsForHelp(commandLine));
}
else {
printAllHelp(sys, getOptionsForHelp(commandLine), optionsForBuild, optionsForWatch);
}
}
@@ -232,8 +452,7 @@ namespace ts {
}
if (commandLine.options.help || commandLine.options.all) {
printVersion(sys);
printHelp(sys, getOptionsForHelp(commandLine));
printHelp(sys, commandLine);
return sys.exit(ExitStatus.Success);
}
@@ -275,7 +494,7 @@ namespace ts {
}
else {
printVersion(sys);
printHelp(sys, getOptionsForHelp(commandLine));
printHelp(sys, commandLine);
}
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
}
@@ -463,13 +682,13 @@ namespace ts {
if (buildOptions.help) {
printVersion(sys);
printHelp(sys, buildOpts, "--build ");
printBuildHelp(sys, buildOpts);
return sys.exit(ExitStatus.Success);
}
if (projects.length === 0) {
printVersion(sys);
printHelp(sys, buildOpts, "--build ");
printBuildHelp(sys, buildOpts);
return sys.exit(ExitStatus.Success);
}
@@ -671,7 +890,7 @@ namespace ts {
if (canTrace(system, compilerOptions)) {
startTracing(isBuildMode ? "build" : "project",
compilerOptions.generateTrace!, compilerOptions.configFilePath);
compilerOptions.generateTrace!, compilerOptions.configFilePath);
}
}