From 8faa7d61fd13090c890fa1646ef5d0eb542b9056 Mon Sep 17 00:00:00 2001 From: Jason Dent Date: Wed, 8 Jan 2025 15:28:18 +0100 Subject: [PATCH] fix: Upgrade Commander to v13 (#6764) --- integration-tests/package.json | 2 +- packages/cspell-tools/package.json | 2 +- packages/cspell-trie/package.json | 2 +- packages/cspell/package.json | 2 +- .../src/app/__snapshots__/app.test.ts.snap | 35 +- packages/cspell/src/app/commandLint.ts | 11 +- packages/hunspell-reader/package.json | 2 +- pnpm-lock.yaml | 32 +- tools/perf-chart/lib/app.cjs | 545 +++++++++++++----- tools/perf-chart/package.json | 2 +- 10 files changed, 458 insertions(+), 177 deletions(-) diff --git a/integration-tests/package.json b/integration-tests/package.json index debdff203d3b..c7a8cbbda107 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -32,7 +32,7 @@ "@octokit/rest": "^21.0.2", "ansi-escapes": "^7.0.0", "chalk": "^5.4.1", - "commander": "^12.1.0", + "commander": "^13.0.0", "csv-parse": "^5.6.0", "csv-stringify": "^6.5.2", "jest-diff": "^29.7.0", diff --git a/packages/cspell-tools/package.json b/packages/cspell-tools/package.json index 2ae3aff1384e..6a5bbc0849a4 100644 --- a/packages/cspell-tools/package.json +++ b/packages/cspell-tools/package.json @@ -52,7 +52,7 @@ "homepage": "https://github.com/streetsidesoftware/cspell/tree/main/packages/cspell-tools#readme", "dependencies": { "@cspell/cspell-pipe": "workspace:*", - "commander": "^12.1.0", + "commander": "^13.0.0", "cosmiconfig": "9.0.0", "cspell-trie-lib": "workspace:*", "glob": "^10.4.5", diff --git a/packages/cspell-trie/package.json b/packages/cspell-trie/package.json index 2dec57814d93..22faa45de7db 100644 --- a/packages/cspell-trie/package.json +++ b/packages/cspell-trie/package.json @@ -45,7 +45,7 @@ }, "homepage": "https://github.com/streetsidesoftware/cspell/tree/main/packages/cspell-trie#readme", "dependencies": { - "commander": "^12.1.0", + "commander": "^13.0.0", "cspell-trie-lib": "workspace:*", "gensequence": "^7.0.0" }, diff --git a/packages/cspell/package.json b/packages/cspell/package.json index ee7cb8099589..255fdf5f76de 100644 --- a/packages/cspell/package.json +++ b/packages/cspell/package.json @@ -88,7 +88,7 @@ "@cspell/url": "workspace:*", "chalk": "^5.4.1", "chalk-template": "^1.1.0", - "commander": "^12.1.0", + "commander": "^13.0.0", "cspell-dictionary": "workspace:*", "cspell-gitignore": "workspace:*", "cspell-glob": "workspace:*", diff --git a/packages/cspell/src/app/__snapshots__/app.test.ts.snap b/packages/cspell/src/app/__snapshots__/app.test.ts.snap index 80fe5a176210..93cc8404b1ff 100644 --- a/packages/cspell/src/app/__snapshots__/app.test.ts.snap +++ b/packages/cspell/src/app/__snapshots__/app.test.ts.snap @@ -183,8 +183,7 @@ exports[`Validate cli > app 'check help' Expect Error: 'outputHelp' 1`] = ` [ "Usage: cspell check [options] ", "", - "Spell check file(s) and display the result. The full file is displayed in", - "color.", + "Spell check file(s) and display the result. The full file is displayed in color.", "", "Options:", " -c, --config Configuration file to use. By default cspell", @@ -628,8 +627,8 @@ exports[`Validate cli > app 'lint --help --issue-template' Expect Error: 'output " -u, --unique Only output the first instance of a word not", " found in the dictionaries.", " -e, --exclude Exclude files matching the glob pattern. This", - " option can be used multiple times to add", - " multiple globs.", + " option can be used multiple times to add multiple", + " globs.", " --file-list Specify a list of files to be spell checked. The", " list is filtered against the glob file patterns.", " Note: the format is 1 file path per line.", @@ -662,8 +661,7 @@ exports[`Validate cli > app 'lint --help --issue-template' Expect Error: 'output " --gitignore Ignore files matching glob patterns found in", " .gitignore files.", " --no-gitignore Do NOT use .gitignore files.", - " --gitignore-root Prevent searching for .gitignore files past", - " root.", + " --gitignore-root Prevent searching for .gitignore files past root.", " --validate-directives Validate in-document CSpell directives.", " --color Force color.", " --no-color Turn off color.", @@ -772,8 +770,8 @@ exports[`Validate cli > app 'lint --help --verbose' Expect Error: 'outputHelp' 1 " -u, --unique Only output the first instance of a word not", " found in the dictionaries.", " -e, --exclude Exclude files matching the glob pattern. This", - " option can be used multiple times to add", - " multiple globs.", + " option can be used multiple times to add multiple", + " globs.", " --file-list Specify a list of files to be spell checked. The", " list is filtered against the glob file patterns.", " Note: the format is 1 file path per line.", @@ -806,8 +804,7 @@ exports[`Validate cli > app 'lint --help --verbose' Expect Error: 'outputHelp' 1 " --gitignore Ignore files matching glob patterns found in", " .gitignore files.", " --no-gitignore Do NOT use .gitignore files.", - " --gitignore-root Prevent searching for .gitignore files past", - " root.", + " --gitignore-root Prevent searching for .gitignore files past root.", " --validate-directives Validate in-document CSpell directives.", " --color Force color.", " --no-color Turn off color.", @@ -909,8 +906,8 @@ exports[`Validate cli > app 'lint --help' Expect Error: 'outputHelp' 1`] = ` " -u, --unique Only output the first instance of a word not", " found in the dictionaries.", " -e, --exclude Exclude files matching the glob pattern. This", - " option can be used multiple times to add", - " multiple globs.", + " option can be used multiple times to add multiple", + " globs.", " --file-list Specify a list of files to be spell checked. The", " list is filtered against the glob file patterns.", " Note: the format is 1 file path per line.", @@ -943,8 +940,7 @@ exports[`Validate cli > app 'lint --help' Expect Error: 'outputHelp' 1`] = ` " --gitignore Ignore files matching glob patterns found in", " .gitignore files.", " --no-gitignore Do NOT use .gitignore files.", - " --gitignore-root Prevent searching for .gitignore files past", - " root.", + " --gitignore-root Prevent searching for .gitignore files past root.", " --validate-directives Validate in-document CSpell directives.", " --color Force color.", " --no-color Turn off color.", @@ -1044,8 +1040,8 @@ exports[`Validate cli > app 'no-args' Expect Error: 'outputHelp' 1`] = ` " -u, --unique Only output the first instance of a word not", " found in the dictionaries.", " -e, --exclude Exclude files matching the glob pattern. This", - " option can be used multiple times to add", - " multiple globs.", + " option can be used multiple times to add multiple", + " globs.", " --file-list Specify a list of files to be spell checked. The", " list is filtered against the glob file patterns.", " Note: the format is 1 file path per line.", @@ -1078,8 +1074,7 @@ exports[`Validate cli > app 'no-args' Expect Error: 'outputHelp' 1`] = ` " --gitignore Ignore files matching glob patterns found in", " .gitignore files.", " --no-gitignore Do NOT use .gitignore files.", - " --gitignore-root Prevent searching for .gitignore files past", - " root.", + " --gitignore-root Prevent searching for .gitignore files past root.", " --validate-directives Validate in-document CSpell directives.", " --color Force color.", " --no-color Turn off color.", @@ -1660,8 +1655,8 @@ exports[`Validate cli > app 'trace help' Expect Error: 'outputHelp' 1`] = ` "Options:", " -c, --config Configuration file to use. By default cspell", " looks for cspell.json in the current directory.", - " --locale Set language locales. i.e. "en,fr" for English", - " and French, or "en-GB" for British English.", + " --locale Set language locales. i.e. "en,fr" for English and", + " French, or "en-GB" for British English.", " --language-id Use programming language. i.e. "php" or "scala".", " --allow-compound-words Turn on allowCompoundWords", " --no-allow-compound-words Turn off allowCompoundWords", diff --git a/packages/cspell/src/app/commandLint.ts b/packages/cspell/src/app/commandLint.ts index 26dba4cf4db2..209ada30a879 100644 --- a/packages/cspell/src/app/commandLint.ts +++ b/packages/cspell/src/app/commandLint.ts @@ -244,20 +244,15 @@ function augmentCommandHelp(context: AddHelpTextContext) { const showHidden = !!opts.verbose; const hiddenHelp: string[] = []; const help = command.createHelp(); + help.helpWidth = process.stdout.columns || 80; const hiddenOptions = command.options.filter((opt) => opt.hidden && showHidden); const flagColWidth = Math.max(...command.options.map((opt) => opt.flags.length), 0); - const indent = flagColWidth + 4; + // const indent = flagColWidth + 4; for (const options of hiddenOptions) { if (!hiddenHelp.length) { hiddenHelp.push('\nHidden Options:'); } - hiddenHelp.push( - help.wrap( - ` ${options.flags.padEnd(flagColWidth)} ${options.description}`, - process.stdout.columns || 80, - indent, - ), - ); + hiddenHelp.push(help.formatItem(options.flags, flagColWidth, options.description, help)); } output.push(...hiddenHelp, advanced); return helpIssueTemplate(opts) + output.join('\n'); diff --git a/packages/hunspell-reader/package.json b/packages/hunspell-reader/package.json index 55ab7822a1cd..80adf5368cc9 100644 --- a/packages/hunspell-reader/package.json +++ b/packages/hunspell-reader/package.json @@ -50,7 +50,7 @@ "dependencies": { "@cspell/cspell-pipe": "workspace:^", "@cspell/cspell-types": "workspace:^", - "commander": "^12.1.0", + "commander": "^13.0.0", "gensequence": "^7.0.0", "html-entities": "^2.5.2", "iconv-lite": "^0.6.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e06336cb3e13..dfdb9abf874d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -161,8 +161,8 @@ importers: specifier: ^5.4.1 version: 5.4.1 commander: - specifier: ^12.1.0 - version: 12.1.0 + specifier: ^13.0.0 + version: 13.0.0 csv-parse: specifier: ^5.6.0 version: 5.6.0 @@ -234,8 +234,8 @@ importers: specifier: ^1.1.0 version: 1.1.0 commander: - specifier: ^12.1.0 - version: 12.1.0 + specifier: ^13.0.0 + version: 13.0.0 cspell-dictionary: specifier: workspace:* version: link:../cspell-dictionary @@ -843,8 +843,8 @@ importers: specifier: workspace:* version: link:../cspell-pipe commander: - specifier: ^12.1.0 - version: 12.1.0 + specifier: ^13.0.0 + version: 13.0.0 cosmiconfig: specifier: 9.0.0 version: 9.0.0(typescript@5.7.2) @@ -871,8 +871,8 @@ importers: packages/cspell-trie: dependencies: commander: - specifier: ^12.1.0 - version: 12.1.0 + specifier: ^13.0.0 + version: 13.0.0 cspell-trie-lib: specifier: workspace:* version: link:../cspell-trie-lib @@ -936,8 +936,8 @@ importers: specifier: workspace:^ version: link:../cspell-types commander: - specifier: ^12.1.0 - version: 12.1.0 + specifier: ^13.0.0 + version: 13.0.0 gensequence: specifier: ^7.0.0 version: 7.0.0 @@ -1336,8 +1336,8 @@ importers: tools/perf-chart: dependencies: commander: - specifier: ^12.1.0 - version: 12.1.0 + specifier: ^13.0.0 + version: 13.0.0 csv-parse: specifier: ^5.6.0 version: 5.6.0 @@ -5128,6 +5128,10 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@13.0.0: + resolution: {integrity: sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==} + engines: {node: '>=18'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -12775,7 +12779,7 @@ snapshots: '@docusaurus/react-loadable@6.0.0(react@18.3.1)': dependencies: - '@types/react': 19.0.3 + '@types/react': 18.3.18 react: 18.3.1 '@docusaurus/remark-plugin-npm2yarn@3.7.0': @@ -15718,6 +15722,8 @@ snapshots: commander@12.1.0: {} + commander@13.0.0: {} + commander@2.20.3: {} commander@4.1.1: {} diff --git a/tools/perf-chart/lib/app.cjs b/tools/perf-chart/lib/app.cjs index 734eadf76010..72907a163081 100644 --- a/tools/perf-chart/lib/app.cjs +++ b/tools/perf-chart/lib/app.cjs @@ -25,9 +25,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge mod )); -// ../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/error.js +// ../../node_modules/.pnpm/commander@13.0.0/node_modules/commander/lib/error.js var require_error = __commonJS({ - "../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/error.js"(exports2) { + "../../node_modules/.pnpm/commander@13.0.0/node_modules/commander/lib/error.js"(exports2) { var CommanderError2 = class extends Error { /** * Constructs the CommanderError class @@ -60,9 +60,9 @@ var require_error = __commonJS({ } }); -// ../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/argument.js +// ../../node_modules/.pnpm/commander@13.0.0/node_modules/commander/lib/argument.js var require_argument = __commonJS({ - "../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/argument.js"(exports2) { + "../../node_modules/.pnpm/commander@13.0.0/node_modules/commander/lib/argument.js"(exports2) { var { InvalidArgumentError: InvalidArgumentError2 } = require_error(); var Argument2 = class { /** @@ -187,17 +187,29 @@ var require_argument = __commonJS({ } }); -// ../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/help.js +// ../../node_modules/.pnpm/commander@13.0.0/node_modules/commander/lib/help.js var require_help = __commonJS({ - "../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/help.js"(exports2) { + "../../node_modules/.pnpm/commander@13.0.0/node_modules/commander/lib/help.js"(exports2) { var { humanReadableArgName } = require_argument(); var Help2 = class { constructor() { this.helpWidth = void 0; + this.minWidthToWrap = 40; this.sortSubcommands = false; this.sortOptions = false; this.showGlobalOptions = false; } + /** + * prepareContext is called by Commander after applying overrides from `Command.configureHelp()` + * and just before calling `formatHelp()`. + * + * Commander just uses the helpWidth and the rest is provided for optional use by more complex subclasses. + * + * @param {{ error?: boolean, helpWidth?: number, outputHasColors?: boolean }} contextOptions + */ + prepareContext(contextOptions) { + this.helpWidth = this.helpWidth ?? contextOptions.helpWidth ?? 80; + } /** * Get an array of the visible subcommands. Includes a placeholder for the implicit help command, if there is one. * @@ -334,7 +346,12 @@ var require_help = __commonJS({ */ longestSubcommandTermLength(cmd, helper) { return helper.visibleCommands(cmd).reduce((max, command) => { - return Math.max(max, helper.subcommandTerm(command).length); + return Math.max( + max, + this.displayWidth( + helper.styleSubcommandTerm(helper.subcommandTerm(command)) + ) + ); }, 0); } /** @@ -346,7 +363,10 @@ var require_help = __commonJS({ */ longestOptionTermLength(cmd, helper) { return helper.visibleOptions(cmd).reduce((max, option) => { - return Math.max(max, helper.optionTerm(option).length); + return Math.max( + max, + this.displayWidth(helper.styleOptionTerm(helper.optionTerm(option))) + ); }, 0); } /** @@ -358,7 +378,10 @@ var require_help = __commonJS({ */ longestGlobalOptionTermLength(cmd, helper) { return helper.visibleGlobalOptions(cmd).reduce((max, option) => { - return Math.max(max, helper.optionTerm(option).length); + return Math.max( + max, + this.displayWidth(helper.styleOptionTerm(helper.optionTerm(option))) + ); }, 0); } /** @@ -370,7 +393,12 @@ var require_help = __commonJS({ */ longestArgumentTermLength(cmd, helper) { return helper.visibleArguments(cmd).reduce((max, argument) => { - return Math.max(max, helper.argumentTerm(argument).length); + return Math.max( + max, + this.displayWidth( + helper.styleArgumentTerm(helper.argumentTerm(argument)) + ) + ); }, 0); } /** @@ -462,11 +490,11 @@ var require_help = __commonJS({ ); } if (extraInfo.length > 0) { - const extraDescripton = `(${extraInfo.join(", ")})`; + const extraDescription = `(${extraInfo.join(", ")})`; if (argument.description) { - return `${argument.description} ${extraDescripton}`; + return `${argument.description} ${extraDescription}`; } - return extraDescripton; + return extraDescription; } return argument.description; } @@ -479,75 +507,148 @@ var require_help = __commonJS({ */ formatHelp(cmd, helper) { const termWidth = helper.padWidth(cmd, helper); - const helpWidth = helper.helpWidth || 80; - const itemIndentWidth = 2; - const itemSeparatorWidth = 2; - function formatItem(term, description) { - if (description) { - const fullText = `${term.padEnd(termWidth + itemSeparatorWidth)}${description}`; - return helper.wrap( - fullText, - helpWidth - itemIndentWidth, - termWidth + itemSeparatorWidth - ); - } - return term; - } - function formatList(textArray) { - return textArray.join("\n").replace(/^/gm, " ".repeat(itemIndentWidth)); - } - let output = [`Usage: ${helper.commandUsage(cmd)}`, ""]; + const helpWidth = helper.helpWidth ?? 80; + function callFormatItem(term, description) { + return helper.formatItem(term, termWidth, description, helper); + } + let output = [ + `${helper.styleTitle("Usage:")} ${helper.styleUsage(helper.commandUsage(cmd))}`, + "" + ]; const commandDescription = helper.commandDescription(cmd); if (commandDescription.length > 0) { output = output.concat([ - helper.wrap(commandDescription, helpWidth, 0), + helper.boxWrap( + helper.styleCommandDescription(commandDescription), + helpWidth + ), "" ]); } const argumentList = helper.visibleArguments(cmd).map((argument) => { - return formatItem( - helper.argumentTerm(argument), - helper.argumentDescription(argument) + return callFormatItem( + helper.styleArgumentTerm(helper.argumentTerm(argument)), + helper.styleArgumentDescription(helper.argumentDescription(argument)) ); }); if (argumentList.length > 0) { - output = output.concat(["Arguments:", formatList(argumentList), ""]); + output = output.concat([ + helper.styleTitle("Arguments:"), + ...argumentList, + "" + ]); } const optionList = helper.visibleOptions(cmd).map((option) => { - return formatItem( - helper.optionTerm(option), - helper.optionDescription(option) + return callFormatItem( + helper.styleOptionTerm(helper.optionTerm(option)), + helper.styleOptionDescription(helper.optionDescription(option)) ); }); if (optionList.length > 0) { - output = output.concat(["Options:", formatList(optionList), ""]); + output = output.concat([ + helper.styleTitle("Options:"), + ...optionList, + "" + ]); } - if (this.showGlobalOptions) { + if (helper.showGlobalOptions) { const globalOptionList = helper.visibleGlobalOptions(cmd).map((option) => { - return formatItem( - helper.optionTerm(option), - helper.optionDescription(option) + return callFormatItem( + helper.styleOptionTerm(helper.optionTerm(option)), + helper.styleOptionDescription(helper.optionDescription(option)) ); }); if (globalOptionList.length > 0) { output = output.concat([ - "Global Options:", - formatList(globalOptionList), + helper.styleTitle("Global Options:"), + ...globalOptionList, "" ]); } } const commandList = helper.visibleCommands(cmd).map((cmd2) => { - return formatItem( - helper.subcommandTerm(cmd2), - helper.subcommandDescription(cmd2) + return callFormatItem( + helper.styleSubcommandTerm(helper.subcommandTerm(cmd2)), + helper.styleSubcommandDescription(helper.subcommandDescription(cmd2)) ); }); if (commandList.length > 0) { - output = output.concat(["Commands:", formatList(commandList), ""]); + output = output.concat([ + helper.styleTitle("Commands:"), + ...commandList, + "" + ]); } return output.join("\n"); } + /** + * Return display width of string, ignoring ANSI escape sequences. Used in padding and wrapping calculations. + * + * @param {string} str + * @returns {number} + */ + displayWidth(str) { + return stripColor(str).length; + } + /** + * Style the title for displaying in the help. Called with 'Usage:', 'Options:', etc. + * + * @param {string} str + * @returns {string} + */ + styleTitle(str) { + return str; + } + styleUsage(str) { + return str.split(" ").map((word) => { + if (word === "[options]") return this.styleOptionText(word); + if (word === "[command]") return this.styleSubcommandText(word); + if (word[0] === "[" || word[0] === "<") + return this.styleArgumentText(word); + return this.styleCommandText(word); + }).join(" "); + } + styleCommandDescription(str) { + return this.styleDescriptionText(str); + } + styleOptionDescription(str) { + return this.styleDescriptionText(str); + } + styleSubcommandDescription(str) { + return this.styleDescriptionText(str); + } + styleArgumentDescription(str) { + return this.styleDescriptionText(str); + } + styleDescriptionText(str) { + return str; + } + styleOptionTerm(str) { + return this.styleOptionText(str); + } + styleSubcommandTerm(str) { + return str.split(" ").map((word) => { + if (word === "[options]") return this.styleOptionText(word); + if (word[0] === "[" || word[0] === "<") + return this.styleArgumentText(word); + return this.styleSubcommandText(word); + }).join(" "); + } + styleArgumentTerm(str) { + return this.styleArgumentText(str); + } + styleOptionText(str) { + return str; + } + styleArgumentText(str) { + return str; + } + styleSubcommandText(str) { + return str; + } + styleCommandText(str) { + return str; + } /** * Calculate the pad width from the maximum term length. * @@ -564,46 +665,100 @@ var require_help = __commonJS({ ); } /** - * Wrap the given string to width characters per line, with lines after the first indented. - * Do not wrap if insufficient room for wrapping (minColumnWidth), or string is manually formatted. + * Detect manually wrapped and indented strings by checking for line break followed by whitespace. * * @param {string} str - * @param {number} width - * @param {number} indent - * @param {number} [minColumnWidth=40] - * @return {string} + * @returns {boolean} + */ + preformatted(str) { + return /\n[^\S\r\n]/.test(str); + } + /** + * Format the "item", which consists of a term and description. Pad the term and wrap the description, indenting the following lines. * + * So "TTT", 5, "DDD DDDD DD DDD" might be formatted for this.helpWidth=17 like so: + * TTT DDD DDDD + * DD DDD + * + * @param {string} term + * @param {number} termWidth + * @param {string} description + * @param {Help} helper + * @returns {string} */ - wrap(str, width, indent, minColumnWidth = 40) { - const indents = " \\f\\t\\v\xA0\u1680\u2000-\u200A\u202F\u205F\u3000\uFEFF"; - const manualIndent = new RegExp(`[\\n][${indents}]+`); - if (str.match(manualIndent)) return str; - const columnWidth = width - indent; - if (columnWidth < minColumnWidth) return str; - const leadingStr = str.slice(0, indent); - const columnText = str.slice(indent).replace("\r\n", "\n"); - const indentString = " ".repeat(indent); - const zeroWidthSpace = "\u200B"; - const breaks = `\\s${zeroWidthSpace}`; - const regex = new RegExp( - ` -|.{1,${columnWidth - 1}}([${breaks}]|$)|[^${breaks}]+?([${breaks}]|$)`, - "g" + formatItem(term, termWidth, description, helper) { + const itemIndent = 2; + const itemIndentStr = " ".repeat(itemIndent); + if (!description) return itemIndentStr + term; + const paddedTerm = term.padEnd( + termWidth + term.length - helper.displayWidth(term) ); - const lines = columnText.match(regex) || []; - return leadingStr + lines.map((line, i) => { - if (line === "\n") return ""; - return (i > 0 ? indentString : "") + line.trimEnd(); - }).join("\n"); + const spacerWidth = 2; + const helpWidth = this.helpWidth ?? 80; + const remainingWidth = helpWidth - termWidth - spacerWidth - itemIndent; + let formattedDescription; + if (remainingWidth < this.minWidthToWrap || helper.preformatted(description)) { + formattedDescription = description; + } else { + const wrappedDescription = helper.boxWrap(description, remainingWidth); + formattedDescription = wrappedDescription.replace( + /\n/g, + "\n" + " ".repeat(termWidth + spacerWidth) + ); + } + return itemIndentStr + paddedTerm + " ".repeat(spacerWidth) + formattedDescription.replace(/\n/g, ` +${itemIndentStr}`); + } + /** + * Wrap a string at whitespace, preserving existing line breaks. + * Wrapping is skipped if the width is less than `minWidthToWrap`. + * + * @param {string} str + * @param {number} width + * @returns {string} + */ + boxWrap(str, width) { + if (width < this.minWidthToWrap) return str; + const rawLines = str.split(/\r\n|\n/); + const chunkPattern = /[\s]*[^\s]+/g; + const wrappedLines = []; + rawLines.forEach((line) => { + const chunks = line.match(chunkPattern); + if (chunks === null) { + wrappedLines.push(""); + return; + } + let sumChunks = [chunks.shift()]; + let sumWidth = this.displayWidth(sumChunks[0]); + chunks.forEach((chunk) => { + const visibleWidth = this.displayWidth(chunk); + if (sumWidth + visibleWidth <= width) { + sumChunks.push(chunk); + sumWidth += visibleWidth; + return; + } + wrappedLines.push(sumChunks.join("")); + const nextChunk = chunk.trimStart(); + sumChunks = [nextChunk]; + sumWidth = this.displayWidth(nextChunk); + }); + wrappedLines.push(sumChunks.join("")); + }); + return wrappedLines.join("\n"); } }; + function stripColor(str) { + const sgrPattern = /\x1b\[\d*(;\d*)*m/g; + return str.replace(sgrPattern, ""); + } exports2.Help = Help2; + exports2.stripColor = stripColor; } }); -// ../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/option.js +// ../../node_modules/.pnpm/commander@13.0.0/node_modules/commander/lib/option.js var require_option = __commonJS({ - "../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/option.js"(exports2) { + "../../node_modules/.pnpm/commander@13.0.0/node_modules/commander/lib/option.js"(exports2) { var { InvalidArgumentError: InvalidArgumentError2 } = require_error(); var Option2 = class { /** @@ -785,12 +940,15 @@ var require_option = __commonJS({ } /** * Return option name, in a camelcase format that can be used - * as a object attribute key. + * as an object attribute key. * * @return {string} */ attributeName() { - return camelcase(this.name().replace(/^no-/, "")); + if (this.negate) { + return camelcase(this.name().replace(/^no-/, "")); + } + return camelcase(this.name()); } /** * Check if `arg` matches the short or long flag. @@ -858,14 +1016,25 @@ var require_option = __commonJS({ function splitOptionFlags(flags) { let shortFlag; let longFlag; - const flagParts = flags.split(/[ |,]+/); - if (flagParts.length > 1 && !/^[[<]/.test(flagParts[1])) - shortFlag = flagParts.shift(); - longFlag = flagParts.shift(); - if (!shortFlag && /^-[^-]$/.test(longFlag)) { - shortFlag = longFlag; - longFlag = void 0; - } + const shortFlagExp = /^-[^-]$/; + const longFlagExp = /^--[^-]/; + const flagParts = flags.split(/[ |,]+/).concat("guard"); + if (shortFlagExp.test(flagParts[0])) shortFlag = flagParts.shift(); + if (longFlagExp.test(flagParts[0])) longFlag = flagParts.shift(); + if (/^-[^-][^-]/.test(flagParts[0])) + throw new Error( + `invalid Option flags, short option is dash and single character: '${flags}'` + ); + if (shortFlag && shortFlagExp.test(flagParts[0])) + throw new Error( + `invalid Option flags, more than one short flag: '${flags}'` + ); + if (longFlag && longFlagExp.test(flagParts[0])) + throw new Error( + `invalid Option flags, more than one long flag: '${flags}'` + ); + if (!(shortFlag || longFlag) || flagParts[0].startsWith("-")) + throw new Error(`invalid Option flags: '${flags}'`); return { shortFlag, longFlag }; } exports2.Option = Option2; @@ -873,9 +1042,9 @@ var require_option = __commonJS({ } }); -// ../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/suggestSimilar.js +// ../../node_modules/.pnpm/commander@13.0.0/node_modules/commander/lib/suggestSimilar.js var require_suggestSimilar = __commonJS({ - "../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/suggestSimilar.js"(exports2) { + "../../node_modules/.pnpm/commander@13.0.0/node_modules/commander/lib/suggestSimilar.js"(exports2) { var maxDistance = 3; function editDistance(a, b) { if (Math.abs(a.length - b.length) > maxDistance) @@ -953,9 +1122,9 @@ var require_suggestSimilar = __commonJS({ } }); -// ../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/command.js +// ../../node_modules/.pnpm/commander@13.0.0/node_modules/commander/lib/command.js var require_command = __commonJS({ - "../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/command.js"(exports2) { + "../../node_modules/.pnpm/commander@13.0.0/node_modules/commander/lib/command.js"(exports2) { var EventEmitter = require("node:events").EventEmitter; var childProcess = require("node:child_process"); var path = require("node:path"); @@ -963,7 +1132,7 @@ var require_command = __commonJS({ var process = require("node:process"); var { Argument: Argument2, humanReadableArgName } = require_argument(); var { CommanderError: CommanderError2 } = require_error(); - var { Help: Help2 } = require_help(); + var { Help: Help2, stripColor } = require_help(); var { Option: Option2, DualOptions } = require_option(); var { suggestSimilar } = require_suggestSimilar(); var Command2 = class _Command extends EventEmitter { @@ -978,7 +1147,7 @@ var require_command = __commonJS({ this.options = []; this.parent = null; this._allowUnknownOption = false; - this._allowExcessArguments = true; + this._allowExcessArguments = false; this.registeredArguments = []; this._args = this.registeredArguments; this.args = []; @@ -1005,12 +1174,16 @@ var require_command = __commonJS({ this._lifeCycleHooks = {}; this._showHelpAfterError = false; this._showSuggestionAfterError = true; + this._savedState = null; this._outputConfiguration = { writeOut: (str) => process.stdout.write(str), writeErr: (str) => process.stderr.write(str), + outputError: (str, write) => write(str), getOutHelpWidth: () => process.stdout.isTTY ? process.stdout.columns : void 0, getErrHelpWidth: () => process.stderr.isTTY ? process.stderr.columns : void 0, - outputError: (str, write) => write(str) + getOutHasColors: () => useColor() ?? (process.stdout.isTTY && process.stdout.hasColors?.()), + getErrHasColors: () => useColor() ?? (process.stderr.isTTY && process.stderr.hasColors?.()), + stripColor: (str) => stripColor(str) }; this._hidden = false; this._helpOption = void 0; @@ -1138,14 +1311,18 @@ var require_command = __commonJS({ * * The configuration properties are all functions: * - * // functions to change where being written, stdout and stderr + * // change how output being written, defaults to stdout and stderr * writeOut(str) * writeErr(str) - * // matching functions to specify width for wrapping help + * // change how output being written for errors, defaults to writeErr + * outputError(str, write) // used for displaying errors and not used for displaying help + * // specify width for wrapping help * getOutHelpWidth() * getErrHelpWidth() - * // functions based on what is being written out - * outputError(str, write) // used for displaying errors, and not used for displaying help + * // color support, currently only used with Help + * getOutHasColors() + * getErrHasColors() + * stripColor() // used to remove ANSI escape codes if output does not have colors * * @param {object} [configuration] - configuration options * @return {(Command | object)} `this` command for chaining, or stored configuration @@ -1852,6 +2029,7 @@ Expecting one of '${allowedValues.join("', '")}'`); * @return {Command} `this` command for chaining */ parse(argv, parseOptions) { + this._prepareForParse(); const userArgs = this._prepareUserArgs(argv, parseOptions); this._parseCommand([], userArgs); return this; @@ -1877,10 +2055,68 @@ Expecting one of '${allowedValues.join("', '")}'`); * @return {Promise} */ async parseAsync(argv, parseOptions) { + this._prepareForParse(); const userArgs = this._prepareUserArgs(argv, parseOptions); await this._parseCommand([], userArgs); return this; } + _prepareForParse() { + if (this._savedState === null) { + this.saveStateBeforeParse(); + } else { + this.restoreStateBeforeParse(); + } + } + /** + * Called the first time parse is called to save state and allow a restore before subsequent calls to parse. + * Not usually called directly, but available for subclasses to save their custom state. + * + * This is called in a lazy way. Only commands used in parsing chain will have state saved. + */ + saveStateBeforeParse() { + this._savedState = { + // name is stable if supplied by author, but may be unspecified for root command and deduced during parsing + _name: this._name, + // option values before parse have default values (including false for negated options) + // shallow clones + _optionValues: { ...this._optionValues }, + _optionValueSources: { ...this._optionValueSources } + }; + } + /** + * Restore state before parse for calls after the first. + * Not usually called directly, but available for subclasses to save their custom state. + * + * This is called in a lazy way. Only commands used in parsing chain will have state restored. + */ + restoreStateBeforeParse() { + if (this._storeOptionsAsProperties) + throw new Error(`Can not call parse again when storeOptionsAsProperties is true. +- either make a new Command for each call to parse, or stop storing options as properties`); + this._name = this._savedState._name; + this._scriptPath = null; + this.rawArgs = []; + this._optionValues = { ...this._savedState._optionValues }; + this._optionValueSources = { ...this._savedState._optionValueSources }; + this.args = []; + this.processedArgs = []; + } + /** + * Throw if expected executable is missing. Add lots of help for author. + * + * @param {string} executableFile + * @param {string} executableDir + * @param {string} subcommandName + */ + _checkForMissingExecutable(executableFile, executableDir, subcommandName) { + if (fs2.existsSync(executableFile)) return; + const executableDirMessage = executableDir ? `searched for local subcommand relative to directory '${executableDir}'` : "no directory for search for local subcommand, use .executableDir() to supply a custom directory"; + const executableMissing = `'${executableFile}' does not exist + - if '${subcommandName}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead + - if the default executable name is not suitable, use the executableFile option to supply a custom name or path + - ${executableDirMessage}`; + throw new Error(executableMissing); + } /** * Execute a sub-command executable. * @@ -1908,7 +2144,7 @@ Expecting one of '${allowedValues.join("', '")}'`); let resolvedScriptPath; try { resolvedScriptPath = fs2.realpathSync(this._scriptPath); - } catch (err) { + } catch { resolvedScriptPath = this._scriptPath; } executableDir = path.resolve( @@ -1943,6 +2179,11 @@ Expecting one of '${allowedValues.join("', '")}'`); proc = childProcess.spawn(executableFile, args, { stdio: "inherit" }); } } else { + this._checkForMissingExecutable( + executableFile, + executableDir, + subcommand._name + ); args.unshift(executableFile); args = incrementNodeInspectorPort(process.execArgv).concat(args); proc = childProcess.spawn(process.execPath, args, { stdio: "inherit" }); @@ -1974,12 +2215,11 @@ Expecting one of '${allowedValues.join("', '")}'`); }); proc.on("error", (err) => { if (err.code === "ENOENT") { - const executableDirMessage = executableDir ? `searched for local subcommand relative to directory '${executableDir}'` : "no directory for search for local subcommand, use .executableDir() to supply a custom directory"; - const executableMissing = `'${executableFile}' does not exist - - if '${subcommand._name}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead - - if the default executable name is not suitable, use the executableFile option to supply a custom name or path - - ${executableDirMessage}`; - throw new Error(executableMissing); + this._checkForMissingExecutable( + executableFile, + executableDir, + subcommand._name + ); } else if (err.code === "EACCES") { throw new Error(`'${executableFile}' not executable`); } @@ -2003,6 +2243,7 @@ Expecting one of '${allowedValues.join("', '")}'`); _dispatchSubcommand(commandName, operands, unknown) { const subCommand = this._findCommand(commandName); if (!subCommand) this.help({ error: true }); + subCommand._prepareForParse(); let promiseChain; promiseChain = this._chainOrCallSubCommandHook( promiseChain, @@ -2315,6 +2556,8 @@ Expecting one of '${allowedValues.join("', '")}'`); * Parse options from `argv` removing known options, * and return argv split into operands and unknown arguments. * + * Side effects: modifies command by storing options. Does not reset state if called again. + * * Examples: * * argv => operands, unknown @@ -2799,26 +3042,47 @@ Expecting one of '${allowedValues.join("', '")}'`); */ helpInformation(contextOptions) { const helper = this.createHelp(); - if (helper.helpWidth === void 0) { - helper.helpWidth = contextOptions && contextOptions.error ? this._outputConfiguration.getErrHelpWidth() : this._outputConfiguration.getOutHelpWidth(); - } - return helper.formatHelp(this, helper); + const context = this._getOutputContext(contextOptions); + helper.prepareContext({ + error: context.error, + helpWidth: context.helpWidth, + outputHasColors: context.hasColors + }); + const text = helper.formatHelp(this, helper); + if (context.hasColors) return text; + return this._outputConfiguration.stripColor(text); } /** + * @typedef HelpContext + * @type {object} + * @property {boolean} error + * @property {number} helpWidth + * @property {boolean} hasColors + * @property {function} write - includes stripColor if needed + * + * @returns {HelpContext} * @private */ - _getHelpContext(contextOptions) { + _getOutputContext(contextOptions) { contextOptions = contextOptions || {}; - const context = { error: !!contextOptions.error }; - let write; - if (context.error) { - write = (arg) => this._outputConfiguration.writeErr(arg); + const error = !!contextOptions.error; + let baseWrite; + let hasColors; + let helpWidth; + if (error) { + baseWrite = (str) => this._outputConfiguration.writeErr(str); + hasColors = this._outputConfiguration.getErrHasColors(); + helpWidth = this._outputConfiguration.getErrHelpWidth(); } else { - write = (arg) => this._outputConfiguration.writeOut(arg); + baseWrite = (str) => this._outputConfiguration.writeOut(str); + hasColors = this._outputConfiguration.getOutHasColors(); + helpWidth = this._outputConfiguration.getOutHelpWidth(); } - context.write = contextOptions.write || write; - context.command = this; - return context; + const write = (str) => { + if (!hasColors) str = this._outputConfiguration.stripColor(str); + return baseWrite(str); + }; + return { error, write, hasColors, helpWidth }; } /** * Output help information for this command. @@ -2833,23 +3097,28 @@ Expecting one of '${allowedValues.join("', '")}'`); deprecatedCallback = contextOptions; contextOptions = void 0; } - const context = this._getHelpContext(contextOptions); - this._getCommandAndAncestors().reverse().forEach((command) => command.emit("beforeAllHelp", context)); - this.emit("beforeHelp", context); - let helpInformation = this.helpInformation(context); + const outputContext = this._getOutputContext(contextOptions); + const eventContext = { + error: outputContext.error, + write: outputContext.write, + command: this + }; + this._getCommandAndAncestors().reverse().forEach((command) => command.emit("beforeAllHelp", eventContext)); + this.emit("beforeHelp", eventContext); + let helpInformation = this.helpInformation({ error: outputContext.error }); if (deprecatedCallback) { helpInformation = deprecatedCallback(helpInformation); if (typeof helpInformation !== "string" && !Buffer.isBuffer(helpInformation)) { throw new Error("outputHelp callback must return a string or a Buffer"); } } - context.write(helpInformation); + outputContext.write(helpInformation); if (this._getHelpOption()?.long) { this.emit(this._getHelpOption().long); } - this.emit("afterHelp", context); + this.emit("afterHelp", eventContext); this._getCommandAndAncestors().forEach( - (command) => command.emit("afterAllHelp", context) + (command) => command.emit("afterAllHelp", eventContext) ); } /** @@ -2911,12 +3180,20 @@ Expecting one of '${allowedValues.join("', '")}'`); */ help(contextOptions) { this.outputHelp(contextOptions); - let exitCode = process.exitCode || 0; + let exitCode = Number(process.exitCode ?? 0); if (exitCode === 0 && contextOptions && typeof contextOptions !== "function" && contextOptions.error) { exitCode = 1; } this._exit(exitCode, "commander.help", "(outputHelp)"); } + /** + * // Do a little typing to coordinate emit and listener for the help text events. + * @typedef HelpTextEventContext + * @type {object} + * @property {boolean} error + * @property {Command} command + * @property {function} write + */ /** * Add additional text to be displayed with the built-in help. * @@ -2992,13 +3269,21 @@ Expecting one of '${allowedValues.join("', '")}'`); return arg; }); } + function useColor() { + if (process.env.NO_COLOR || process.env.FORCE_COLOR === "0" || process.env.FORCE_COLOR === "false") + return false; + if (process.env.FORCE_COLOR || process.env.CLICOLOR_FORCE !== void 0) + return true; + return void 0; + } exports2.Command = Command2; + exports2.useColor = useColor; } }); -// ../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/index.js +// ../../node_modules/.pnpm/commander@13.0.0/node_modules/commander/index.js var require_commander = __commonJS({ - "../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/index.js"(exports2) { + "../../node_modules/.pnpm/commander@13.0.0/node_modules/commander/index.js"(exports2) { var { Argument: Argument2 } = require_argument(); var { Command: Command2 } = require_command(); var { CommanderError: CommanderError2, InvalidArgumentError: InvalidArgumentError2 } = require_error(); @@ -3018,7 +3303,7 @@ var require_commander = __commonJS({ } }); -// ../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/esm.mjs +// ../../node_modules/.pnpm/commander@13.0.0/node_modules/commander/esm.mjs var import_index = __toESM(require_commander(), 1); var { program, diff --git a/tools/perf-chart/package.json b/tools/perf-chart/package.json index aa0098d0d715..a0767343ba04 100644 --- a/tools/perf-chart/package.json +++ b/tools/perf-chart/package.json @@ -22,7 +22,7 @@ "author": "Jason Dent", "license": "MIT", "dependencies": { - "commander": "^12.1.0", + "commander": "^13.0.0", "csv-parse": "^5.6.0", "thistogram": "^1.1.1" },