diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index bbc9aba..a2addad 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -262,7 +262,9 @@ const runFmtCLI = async (args: string[]): Promise => { }); if (files.length === 0) { - if (noErrorOnUnmatchedPattern) { + // Staged tasks may pass only paths excluded by formatter ignore rules. + const allowUnmatched = noErrorOnUnmatchedPattern || process.env.RSTACK_STAGED === '1'; + if (allowUnmatched) { return; } reportNoSupportedFiles(patterns); diff --git a/packages/rstack/src/staged.ts b/packages/rstack/src/staged.ts index da46b72..7aee883 100644 --- a/packages/rstack/src/staged.ts +++ b/packages/rstack/src/staged.ts @@ -70,6 +70,9 @@ export async function runStagedCLI(args: string[]): Promise { ); } + // Let child commands detect that they are running through `rs staged`. + process.env.RSTACK_STAGED = '1'; + const success = await lintStaged({ allowEmpty: values.allowEmpty, concurrent: values.concurrent === undefined ? undefined : JSON.parse(values.concurrent), diff --git a/packages/rstack/tests/cli/staged/fmt.test.ts b/packages/rstack/tests/cli/staged/fmt.test.ts index e9c4829..3b892bb 100644 --- a/packages/rstack/tests/cli/staged/fmt.test.ts +++ b/packages/rstack/tests/cli/staged/fmt.test.ts @@ -81,6 +81,29 @@ test('formats staged files with rs fmt and applies ignore rules', () => { expect(git(['show', ':ignored-by-git.ts'])).toBe('const gitIgnored = "git ignored";\n'); }); +test('allows rs fmt when all staged files are ignored', () => { + const source = 'const fmtIgnored="fmt ignored"'; + writeProjectFile('ignored-by-fmt.ts', source); + git(['add', '--', 'ignored-by-fmt.ts']); + + const result = runStaged(); + + expect(result.status).toBe(0); + expect(readProjectFile('ignored-by-fmt.ts')).toBe(source); + expect(git(['show', ':ignored-by-fmt.ts'])).toBe(source); + expect(`${result.stdout}\n${result.stderr}`).not.toContain('No supported files matched'); +}); + +test('still rejects staged files unsupported by rs fmt', () => { + writeProjectFile('notes.unknown', 'plain text'); + git(['add', '--', 'notes.unknown']); + + const result = runStaged(); + + expect(result.status).toBe(1); + expect(`${result.stdout}\n${result.stderr}`).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/cli/staged/index.test.ts b/packages/rstack/tests/cli/staged/index.test.ts index a5176b2..d7c0146 100644 --- a/packages/rstack/tests/cli/staged/index.test.ts +++ b/packages/rstack/tests/cli/staged/index.test.ts @@ -1,5 +1,5 @@ import lintStaged from 'lint-staged'; -import { beforeEach, rs } from 'rstack/test'; +import { afterEach, beforeEach, rs } from 'rstack/test'; import { test } from '#test-helpers'; import { loadRstackConfig } from '../../../src/config.ts'; import { runStagedCLI, type StagedConfig } from '../../../src/staged.ts'; @@ -17,6 +17,7 @@ const stagedConfig: StagedConfig = { }; beforeEach(() => { + delete process.env.RSTACK_STAGED; rs.resetAllMocks(); mocks.lintStaged.mockResolvedValue(true); mocks.loadRstackConfig.mockResolvedValue({ @@ -26,6 +27,10 @@ beforeEach(() => { }); }); +afterEach(() => { + delete process.env.RSTACK_STAGED; +}); + test('should display the staged help message', ({ execCli, expect }) => { const output = execCli('staged --help'); @@ -62,6 +67,17 @@ test('should pass default options to lint-staged', async ({ expect }) => { }); }); +test('should set the staged environment', async ({ expect }) => { + mocks.lintStaged.mockImplementation(async () => { + expect(process.env.RSTACK_STAGED).toBe('1'); + return true; + }); + + await runStagedCLI([]); + + expect(process.env.RSTACK_STAGED).toBe('1'); +}); + test('should pass long options to lint-staged', async ({ expect }) => { await runStagedCLI([ '--allow-empty', diff --git a/website/docs/en/guide/cli/fmt.mdx b/website/docs/en/guide/cli/fmt.mdx index 43a9975..78187c7 100644 --- a/website/docs/en/guide/cli/fmt.mdx +++ b/website/docs/en/guide/cli/fmt.mdx @@ -107,6 +107,8 @@ rs fmt --no-error-on-unmatched-pattern 'src/**/*.ts' For example, a pre-commit script may always run `rs fmt`, even when the staged changes contain no supported files. This option lets the command exit successfully in that case instead of blocking the commit. +> [`rs staged`](./staged) enables this behavior automatically for its `rs fmt` tasks. + ### `--parallel-workers ` Set the maximum number of formatting workers to a positive integer: diff --git a/website/docs/zh/guide/cli/fmt.mdx b/website/docs/zh/guide/cli/fmt.mdx index f3bd105..1c58ea5 100644 --- a/website/docs/zh/guide/cli/fmt.mdx +++ b/website/docs/zh/guide/cli/fmt.mdx @@ -107,6 +107,8 @@ rs fmt --no-error-on-unmatched-pattern 'src/**/*.ts' 例如,pre-commit 脚本可能会始终运行 `rs fmt`,即使暂存的改动中没有支持的文件。此选项可让命令在这种情况下成功退出,避免阻止提交。 +> [`rs staged`](./staged) 会为其中的 `rs fmt` 任务自动启用此行为。 + ### `--parallel-workers ` 将格式化 worker 的最大数量设置为正整数: