From 2b6b85a6acda3ab7184cb1b13ddab3cf85b53e18 Mon Sep 17 00:00:00 2001 From: neverland Date: Tue, 4 Aug 2026 14:14:43 +0800 Subject: [PATCH] feat(fmt): support --ignore-path --- .gitignore | 2 + packages/rstack/src/fmt/cli.ts | 20 +++++- packages/rstack/src/fmt/discovery.ts | 16 +++-- packages/rstack/src/fmt/ignore.ts | 54 +++++++++++++-- packages/rstack/src/fmt/stdin.ts | 23 +++++-- packages/rstack/src/fmt/types.ts | 2 + packages/rstack/tests/cli/fmt/index.test.ts | 49 ++++++++++++++ packages/rstack/tests/fmt/cli.test.ts | 15 +++++ packages/rstack/tests/fmt/ignore.test.ts | 73 ++++++++++++++++----- website/docs/en/guide/cli/fmt.mdx | 20 +++++- website/docs/en/guide/formatting.mdx | 12 +--- website/docs/zh/guide/cli/fmt.mdx | 16 ++++- website/docs/zh/guide/formatting.mdx | 12 +--- 13 files changed, 255 insertions(+), 59 deletions(-) diff --git a/.gitignore b/.gitignore index c411c06..c584b86 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ doc_build # Temp files test-temp-* +TODO.md +TODO-*.md # IDE .vscode/* diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index 06fda1d..480acb8 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -11,6 +11,7 @@ import type { FmtMode, FmtRunResult, ResolvedFmtConfig } from './types.ts'; interface ParsedFmtCLIArgs { mode: FmtMode; patterns: string[]; + ignorePaths: string[]; maxWorkers?: number; help: boolean; /** Path the stdin content is formatted as; it need not exist on disk. */ @@ -28,6 +29,7 @@ ${color.cyan('Options')}: --write Write formatted files in place (default) --check Check whether files are formatted --list-different Print paths of unformatted files + --ignore-path Path to an additional ignore file (repeatable) --parallel-workers Number of parallel workers --stdin-filepath Format stdin as if it were saved at -h, --help Display this help message`; @@ -57,6 +59,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { check: { type: 'boolean' }, 'list-different': { type: 'boolean' }, listDifferent: { type: 'boolean' }, + 'ignore-path': { type: 'string', multiple: true }, 'parallel-workers': { type: 'string' }, parallelWorkers: { type: 'string' }, 'stdin-filepath': { type: 'string' }, @@ -92,6 +95,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { return { mode, patterns: positionals, + ignorePaths: values['ignore-path'] ?? [], maxWorkers, help: values.help ?? false, stdinFilepath, @@ -210,7 +214,7 @@ const runFmtCLI = async (args: string[]): Promise => { // Argument errors are reported like every other failure so that a single // exit code identifies "rs fmt refused to run". try { - const { help, maxWorkers, mode, patterns, stdinFilepath } = parseFmtCLIArgs(args); + const { help, ignorePaths, maxWorkers, mode, patterns, stdinFilepath } = parseFmtCLIArgs(args); if (help) { logger.log(fmtHelpMessage); return; @@ -221,12 +225,22 @@ const runFmtCLI = async (args: string[]): Promise => { /* rspackChunkName: 'fmtStdin' */ './stdin.ts' ); - await runFmtStdin({ filepath: stdinFilepath, cwd, loadConfig: () => loadFmtConfig(cwd) }); + await runFmtStdin({ + filepath: stdinFilepath, + cwd, + ignorePaths, + loadConfig: () => loadFmtConfig(cwd), + }); return; } const config = await loadFmtConfig(cwd); - const files = await discoverFmtFiles({ cwd, patterns, config }); + const files = await discoverFmtFiles({ + cwd, + patterns, + config, + ignorePaths, + }); if (files.length === 0) { if (mode !== 'list-different') { diff --git a/packages/rstack/src/fmt/discovery.ts b/packages/rstack/src/fmt/discovery.ts index 9a90a28..77d992d 100644 --- a/packages/rstack/src/fmt/discovery.ts +++ b/packages/rstack/src/fmt/discovery.ts @@ -1,6 +1,6 @@ import { resolveFmtOptions } from './config.ts'; import { discoverFmtPaths } from './discoverPaths.ts'; -import { createFmtIgnoreMatcher } from './ignore.ts'; +import { createIgnoreMatcher } from './ignore.ts'; import type { DiscoverFmtFilesOptions, FmtFileRequest, ResolvedFmtConfig } from './types.ts'; const createFileRequest = (filePath: string, config: ResolvedFmtConfig): FmtFileRequest => ({ @@ -12,15 +12,18 @@ const createFileRequest = (filePath: string, config: ResolvedFmtConfig): FmtFile const discoverFmtFiles = async ({ cwd, patterns, + ignorePaths, config, }: DiscoverFmtFilesOptions): Promise => { - const candidates = await discoverFmtPaths({ cwd, patterns }); + const [candidates, isIgnored] = await Promise.all([ + discoverFmtPaths({ cwd, patterns }), + createIgnoreMatcher({ config, cwd, ignorePaths }), + ]); if (candidates.length === 0) { return []; } - const isFmtIgnored = createFmtIgnoreMatcher(config); - const filePaths = candidates.filter((filePath) => !isFmtIgnored(filePath)); + const filePaths = candidates.filter((filePath) => !isIgnored(filePath)); const files = filePaths.map((filePath) => createFileRequest(filePath, config)); if (!files.some((file) => file.options.plugins?.length)) { return files; @@ -32,7 +35,10 @@ const discoverFmtFiles = async ({ ); const resolvePlugins = createFmtPluginResolver(config.rootPath); - return files.map((file) => ({ ...file, options: resolvePlugins(file.options) })); + return files.map((file) => ({ + ...file, + options: resolvePlugins(file.options), + })); }; export { createFileRequest, discoverFmtFiles }; diff --git a/packages/rstack/src/fmt/ignore.ts b/packages/rstack/src/fmt/ignore.ts index 41f80bf..21d5860 100644 --- a/packages/rstack/src/fmt/ignore.ts +++ b/packages/rstack/src/fmt/ignore.ts @@ -1,4 +1,5 @@ -import { relative } from 'node:path'; +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; import fastIgnore from 'fast-ignore'; import type { ResolvedFmtConfig } from './types.ts'; @@ -10,11 +11,52 @@ import type { ResolvedFmtConfig } from './types.ts'; */ const defaultIgnorePatterns = ['package-lock.json', 'pnpm-lock.yaml']; -/** Creates a reusable matcher for default and config-level ignore patterns. */ -const createFmtIgnoreMatcher = (config: ResolvedFmtConfig): ((filePath: string) => boolean) => { - const matches = fastIgnore([...defaultIgnorePatterns, ...config.ignorePatterns].join('\n')); +type IgnoreMatcher = (filePath: string) => boolean; - return (filePath) => matches(relative(config.rootPath, filePath)); +interface CreateIgnoreMatcherOptions { + config: ResolvedFmtConfig; + /** Base directory for relative ignore paths. */ + cwd: string; + ignorePaths?: string[]; +} + +const createPatternMatcher = (rootPath: string, patterns: string): IgnoreMatcher => { + const matches = fastIgnore(patterns); + + return (filePath) => matches(path.relative(rootPath, filePath)); +}; + +const loadIgnoreMatcher = async (cwd: string, ignorePath: string): Promise => { + const filePath = path.resolve(cwd, ignorePath); + let patterns: string; + + try { + patterns = await readFile(filePath, 'utf8'); + } catch (error) { + throw new Error(`Failed to read ignore file "${ignorePath}".`, { + cause: error, + }); + } + + return createPatternMatcher(path.dirname(filePath), patterns); +}; + +/** Creates a reusable matcher for default, config-level, and CLI-provided ignore patterns. */ +const createIgnoreMatcher = async ({ + config, + cwd, + ignorePaths = [], +}: CreateIgnoreMatcherOptions): Promise => { + const configMatcher = createPatternMatcher( + config.rootPath, + [...defaultIgnorePatterns, ...config.ignorePatterns].join('\n'), + ); + const ignoreMatchers = await Promise.all( + ignorePaths.map((ignorePath) => loadIgnoreMatcher(cwd, ignorePath)), + ); + + return (filePath) => + configMatcher(filePath) || ignoreMatchers.some((matches) => matches(filePath)); }; -export { createFmtIgnoreMatcher }; +export { createIgnoreMatcher }; diff --git a/packages/rstack/src/fmt/stdin.ts b/packages/rstack/src/fmt/stdin.ts index dc946dc..3534d54 100644 --- a/packages/rstack/src/fmt/stdin.ts +++ b/packages/rstack/src/fmt/stdin.ts @@ -1,7 +1,7 @@ import { resolve } from 'node:path'; import { createFileRequest } from './discovery.ts'; import { formatFmtSource } from './format.ts'; -import { createFmtIgnoreMatcher } from './ignore.ts'; +import { createIgnoreMatcher } from './ignore.ts'; import type { ResolvedFmtConfig } from './types.ts'; interface RunFmtStdinOptions { @@ -9,6 +9,8 @@ interface RunFmtStdinOptions { filepath: string; /** Absolute directory used to resolve the path. */ cwd: string; + /** Ignore files resolved from `cwd`. */ + ignorePaths?: string[]; /** Loads the project config; its failures surface only after stdin is drained. */ loadConfig: () => Promise; } @@ -45,7 +47,12 @@ const writeStdout = (output: string): Promise => * Formats stdin on the main thread and writes the result to stdout. * Nothing but the formatted output may reach stdout in this mode. */ -const runFmtStdin = async ({ filepath, cwd, loadConfig }: RunFmtStdinOptions): Promise => { +const runFmtStdin = async ({ + filepath, + cwd, + ignorePaths, + loadConfig, +}: RunFmtStdinOptions): Promise => { const configPromise = loadConfig(); // Drain stdin before surfacing any failure, otherwise a writer that already // queued more than the pipe buffer sees EPIPE instead of the real error. @@ -54,7 +61,12 @@ const runFmtStdin = async ({ filepath, cwd, loadConfig }: RunFmtStdinOptions): P const config = await configPromise; const absolutePath = resolve(cwd, filepath); - if (createFmtIgnoreMatcher(config)(absolutePath)) { + const isIgnored = await createIgnoreMatcher({ + config, + cwd, + ignorePaths, + }); + if (isIgnored(absolutePath)) { await writeStdout(source); return; } @@ -69,7 +81,10 @@ const runFmtStdin = async ({ filepath, cwd, loadConfig }: RunFmtStdinOptions): P /* rspackChunkName: 'fmtPlugins' */ './plugins.ts' ); - file = { ...file, options: createFmtPluginResolver(config.rootPath)(file.options) }; + file = { + ...file, + options: createFmtPluginResolver(config.rootPath)(file.options), + }; } const result = await formatFmtSource(file, () => source); diff --git a/packages/rstack/src/fmt/types.ts b/packages/rstack/src/fmt/types.ts index 451f65d..111ddb4 100644 --- a/packages/rstack/src/fmt/types.ts +++ b/packages/rstack/src/fmt/types.ts @@ -56,6 +56,8 @@ interface DiscoverFmtFilesOptions { cwd: string; /** Files, directories, and positive or negative globs. Defaults to the current directory. */ patterns?: string[]; + /** Ignore files resolved from `cwd`; each file's patterns are relative to its own directory. */ + ignorePaths?: string[]; /** Resolved project config applied to discovered files. */ config: ResolvedFmtConfig; } diff --git a/packages/rstack/tests/cli/fmt/index.test.ts b/packages/rstack/tests/cli/fmt/index.test.ts index bcfbba5..75aef5e 100644 --- a/packages/rstack/tests/cli/fmt/index.test.ts +++ b/packages/rstack/tests/cli/fmt/index.test.ts @@ -195,6 +195,41 @@ test('does not load Prettier config or ignore files', () => { expect(readProjectFile('index.ts')).toBe('function getMessage() {\n return "hello";\n}\n'); }); +test('applies repeated ignore paths to explicit files', () => { + writeProjectFile('.prettierignore', 'src/ignored-by-root.ts\n'); + writeProjectFile('config/extra.ignore', '../src/ignored-by-extra.ts\n'); + writeProjectFile('src/ignored-by-root.ts', 'const root="ignored"'); + writeProjectFile('src/ignored-by-extra.ts', 'const extra="ignored"'); + writeProjectFile('src/index.ts', 'const index="formatted"'); + + const result = runFmt([ + '--ignore-path', + '.prettierignore', + '--ignore-path=config/extra.ignore', + 'src/ignored-by-root.ts', + 'src/ignored-by-extra.ts', + 'src/index.ts', + ]); + + expect(result.status).toBe(0); + expectWriteSummary(result.stdout, 1, 1); + expect(result.stderr).toBe(''); + expect(readProjectFile('src/ignored-by-root.ts')).toBe('const root="ignored"'); + expect(readProjectFile('src/ignored-by-extra.ts')).toBe('const extra="ignored"'); + expect(readProjectFile('src/index.ts')).toBe('const index = "formatted";\n'); +}); + +test('returns exit code 2 for an unreadable ignore path', () => { + writeProjectFile('index.ts', 'const value=true'); + + const result = runFmt(['--ignore-path', 'missing.ignore', 'index.ts']); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain('Failed to read ignore file "missing.ignore".'); + expect(readProjectFile('index.ts')).toBe('const value=true'); +}); + test('applies define.fmt options, overrides, ignore patterns, and globs', () => { writeProjectFile( 'rstack.config.ts', @@ -469,6 +504,20 @@ define.fmt({ ignorePatterns: ['src/ignored.ts'] }); expect(result.stderr).toBe(''); }); +test('echoes stdin paths ignored by --ignore-path', () => { + writeProjectFile('.prettierignore', 'src/ignored.ts\n'); + + const source = 'const ignored="ignored"'; + const result = runFmtStdin( + ['--ignore-path', '.prettierignore', '--stdin-filepath', 'src/ignored.ts'], + source, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toBe(source); + expect(result.stderr).toBe(''); +}); + test('echoes stdin for default ignored lock files', () => { const source = 'lockfileVersion: "9.0"\n'; const result = runFmtStdin(['--stdin-filepath', 'pnpm-lock.yaml'], source); diff --git a/packages/rstack/tests/fmt/cli.test.ts b/packages/rstack/tests/fmt/cli.test.ts index e9639fb..bc97edb 100644 --- a/packages/rstack/tests/fmt/cli.test.ts +++ b/packages/rstack/tests/fmt/cli.test.ts @@ -21,6 +21,7 @@ test('uses write mode by default', () => { expect(parseFmtCLIArgs([])).toEqual({ mode: 'write', patterns: [], + ignorePaths: [], maxWorkers: undefined, help: false, }); @@ -35,6 +36,7 @@ test.each([ expect(parseFmtCLIArgs([option])).toEqual({ mode, patterns: [], + ignorePaths: [], maxWorkers: undefined, help: false, }); @@ -46,6 +48,7 @@ test.each(['--parallel-workers', '--parallelWorkers'])( expect(parseFmtCLIArgs([option, '3'])).toEqual({ mode: 'write', patterns: [], + ignorePaths: [], maxWorkers: 3, help: false, }); @@ -71,6 +74,7 @@ test('preserves file paths and globs', () => { expect(parseFmtCLIArgs([patterns[0], '--check', ...patterns.slice(1)])).toEqual({ mode: 'check', patterns, + ignorePaths: [], maxWorkers: undefined, help: false, }); @@ -80,6 +84,7 @@ test('treats arguments after the terminator as paths', () => { expect(parseFmtCLIArgs(['--check', '--', '--write', '--help'])).toEqual({ mode: 'check', patterns: ['--write', '--help'], + ignorePaths: [], maxWorkers: undefined, help: false, }); @@ -89,10 +94,18 @@ test.each(['--help', '-h'])('parses %s', (option) => { expect(parseFmtCLIArgs([option]).help).toBe(true); }); +test('collects repeated ignore paths', () => { + expect( + parseFmtCLIArgs(['--ignore-path', '.prettierignore', '--ignore-path=config/format.ignore']) + .ignorePaths, + ).toEqual(['.prettierignore', 'config/format.ignore']); +}); + test.each(['--stdin-filepath', '--stdinFilepath'])('parses %s', (option) => { expect(parseFmtCLIArgs([option, 'src/index.ts'])).toEqual({ mode: 'write', patterns: [], + ignorePaths: [], maxWorkers: undefined, help: false, stdinFilepath: 'src/index.ts', @@ -103,6 +116,7 @@ test('accepts a worker count with --stdin-filepath', () => { expect(parseFmtCLIArgs(['--stdin-filepath', 'index.ts', '--parallel-workers', '2'])).toEqual({ mode: 'write', patterns: [], + ignorePaths: [], maxWorkers: 2, help: false, stdinFilepath: 'index.ts', @@ -129,6 +143,7 @@ test('provides command help', () => { expect(fmtHelpMessage).toContain('--write'); expect(fmtHelpMessage).toContain('--check'); expect(fmtHelpMessage).toContain('--list-different'); + expect(fmtHelpMessage).toContain('--ignore-path '); expect(fmtHelpMessage).toContain('--parallel-workers '); expect(fmtHelpMessage).toContain('--stdin-filepath '); expect(fmtHelpMessage).toContain('-h, --help'); diff --git a/packages/rstack/tests/fmt/ignore.test.ts b/packages/rstack/tests/fmt/ignore.test.ts index 63501dd..c88de37 100644 --- a/packages/rstack/tests/fmt/ignore.test.ts +++ b/packages/rstack/tests/fmt/ignore.test.ts @@ -1,15 +1,25 @@ import path from 'node:path'; import { expect, test } from 'rstack/test'; import { normalizeFmtConfig } from '../../src/fmt/config.ts'; -import { createFmtIgnoreMatcher } from '../../src/fmt/ignore.ts'; +import { createIgnoreMatcher } from '../../src/fmt/ignore.ts'; +import { withTempProject, writeProjectFile } from './helpers.ts'; const rootPath = path.join(import.meta.dirname, 'project'); const createMatcher = (ignorePatterns: string[]) => - createFmtIgnoreMatcher(normalizeFmtConfig({ ignorePatterns }, rootPath)); + createIgnoreMatcher({ + config: normalizeFmtConfig({ ignorePatterns }, rootPath), + cwd: rootPath, + }); -test('matches gitignore patterns relative to the config root', () => { - const isIgnored = createMatcher(['dist/', '*.snap', '/root.js', '# comment', '\\#generated.js']); +test('matches gitignore patterns relative to the config root', async () => { + const isIgnored = await createMatcher([ + 'dist/', + '*.snap', + '/root.js', + '# comment', + '\\#generated.js', + ]); expect(isIgnored(path.join(rootPath, 'dist/index.js'))).toBe(true); expect(isIgnored(path.join(rootPath, 'src/data.snap'))).toBe(true); @@ -19,10 +29,10 @@ test('matches gitignore patterns relative to the config root', () => { expect(isIgnored(path.join(rootPath, 'src/index.js'))).toBe(false); }); -test('applies negated patterns in declaration order', () => { - const isIgnored = createMatcher(['*.js', '!src/keep.js']); - const isIgnoredAgain = createMatcher(['*.js', '!src/keep.js', 'src/keep.js']); - const isIgnoredAfterReinclude = createMatcher(['dist', '!dist']); +test('applies negated patterns in declaration order', async () => { + const isIgnored = await createMatcher(['*.js', '!src/keep.js']); + const isIgnoredAgain = await createMatcher(['*.js', '!src/keep.js', 'src/keep.js']); + const isIgnoredAfterReinclude = await createMatcher(['dist', '!dist']); const filePath = path.join(rootPath, 'src/keep.js'); expect(isIgnored(filePath)).toBe(false); @@ -31,31 +41,60 @@ test('applies negated patterns in declaration order', () => { expect(isIgnoredAfterReinclude(path.join(rootPath, 'dist'))).toBe(false); }); -test('ignores common lock files by default and allows explicit negation', () => { - const isIgnored = createMatcher([]); - const isIgnoredAfterReinclude = createMatcher(['!pnpm-lock.yaml']); +test('ignores common lock files by default and allows explicit negation', async () => { + const isIgnored = await createMatcher([]); + const isIgnoredAfterReinclude = await createMatcher(['!pnpm-lock.yaml']); expect(isIgnored(path.join(rootPath, 'package-lock.json'))).toBe(true); expect(isIgnored(path.join(rootPath, 'packages/app/pnpm-lock.yaml'))).toBe(true); expect(isIgnoredAfterReinclude(path.join(rootPath, 'pnpm-lock.yaml'))).toBe(false); }); -test('does not let explicit files bypass ignore patterns', () => { - const isIgnored = createMatcher(['generated/']); +test('does not let explicit files bypass ignore patterns', async () => { + const isIgnored = await createMatcher(['generated/']); const explicitFilePath = path.join(rootPath, 'generated/output.js'); expect(isIgnored(explicitFilePath)).toBe(true); }); -test('matches parent directory patterns without validation', () => { - const isIgnored = createMatcher(['../shared/*.js']); +test('matches parent directory patterns without validation', async () => { + const isIgnored = await createMatcher(['../shared/*.js']); expect(isIgnored(path.join(rootPath, '../shared/index.js'))).toBe(true); expect(isIgnored(path.join(rootPath, 'shared/index.js'))).toBe(false); }); -test('does not ignore other files when no patterns are configured', () => { - const isIgnored = createMatcher([]); +test('does not ignore other files when no patterns are configured', async () => { + const isIgnored = await createMatcher([]); expect(isIgnored(path.join(rootPath, 'src/index.js'))).toBe(false); }); + +test('loads repeated ignore paths relative to cwd and each ignore file', async () => { + await withTempProject(async (projectPath) => { + writeProjectFile(projectPath, '.prettierignore', 'src/*.js\n!src/keep.js\n'); + writeProjectFile(projectPath, 'config/extra.ignore', '../generated/*.js\n'); + + const isIgnored = await createIgnoreMatcher({ + config: normalizeFmtConfig({ ignorePatterns: ['configured.js'] }, projectPath), + cwd: projectPath, + ignorePaths: ['.prettierignore', 'config/extra.ignore'], + }); + + expect(isIgnored(path.join(projectPath, 'configured.js'))).toBe(true); + expect(isIgnored(path.join(projectPath, 'src/drop.js'))).toBe(true); + expect(isIgnored(path.join(projectPath, 'src/keep.js'))).toBe(false); + expect(isIgnored(path.join(projectPath, 'generated/output.js'))).toBe(true); + expect(isIgnored(path.join(projectPath, 'other.js'))).toBe(false); + }); +}); + +test('reports unreadable ignore paths', async () => { + await expect( + createIgnoreMatcher({ + config: normalizeFmtConfig(undefined, rootPath), + cwd: rootPath, + ignorePaths: ['missing.ignore'], + }), + ).rejects.toThrow('Failed to read ignore file "missing.ignore".'); +}); diff --git a/website/docs/en/guide/cli/fmt.mdx b/website/docs/en/guide/cli/fmt.mdx index d30a0e1..72e9402 100644 --- a/website/docs/en/guide/cli/fmt.mdx +++ b/website/docs/en/guide/cli/fmt.mdx @@ -57,6 +57,20 @@ Display usage and option information without formatting files: rs fmt --help ``` +### `--ignore-path ` + +Load additional Gitignore-compatible rules from ``. Repeat the option to load multiple +ignore files: + +```bash +rs fmt --ignore-path .prettierignore --ignore-path config/docs.ignore +``` + +Relative ignore paths are resolved from the current working directory. Rules in each file are +resolved from the directory containing that ignore file and extend the built-in ignore rules and +`define.fmt.ignorePatterns`. They apply to scanned paths, explicitly passed files, and +`--stdin-filepath`. An unreadable ignore file causes the command to exit with code `2`. + ### `--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: @@ -79,13 +93,15 @@ When this option is omitted, `rs fmt` automatically chooses up to eight workers ### `--stdin-filepath ` -Format content received from stdin as if it were saved at ``. The path determines the parser and matching configuration overrides, but it does not need to exist on disk. The formatted content is written to stdout: +Format content received from stdin as if it were saved at ``, for example when integrating with an editor. The path determines the parser and matching [configuration overrides](../formatting#overrides), but it does not need to exist on disk: ```bash cat src/index.ts | rs fmt --stdin-filepath src/index.ts ``` -`--stdin-filepath` cannot be combined with file arguments or with `--write`, `--check`, or `--list-different`. See [Formatting stdin](../formatting#formatting-stdin) for details. +Formatted output is written to stdout and diagnostics to stderr. If the input path is ignored, `rs fmt` skips formatting and writes the input unchanged. If it cannot infer a parser from the path or parse the content, it reports an error and exits with code `2`. + +> `--stdin-filepath` cannot be combined with file arguments or with `--write`, `--check`, or `--list-different`. ### `--write` diff --git a/website/docs/en/guide/formatting.mdx b/website/docs/en/guide/formatting.mdx index 9fdddb5..e814ba6 100644 --- a/website/docs/en/guide/formatting.mdx +++ b/website/docs/en/guide/formatting.mdx @@ -71,16 +71,6 @@ When scanning directories or globs, `rs fmt` follows `.gitignore` rules, skips b `.gitignore` applies only when scanning directories and globs. It does not exclude files passed explicitly on the command line. To always exclude a file, use [`ignorePatterns`](#ignore-files). -## Formatting stdin - -Use `--stdin-filepath` to format content piped through stdin, for example from an editor integration. The provided path determines the parser and the matching [overrides](#overrides); it does not need to exist on disk: - -```bash -cat src/index.ts | rs fmt --stdin-filepath src/index.ts -``` - -The formatted result is written to stdout, and diagnostics go to stderr. When the path matches [`ignorePatterns`](#ignore-files) or a default [lock file](#lock-files), `rs fmt` skips formatting and writes the input unchanged. When no parser can be inferred from the path, or the content cannot be parsed, `rs fmt` prints an error and exits with code 2. - ## Ignore files Use `ignorePatterns` to exclude files from formatting: @@ -95,6 +85,8 @@ define.fmt({ Patterns follow Gitignore syntax and are resolved relative to the directory containing the Rstack configuration file. Because they are applied after the files are selected, they also exclude files passed explicitly on the command line. +> You can also use the [`--ignore-path`](./cli/fmt#--ignore-path-path) CLI option to ignore files. + ### Lock files By default, `rs fmt` ignores common lock files, including `package-lock.json` and `pnpm-lock.yaml`. diff --git a/website/docs/zh/guide/cli/fmt.mdx b/website/docs/zh/guide/cli/fmt.mdx index cda3f3c..6b8ed96 100644 --- a/website/docs/zh/guide/cli/fmt.mdx +++ b/website/docs/zh/guide/cli/fmt.mdx @@ -57,6 +57,16 @@ rs fmt . --check rs fmt --help ``` +### `--ignore-path ` + +从 `` 加载额外的 Gitignore 兼容规则。重复传入该选项可以加载多个 ignore 文件: + +```bash +rs fmt --ignore-path .prettierignore --ignore-path config/docs.ignore +``` + +相对 ignore 路径基于当前工作目录解析;每个文件中的规则基于该 ignore 文件所在目录解析,并追加到内置忽略规则和 `define.fmt.ignorePatterns`。这些规则会作用于扫描得到的路径、显式传入的文件以及 `--stdin-filepath`。ignore 文件无法读取时,命令以状态码 `2` 退出。 + ### `--list-different` 输出未格式化文件的路径,但不提供 `--check` 的汇总信息。需要将结果交给其他命令处理时,可以使用此选项: @@ -79,13 +89,15 @@ rs fmt . --parallel-workers 4 ### `--stdin-filepath ` -将 stdin 传入的内容按保存在 `` 的文件进行格式化。该路径用于确定 parser 和匹配的覆盖配置,但不需要在磁盘上真实存在。格式化结果会输出到 stdout: +将 stdin 传入的内容按保存在 `` 的文件进行格式化,例如用于编辑器集成。该路径用于确定 parser 和匹配的[覆盖配置](../formatting#overrides),但不需要在磁盘上真实存在: ```bash cat src/index.ts | rs fmt --stdin-filepath src/index.ts ``` -`--stdin-filepath` 不能与文件参数或 `--write`、`--check`、`--list-different` 同时使用。详细说明请参考[格式化标准输入](../formatting#formatting-stdin)。 +格式化结果写入 stdout,诊断信息写入 stderr。若输入路径被忽略,`rs fmt` 会跳过格式化并原样输出内容;若无法根据路径推断 parser 或内容解析失败,则输出错误并以状态码 `2` 退出。 + +> `--stdin-filepath` 不能与文件参数或 `--write`、`--check`、`--list-different` 同时使用。 ### `--write` diff --git a/website/docs/zh/guide/formatting.mdx b/website/docs/zh/guide/formatting.mdx index 9f04b82..d9e68c4 100644 --- a/website/docs/zh/guide/formatting.mdx +++ b/website/docs/zh/guide/formatting.mdx @@ -71,16 +71,6 @@ rs fmt "src/**/*.{js,ts}" "!src/generated/**" `.gitignore` 只在扫描目录和 glob 时生效,不会排除命令行中显式传入的文件。如果需要始终排除某个文件,请使用 [`ignorePatterns`](#ignore-files)。 -## 格式化标准输入 \{#formatting-stdin} - -使用 `--stdin-filepath` 可以格式化通过 stdin 传入的内容,例如来自编辑器集成的调用。传入的路径决定使用的 parser 和匹配的[覆盖配置](#overrides),并不需要在磁盘上真实存在: - -```bash -cat src/index.ts | rs fmt --stdin-filepath src/index.ts -``` - -格式化结果输出到 stdout,诊断信息输出到 stderr。当路径匹配 [`ignorePatterns`](#ignore-files) 或默认的 [lock 文件](#lock-files)时,`rs fmt` 会跳过格式化,将输入原样输出。当无法从路径推断 parser 或内容无法解析时,`rs fmt` 输出错误并以状态码 2 退出。 - ## 忽略文件 \{#ignore-files} 使用 `ignorePatterns` 排除不需要格式化的文件: @@ -95,6 +85,8 @@ define.fmt({ 这些模式遵循 Gitignore 语法,并且基于 Rstack 配置文件所在的目录解析。由于规则会在确定格式化范围后生效,因此也会排除命令行中显式传入的文件。 +> 你也可以使用使用 [`--ignore-path`](./cli/fmt#--ignore-path-path) CLI 选项来忽略文件。 + ### Lock 文件 \{#lock-files} `rs fmt` 默认忽略常见的 lock 文件,包括 `package-lock.json` 和 `pnpm-lock.yaml`。