diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index d16c6b6..6fcc085 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -6,13 +6,16 @@ import { loadRstackConfig } from '../config.ts'; import { resolveFmtConfig } from './config.ts'; import { discoverFmtFiles } from './discovery.ts'; import { runFmtFiles } from './runner.ts'; -import type { FmtMode, FmtRunResult } from './types.ts'; +import { runFmtStdin } from './stdin.ts'; +import type { FmtMode, FmtRunResult, ResolvedFmtConfig } from './types.ts'; interface ParsedFmtCLIArgs { mode: FmtMode; patterns: string[]; maxWorkers?: number; help: boolean; + /** Path the stdin content is formatted as; it need not exist on disk. */ + stdinFilepath?: string; } const fmtHelpMessage: string = `Rstack v${RSTACK_VERSION} @@ -27,6 +30,7 @@ ${color.cyan('Options')}: --check Check whether files are formatted --list-different Print paths of unformatted files --parallel-workers Number of parallel workers + --stdin-filepath Format stdin as if it were saved at -h, --help Display this help message`; const parseMaxWorkers = ( @@ -56,6 +60,8 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { listDifferent: { type: 'boolean' }, 'parallel-workers': { type: 'string' }, parallelWorkers: { type: 'string' }, + 'stdin-filepath': { type: 'string' }, + stdinFilepath: { type: 'string' }, help: { type: 'boolean', short: 'h' }, }, allowPositionals: true, @@ -70,12 +76,26 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { const mode = values.check ? 'check' : listDifferent ? 'list-different' : 'write'; const maxWorkers = parseMaxWorkers(values['parallel-workers'], values.parallelWorkers); + const stdinFilepath = values['stdin-filepath'] ?? values.stdinFilepath; + + if (stdinFilepath !== undefined) { + if (modes.length > 0) { + throw new Error( + 'The --stdin-filepath option cannot be used with --write, --check, or --list-different.', + ); + } + + if (positionals.length > 0) { + throw new Error('The --stdin-filepath option cannot be used with file arguments.'); + } + } return { mode, patterns: positionals, maxWorkers, help: values.help ?? false, + stdinFilepath, }; }; @@ -174,23 +194,35 @@ const logFmtResult = ( } }; -const runFmtCLI = async (args: string[]): Promise => { - const { help, maxWorkers, mode, patterns } = parseFmtCLIArgs(args); - if (help) { - logger.log(fmtHelpMessage); - return; - } +const loadFmtConfig = async (cwd: string): Promise => { + const { configs, filePath } = await loadRstackConfig(); + return resolveFmtConfig({ + definition: configs.fmt, + configFilePath: filePath, + cwd, + }); +}; + +const runFmtCLI = async (args: string[]): Promise => { const cwd = process.cwd(); const startTime = performance.now(); + // Argument errors are reported like every other failure so that a single + // exit code identifies "rs fmt refused to run". try { - const { configs, filePath } = await loadRstackConfig(); - const config = await resolveFmtConfig({ - definition: configs.fmt, - configFilePath: filePath, - cwd, - }); + const { help, maxWorkers, mode, patterns, stdinFilepath } = parseFmtCLIArgs(args); + if (help) { + logger.log(fmtHelpMessage); + return; + } + + if (stdinFilepath !== undefined) { + await runFmtStdin({ filepath: stdinFilepath, cwd, loadConfig: () => loadFmtConfig(cwd) }); + return; + } + + const config = await loadFmtConfig(cwd); const files = await discoverFmtFiles({ cwd, patterns, config }); if (files.length === 0) { diff --git a/packages/rstack/src/fmt/discovery.ts b/packages/rstack/src/fmt/discovery.ts index dec3f68..9a90a28 100644 --- a/packages/rstack/src/fmt/discovery.ts +++ b/packages/rstack/src/fmt/discovery.ts @@ -35,4 +35,4 @@ const discoverFmtFiles = async ({ return files.map((file) => ({ ...file, options: resolvePlugins(file.options) })); }; -export { discoverFmtFiles }; +export { createFileRequest, discoverFmtFiles }; diff --git a/packages/rstack/src/fmt/format.ts b/packages/rstack/src/fmt/format.ts new file mode 100644 index 0000000..0aa653a --- /dev/null +++ b/packages/rstack/src/fmt/format.ts @@ -0,0 +1,59 @@ +// Derived from @prettier/cli, see THIRD_PARTY_NOTICES.md + +import { + format, + getFileInfo, + type FileInfoOptions, + type Options as PrettierOptions, +} from 'prettier'; +import { getPrettierPlugins } from './prettierPlugins.ts'; +import type { FmtFileRequest } from './types.ts'; + +type PrettierPlugins = NonNullable; + +type FormatFmtSourceResult = + { status: 'unsupported' } | { status: 'formatted'; source: string; formatted: string }; + +const fileInfoOptions = { + ignorePath: [], + resolveConfig: false, + withNodeModules: true, +} satisfies FileInfoOptions; + +/** Uses the configured parser or infers one without loading Prettier config. */ +const resolveFmtParser = async ( + filePath: string, + options: PrettierOptions, + plugins: PrettierPlugins, +): Promise => + options.parser ?? + ( + await getFileInfo(filePath, { + ...fileInfoOptions, + plugins, + }) + ).inferredParser; + +/** Formats file contents, requesting the source only once a parser is known. */ +const formatFmtSource = async ( + { path, options }: FmtFileRequest, + readSource: () => string, +): Promise => { + const plugins = await getPrettierPlugins(options, path); + const parser = await resolveFmtParser(path, options, plugins); + if (!parser) { + return { status: 'unsupported' }; + } + + const source = readSource(); + const formatted = await format(source, { + ...options, + filepath: path, + parser, + plugins, + }); + + return { status: 'formatted', source, formatted }; +}; + +export { formatFmtSource }; diff --git a/packages/rstack/src/fmt/stdin.ts b/packages/rstack/src/fmt/stdin.ts new file mode 100644 index 0000000..dc946dc --- /dev/null +++ b/packages/rstack/src/fmt/stdin.ts @@ -0,0 +1,84 @@ +import { resolve } from 'node:path'; +import { createFileRequest } from './discovery.ts'; +import { formatFmtSource } from './format.ts'; +import { createFmtIgnoreMatcher } from './ignore.ts'; +import type { ResolvedFmtConfig } from './types.ts'; + +interface RunFmtStdinOptions { + /** Path used for per-file options and parser inference; it need not exist on disk. */ + filepath: string; + /** Absolute directory used to resolve the path. */ + cwd: string; + /** Loads the project config; its failures surface only after stdin is drained. */ + loadConfig: () => Promise; +} + +const readStdin = async (): Promise => { + const chunks: Buffer[] = []; + for await (const chunk of process.stdin) { + chunks.push(chunk as Buffer); + } + + return Buffer.concat(chunks).toString('utf8'); +}; + +/** + * Writes to stdout directly to keep the output byte-exact. A reader that + * closes the pipe early (`| head`) surfaces EPIPE, which is not a failure; + * other stream errors reject so the CLI-level handler reports them. + */ +const writeStdout = (output: string): Promise => + new Promise((resolvePromise, reject) => { + // The stream also emits 'error' for the same failure; swallow the event so + // only the write callback reports it. + process.stdout.once('error', () => {}); + process.stdout.write(output, (error) => { + if (error && (error as NodeJS.ErrnoException).code !== 'EPIPE') { + reject(error); + } else { + resolvePromise(); + } + }); + }); + +/** + * 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 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. + configPromise.catch(() => {}); + const source = await readStdin(); + const config = await configPromise; + + const absolutePath = resolve(cwd, filepath); + if (createFmtIgnoreMatcher(config)(absolutePath)) { + await writeStdout(source); + return; + } + + if (source === '') { + return; + } + + let file = createFileRequest(absolutePath, config); + if (file.options.plugins?.length) { + const { createFmtPluginResolver } = await import( + /* rspackChunkName: 'fmtPlugins' */ + './plugins.ts' + ); + file = { ...file, options: createFmtPluginResolver(config.rootPath)(file.options) }; + } + + const result = await formatFmtSource(file, () => source); + + if (result.status === 'unsupported') { + throw new Error(`No parser could be inferred for "${filepath}".`); + } + + await writeStdout(result.formatted); +}; + +export { runFmtStdin }; diff --git a/packages/rstack/src/fmt/worker.ts b/packages/rstack/src/fmt/worker.ts index b346b38..1b861e7 100644 --- a/packages/rstack/src/fmt/worker.ts +++ b/packages/rstack/src/fmt/worker.ts @@ -1,16 +1,9 @@ // Derived from @prettier/cli, see THIRD_PARTY_NOTICES.md import { readFileSync, writeFileSync } from 'node:fs'; -import { - format, - getFileInfo, - type FileInfoOptions, - type Options as PrettierOptions, -} from 'prettier'; -import { getPrettierPlugins } from './prettierPlugins.ts'; +import { formatFmtSource } from './format.ts'; import type { FmtFileRequest } from './types.ts'; -type PrettierPlugins = NonNullable; type FormatFileResult = 'changed' | 'unchanged' | 'unsupported'; interface FormatFileTask { @@ -18,54 +11,23 @@ interface FormatFileTask { shouldWrite: boolean; } -const fileInfoOptions = { - ignorePath: [], - resolveConfig: false, - withNodeModules: true, -} satisfies FileInfoOptions; - -/** Uses the configured parser or infers one without loading Prettier config. */ -const resolveFmtParser = async ( - filePath: string, - options: PrettierOptions, - plugins: PrettierPlugins, -): Promise => - options.parser ?? - ( - await getFileInfo(filePath, { - ...fileInfoOptions, - plugins, - }) - ).inferredParser; - /** * Use synchronous direct I/O inside the dedicated worker to avoid libuv * scheduling overhead. This prioritizes throughput over crash-safe replacement. */ -const formatFile = async ({ - file: { path, options }, - shouldWrite, -}: FormatFileTask): Promise => { - const plugins = await getPrettierPlugins(options, path); - const parser = await resolveFmtParser(path, options, plugins); - if (!parser) { +const formatFile = async ({ file, shouldWrite }: FormatFileTask): Promise => { + const result = await formatFmtSource(file, () => readFileSync(file.path, 'utf8')); + if (result.status === 'unsupported') { return 'unsupported'; } - const source = readFileSync(path, 'utf8'); - const formatted = await format(source, { - ...options, - filepath: path, - parser, - plugins, - }); - + const { source, formatted } = result; if (source === formatted) { return 'unchanged'; } if (shouldWrite) { - writeFileSync(path, formatted, 'utf8'); + writeFileSync(file.path, formatted, 'utf8'); } return 'changed'; diff --git a/packages/rstack/tests/cli/fmt/index.test.ts b/packages/rstack/tests/cli/fmt/index.test.ts index f409693..bcfbba5 100644 --- a/packages/rstack/tests/cli/fmt/index.test.ts +++ b/packages/rstack/tests/cli/fmt/index.test.ts @@ -33,7 +33,7 @@ const writeFixturePlugin = (): void => { ); }; -const runCLI = (args: string[]) => { +const runCLI = (args: string[], input?: string) => { const env: NodeJS.ProcessEnv = { ...process.env, NO_COLOR: '1' }; delete env.FORCE_COLOR; @@ -41,11 +41,14 @@ const runCLI = (args: string[]) => { cwd: projectPath, encoding: 'utf8', env, + input, }); }; const runFmt = (args: string[] = []) => runCLI(['fmt', ...args]); +const runFmtStdin = (args: string[], input: string) => runCLI(['fmt', ...args], input); + const normalizeDuration = (output: string): string => output.replace(/\d+m(?: \d+(?:\.\d+)?s)?|\d+(?:\.\d+)?s/g, ''); @@ -96,16 +99,24 @@ test('supports format as an alias for fmt', () => { expect(readProjectFile('index.ts')).toBe('const message = "hello";\n'); }); -test('returns exit code 1 for invalid arguments', () => { +test('returns exit code 2 for invalid arguments', () => { const result = runFmt(['--write', '--check']); - expect(result.status).toBe(1); + expect(result.status).toBe(2); expect(result.stdout).toBe(''); expect(result.stderr).toContain( 'The --write, --check, and --list-different options cannot be used together.', ); }); +test('returns exit code 2 for unknown options', () => { + const result = runFmt(['--bogus']); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain('--bogus'); +}); + test('formats the current directory with Prettier defaults', () => { writeProjectFile('index.ts', 'const message="hello"'); @@ -383,6 +394,148 @@ test('reports partial writes when formatting fails', () => { expect(readProjectFile('invalid.ts')).toBe('const invalid = ;'); }); +test('formats stdin for the given filepath', () => { + const result = runFmtStdin(['--stdin-filepath', 'src/index.ts'], 'const message="hello"'); + + expect(result.status).toBe(0); + expect(result.stdout).toBe('const message = "hello";\n'); + expect(result.stderr).toBe(''); +}); + +test('formats stdin with the camel-case option', () => { + const result = runFmtStdin(['--stdinFilepath', 'data.json'], '{"a":1,"b":[2,3]}'); + + expect(result.status).toBe(0); + expect(result.stdout).toBe('{ "a": 1, "b": [2, 3] }\n'); + expect(result.stderr).toBe(''); +}); + +test('applies define.fmt options and overrides to stdin', () => { + writeProjectFile( + 'rstack.config.ts', + `import { define } from 'rstack'; + +define.fmt({ + singleQuote: true, + overrides: [ + { + files: '*.test.ts', + options: { + semi: false, + }, + }, + ], +}); +`, + ); + + const result = runFmtStdin(['--stdin-filepath', 'src/index.test.ts'], 'const test="test"'); + + expect(result.status).toBe(0); + expect(result.stdout).toBe("const test = 'test'\n"); + expect(result.stderr).toBe(''); +}); + +test('sorts package.json from stdin', () => { + writeProjectFile( + 'rstack.config.ts', + `import { define } from 'rstack'; + +define.fmt({ sortPackageJson: true }); +`, + ); + + const result = runFmtStdin(['--stdin-filepath', 'package.json'], packageJsonSource); + + expect(result.status).toBe(0); + expect(result.stdout).toBe(sortedPackageJson); + expect(result.stderr).toBe(''); +}); + +test('echoes ignored stdin paths verbatim', () => { + writeProjectFile( + 'rstack.config.ts', + `import { define } from 'rstack'; + +define.fmt({ ignorePatterns: ['src/ignored.ts'] }); +`, + ); + + const source = 'const ignored="ignored"'; + const result = runFmtStdin(['--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); + + expect(result.status).toBe(0); + expect(result.stdout).toBe(source); + expect(result.stderr).toBe(''); +}); + +test('returns exit code 2 when no parser can be inferred for stdin', () => { + const result = runFmtStdin(['--stdin-filepath', 'data.unknown'], 'value'); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain('No parser could be inferred for "data.unknown".'); +}); + +test('returns exit code 2 for stdin parse errors', () => { + const result = runFmtStdin(['--stdin-filepath', 'index.ts'], 'const value = ;'); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain("Unexpected token ';'"); +}); + +test.each(['--write', '--check', '--list-different'])( + 'returns exit code 2 for %s with --stdin-filepath', + (option) => { + const result = runFmtStdin(['--stdin-filepath', 'index.ts', option], 'const value=1'); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain( + 'The --stdin-filepath option cannot be used with --write, --check, or --list-different.', + ); + }, +); + +test('returns exit code 2 for file arguments with --stdin-filepath', () => { + const result = runFmtStdin(['--stdin-filepath', 'index.ts', 'src/other.ts'], 'const value=1'); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain( + 'The --stdin-filepath option cannot be used with file arguments.', + ); +}); + +test('accepts --parallel-workers with --stdin-filepath', () => { + const result = runFmtStdin( + ['--stdin-filepath', 'index.ts', '--parallel-workers', '2'], + 'const value=1', + ); + + expect(result.status).toBe(0); + expect(result.stdout).toBe('const value = 1;\n'); + expect(result.stderr).toBe(''); +}); + +test('writes nothing for empty stdin', () => { + const result = runFmtStdin(['--stdin-filepath', 'index.ts'], ''); + + expect(result.status).toBe(0); + expect(result.stdout).toBe(''); + expect(result.stderr).toBe(''); +}); + test('reports when no files match', () => { const writeResult = runFmt(['missing/**/*.ts']); diff --git a/packages/rstack/tests/fmt/cli.test.ts b/packages/rstack/tests/fmt/cli.test.ts index 6708c83..e9639fb 100644 --- a/packages/rstack/tests/fmt/cli.test.ts +++ b/packages/rstack/tests/fmt/cli.test.ts @@ -89,12 +89,48 @@ test.each(['--help', '-h'])('parses %s', (option) => { expect(parseFmtCLIArgs([option]).help).toBe(true); }); +test.each(['--stdin-filepath', '--stdinFilepath'])('parses %s', (option) => { + expect(parseFmtCLIArgs([option, 'src/index.ts'])).toEqual({ + mode: 'write', + patterns: [], + maxWorkers: undefined, + help: false, + stdinFilepath: 'src/index.ts', + }); +}); + +test('accepts a worker count with --stdin-filepath', () => { + expect(parseFmtCLIArgs(['--stdin-filepath', 'index.ts', '--parallel-workers', '2'])).toEqual({ + mode: 'write', + patterns: [], + maxWorkers: 2, + help: false, + stdinFilepath: 'index.ts', + }); +}); + +test.each(['--write', '--check', '--list-different', '--listDifferent'])( + 'rejects %s with --stdin-filepath', + (option) => { + expect(() => parseFmtCLIArgs(['--stdin-filepath', 'index.ts', option])).toThrow( + 'The --stdin-filepath option cannot be used with --write, --check, or --list-different.', + ); + }, +); + +test('rejects file arguments with --stdin-filepath', () => { + expect(() => parseFmtCLIArgs(['--stdin-filepath', 'index.ts', 'src/other.ts'])).toThrow( + 'The --stdin-filepath option cannot be used with file arguments.', + ); +}); + test('provides command help', () => { expect(fmtHelpMessage).toContain('Usage:\n $ rs fmt [options] [files/globs...]'); expect(fmtHelpMessage).toContain('--write'); expect(fmtHelpMessage).toContain('--check'); expect(fmtHelpMessage).toContain('--list-different'); expect(fmtHelpMessage).toContain('--parallel-workers '); + expect(fmtHelpMessage).toContain('--stdin-filepath '); expect(fmtHelpMessage).toContain('-h, --help'); }); diff --git a/packages/rstack/tests/fmt/format.test.ts b/packages/rstack/tests/fmt/format.test.ts new file mode 100644 index 0000000..e8a7abe --- /dev/null +++ b/packages/rstack/tests/fmt/format.test.ts @@ -0,0 +1,62 @@ +import path from 'node:path'; +import { expect, test } from 'rstack/test'; +import { formatFmtSource } from '../../src/fmt/format.ts'; + +const rootPath = path.join(import.meta.dirname, 'fixture'); + +test('formats sources without touching the file system', async () => { + await expect( + formatFmtSource( + { path: path.join(rootPath, 'missing.ts'), options: {} }, + () => 'const value=1', + ), + ).resolves.toEqual({ + status: 'formatted', + source: 'const value=1', + formatted: 'const value = 1;\n', + }); +}); + +test('applies resolved options to the source', async () => { + const result = await formatFmtSource( + { path: path.join(rootPath, 'missing.ts'), options: { singleQuote: true, semi: false } }, + () => 'const message="hello"', + ); + + expect(result).toEqual({ + status: 'formatted', + source: 'const message="hello"', + formatted: "const message = 'hello'\n", + }); +}); + +test('sorts package.json when the option is enabled', async () => { + const result = await formatFmtSource( + { path: path.join(rootPath, 'package.json'), options: { sortPackageJson: true } }, + () => '{"version":"1.0.0","name":"fixture"}', + ); + + expect(result).toEqual({ + status: 'formatted', + source: '{"version":"1.0.0","name":"fixture"}', + formatted: '{\n "name": "fixture",\n "version": "1.0.0"\n}\n', + }); +}); + +test('reports unsupported files before reading the source', async () => { + let read = false; + + await expect( + formatFmtSource({ path: path.join(rootPath, 'missing.unknown'), options: {} }, () => { + read = true; + return ''; + }), + ).resolves.toEqual({ status: 'unsupported' }); + expect(read).toBe(false); +}); + +test('rejects sources that cannot be parsed', async () => { + await expect( + formatFmtSource({ path: path.join(rootPath, 'invalid.ts'), options: {} }, () => 'const x = ;'), + ).rejects.toThrow("Unexpected token ';'"); +}); diff --git a/packages/rstack/tests/fmt/worker.test.ts b/packages/rstack/tests/fmt/worker.test.ts index 530618c..78730aa 100644 --- a/packages/rstack/tests/fmt/worker.test.ts +++ b/packages/rstack/tests/fmt/worker.test.ts @@ -1,5 +1,4 @@ import { readFileSync } from 'node:fs'; -import path from 'node:path'; import { expect, test } from 'rstack/test'; import { formatFile } from '../../src/fmt/worker.ts'; import { withTempProject, writeProjectFile } from './helpers.ts'; @@ -42,19 +41,3 @@ test('infers the parser for an explicitly provided node_modules file', async () expect(readFileSync(filePath, 'utf8')).toBe(source); }); }); - -test('skips unsupported files before reading them', async () => { - await withTempProject(async (rootPath) => { - const filePath = path.join(rootPath, 'missing.unknown'); - - await expect( - formatFile({ - file: { - path: filePath, - options: {}, - }, - shouldWrite: true, - }), - ).resolves.toBe('unsupported'); - }); -}); diff --git a/website/docs/en/guide/cli/fmt.mdx b/website/docs/en/guide/cli/fmt.mdx index fd1eb63..f6a59b1 100644 --- a/website/docs/en/guide/cli/fmt.mdx +++ b/website/docs/en/guide/cli/fmt.mdx @@ -37,6 +37,9 @@ rs format | `--check` | Check formatting without writing files. Exits with code 1 when files are different. | | `--list-different` | Print only unformatted paths. Exits with code 1 when files are different. | | `--parallel-workers ` | Set the maximum number of formatting workers. | +| `--stdin-filepath ` | Format stdin as if it were saved at `` and print the result to stdout. | | `-h, --help` | Display usage and option information. | > `--write`, `--check`, and `--list-different` are mutually exclusive. + +> `--stdin-filepath` cannot be combined with `--write`, `--check`, `--list-different`, or file arguments. diff --git a/website/docs/en/guide/formatting.mdx b/website/docs/en/guide/formatting.mdx index 3ed9eb6..9fdddb5 100644 --- a/website/docs/en/guide/formatting.mdx +++ b/website/docs/en/guide/formatting.mdx @@ -71,6 +71,16 @@ 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: diff --git a/website/docs/zh/guide/cli/fmt.mdx b/website/docs/zh/guide/cli/fmt.mdx index 848dee2..689a9ce 100644 --- a/website/docs/zh/guide/cli/fmt.mdx +++ b/website/docs/zh/guide/cli/fmt.mdx @@ -31,12 +31,15 @@ rs format ## 选项 \{#options} -| 选项 | 说明 | -| ---------------------------- | ----------------------------------------------------- | -| `--write` | 将格式化结果写回文件。这是默认模式。 | -| `--check` | 检查格式但不写入文件;存在格式差异时以状态码 1 退出。 | -| `--list-different` | 仅输出未格式化的路径;存在格式差异时以状态码 1 退出。 | -| `--parallel-workers ` | 设置格式化 worker 的最大数量。 | -| `-h, --help` | 显示命令用法和选项。 | +| 选项 | 说明 | +| ---------------------------- | --------------------------------------------------------- | +| `--write` | 将格式化结果写回文件。这是默认模式。 | +| `--check` | 检查格式但不写入文件;存在格式差异时以状态码 1 退出。 | +| `--list-different` | 仅输出未格式化的路径;存在格式差异时以状态码 1 退出。 | +| `--parallel-workers ` | 设置格式化 worker 的最大数量。 | +| `--stdin-filepath ` | 将标准输入按保存在 `` 的文件格式化并输出到 stdout。 | +| `-h, --help` | 显示命令用法和选项。 | > `--write`、`--check` 和 `--list-different` 不能同时使用。 + +> `--stdin-filepath` 不能与 `--write`、`--check`、`--list-different` 或文件参数同时使用。 diff --git a/website/docs/zh/guide/formatting.mdx b/website/docs/zh/guide/formatting.mdx index 4ba9925..9f04b82 100644 --- a/website/docs/zh/guide/formatting.mdx +++ b/website/docs/zh/guide/formatting.mdx @@ -71,6 +71,16 @@ 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` 排除不需要格式化的文件: