diff --git a/packages/rstack/src/cli/commandHelp.ts b/packages/rstack/src/cli/commandHelp.ts new file mode 100644 index 00000000..87ebab93 --- /dev/null +++ b/packages/rstack/src/cli/commandHelp.ts @@ -0,0 +1,477 @@ +import { color } from 'rslog'; + +declare global { + const RSTACK_VERSION: string; +} + +type HelpItem = readonly [label: string, description: string]; + +type HelpSection = + | { + title: string; + items: readonly HelpItem[]; + } + | { + content: string; + dim?: boolean; + }; + +type HelpDefinition = { + usage: string; + description?: string; + sections?: readonly HelpSection[]; +}; + +export type HelpTopic = + | 'root' + | 'check' + | 'dev' + | 'build' + | 'preview' + | 'doc' + | 'doc build' + | 'doc preview' + | 'doc eject' + | 'test' + | 'test run' + | 'test watch' + | 'test list' + | 'test merge-reports' + | 'test init' + | 'lib' + | 'lib build' + | 'lib inspect' + | 'lib mf-dev' + | 'lint' + | 'fmt' + | 'staged' + | 'setup'; + +const CONFIG_OPTION: HelpItem = ['-c, --config ', 'Specify Rstack config file path']; +const HELP_OPTION: HelpItem = ['-h, --help', 'Display this help message']; +const VERSION_OPTION: HelpItem = ['-v, --version', 'Display version number']; +const CONFIG_HELP_OPTIONS = [CONFIG_OPTION, HELP_OPTION]; + +const OPEN_OPTION: HelpItem = ['-o, --open [url]', 'Open the page in browser on startup']; +const PORT_OPTION: HelpItem = ['--port ', 'Set the port number for the server']; +const STRICT_PORT_OPTION: HelpItem = [ + '--strict-port', + 'Exit if the specified port is already in use', +]; +const HOST_OPTION: HelpItem = ['--host [host]', 'Set the host that the server listens to']; +const BASE_OPTION: HelpItem = ['--base ', 'Set the base path and override config.base']; +const SERVER_OPTIONS = [OPEN_OPTION, PORT_OPTION, STRICT_PORT_OPTION, HOST_OPTION]; + +const TEST_UPDATE_OPTION: HelpItem = ['-u, --update', 'Update snapshot files']; +const TEST_COVERAGE_OPTION: HelpItem = ['--coverage', 'Enable code coverage']; +const TEST_PROJECT_OPTION: HelpItem = ['--project ', 'Filter test projects by name']; +const TEST_NAME_OPTION: HelpItem = [ + '-t, --test-name-pattern ', + 'Run tests with names matching the pattern', +]; +const TEST_OPTIONS = [ + TEST_UPDATE_OPTION, + TEST_COVERAGE_OPTION, + TEST_PROJECT_OPTION, + TEST_NAME_OPTION, +]; + +const LIB_WATCH_OPTION: HelpItem = ['-w, --watch', 'Enable watch mode and rebuild on changes']; +const LIB_DTS_OPTION: HelpItem = ['--dts', 'Emit declaration files (use --no-dts to disable)']; +const LIB_BUILD_OPTIONS = [LIB_WATCH_OPTION, LIB_DTS_OPTION]; + +const commandHint = (command: string): HelpSection => ({ + content: `For command-specific options, run: + $ rs ${command} -h`, + dim: true, +}); + +const HELP_DEFINITIONS = { + root: { + usage: 'rs [command] [options]', + sections: [ + { + title: 'Commands', + items: [ + ['dev', 'Run the app dev server'], + ['build', 'Build the app for production'], + ['preview', 'Preview the app production build'], + ['lib', 'Build library'], + ['doc', 'Serve or build docs'], + ['fmt, format', 'Format code'], + ['lint', 'Lint code'], + ['check', 'Run static checks, including lint and format'], + ['test', 'Run tests'], + ['staged', 'Run tasks on staged Git files'], + ['setup', 'Install Git hooks'], + ], + }, + { + content: `For command-specific options, run: + $ rs -h`, + dim: true, + }, + { + title: 'Options', + items: [CONFIG_OPTION, HELP_OPTION, VERSION_OPTION], + }, + ], + }, + check: { + usage: 'rs check [options]', + description: 'Run static checks, including lint and format', + sections: [ + { + title: 'Options', + items: [['--type-check', 'Enable TypeScript type checking'], ...CONFIG_HELP_OPTIONS], + }, + ], + }, + dev: { + usage: 'rs dev [options]', + description: 'Run the app dev server', + sections: [ + { + title: 'Options', + items: [...SERVER_OPTIONS, ...CONFIG_HELP_OPTIONS], + }, + ], + }, + build: { + usage: 'rs build [options]', + description: 'Build the app for production', + sections: [ + { + title: 'Options', + items: [ + ['-w, --watch', 'Enable watch mode to automatically rebuild on file changes'], + ['--dist-path ', 'Set the root directory of output files'], + ['--source-map', 'Enable source map'], + ...CONFIG_HELP_OPTIONS, + ], + }, + ], + }, + preview: { + usage: 'rs preview [options]', + description: 'Preview the app production build', + sections: [ + { + title: 'Options', + items: [...SERVER_OPTIONS, ...CONFIG_HELP_OPTIONS], + }, + ], + }, + doc: { + usage: 'rs doc [command] [root] [options]', + sections: [ + { + title: 'Commands', + items: [ + ['[root]', 'Run the docs dev server (default)'], + ['build [root]', 'Build docs for production'], + ['preview [root]', 'Preview the docs production build'], + ['eject [component]', 'Eject a theme component'], + ], + }, + commandHint('doc'), + { + title: 'Options', + items: [PORT_OPTION, HOST_OPTION, BASE_OPTION, ...CONFIG_HELP_OPTIONS], + }, + ], + }, + 'doc build': { + usage: 'rs doc build [root] [options]', + description: 'Build docs for production', + sections: [ + { + title: 'Options', + items: [BASE_OPTION, ...CONFIG_HELP_OPTIONS], + }, + ], + }, + 'doc preview': { + usage: 'rs doc preview [root] [options]', + description: 'Preview the docs production build', + sections: [ + { + title: 'Options', + items: [PORT_OPTION, HOST_OPTION, BASE_OPTION, ...CONFIG_HELP_OPTIONS], + }, + ], + }, + 'doc eject': { + usage: 'rs doc eject [component] [options]', + description: 'Eject a theme component', + sections: [ + { + title: 'Options', + items: [HELP_OPTION], + }, + ], + }, + test: { + usage: 'rs test [command] [...filters] [options]', + sections: [ + { + title: 'Commands', + items: [ + ['[...filters]', 'Run tests (default)'], + ['run [...filters]', 'Run tests once'], + ['watch [...filters]', 'Run tests in watch mode'], + ['list [...filters]', 'List matching tests'], + ['merge-reports [path]', 'Merge blob reports'], + ['init [project]', 'Initialize Rstest configuration'], + ], + }, + commandHint('test'), + { + title: 'Options', + items: [['-w, --watch', 'Enable watch mode'], ...TEST_OPTIONS, ...CONFIG_HELP_OPTIONS], + }, + ], + }, + 'test run': { + usage: 'rs test run [...filters] [options]', + description: 'Run tests once', + sections: [ + { + title: 'Options', + items: [ + ['--related', 'Run tests related to source files'], + ['--changed [commit]', 'Run tests related to changed files'], + ['--shard ', 'Split tests into shards'], + ...TEST_OPTIONS, + ...CONFIG_HELP_OPTIONS, + ], + }, + ], + }, + 'test watch': { + usage: 'rs test watch [...filters] [options]', + description: 'Run tests in watch mode', + sections: [ + { + title: 'Options', + items: [...TEST_OPTIONS, ...CONFIG_HELP_OPTIONS], + }, + ], + }, + 'test list': { + usage: 'rs test list [...filters] [options]', + description: 'List matching tests', + sections: [ + { + title: 'Options', + items: [ + ['--related', 'List tests related to source files'], + ['--changed [commit]', 'List tests related to changed files'], + ['--files-only', 'List matching test files only'], + ['--json [path]', 'Print JSON or write it to a file'], + ['--include-suites', 'Include test suites'], + ['--print-location', 'Print test locations'], + ['--summary', 'Print a summary'], + TEST_PROJECT_OPTION, + ['-t, --test-name-pattern ', 'List tests with names matching the pattern'], + ...CONFIG_HELP_OPTIONS, + ], + }, + ], + }, + 'test merge-reports': { + usage: 'rs test merge-reports [path] [options]', + description: 'Merge blob reports', + sections: [ + { + title: 'Options', + items: [ + ['--coverage', 'Generate coverage reports'], + ['--reporters, --reporter ', 'Specify test reporters'], + ['--cleanup', 'Remove blob reports after merging'], + ...CONFIG_HELP_OPTIONS, + ], + }, + ], + }, + 'test init': { + usage: 'rs test init [project] [options]', + description: 'Initialize Rstest configuration', + sections: [ + { + title: 'Options', + items: [['--yes', 'Use default options without prompts'], HELP_OPTION], + }, + ], + }, + lib: { + usage: 'rs lib [command] [options]', + sections: [ + { + title: 'Commands', + items: [ + ['build', 'Build the library for production (default)'], + ['inspect', 'Inspect Rslib, Rsbuild, and Rspack configs'], + ['mf-dev', 'Start Rsbuild dev server for Module Federation'], + ], + }, + commandHint('lib'), + { + title: 'Options', + items: [...LIB_BUILD_OPTIONS, ...CONFIG_HELP_OPTIONS], + }, + ], + }, + 'lib build': { + usage: 'rs lib build [options]', + description: 'Build the library for production', + sections: [ + { + title: 'Options', + items: [...LIB_BUILD_OPTIONS, ...CONFIG_HELP_OPTIONS], + }, + ], + }, + 'lib inspect': { + usage: 'rs lib inspect [options]', + description: 'Inspect Rslib, Rsbuild, and Rspack configs', + sections: [ + { + title: 'Options', + items: [ + ['--output ', 'Set the output path for inspection results (default: .rsbuild)'], + ['--verbose', 'Show complete function definitions in output'], + ...CONFIG_HELP_OPTIONS, + ], + }, + ], + }, + 'lib mf-dev': { + usage: 'rs lib mf-dev [options]', + description: 'Start Rsbuild dev server for Module Federation', + sections: [ + { + title: 'Options', + items: [...CONFIG_HELP_OPTIONS], + }, + ], + }, + lint: { + usage: 'rs lint [options] [files...]', + description: 'Lint code', + sections: [ + { + title: 'Options', + items: [ + ['--fix', 'Automatically fix problems'], + ['--type-check', 'Enable TypeScript type checking'], + ['--type-check-only', 'Run only TypeScript type checking'], + ['--format ', 'Set output format (default | jsonline | github | gitlab)'], + ['--quiet', 'Report errors only'], + ['--timing [all|N]', 'Print a per-rule timing table (all rules or top N)'], + ['--max-warnings ', 'Set the maximum number of warnings'], + ['--rule ', 'Override a rule (repeatable)'], + ['--no-color', 'Disable colored output'], + ['--force-color', 'Force colored output'], + ...CONFIG_HELP_OPTIONS, + ], + }, + ], + }, + fmt: { + usage: 'rs fmt [options] [files/globs...]', + description: 'Format code', + sections: [ + { + title: 'Options', + items: [ + ['-w, --write', 'Write formatted files in place (default)'], + ['--check', 'Check whether files are formatted'], + ['-l, --list-different', 'Print paths of unformatted files'], + ['--ignore-path ', 'Path to an additional ignore file (repeatable)'], + ['-u, --ignore-unknown', 'Ignore unknown files'], + ['--no-cache', 'Disable the formatting cache'], + ['--cache-location ', 'Path to the formatting cache directory'], + ['--no-error-on-unmatched-pattern', 'Do not error when no files match'], + ['--with-node-modules', 'Process files inside node_modules'], + ['--parallel-workers ', 'Number of parallel workers'], + ['--stdin-filepath ', 'Format stdin as if it were saved at '], + ['--lsp', 'Run a language server on stdio'], + ...CONFIG_HELP_OPTIONS, + ], + }, + ], + }, + staged: { + usage: 'rs staged [options]', + description: 'Run tasks on staged Git files', + sections: [ + { + title: 'Options', + items: [ + ['--allow-empty', 'Allow empty commits when tasks revert all staged changes'], + [ + '-p, --concurrent ', + 'The number of tasks to run concurrently, or false for serial', + ], + ['--cwd ', 'Working directory to run all tasks in'], + ['-d, --debug', 'Print additional debug information'], + ['--no-stash', 'Disable backup stash and automatic revert'], + ['-q, --quiet', "Disable lint-staged's own console output"], + ['-r, --relative', 'Pass relative filepaths to tasks'], + [ + '-v, --verbose', + 'Show task output even when tasks succeed; by default only failed output is shown', + ], + ...CONFIG_HELP_OPTIONS, + ], + }, + ], + }, + setup: { + usage: 'rs setup [options]', + description: 'Install Git hooks in the current repository', + sections: [ + { + title: 'Options', + items: [ + ['--hooks-dir ', 'Specify hooks directory relative to the Git repository root'], + HELP_OPTION, + ], + }, + ], + }, +} satisfies Record; + +const renderItems = (items: readonly HelpItem[]): string => { + const labelWidth = items.reduce((width, [label]) => Math.max(width, label.length), 0); + + return items + .map(([label, description]) => ` ${label.padEnd(labelWidth)} ${description}`) + .join('\n'); +}; + +const renderSection = (section: HelpSection): string => { + if ('items' in section) { + return `${color.cyan(section.title)}:\n${renderItems(section.items)}`; + } + + return section.dim ? color.dim(section.content) : section.content; +}; + +const renderHelp = ({ usage, description, sections = [] }: HelpDefinition): string => { + const blocks = [ + color.bold(`Rstack v${RSTACK_VERSION}`), + `${color.cyan('Usage')}:\n${color.yellow(` $ ${usage}`)}`, + ]; + + if (description) { + blocks.push(description); + } + + blocks.push(...sections.map(renderSection)); + + return blocks.join('\n\n'); +}; + +export const renderCommandHelp = (topic: HelpTopic): string => renderHelp(HELP_DEFINITIONS[topic]); diff --git a/packages/rstack/src/cli/commands.ts b/packages/rstack/src/cli/commands.ts index e6708d92..07ed732e 100644 --- a/packages/rstack/src/cli/commands.ts +++ b/packages/rstack/src/cli/commands.ts @@ -1,441 +1,17 @@ import { join, resolve } from 'node:path'; import { getConfigState } from '../config.ts'; import { insertConfigArg, parseArgs, parseCliArgs } from './args.ts'; -import { hasHelpFlag, renderHelp } from './help.ts'; - -const renderRootHelp = (): string => - renderHelp({ - usage: 'rs [command] [options]', - sections: [ - { - title: 'Commands', - items: [ - ['dev', 'Run the app dev server'], - ['build', 'Build the app for production'], - ['preview', 'Preview the app production build'], - ['lib', 'Build library'], - ['doc', 'Serve or build docs'], - ['fmt, format', 'Format code'], - ['lint', 'Lint code'], - ['check', 'Run static checks, including lint and format'], - ['test', 'Run tests'], - ['staged', 'Run tasks on staged Git files'], - ['setup', 'Install Git hooks'], - ], - }, - { - content: `For command-specific options, run: - $ rs -h`, - dim: true, - }, - { - title: 'Options', - items: [ - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ['-v, --version', 'Display version number'], - ], - }, - ], - }); - -const renderCheckHelp = (): string => - renderHelp({ - usage: 'rs check [options]', - description: 'Run static checks, including lint and format', - sections: [ - { - title: 'Options', - items: [ - ['--type-check', 'Enable TypeScript type checking'], - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - -const renderDevHelp = (): string => - renderHelp({ - usage: 'rs dev [options]', - description: 'Run the app dev server', - sections: [ - { - title: 'Options', - items: [ - ['-o, --open [url]', 'Open the page in browser on startup'], - ['--port ', 'Set the port number for the server'], - ['--strict-port', 'Exit if the specified port is already in use'], - ['--host [host]', 'Set the host that the server listens to'], - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - -const renderBuildHelp = (): string => - renderHelp({ - usage: 'rs build [options]', - description: 'Build the app for production', - sections: [ - { - title: 'Options', - items: [ - ['-w, --watch', 'Enable watch mode to automatically rebuild on file changes'], - ['--dist-path ', 'Set the root directory of output files'], - ['--source-map', 'Enable source map'], - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - -const renderPreviewHelp = (): string => - renderHelp({ - usage: 'rs preview [options]', - description: 'Preview the app production build', - sections: [ - { - title: 'Options', - items: [ - ['-o, --open [url]', 'Open the page in browser on startup'], - ['--port ', 'Set the port number for the server'], - ['--strict-port', 'Exit if the specified port is already in use'], - ['--host [host]', 'Set the host that the server listens to'], - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - -const renderDocHelp = (): string => - renderHelp({ - usage: 'rs doc [command] [root] [options]', - sections: [ - { - title: 'Commands', - items: [ - ['[root]', 'Run the docs dev server (default)'], - ['build [root]', 'Build docs for production'], - ['preview [root]', 'Preview the docs production build'], - ['eject [component]', 'Eject a theme component'], - ], - }, - { - content: `For command-specific options, run: - $ rs doc -h`, - dim: true, - }, - { - title: 'Options', - items: [ - ['--port ', 'Set the port number for the server'], - ['--host [host]', 'Set the host that the server listens to'], - ['--base ', 'Set the base path and override config.base'], - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - -const renderDocBuildHelp = (): string => - renderHelp({ - usage: 'rs doc build [root] [options]', - description: 'Build docs for production', - sections: [ - { - title: 'Options', - items: [ - ['--base ', 'Set the base path and override config.base'], - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - -const renderDocPreviewHelp = (): string => - renderHelp({ - usage: 'rs doc preview [root] [options]', - description: 'Preview the docs production build', - sections: [ - { - title: 'Options', - items: [ - ['--port ', 'Set the port number for the server'], - ['--host [host]', 'Set the host that the server listens to'], - ['--base ', 'Set the base path and override config.base'], - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - -const renderDocEjectHelp = (): string => - renderHelp({ - usage: 'rs doc eject [component] [options]', - description: 'Eject a theme component', - sections: [ - { - title: 'Options', - items: [['-h, --help', 'Display this help message']], - }, - ], - }); - -const renderTestHelp = (): string => - renderHelp({ - usage: 'rs test [command] [...filters] [options]', - sections: [ - { - title: 'Commands', - items: [ - ['[...filters]', 'Run tests (default)'], - ['run [...filters]', 'Run tests once'], - ['watch [...filters]', 'Run tests in watch mode'], - ['list [...filters]', 'List matching tests'], - ['merge-reports [path]', 'Merge blob reports'], - ['init [project]', 'Initialize Rstest configuration'], - ], - }, - { - content: `For command-specific options, run: - $ rs test -h`, - dim: true, - }, - { - title: 'Options', - items: [ - ['-w, --watch', 'Enable watch mode'], - ['-u, --update', 'Update snapshot files'], - ['--coverage', 'Enable code coverage'], - ['--project ', 'Filter test projects by name'], - ['-t, --test-name-pattern ', 'Run tests with names matching the pattern'], - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - -const renderTestRunHelp = (): string => - renderHelp({ - usage: 'rs test run [...filters] [options]', - description: 'Run tests once', - sections: [ - { - title: 'Options', - items: [ - ['--related', 'Run tests related to source files'], - ['--changed [commit]', 'Run tests related to changed files'], - ['--shard ', 'Split tests into shards'], - ['-u, --update', 'Update snapshot files'], - ['--coverage', 'Enable code coverage'], - ['--project ', 'Filter test projects by name'], - ['-t, --test-name-pattern ', 'Run tests with names matching the pattern'], - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - -const renderTestWatchHelp = (): string => - renderHelp({ - usage: 'rs test watch [...filters] [options]', - description: 'Run tests in watch mode', - sections: [ - { - title: 'Options', - items: [ - ['-u, --update', 'Update snapshot files'], - ['--coverage', 'Enable code coverage'], - ['--project ', 'Filter test projects by name'], - ['-t, --test-name-pattern ', 'Run tests with names matching the pattern'], - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - -const renderTestListHelp = (): string => - renderHelp({ - usage: 'rs test list [...filters] [options]', - description: 'List matching tests', - sections: [ - { - title: 'Options', - items: [ - ['--related', 'List tests related to source files'], - ['--changed [commit]', 'List tests related to changed files'], - ['--files-only', 'List matching test files only'], - ['--json [path]', 'Print JSON or write it to a file'], - ['--include-suites', 'Include test suites'], - ['--print-location', 'Print test locations'], - ['--summary', 'Print a summary'], - ['--project ', 'Filter test projects by name'], - ['-t, --test-name-pattern ', 'List tests with names matching the pattern'], - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - -const renderTestMergeReportsHelp = (): string => - renderHelp({ - usage: 'rs test merge-reports [path] [options]', - description: 'Merge blob reports', - sections: [ - { - title: 'Options', - items: [ - ['--coverage', 'Generate coverage reports'], - ['--reporters, --reporter ', 'Specify test reporters'], - ['--cleanup', 'Remove blob reports after merging'], - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - -const renderTestInitHelp = (): string => - renderHelp({ - usage: 'rs test init [project] [options]', - description: 'Initialize Rstest configuration', - sections: [ - { - title: 'Options', - items: [ - ['--yes', 'Use default options without prompts'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - -const renderLibHelp = (): string => - renderHelp({ - usage: 'rs lib [command] [options]', - sections: [ - { - title: 'Commands', - items: [ - ['build', 'Build the library for production (default)'], - ['inspect', 'Inspect Rslib, Rsbuild, and Rspack configs'], - ['mf-dev', 'Start Rsbuild dev server for Module Federation'], - ], - }, - { - content: `For command-specific options, run: - $ rs lib -h`, - dim: true, - }, - { - title: 'Options', - items: [ - ['-w, --watch', 'Enable watch mode and rebuild on changes'], - ['--dts', 'Emit declaration files (use --no-dts to disable)'], - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - -const renderLibBuildHelp = (): string => - renderHelp({ - usage: 'rs lib build [options]', - description: 'Build the library for production', - sections: [ - { - title: 'Options', - items: [ - ['-w, --watch', 'Enable watch mode and rebuild on changes'], - ['--dts', 'Emit declaration files (use --no-dts to disable)'], - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - -const renderLibInspectHelp = (): string => - renderHelp({ - usage: 'rs lib inspect [options]', - description: 'Inspect Rslib, Rsbuild, and Rspack configs', - sections: [ - { - title: 'Options', - items: [ - ['--output ', 'Set the output path for inspection results (default: .rsbuild)'], - ['--verbose', 'Show complete function definitions in output'], - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - -const renderLibMfDevHelp = (): string => - renderHelp({ - usage: 'rs lib mf-dev [options]', - description: 'Start Rsbuild dev server for Module Federation', - sections: [ - { - title: 'Options', - items: [ - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - -const renderLintHelp = (): string => - renderHelp({ - usage: 'rs lint [options] [files...]', - description: 'Lint code', - sections: [ - { - title: 'Options', - items: [ - ['--fix', 'Automatically fix problems'], - ['--type-check', 'Enable TypeScript type checking'], - ['--type-check-only', 'Run only TypeScript type checking'], - ['--format ', 'Set output format (default | jsonline | github | gitlab)'], - ['--quiet', 'Report errors only'], - ['--timing [all|N]', 'Print a per-rule timing table (all rules or top N)'], - ['--max-warnings ', 'Set the maximum number of warnings'], - ['--rule ', 'Override a rule (repeatable)'], - ['--no-color', 'Disable colored output'], - ['--force-color', 'Force colored output'], - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); +import { hasHelpFlag, printCommandHelp } from './help.ts'; async function runRsbuildCLI(args: string[]): Promise { if (hasHelpFlag(args)) { switch (args[0]) { case 'dev': - console.log(renderDevHelp()); - return; + return printCommandHelp('dev'); case 'build': - console.log(renderBuildHelp()); - return; + return printCommandHelp('build'); case 'preview': - console.log(renderPreviewHelp()); - return; + return printCommandHelp('preview'); } } @@ -453,23 +29,17 @@ async function runRstestCLI(args: string[]): Promise { if (hasHelpFlag(args)) { switch (args[0]) { case 'run': - console.log(renderTestRunHelp()); - return; + return printCommandHelp('test run'); case 'watch': - console.log(renderTestWatchHelp()); - return; + return printCommandHelp('test watch'); case 'list': - console.log(renderTestListHelp()); - return; + return printCommandHelp('test list'); case 'merge-reports': - console.log(renderTestMergeReportsHelp()); - return; + return printCommandHelp('test merge-reports'); case 'init': - console.log(renderTestInitHelp()); - return; + return printCommandHelp('test init'); default: - console.log(renderTestHelp()); - return; + return printCommandHelp('test'); } } @@ -487,17 +57,13 @@ async function runRslibCLI(args: string[]): Promise { if (hasHelpFlag(args)) { switch (args[0]) { case 'build': - console.log(renderLibBuildHelp()); - return; + return printCommandHelp('lib build'); case 'inspect': - console.log(renderLibInspectHelp()); - return; + return printCommandHelp('lib inspect'); case 'mf-dev': - console.log(renderLibMfDevHelp()); - return; + return printCommandHelp('lib mf-dev'); default: - console.log(renderLibHelp()); - return; + return printCommandHelp('lib'); } } @@ -524,17 +90,13 @@ async function runRspressCLI(args: string[]): Promise { if (hasHelpFlag(args)) { switch (args[0]) { case 'build': - console.log(renderDocBuildHelp()); - return; + return printCommandHelp('doc build'); case 'preview': - console.log(renderDocPreviewHelp()); - return; + return printCommandHelp('doc preview'); case 'eject': - console.log(renderDocEjectHelp()); - return; + return printCommandHelp('doc eject'); default: - console.log(renderDocHelp()); - return; + return printCommandHelp('doc'); } } @@ -560,8 +122,7 @@ async function runRspressCLI(args: string[]): Promise { async function runRslintCLI(args: string[]): Promise { if (hasHelpFlag(args)) { - console.log(renderLintHelp()); - return; + return printCommandHelp('lint'); } const argv = [ @@ -586,8 +147,7 @@ async function runCheckCLI(args: string[]): Promise { }); if (values.help) { - console.log(renderCheckHelp()); - return; + return printCommandHelp('check'); } await runRslintCLI(values.typeCheck ? ['--type-check'] : []); @@ -614,8 +174,7 @@ export async function setupCommands(): Promise { getConfigState().configPath = configPath === undefined ? undefined : resolve(configPath); if (!command || command === '-h' || command === '--help') { - console.log(renderRootHelp()); - return; + return printCommandHelp('root'); } if (command === '-v' || command === '--version') { @@ -671,7 +230,7 @@ export async function setupCommands(): Promise { /* rspackChunkName: 'setup' */ '../setup/index.ts' ); - runSetupCLI(args.slice(1)); + await runSetupCLI(args.slice(1)); return; } diff --git a/packages/rstack/src/cli/help.ts b/packages/rstack/src/cli/help.ts index 1936ecfa..e595e139 100644 --- a/packages/rstack/src/cli/help.ts +++ b/packages/rstack/src/cli/help.ts @@ -1,27 +1,3 @@ -import { color } from 'rslog'; - -declare global { - const RSTACK_VERSION: string; -} - -export type HelpItem = readonly [label: string, description: string]; - -export type HelpSection = - | { - title: string; - items: readonly HelpItem[]; - } - | { - content: string; - dim?: boolean; - }; - -export type HelpDefinition = { - usage: string; - description?: string; - sections?: readonly HelpSection[]; -}; - export const hasHelpFlag = (args: readonly string[]): boolean => { const end = args.indexOf('--'); const flags = end === -1 ? args : args.slice(0, end); @@ -29,33 +5,12 @@ export const hasHelpFlag = (args: readonly string[]): boolean => { return flags.some((flag) => flag === '-h' || flag === '--help'); }; -const renderItems = (items: readonly HelpItem[]): string => { - const labelWidth = items.reduce((width, [label]) => Math.max(width, label.length), 0); - - return items - .map(([label, description]) => ` ${label.padEnd(labelWidth)} ${description}`) - .join('\n'); -}; - -const renderSection = (section: HelpSection): string => { - if ('items' in section) { - return `${color.cyan(section.title)}:\n${renderItems(section.items)}`; - } - - return section.dim ? color.dim(section.content) : section.content; -}; - -export const renderHelp = ({ usage, description, sections = [] }: HelpDefinition): string => { - const blocks = [ - color.bold(`Rstack v${RSTACK_VERSION}`), - `${color.cyan('Usage')}:\n${color.yellow(` $ ${usage}`)}`, - ]; - - if (description) { - blocks.push(description); - } - - blocks.push(...sections.map(renderSection)); - - return blocks.join('\n\n'); +export const printCommandHelp = async ( + topic: import('./commandHelp.ts').HelpTopic, +): Promise => { + const { renderCommandHelp } = await import( + /* rspackChunkName: 'commandHelp' */ + './commandHelp.ts' + ); + console.log(renderCommandHelp(topic)); }; diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index 7728440e..6bd314f2 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -2,7 +2,7 @@ import path from 'node:path'; import { performance } from 'node:perf_hooks'; import { color, logger } from 'rslog'; import { parseArgs } from '../cli/args.ts'; -import { renderHelp } from '../cli/help.ts'; +import { printCommandHelp } from '../cli/help.ts'; import { loadRstackConfig } from '../config.ts'; import { ensureProjectCacheDir } from '../projectCache.ts'; import { fmtCacheFileName } from './cacheStore.ts'; @@ -29,33 +29,6 @@ interface ParsedFmtCLIArgs { lsp: boolean; } -const renderFmtHelp = (): string => - renderHelp({ - usage: 'rs fmt [options] [files/globs...]', - description: 'Format code', - sections: [ - { - title: 'Options', - items: [ - ['-w, --write', 'Write formatted files in place (default)'], - ['--check', 'Check whether files are formatted'], - ['-l, --list-different', 'Print paths of unformatted files'], - ['--ignore-path ', 'Path to an additional ignore file (repeatable)'], - ['-u, --ignore-unknown', 'Ignore unknown files'], - ['--no-cache', 'Disable the formatting cache'], - ['--cache-location ', 'Path to the formatting cache directory'], - ['--no-error-on-unmatched-pattern', 'Do not error when no files match'], - ['--with-node-modules', 'Process files inside node_modules'], - ['--parallel-workers ', 'Number of parallel workers'], - ['--stdin-filepath ', 'Format stdin as if it were saved at '], - ['--lsp', 'Run a language server on stdio'], - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - const parseMaxWorkers = (value: string | undefined): number | undefined => { if (value === undefined) { return undefined; @@ -294,7 +267,7 @@ const runFmtCLI = async (args: string[]): Promise => { withNodeModules, } = parseFmtArgs(args); if (help) { - logger.log(renderFmtHelp()); + await printCommandHelp('fmt'); return; } diff --git a/packages/rstack/src/setup/index.ts b/packages/rstack/src/setup/index.ts index 5272ab98..c090404f 100644 --- a/packages/rstack/src/setup/index.ts +++ b/packages/rstack/src/setup/index.ts @@ -1,24 +1,9 @@ import { color, logger } from 'rslog'; import { parseArgs } from '../cli/args.ts'; -import { renderHelp } from '../cli/help.ts'; +import { printCommandHelp } from '../cli/help.ts'; import { installHooks } from './install.ts'; -const renderSetupHelp = (): string => - renderHelp({ - usage: 'rs setup [options]', - description: 'Install Git hooks in the current repository', - sections: [ - { - title: 'Options', - items: [ - ['--hooks-dir ', 'Specify hooks directory relative to the Git repository root'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - -export const runSetupCLI = (args: string[]): void => { +export const runSetupCLI = async (args: string[]): Promise => { const { values } = parseArgs({ args, options: { @@ -37,7 +22,7 @@ export const runSetupCLI = (args: string[]): void => { const hooksDir = hooksDirs?.[0]; if (values.help) { - console.log(renderSetupHelp()); + await printCommandHelp('setup'); return; } diff --git a/packages/rstack/src/staged.ts b/packages/rstack/src/staged.ts index 339ddccd..b1a8e6dc 100644 --- a/packages/rstack/src/staged.ts +++ b/packages/rstack/src/staged.ts @@ -1,6 +1,6 @@ import lintStaged from 'lint-staged'; import { parseArgs } from './cli/args.ts'; -import { renderHelp } from './cli/help.ts'; +import { printCommandHelp } from './cli/help.ts'; import { loadRstackConfig } from './config.ts'; export type StagedSyncTaskGenerator = (stagedFileNames: readonly string[]) => string | string[]; @@ -21,35 +21,6 @@ export type StagedTask = export type StagedConfig = Record | StagedTaskGenerator; -const renderStagedHelp = (): string => - renderHelp({ - usage: 'rs staged [options]', - description: 'Run tasks on staged Git files', - sections: [ - { - title: 'Options', - items: [ - ['--allow-empty', 'Allow empty commits when tasks revert all staged changes'], - [ - '-p, --concurrent ', - 'The number of tasks to run concurrently, or false for serial', - ], - ['--cwd ', 'Working directory to run all tasks in'], - ['-d, --debug', 'Print additional debug information'], - ['--no-stash', 'Disable backup stash and automatic revert'], - ['-q, --quiet', "Disable lint-staged's own console output"], - ['-r, --relative', 'Pass relative filepaths to tasks'], - [ - '-v, --verbose', - 'Show task output even when tasks succeed; by default only failed output is shown', - ], - ['-c, --config ', 'Specify Rstack config file path'], - ['-h, --help', 'Display this help message'], - ], - }, - ], - }); - export async function runStagedCLI(args: string[]): Promise { const { values } = parseArgs({ args, @@ -69,7 +40,7 @@ export async function runStagedCLI(args: string[]): Promise { }); if (values.help) { - console.log(renderStagedHelp()); + await printCommandHelp('staged'); return; }