diff --git a/packages/rstack/src/cli/commands.ts b/packages/rstack/src/cli/commands.ts index cbf2eae0..39459cc7 100644 --- a/packages/rstack/src/cli/commands.ts +++ b/packages/rstack/src/cli/commands.ts @@ -1,7 +1,7 @@ import { join } from 'node:path'; import { color } from 'rslog'; import { getConfigState } from '../config.ts'; -import { insertConfigArg, parseCliArgs } from './args.ts'; +import { insertConfigArg, parseArgs, parseCliArgs } from './args.ts'; declare global { const RSTACK_VERSION: string; @@ -20,6 +20,7 @@ ${color.cyan('Commands')}: doc Serve or build docs fmt, format Format code lint Lint code + check Run static checks, including linting and formatting test Run tests staged Run tasks on staged Git files setup Install Git hooks @@ -32,6 +33,17 @@ ${color.cyan('Options')}: -h, --help Display this help message -v, --version Display version number`; +const checkHelpMessage = `Rstack v${RSTACK_VERSION} + +${color.cyan('Usage')}: +${color.yellow(' $ rs check [options]')} + +Run static checks, including linting and formatting. + +${color.cyan('Options')}: + --type-check Enable TypeScript type checking + -h, --help Display this help message`; + async function runRsbuildCLI(args: string[]): Promise { const argv = [ process.execPath, @@ -106,6 +118,34 @@ async function runRslintCLI(args: string[]): Promise { await runCLI({ argv }); } +async function runCheckCLI(args: string[]): Promise { + const { values } = parseArgs({ + args, + options: { + 'type-check': { type: 'boolean' }, + help: { type: 'boolean', short: 'h' }, + }, + allowPositionals: false, + strict: true, + }); + + if (values.help) { + console.log(checkHelpMessage); + return; + } + + await runRslintCLI(values.typeCheck ? ['--type-check'] : []); + if (process.exitCode) { + return; + } + + const { runFmtCLI } = await import( + /* rspackChunkName: 'fmt' */ + '../fmt/cli.ts' + ); + await runFmtCLI(['--check']); +} + export async function setupCommands(): Promise { const { args, configPath } = parseCliArgs(process.argv.slice(2)); const command = args[0]; @@ -142,6 +182,11 @@ export async function setupCommands(): Promise { return; } + if (command === 'check') { + await runCheckCLI(args.slice(1)); + return; + } + if (command === 'fmt' || command === 'format') { const { runFmtCLI } = await import( /* rspackChunkName: 'fmt' */ diff --git a/packages/rstack/tests/cli/__snapshots__/check.test.ts.snap b/packages/rstack/tests/cli/__snapshots__/check.test.ts.snap new file mode 100644 index 00000000..7aea98cf --- /dev/null +++ b/packages/rstack/tests/cli/__snapshots__/check.test.ts.snap @@ -0,0 +1,15 @@ +// Rstest Snapshot v1 + +exports[`displays check help without loading config 1`] = ` +"Rstack v + +Usage: + $ rs check [options] + +Run static checks, including linting and formatting. + +Options: + --type-check Enable TypeScript type checking + -h, --help Display this help message +" +`; diff --git a/packages/rstack/tests/cli/check.test.ts b/packages/rstack/tests/cli/check.test.ts new file mode 100644 index 00000000..ec95d38f --- /dev/null +++ b/packages/rstack/tests/cli/check.test.ts @@ -0,0 +1,79 @@ +import { expect, test } from 'rstack/test'; +import { setupFmtTest } from './fmt/helpers.ts'; + +const { runCLI, writeProjectFile } = setupFmtTest(); +const runCheck = (args: string[] = []) => runCLI(['check', ...args]); + +const writeLintConfig = (): void => { + writeProjectFile( + 'rstack.config.ts', + `import { define } from "rstack"; + +define.lint([ + { + files: ["**/*.{js,ts}"], + rules: { "no-debugger": "error" }, + }, +]); +`, + ); +}; + +test('displays check help without loading config', () => { + writeProjectFile('rstack.config.ts', 'throw new Error("must not load");\n'); + + const result = runCheck(['--help']); + + expect(result.stdout.replace(/^Rstack v.+/u, 'Rstack v')).toMatchSnapshot(); +}); + +test('runs lint followed by a formatting check', () => { + writeLintConfig(); + writeProjectFile('src/index.ts', 'const value=true'); + + const unformatted = runCheck(); + + expect(unformatted.status).toBe(1); + expect(unformatted.stdout).toContain('Checking formatting...'); + expect(unformatted.stderr).toContain('Formatting issues found in 1 file.'); + + writeProjectFile('src/index.ts', 'const value = true;\n'); + const formatted = runCheck(); + + expect(formatted.status).toBe(0); + expect(formatted.stdout).toContain('No issues found.'); + expect(formatted.stderr).toBe(''); +}); + +test('enables type checking only with --type-check', () => { + writeLintConfig(); + writeProjectFile( + 'tsconfig.json', + `{ + "compilerOptions": { + "strict": true + }, + "include": ["src"] +} +`, + ); + writeProjectFile('src/index.ts', 'const value: string = 1;\n'); + + const withoutTypeCheck = runCheck(); + const withTypeCheck = runCheck(['--type-check']); + + expect(withoutTypeCheck.status).toBe(0); + expect(withTypeCheck.status).toBe(1); + expect(`${withTypeCheck.stdout}\n${withTypeCheck.stderr}`).toContain('TS2322'); +}); + +test('does not run the formatting check when lint fails', () => { + writeLintConfig(); + writeProjectFile('src/index.js', 'debugger;\n'); + + const result = runCheck(); + + expect(result.status).toBe(1); + expect(`${result.stdout}\n${result.stderr}`).toContain("Unexpected 'debugger' statement"); + expect(result.stdout).not.toContain('Checking formatting...'); +});