diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index a2addad..cff04bb 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -12,6 +12,7 @@ interface ParsedFmtCLIArgs { mode: FmtMode; patterns: string[]; ignorePaths: string[]; + ignoreUnknown: boolean; noErrorOnUnmatchedPattern: boolean; maxWorkers?: number; help: boolean; @@ -31,6 +32,7 @@ ${color.cyan('Options')}: --check Check whether files are formatted --list-different Print paths of unformatted files --ignore-path Path to an additional ignore file (repeatable) + --ignore-unknown Ignore unknown files --no-error-on-unmatched-pattern Do not error when no files match --parallel-workers Number of parallel workers --stdin-filepath Format stdin as if it were saved at @@ -57,6 +59,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { check: { type: 'boolean' }, 'list-different': { type: 'boolean' }, 'ignore-path': { type: 'string', multiple: true }, + 'ignore-unknown': { type: 'boolean' }, 'no-error-on-unmatched-pattern': { type: 'boolean' }, 'parallel-workers': { type: 'string' }, 'stdin-filepath': { type: 'string' }, @@ -76,6 +79,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { const mode = check ? 'check' : listDifferent ? 'list-different' : 'write'; const ignorePaths = values.ignorePath ?? []; + const ignoreUnknown = values.ignoreUnknown ?? false; const noErrorOnUnmatchedPattern = values.noErrorOnUnmatchedPattern ?? false; const parallelWorkers = values.parallelWorkers; const maxWorkers = parseMaxWorkers(parallelWorkers); @@ -98,6 +102,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { mode, patterns: positionals, ignorePaths, + ignoreUnknown, noErrorOnUnmatchedPattern, maxWorkers, help, @@ -228,6 +233,7 @@ const runFmtCLI = async (args: string[]): Promise => { const { help, ignorePaths, + ignoreUnknown, maxWorkers, mode, noErrorOnUnmatchedPattern, @@ -248,6 +254,7 @@ const runFmtCLI = async (args: string[]): Promise => { filepath: stdinFilepath, cwd, ignorePaths, + ignoreUnknown, loadConfig: () => loadFmtConfig(cwd), }); return; @@ -282,6 +289,12 @@ const runFmtCLI = async (args: string[]): Promise => { }); if (result.processedFileCount === 0) { + if (ignoreUnknown) { + if (mode === 'check') { + logger.success('No supported files to check.'); + } + return; + } reportNoSupportedFiles(patterns); return; } diff --git a/packages/rstack/src/fmt/stdin.ts b/packages/rstack/src/fmt/stdin.ts index 3534d54..ec69200 100644 --- a/packages/rstack/src/fmt/stdin.ts +++ b/packages/rstack/src/fmt/stdin.ts @@ -11,6 +11,8 @@ interface RunFmtStdinOptions { cwd: string; /** Ignore files resolved from `cwd`. */ ignorePaths?: string[]; + /** Skip input when no parser can be inferred from `filepath`. */ + ignoreUnknown?: boolean; /** Loads the project config; its failures surface only after stdin is drained. */ loadConfig: () => Promise; } @@ -51,6 +53,7 @@ const runFmtStdin = async ({ filepath, cwd, ignorePaths, + ignoreUnknown, loadConfig, }: RunFmtStdinOptions): Promise => { const configPromise = loadConfig(); @@ -90,6 +93,9 @@ const runFmtStdin = async ({ const result = await formatFmtSource(file, () => source); if (result.status === 'unsupported') { + if (ignoreUnknown) { + return; + } throw new Error(`No parser could be inferred for "${filepath}".`); } diff --git a/packages/rstack/tests/cli/fmt/index.test.ts b/packages/rstack/tests/cli/fmt/index.test.ts index bb47ddf..33ba3ca 100644 --- a/packages/rstack/tests/cli/fmt/index.test.ts +++ b/packages/rstack/tests/cli/fmt/index.test.ts @@ -527,6 +527,14 @@ test('returns exit code 2 when no parser can be inferred for stdin', () => { expect(result.stderr).toContain('No parser could be inferred for "data.unknown".'); }); +test('ignores stdin when no parser can be inferred with --ignore-unknown', () => { + const result = runFmtStdin(['--stdin-filepath', 'data.unknown', '--ignore-unknown'], 'value'); + + expect(result.status).toBe(0); + expect(result.stdout).toBe(''); + expect(result.stderr).toBe(''); +}); + test('returns exit code 2 for stdin parse errors', () => { const result = runFmtStdin(['--stdin-filepath', 'index.ts'], 'const value = ;'); @@ -628,6 +636,30 @@ test('returns exit code 2 when all matched files are unsupported', () => { } }); +test('ignores unsupported files with --ignore-unknown', () => { + writeProjectFile('notes.unknown', 'plain text'); + + for (const modeArgs of [[], ['--check'], ['--list-different']]) { + const result = runFmt([...modeArgs, '--ignoreUnknown', 'notes.unknown']); + + expect(result.status).toBe(0); + expect(result.stdout).toBe( + modeArgs.includes('--check') + ? 'start Checking formatting...\nsuccess No supported files to check.\n' + : '', + ); + expect(result.stderr).toBe(''); + } +}); + +test('does not treat unmatched patterns as unknown files', () => { + const result = runFmt(['--ignore-unknown', 'missing/**/*.unknown']); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain('No supported files matched "missing/**/*.unknown"'); +}); + test('does not treat unsupported files as unmatched patterns', () => { writeProjectFile('notes.unknown', 'plain text'); diff --git a/packages/rstack/tests/cli/staged/fmt.test.ts b/packages/rstack/tests/cli/staged/fmt.test.ts index 3b892bb..fa156f5 100644 --- a/packages/rstack/tests/cli/staged/fmt.test.ts +++ b/packages/rstack/tests/cli/staged/fmt.test.ts @@ -104,6 +104,25 @@ test('still rejects staged files unsupported by rs fmt', () => { expect(`${result.stdout}\n${result.stderr}`).toContain('No supported files matched'); }); +test('allows staged files unsupported by rs fmt with --ignore-unknown', () => { + writeProjectFile( + 'rstack.config.ts', + `import { define } from 'rstack'; + +define.staged({ + '*': 'rs fmt --ignore-unknown', +}); +`, + ); + writeProjectFile('notes.unknown', 'plain text'); + git(['add', '--', 'notes.unknown']); + + const result = runStaged(); + + expect(result.status).toBe(0); + expect(`${result.stdout}\n${result.stderr}`).not.toContain('No supported files matched'); +}); + test('propagates rs fmt failures', () => { writeProjectFile('invalid.ts', 'const value = ;'); git(['add', '--', 'invalid.ts']); diff --git a/packages/rstack/tests/fmt/__snapshots__/cli.test.ts.snap b/packages/rstack/tests/fmt/__snapshots__/cli.test.ts.snap index e794c9d..62ae169 100644 --- a/packages/rstack/tests/fmt/__snapshots__/cli.test.ts.snap +++ b/packages/rstack/tests/fmt/__snapshots__/cli.test.ts.snap @@ -11,6 +11,7 @@ Options: --check Check whether files are formatted --list-different Print paths of unformatted files --ignore-path Path to an additional ignore file (repeatable) + --ignore-unknown Ignore unknown files --no-error-on-unmatched-pattern Do not error when no files match --parallel-workers Number of parallel workers --stdin-filepath Format stdin as if it were saved at diff --git a/packages/rstack/tests/fmt/cli.test.ts b/packages/rstack/tests/fmt/cli.test.ts index f4142c2..cf5e190 100644 --- a/packages/rstack/tests/fmt/cli.test.ts +++ b/packages/rstack/tests/fmt/cli.test.ts @@ -22,6 +22,7 @@ test('uses write mode by default', () => { mode: 'write', patterns: [], ignorePaths: [], + ignoreUnknown: false, noErrorOnUnmatchedPattern: false, maxWorkers: undefined, help: false, @@ -37,6 +38,7 @@ test.each([ mode, patterns: [], ignorePaths: [], + ignoreUnknown: false, noErrorOnUnmatchedPattern: false, maxWorkers: undefined, help: false, @@ -48,6 +50,7 @@ test('configures parallel worker count', () => { mode: 'write', patterns: [], ignorePaths: [], + ignoreUnknown: false, noErrorOnUnmatchedPattern: false, maxWorkers: 3, help: false, @@ -70,6 +73,7 @@ test('preserves file paths and globs', () => { mode: 'check', patterns, ignorePaths: [], + ignoreUnknown: false, noErrorOnUnmatchedPattern: false, maxWorkers: undefined, help: false, @@ -81,6 +85,7 @@ test('treats arguments after the terminator as paths', () => { mode: 'check', patterns: ['--write', '--help'], ignorePaths: [], + ignoreUnknown: false, noErrorOnUnmatchedPattern: false, maxWorkers: undefined, help: false, @@ -102,11 +107,16 @@ test('parses --no-error-on-unmatched-pattern', () => { expect(parseFmtCLIArgs(['--no-error-on-unmatched-pattern']).noErrorOnUnmatchedPattern).toBe(true); }); +test.each(['--ignore-unknown', '--ignoreUnknown'])('parses %s', (option) => { + expect(parseFmtCLIArgs([option]).ignoreUnknown).toBe(true); +}); + test('parses --stdin-filepath', () => { expect(parseFmtCLIArgs(['--stdin-filepath', 'src/index.ts'])).toEqual({ mode: 'write', patterns: [], ignorePaths: [], + ignoreUnknown: false, noErrorOnUnmatchedPattern: false, maxWorkers: undefined, help: false, @@ -119,6 +129,7 @@ test('accepts a worker count with --stdin-filepath', () => { mode: 'write', patterns: [], ignorePaths: [], + ignoreUnknown: false, noErrorOnUnmatchedPattern: false, maxWorkers: 2, help: false, diff --git a/website/docs/en/guide/cli/fmt.mdx b/website/docs/en/guide/cli/fmt.mdx index 78187c7..818140e 100644 --- a/website/docs/en/guide/cli/fmt.mdx +++ b/website/docs/en/guide/cli/fmt.mdx @@ -43,11 +43,11 @@ rs fmt . --check The command uses the following exit codes: -| Code | Meaning | -| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `0` | All matched files are formatted; or no supported files matched, but [`--no-error-on-unmatched-pattern`](#--no-error-on-unmatched-pattern) was specified. | -| `1` | One or more matched files have formatting issues. | -| `2` | `rs fmt` could not run or encountered a formatting error. | +| Code | Meaning | +| ---- | -------------------------------------------------- | +| `0` | The command completed successfully. | +| `1` | One or more files have formatting issues. | +| `2` | The command could not run or encountered an error. | ### `-h, --help` @@ -87,6 +87,18 @@ rs fmt --ignore-path .prettierignore --ignore-path config/format.ignore Each file acts as a separate ignore source. See [Ignore order](../formatting#ignore-order) for how these sources combine with `.gitignore`, default ignore rules, and `ignorePatterns`. +### `--ignore-unknown` + +Ignore matched files when no parser can be inferred. This allows the command to exit successfully even when every matched file has an unknown type: + +```bash +rs fmt --ignore-unknown '**/*' +``` + +This option does not suppress errors for unmatched paths or globs. Combine it with [`--no-error-on-unmatched-pattern`](#--no-error-on-unmatched-pattern) when an integration needs to tolerate both cases. + +When used with `--stdin-filepath`, unsupported input is skipped without writing output. + ### `--list-different` Print the paths of unformatted files without the summary produced by `--check`. This is useful when another command needs to consume the output: @@ -99,7 +111,7 @@ The option uses the same exit codes as `--check` and cannot be combined with `-- ### `--no-error-on-unmatched-pattern` -Exit successfully without diagnostics when no supported files match the provided paths or globs, including when all matching files are ignored: +Exit successfully without diagnostics when no files match the provided paths or globs, including when all matching files are ignored: ```bash rs fmt --no-error-on-unmatched-pattern 'src/**/*.ts' diff --git a/website/docs/zh/guide/cli/fmt.mdx b/website/docs/zh/guide/cli/fmt.mdx index 1c58ea5..3c810f3 100644 --- a/website/docs/zh/guide/cli/fmt.mdx +++ b/website/docs/zh/guide/cli/fmt.mdx @@ -43,11 +43,11 @@ rs fmt . --check 该命令使用以下退出状态码: -| 状态码 | 含义 | -| ------ | -------------------------------------------------------------------------------------------------------------------------------- | -| `0` | 所有匹配的文件均已格式化;或未匹配到支持的文件,但指定了 [`--no-error-on-unmatched-pattern`](#--no-error-on-unmatched-pattern)。 | -| `1` | 一个或多个匹配的文件存在格式问题。 | -| `2` | `rs fmt` 无法运行或在格式化过程中遇到错误。 | +| 状态码 | 含义 | +| ------ | ---------------------------------- | +| `0` | 命令执行成功。 | +| `1` | 一个或多个文件存在格式问题。 | +| `2` | 命令无法运行或执行过程中遇到错误。 | ### `-h, --help` @@ -87,6 +87,18 @@ rs fmt --ignore-path .prettierignore --ignore-path config/format.ignore 每个文件都是独立的忽略来源。关于这些来源与 `.gitignore`、默认忽略规则和 `ignorePatterns` 的组合方式,请参考[忽略顺序](../formatting#ignore-order)。 +### `--ignore-unknown` + +忽略无法推断 parser 的匹配文件。即使所有匹配文件的类型均未知,该选项也可以让命令成功退出: + +```bash +rs fmt --ignore-unknown '**/*' +``` + +此选项不会忽略未匹配路径或 glob 的错误。如果集成需要同时容忍这两种情况,可以将它与 [`--no-error-on-unmatched-pattern`](#--no-error-on-unmatched-pattern) 一起使用。 + +与 `--stdin-filepath` 一起使用时,不支持的输入会被跳过,且不会输出内容。 + ### `--list-different` 输出未格式化文件的路径,但不提供 `--check` 的汇总信息。需要将结果交给其他命令处理时,可以使用此选项: @@ -99,7 +111,7 @@ rs fmt . --list-different ### `--no-error-on-unmatched-pattern` -如果传入的路径或 glob 没有匹配任何支持的文件(包括所有匹配文件均被忽略的情况),则不输出诊断信息并成功退出: +如果传入的路径或 glob 没有匹配任何文件(包括所有匹配文件均被忽略的情况),则不输出诊断信息并成功退出: ```bash rs fmt --no-error-on-unmatched-pattern 'src/**/*.ts'