From 6494ba2ddb33eba2ba3938f42775659795ab6ef2 Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Tue, 4 Aug 2026 13:05:01 +0800 Subject: [PATCH 01/21] docs: improve rs fmt CLI option reference (#171) --- website/docs/en/guide/cli/fmt.mdx | 75 ++++++++++++++++++++++++++----- website/docs/zh/guide/cli/fmt.mdx | 75 ++++++++++++++++++++++++++----- 2 files changed, 128 insertions(+), 22 deletions(-) diff --git a/website/docs/en/guide/cli/fmt.mdx b/website/docs/en/guide/cli/fmt.mdx index f6a59b18..d30a0e16 100644 --- a/website/docs/en/guide/cli/fmt.mdx +++ b/website/docs/en/guide/cli/fmt.mdx @@ -8,7 +8,7 @@ The `rs fmt` command formats files or checks whether they are formatted. For det rs fmt [options] [files/globs...] ``` -Pass files, directories, or glob patterns to choose what to format. When no paths are provided, `rs fmt` formats the current directory. +Pass files, directories, or glob patterns to choose what to format. When no paths are provided, `rs fmt` formats the current directory. See [Formatting scope](../formatting#formatting-scope) for path resolution and ignore rules. Examples: @@ -31,15 +31,68 @@ rs format ## Options -| Option | Description | -| ---------------------------- | ----------------------------------------------------------------------------------- | -| `--write` | Write formatted files in place. This is the default mode. | -| `--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. | +### `--check` -> `--write`, `--check`, and `--list-different` are mutually exclusive. +Check whether files are formatted without changing them. The output lists files with formatting issues and includes a human-friendly summary, making this option useful in CI: -> `--stdin-filepath` cannot be combined with `--write`, `--check`, `--list-different`, or file arguments. +```bash +rs fmt . --check +``` + +`--check` cannot be combined with `--write` or `--list-different`. + +The command uses the following exit codes: + +| Code | Meaning | +| ---- | --------------------------------------------------------- | +| `0` | All matched files are formatted. | +| `1` | One or more matched files have formatting issues. | +| `2` | `rs fmt` could not run or encountered a formatting error. | + +### `-h, --help` + +Display usage and option information without formatting files: + +```bash +rs fmt --help +``` + +### `--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: + +```bash +rs fmt . --list-different +``` + +The option uses the same exit codes as `--check` and cannot be combined with `--write` or `--check`. + +### `--parallel-workers ` + +Set the maximum number of formatting workers to a positive integer: + +```bash +rs fmt . --parallel-workers 4 +``` + +When this option is omitted, `rs fmt` automatically chooses up to eight workers based on the available CPU parallelism and the number of matched files. Set a lower value to limit CPU or memory usage in constrained environments. + +### `--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: + +```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. + +### `--write` + +Write formatted files in place. This is the default mode, so specifying `--write` is optional: + +```bash +rs fmt src --write +``` + +`--write` cannot be combined with `--check` or `--list-different`. diff --git a/website/docs/zh/guide/cli/fmt.mdx b/website/docs/zh/guide/cli/fmt.mdx index 689a9cef..cda3f3c2 100644 --- a/website/docs/zh/guide/cli/fmt.mdx +++ b/website/docs/zh/guide/cli/fmt.mdx @@ -8,7 +8,7 @@ rs fmt [options] [files/globs...] ``` -可以传入文件、目录或 glob 模式来指定格式化范围。不传入路径时,`rs fmt` 会格式化当前目录。 +可以传入文件、目录或 glob 模式来指定格式化范围。不传入路径时,`rs fmt` 会格式化当前目录。路径解析和忽略规则请参考[格式化范围](../formatting#formatting-scope)。 示例: @@ -31,15 +31,68 @@ rs format ## 选项 \{#options} -| 选项 | 说明 | -| ---------------------------- | --------------------------------------------------------- | -| `--write` | 将格式化结果写回文件。这是默认模式。 | -| `--check` | 检查格式但不写入文件;存在格式差异时以状态码 1 退出。 | -| `--list-different` | 仅输出未格式化的路径;存在格式差异时以状态码 1 退出。 | -| `--parallel-workers ` | 设置格式化 worker 的最大数量。 | -| `--stdin-filepath ` | 将标准输入按保存在 `` 的文件格式化并输出到 stdout。 | -| `-h, --help` | 显示命令用法和选项。 | +### `--check` -> `--write`、`--check` 和 `--list-different` 不能同时使用。 +检查文件是否已格式化,但不修改文件。输出会列出存在格式问题的文件,并提供便于阅读的汇总信息,因此适合在 CI 中使用: -> `--stdin-filepath` 不能与 `--write`、`--check`、`--list-different` 或文件参数同时使用。 +```bash +rs fmt . --check +``` + +`--check` 不能与 `--write` 或 `--list-different` 同时使用。 + +该命令使用以下退出状态码: + +| 状态码 | 含义 | +| ------ | ------------------------------------------- | +| `0` | 所有匹配的文件均已格式化。 | +| `1` | 一个或多个匹配的文件存在格式问题。 | +| `2` | `rs fmt` 无法运行或在格式化过程中遇到错误。 | + +### `-h, --help` + +显示命令用法和选项信息,但不格式化文件: + +```bash +rs fmt --help +``` + +### `--list-different` + +输出未格式化文件的路径,但不提供 `--check` 的汇总信息。需要将结果交给其他命令处理时,可以使用此选项: + +```bash +rs fmt . --list-different +``` + +此选项与 `--check` 使用相同的退出状态码,且不能与 `--write` 或 `--check` 同时使用。 + +### `--parallel-workers ` + +将格式化 worker 的最大数量设置为正整数: + +```bash +rs fmt . --parallel-workers 4 +``` + +省略此选项时,`rs fmt` 会根据可用的 CPU 并行度和匹配的文件数量,自动选择最多 8 个 worker。在资源受限的环境中,可以设置较小的值来限制 CPU 或内存用量。 + +### `--stdin-filepath ` + +将 stdin 传入的内容按保存在 `` 的文件进行格式化。该路径用于确定 parser 和匹配的覆盖配置,但不需要在磁盘上真实存在。格式化结果会输出到 stdout: + +```bash +cat src/index.ts | rs fmt --stdin-filepath src/index.ts +``` + +`--stdin-filepath` 不能与文件参数或 `--write`、`--check`、`--list-different` 同时使用。详细说明请参考[格式化标准输入](../formatting#formatting-stdin)。 + +### `--write` + +将格式化结果写回文件。这是默认模式,因此可以省略 `--write`: + +```bash +rs fmt src --write +``` + +`--write` 不能与 `--check` 或 `--list-different` 同时使用。 From 723dc5bf439bcd9ad76c7235e9b55df9cd51394c Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Tue, 4 Aug 2026 14:28:06 +0800 Subject: [PATCH 02/21] feat(fmt): support --ignore-path (#172) --- .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 c411c06f..c584b861 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 06fda1d1..480acb87 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 9a90a287..77d992de 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 41f80bf7..21d58605 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 dc946dc3..3534d54d 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 451f65d8..111ddb4d 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 bcfbba51..75aef5ea 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 e9639fbc..bc97edb4 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 63501dd9..c88de37e 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 d30a0e16..72e94027 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 9fdddb51..e814ba64 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 cda3f3c2..6b8ed964 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 9f04b823..d9e68c47 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`。 From 0b74af37f32ffb281727abf2d09cd82ea8eda0d1 Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Tue, 4 Aug 2026 16:19:47 +0800 Subject: [PATCH 03/21] fix(fmt): align help option descriptions (#173) --- packages/rstack/src/fmt/cli.ts | 12 ++++++------ .../tests/fmt/__snapshots__/cli.test.ts.snap | 17 +++++++++++++++++ packages/rstack/tests/fmt/cli.test.ts | 12 ++++-------- 3 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 packages/rstack/tests/fmt/__snapshots__/cli.test.ts.snap diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index 480acb87..ab565296 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -26,13 +26,13 @@ ${color.yellow(' $ rs fmt [options] [files/globs...]')} Format files with Prettier. ${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) + --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`; + --stdin-filepath Format stdin as if it were saved at + -h, --help Display this help message`; const parseMaxWorkers = ( kebabValue: string | undefined, diff --git a/packages/rstack/tests/fmt/__snapshots__/cli.test.ts.snap b/packages/rstack/tests/fmt/__snapshots__/cli.test.ts.snap new file mode 100644 index 00000000..b22c2561 --- /dev/null +++ b/packages/rstack/tests/fmt/__snapshots__/cli.test.ts.snap @@ -0,0 +1,17 @@ +// Rstest Snapshot v1 + +exports[`provides command help 1`] = ` +"Usage: + $ rs fmt [options] [files/globs...] + +Format files with Prettier. + +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" +`; diff --git a/packages/rstack/tests/fmt/cli.test.ts b/packages/rstack/tests/fmt/cli.test.ts index bc97edb4..61cf2f40 100644 --- a/packages/rstack/tests/fmt/cli.test.ts +++ b/packages/rstack/tests/fmt/cli.test.ts @@ -139,14 +139,10 @@ test('rejects file arguments with --stdin-filepath', () => { }); 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('--ignore-path '); - expect(fmtHelpMessage).toContain('--parallel-workers '); - expect(fmtHelpMessage).toContain('--stdin-filepath '); - expect(fmtHelpMessage).toContain('-h, --help'); + const helpMessage = stripVTControlCharacters(fmtHelpMessage).replace(/^Rstack v.*\n\n/, ''); + + expect(helpMessage).toContain('Usage:\n $ rs fmt [options] [files/globs...]'); + expect(helpMessage).toMatchSnapshot(); }); test.each([ From d5693659bde0424b93307896eae2e5455dea5afa Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Tue, 4 Aug 2026 17:35:12 +0800 Subject: [PATCH 04/21] perf(fmt): prune ignored directories during discovery (#174) --- packages/rstack/src/fmt/discoverPaths.ts | 12 ++++++--- packages/rstack/src/fmt/discovery.ts | 12 +++++---- packages/rstack/src/fmt/ignore.ts | 10 +++++--- .../rstack/tests/fmt/discoverPaths.test.ts | 25 +++++++++++++++++++ packages/rstack/tests/fmt/discovery.test.ts | 21 ++++++++++++++++ packages/rstack/tests/fmt/ignore.test.ts | 9 +++++++ 6 files changed, 77 insertions(+), 12 deletions(-) diff --git a/packages/rstack/src/fmt/discoverPaths.ts b/packages/rstack/src/fmt/discoverPaths.ts index 896aba0b..f0e16f45 100644 --- a/packages/rstack/src/fmt/discoverPaths.ts +++ b/packages/rstack/src/fmt/discoverPaths.ts @@ -11,6 +11,8 @@ interface DiscoverFmtPathsOptions { /** Absolute directory used to resolve input paths. */ cwd: string; patterns?: string[]; + /** Returns whether a scanned directory can be pruned before traversal. */ + isDirectoryIgnored?: (directoryPath: string) => boolean; } const isErrnoException = (error: unknown): error is NodeJS.ErrnoException => @@ -201,6 +203,7 @@ class GitIgnoreMatcher { const createTraversalOptions = ( gitIgnore: GitIgnoreMatcher, isIncluded?: (filePath: string) => boolean, + isDirectoryIgnored?: (directoryPath: string) => boolean, ) => { // tiny-readdir passes only a path to `ignore`, so retain the dirent type briefly. const directories = new Set(); @@ -214,7 +217,7 @@ const createTraversalOptions = ( } if (isDirectory) { - return gitIgnore.isIgnored(targetPath, true); + return gitIgnore.isIgnored(targetPath, true) || isDirectoryIgnored?.(targetPath) === true; } return ( @@ -343,6 +346,7 @@ const getTraversalRoots = (cwd: string, directories: string[], globs: string[]): const discoverFmtPaths = async ({ cwd, patterns: inputPatterns, + isDirectoryIgnored, }: DiscoverFmtPathsOptions): Promise => { const patterns = inputPatterns?.length ? inputPatterns : ['.']; const { @@ -366,7 +370,7 @@ const discoverFmtPaths = async ({ } await gitIgnore.loadThrough(rootPath); - if (gitIgnore.isIgnored(rootPath, true)) { + if (gitIgnore.isIgnored(rootPath, true) || isDirectoryIgnored?.(rootPath) === true) { return []; } @@ -384,7 +388,9 @@ const discoverFmtPaths = async ({ return globMatchers.some((matches) => matches(relativePath)); }; - return (await readdir(rootPath, createTraversalOptions(gitIgnore, isIncluded))).files; + return ( + await readdir(rootPath, createTraversalOptions(gitIgnore, isIncluded, isDirectoryIgnored)) + ).files; }), ); diff --git a/packages/rstack/src/fmt/discovery.ts b/packages/rstack/src/fmt/discovery.ts index 77d992de..47dc1572 100644 --- a/packages/rstack/src/fmt/discovery.ts +++ b/packages/rstack/src/fmt/discovery.ts @@ -8,17 +8,19 @@ const createFileRequest = (filePath: string, config: ResolvedFmtConfig): FmtFile options: resolveFmtOptions(filePath, config), }); -/** Discovers worker-ready files without reading Prettier config files or `.prettierignore`. */ +/** Discovers worker-ready files without automatically reading Prettier config or ignore files. */ const discoverFmtFiles = async ({ cwd, patterns, ignorePaths, config, }: DiscoverFmtFilesOptions): Promise => { - const [candidates, isIgnored] = await Promise.all([ - discoverFmtPaths({ cwd, patterns }), - createIgnoreMatcher({ config, cwd, ignorePaths }), - ]); + const isIgnored = await createIgnoreMatcher({ config, cwd, ignorePaths }); + const candidates = await discoverFmtPaths({ + cwd, + patterns, + isDirectoryIgnored: (directoryPath) => isIgnored(directoryPath, true), + }); if (candidates.length === 0) { return []; } diff --git a/packages/rstack/src/fmt/ignore.ts b/packages/rstack/src/fmt/ignore.ts index 21d58605..08130985 100644 --- a/packages/rstack/src/fmt/ignore.ts +++ b/packages/rstack/src/fmt/ignore.ts @@ -11,7 +11,7 @@ import type { ResolvedFmtConfig } from './types.ts'; */ const defaultIgnorePatterns = ['package-lock.json', 'pnpm-lock.yaml']; -type IgnoreMatcher = (filePath: string) => boolean; +type IgnoreMatcher = (filePath: string, isDirectory?: boolean) => boolean; interface CreateIgnoreMatcherOptions { config: ResolvedFmtConfig; @@ -23,7 +23,8 @@ interface CreateIgnoreMatcherOptions { const createPatternMatcher = (rootPath: string, patterns: string): IgnoreMatcher => { const matches = fastIgnore(patterns); - return (filePath) => matches(path.relative(rootPath, filePath)); + return (filePath, isDirectory = false) => + matches(path.relative(rootPath, filePath), { isDirectory }); }; const loadIgnoreMatcher = async (cwd: string, ignorePath: string): Promise => { @@ -55,8 +56,9 @@ const createIgnoreMatcher = async ({ ignorePaths.map((ignorePath) => loadIgnoreMatcher(cwd, ignorePath)), ); - return (filePath) => - configMatcher(filePath) || ignoreMatchers.some((matches) => matches(filePath)); + return (filePath, isDirectory = false) => + configMatcher(filePath, isDirectory) || + ignoreMatchers.some((matches) => matches(filePath, isDirectory)); }; export { createIgnoreMatcher }; diff --git a/packages/rstack/tests/fmt/discoverPaths.test.ts b/packages/rstack/tests/fmt/discoverPaths.test.ts index 7de16a3e..a712f61e 100644 --- a/packages/rstack/tests/fmt/discoverPaths.test.ts +++ b/packages/rstack/tests/fmt/discoverPaths.test.ts @@ -112,6 +112,31 @@ test('lets explicit files bypass gitignore', async () => { }); }); +test('prunes directories with an external ignore matcher', async () => { + await withTempProject(async (rootPath) => { + writeProjectFile(rootPath, 'generated/nested/output.ts'); + writeProjectFile(rootPath, 'src/index.ts'); + const checkedDirectories: string[] = []; + const generatedPath = path.join(rootPath, 'generated'); + const isDirectoryIgnored = (directoryPath: string): boolean => { + checkedDirectories.push(path.relative(rootPath, directoryPath)); + return directoryPath === generatedPath; + }; + + const files = await discoverFmtPaths({ cwd: rootPath, isDirectoryIgnored }); + const ignoredRoot = await discoverFmtPaths({ + cwd: rootPath, + patterns: ['generated'], + isDirectoryIgnored, + }); + + expect(relativePaths(rootPath, files)).toEqual([path.join('src', 'index.ts')]); + expect(ignoredRoot).toEqual([]); + expect(checkedDirectories).toContain('generated'); + expect(checkedDirectories).not.toContain(path.join('generated', 'nested')); + }); +}); + test.runIf(process.platform !== 'win32')('does not follow file or directory symlinks', async () => { await withTempProject(async (rootPath) => { const targetPath = writeProjectFile(rootPath, 'target/index.ts'); diff --git a/packages/rstack/tests/fmt/discovery.test.ts b/packages/rstack/tests/fmt/discovery.test.ts index bc98e69d..9ee6443f 100644 --- a/packages/rstack/tests/fmt/discovery.test.ts +++ b/packages/rstack/tests/fmt/discovery.test.ts @@ -47,6 +47,27 @@ test('applies config ignore patterns outside the config root', async () => { }); }); +test('keeps files re-included by a CLI ignore file during directory traversal', async () => { + await withTempProject(async (rootPath) => { + writeProjectFile(rootPath, '.prettierignore', 'generated/*\n!generated/keep.ts\n'); + writeProjectFile(rootPath, 'generated/drop.ts'); + writeProjectFile(rootPath, 'generated/keep.ts'); + writeProjectFile(rootPath, 'src/index.ts'); + + const files = await discoverFmtFiles({ + cwd: rootPath, + patterns: ['**/*.ts'], + ignorePaths: ['.prettierignore'], + config: normalizeFmtConfig(undefined, rootPath), + }); + + expect(relativePaths(rootPath, files)).toEqual([ + path.join('generated', 'keep.ts'), + path.join('src', 'index.ts'), + ]); + }); +}); + test('defers parser inference to workers and preserves an explicit parser', async () => { await withTempProject(async (rootPath) => { writeProjectFile(rootPath, 'index.js'); diff --git a/packages/rstack/tests/fmt/ignore.test.ts b/packages/rstack/tests/fmt/ignore.test.ts index c88de37e..192ff6a0 100644 --- a/packages/rstack/tests/fmt/ignore.test.ts +++ b/packages/rstack/tests/fmt/ignore.test.ts @@ -29,6 +29,15 @@ test('matches gitignore patterns relative to the config root', async () => { expect(isIgnored(path.join(rootPath, 'src/index.js'))).toBe(false); }); +test('distinguishes directory-only patterns from files', async () => { + const isIgnored = await createMatcher(['dist/']); + const directoryPath = path.join(rootPath, 'dist'); + + expect(isIgnored(directoryPath)).toBe(false); + expect(isIgnored(directoryPath, true)).toBe(true); + expect(isIgnored(path.join(directoryPath, 'index.js'))).toBe(true); +}); + 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']); From d2647b3d1464009fda0b340468f30721afaf05c0 Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Tue, 4 Aug 2026 18:57:21 +0800 Subject: [PATCH 05/21] docs: clarify rs fmt ignore rules (#175) --- website/docs/en/guide/cli/fmt.mdx | 30 +++++++++++++++++++++------- website/docs/en/guide/formatting.mdx | 14 ++++++++++--- website/docs/zh/guide/cli/fmt.mdx | 26 +++++++++++++++++++++--- website/docs/zh/guide/formatting.mdx | 14 ++++++++++--- 4 files changed, 68 insertions(+), 16 deletions(-) diff --git a/website/docs/en/guide/cli/fmt.mdx b/website/docs/en/guide/cli/fmt.mdx index 72e94027..4276ae22 100644 --- a/website/docs/en/guide/cli/fmt.mdx +++ b/website/docs/en/guide/cli/fmt.mdx @@ -59,17 +59,33 @@ rs fmt --help ### `--ignore-path ` -Load additional Gitignore-compatible rules from ``. Repeat the option to load multiple -ignore files: +Use `--ignore-path` to load additional Gitignore-compatible rules from a file. + +Relative ignore-file paths are resolved from the current working directory. Rules inside a file are resolved from the directory containing that file. + +For example, run the following command from the project root: + +```bash +rs fmt --ignore-path config/format.ignore +``` + +If `config/format.ignore` contains this rule: + +```text title="config/format.ignore" +generated/** +``` + +Here, `config/format.ignore` is located relative to the project root. The `generated/**` rule is relative to `config/`. It therefore ignores `config/generated/**` instead of `generated/**` in the project root. + +Loaded rules apply to scanned paths, explicitly passed files, and `--stdin-filepath`. + +To load multiple ignore files, repeat the option: ```bash -rs fmt --ignore-path .prettierignore --ignore-path config/docs.ignore +rs fmt --ignore-path .prettierignore --ignore-path config/format.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`. +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`. ### `--list-different` diff --git a/website/docs/en/guide/formatting.mdx b/website/docs/en/guide/formatting.mdx index e814ba64..380c5f82 100644 --- a/website/docs/en/guide/formatting.mdx +++ b/website/docs/en/guide/formatting.mdx @@ -45,7 +45,7 @@ In addition to Prettier options and `overrides`, Rstack provides two options: :::warning Prettier configuration files -`rs fmt` does not read Prettier configuration files, `.prettierignore`, or `.editorconfig`. Keep formatting options and additional ignore rules in `define.fmt()`. +`rs fmt` does not automatically load Prettier configuration files, `.prettierignore`, or `.editorconfig`. Keep formatting options and additional ignore rules in `define.fmt()`. To load an ignore file explicitly, use [`--ignore-path`](./cli/fmt#--ignore-path-path). ::: @@ -85,8 +85,6 @@ 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`. @@ -101,6 +99,16 @@ define.fmt({ }); ``` +### Ignore order + +`rs fmt` uses the following three steps to decide which paths to format: + +1. **Process command-line arguments and `.gitignore`**: `rs fmt` first processes the files, directories, and glob patterns passed on the command line. A glob that starts with `!` excludes matching paths. Directory and glob scans follow `.gitignore`, while files passed directly do not. Paths excluded in this step cannot be re-included later. +2. **Apply default ignore rules and `ignorePatterns`**: `rs fmt` ignores [lock files](#lock-files) by default, then applies `ignorePatterns`. These rules are evaluated in order, with later rules taking precedence. For example, `!pnpm-lock.yaml` re-includes the otherwise ignored file. +3. **Apply files specified with [`--ignore-path`](./cli/fmt#--ignore-path-path)**: Each ignore file is evaluated separately, and later rules take precedence within that file. Exclusions from different files and `ignorePatterns` are combined: if any source ignores a path, that path remains excluded, even if another source re-includes it. + +> `rs fmt` still applies the rules from the second and third steps to files passed directly on the command line and to paths specified with [`--stdin-filepath`](./cli/fmt#--stdin-filepath-path). It formats a path only if none of these rules excludes it. + ## Sort package.json fields \{#sort-package-json} Enable `sortPackageJson` to sort fields in each selected `package.json` with [`sort-package-json`](https://github.com/keithamus/sort-package-json): diff --git a/website/docs/zh/guide/cli/fmt.mdx b/website/docs/zh/guide/cli/fmt.mdx index 6b8ed964..a6c06a48 100644 --- a/website/docs/zh/guide/cli/fmt.mdx +++ b/website/docs/zh/guide/cli/fmt.mdx @@ -59,13 +59,33 @@ rs fmt --help ### `--ignore-path ` -从 `` 加载额外的 Gitignore 兼容规则。重复传入该选项可以加载多个 ignore 文件: +使用 `--ignore-path` 从文件中加载额外的 Gitignore 兼容规则。 + +相对的 ignore 文件路径基于当前工作目录解析。文件中的规则基于该文件所在目录解析。 + +例如,在项目根目录执行以下命令: + +```bash +rs fmt --ignore-path config/format.ignore +``` + +假设 `config/format.ignore` 包含以下规则: + +```text title="config/format.ignore" +generated/** +``` + +这里,`config/format.ignore` 相对项目根目录定位。文件中的 `generated/**` 规则则相对 `config/` 目录解析。因此,它会忽略 `config/generated/**`,而不是项目根目录下的 `generated/**`。 + +加载的规则会作用于扫描得到的路径、显式传入的文件和 `--stdin-filepath`。 + +如需加载多个 ignore 文件,可以重复传入该选项: ```bash -rs fmt --ignore-path .prettierignore --ignore-path config/docs.ignore +rs fmt --ignore-path .prettierignore --ignore-path config/format.ignore ``` -相对 ignore 路径基于当前工作目录解析;每个文件中的规则基于该 ignore 文件所在目录解析,并追加到内置忽略规则和 `define.fmt.ignorePatterns`。这些规则会作用于扫描得到的路径、显式传入的文件以及 `--stdin-filepath`。ignore 文件无法读取时,命令以状态码 `2` 退出。 +每个文件都是独立的忽略来源。关于这些来源与 `.gitignore`、默认忽略规则和 `ignorePatterns` 的组合方式,请参考[忽略顺序](../formatting#ignore-order)。 ### `--list-different` diff --git a/website/docs/zh/guide/formatting.mdx b/website/docs/zh/guide/formatting.mdx index d9e68c47..4ebfccad 100644 --- a/website/docs/zh/guide/formatting.mdx +++ b/website/docs/zh/guide/formatting.mdx @@ -45,7 +45,7 @@ define.fmt({ :::warning Prettier 配置文件 -`rs fmt` 不会读取 Prettier 配置文件、`.prettierignore` 或 `.editorconfig`。请在 `define.fmt()` 中设置格式化选项和额外的忽略规则。 +`rs fmt` 不会自动加载 Prettier 配置文件、`.prettierignore` 或 `.editorconfig`。请在 `define.fmt()` 中设置格式化选项和额外的忽略规则。如需显式加载 ignore 文件,请使用 [`--ignore-path`](./cli/fmt#--ignore-path-path)。 ::: @@ -85,8 +85,6 @@ define.fmt({ 这些模式遵循 Gitignore 语法,并且基于 Rstack 配置文件所在的目录解析。由于规则会在确定格式化范围后生效,因此也会排除命令行中显式传入的文件。 -> 你也可以使用使用 [`--ignore-path`](./cli/fmt#--ignore-path-path) CLI 选项来忽略文件。 - ### Lock 文件 \{#lock-files} `rs fmt` 默认忽略常见的 lock 文件,包括 `package-lock.json` 和 `pnpm-lock.yaml`。 @@ -101,6 +99,16 @@ define.fmt({ }); ``` +### 忽略顺序 \{#ignore-order} + +`rs fmt` 会通过以下三个步骤,决定需要格式化哪些路径: + +1. **处理命令行参数和 `.gitignore`**:`rs fmt` 首先处理命令行中指定的文件、目录和 glob 模式。以 `!` 开头的 glob 模式用于排除路径。扫描目录或 glob 模式时会遵循 `.gitignore`,直接指定的文件则不会。在这一步被排除的路径无法被后续规则重新包含。 +2. **应用默认忽略规则和 `ignorePatterns`**:`rs fmt` 默认忽略 [lock 文件](#lock-files),随后应用 `ignorePatterns`。这些规则按顺序匹配,后面的规则优先。例如,`!pnpm-lock.yaml` 可以重新包含默认忽略的文件。 +3. **应用 [`--ignore-path`](./cli/fmt#--ignore-path-path) 指定的文件**:每个 ignore 文件单独匹配,同一文件中后面的规则优先。不同 ignore 文件与 `ignorePatterns` 的排除结果会叠加:只要任一来源忽略某个路径,该路径就会保持排除,即使其他来源尝试重新包含它。 + +> 对于命令行中直接指定的文件,以及通过 [`--stdin-filepath`](./cli/fmt#--stdin-filepath-path) 指定的路径,`rs fmt` 仍会应用第二、三步中的忽略规则。只有未被这些规则排除的路径才会被格式化。 + ## 排序 package.json 字段 \{#sort-package-json} 启用 `sortPackageJson` 后,`rs fmt` 会使用 [`sort-package-json`](https://github.com/keithamus/sort-package-json) 对每个待格式化的 `package.json` 中的字段排序: From dd1070a2abe48a6dec346b3df9c0af90348a6944 Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Tue, 4 Aug 2026 19:14:01 +0800 Subject: [PATCH 06/21] docs: align formatting guide wording (#176) --- website/docs/en/guide/formatting.mdx | 10 +++++----- website/docs/zh/guide/formatting.mdx | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/website/docs/en/guide/formatting.mdx b/website/docs/en/guide/formatting.mdx index 380c5f82..27c1ce68 100644 --- a/website/docs/en/guide/formatting.mdx +++ b/website/docs/en/guide/formatting.mdx @@ -4,8 +4,8 @@ import { PackageManagerTabs } from '@rspress/core/theme'; Rstack CLI includes a formatter built on [Prettier](https://prettier.io/). Compared with running Prettier directly, `rs fmt` offers better performance in two ways: -- **Parallel formatting**: `rs fmt` formats files concurrently in a worker pool. -- **Yuku parser**: `rs fmt` uses the high-performance [Yuku](https://yuku.fyi/) parser by default for JavaScript, JSX, and TypeScript files. +- **Parallel formatting**: Files are formatted concurrently in a worker pool. +- **Yuku parser**: The high-performance [Yuku](https://yuku.fyi/) parser is used by default for JavaScript, JSX, and TypeScript files. `rs fmt` supports Prettier options and plugins and adds built-in capabilities such as [sorting package.json fields](#sort-package-json). @@ -103,11 +103,11 @@ define.fmt({ `rs fmt` uses the following three steps to decide which paths to format: -1. **Process command-line arguments and `.gitignore`**: `rs fmt` first processes the files, directories, and glob patterns passed on the command line. A glob that starts with `!` excludes matching paths. Directory and glob scans follow `.gitignore`, while files passed directly do not. Paths excluded in this step cannot be re-included later. -2. **Apply default ignore rules and `ignorePatterns`**: `rs fmt` ignores [lock files](#lock-files) by default, then applies `ignorePatterns`. These rules are evaluated in order, with later rules taking precedence. For example, `!pnpm-lock.yaml` re-includes the otherwise ignored file. +1. **Process command-line arguments and `.gitignore`**: It first processes the files, directories, and glob patterns passed on the command line. A glob that starts with `!` excludes matching paths. Directory and glob scans follow `.gitignore`, while files passed directly do not. Paths excluded in this step cannot be re-included later. +2. **Apply default ignore rules and `ignorePatterns`**: By default, the command ignores [lock files](#lock-files), then applies `ignorePatterns`. These rules are evaluated in order, with later rules taking precedence. For example, `!pnpm-lock.yaml` re-includes the otherwise ignored file. 3. **Apply files specified with [`--ignore-path`](./cli/fmt#--ignore-path-path)**: Each ignore file is evaluated separately, and later rules take precedence within that file. Exclusions from different files and `ignorePatterns` are combined: if any source ignores a path, that path remains excluded, even if another source re-includes it. -> `rs fmt` still applies the rules from the second and third steps to files passed directly on the command line and to paths specified with [`--stdin-filepath`](./cli/fmt#--stdin-filepath-path). It formats a path only if none of these rules excludes it. +> Even when a file is passed directly on the command line, the default ignore rules, `ignorePatterns`, and rules from `--ignore-path` still apply. The same is true for paths specified with [`--stdin-filepath`](./cli/fmt#--stdin-filepath-path). ## Sort package.json fields \{#sort-package-json} diff --git a/website/docs/zh/guide/formatting.mdx b/website/docs/zh/guide/formatting.mdx index 4ebfccad..3b397051 100644 --- a/website/docs/zh/guide/formatting.mdx +++ b/website/docs/zh/guide/formatting.mdx @@ -1,10 +1,10 @@ -# 格式化 +# 格式化 \{#formatting} import { PackageManagerTabs } from '@rspress/core/theme'; Rstack CLI 提供了基于 [Prettier](https://prettier.io/) 的格式化工具。相比直接使用 Prettier,`rs fmt` 的性能更好,主要得益于以下两点: -- **并行格式化**:`rs fmt` 通过 worker 池并行格式化文件。 +- **并行格式化**:通过 worker 池并行格式化文件。 - **Yuku 解析器**:默认使用高性能的 [Yuku](https://yuku.fyi/) 解析器处理 JavaScript、JSX 和 TypeScript 文件。 `rs fmt` 兼容 Prettier 的选项和插件,并提供更多内置能力,例如支持[排序 package.json 字段](#sort-package-json)。 @@ -103,11 +103,11 @@ define.fmt({ `rs fmt` 会通过以下三个步骤,决定需要格式化哪些路径: -1. **处理命令行参数和 `.gitignore`**:`rs fmt` 首先处理命令行中指定的文件、目录和 glob 模式。以 `!` 开头的 glob 模式用于排除路径。扫描目录或 glob 模式时会遵循 `.gitignore`,直接指定的文件则不会。在这一步被排除的路径无法被后续规则重新包含。 -2. **应用默认忽略规则和 `ignorePatterns`**:`rs fmt` 默认忽略 [lock 文件](#lock-files),随后应用 `ignorePatterns`。这些规则按顺序匹配,后面的规则优先。例如,`!pnpm-lock.yaml` 可以重新包含默认忽略的文件。 +1. **处理命令行参数和 `.gitignore`**:首先处理命令行中指定的文件、目录和 glob 模式。以 `!` 开头的 glob 模式用于排除路径。扫描目录或 glob 模式时会遵循 `.gitignore`,直接指定的文件则不会。在这一步被排除的路径无法被后续规则重新包含。 +2. **应用默认忽略规则和 `ignorePatterns`**:默认忽略 [lock 文件](#lock-files),随后应用 `ignorePatterns`。这些规则按顺序匹配,后面的规则优先。例如,`!pnpm-lock.yaml` 可以重新包含默认忽略的文件。 3. **应用 [`--ignore-path`](./cli/fmt#--ignore-path-path) 指定的文件**:每个 ignore 文件单独匹配,同一文件中后面的规则优先。不同 ignore 文件与 `ignorePatterns` 的排除结果会叠加:只要任一来源忽略某个路径,该路径就会保持排除,即使其他来源尝试重新包含它。 -> 对于命令行中直接指定的文件,以及通过 [`--stdin-filepath`](./cli/fmt#--stdin-filepath-path) 指定的路径,`rs fmt` 仍会应用第二、三步中的忽略规则。只有未被这些规则排除的路径才会被格式化。 +> 即使命令行直接指定了某个文件,默认忽略规则、`ignorePatterns` 和 `--ignore-path` 中的规则仍然有效。通过 [`--stdin-filepath`](./cli/fmt#--stdin-filepath-path) 指定的路径也是如此。 ## 排序 package.json 字段 \{#sort-package-json} From 4626c4f33aab2db0401d7ca653c50f56023433a9 Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Tue, 4 Aug 2026 22:23:56 +0800 Subject: [PATCH 07/21] fix(fmt): error when no files match (#177) --- packages/rstack/src/fmt/cli.ts | 8 +++++--- packages/rstack/tests/cli/fmt/index.test.ts | 21 ++++++++++----------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index ab565296..9abd0dea 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -243,9 +243,11 @@ const runFmtCLI = async (args: string[]): Promise => { }); if (files.length === 0) { - if (mode !== 'list-different') { - logger.info('No files matched.'); - } + const targets = (patterns.length ? patterns : ['.']) + .map((pattern) => color.cyan(JSON.stringify(pattern))) + .join(', '); + logger.error(`No supported files matched ${targets}, or all matching files were ignored.`); + process.exitCode = 2; return; } diff --git a/packages/rstack/tests/cli/fmt/index.test.ts b/packages/rstack/tests/cli/fmt/index.test.ts index 75aef5ea..285b5d20 100644 --- a/packages/rstack/tests/cli/fmt/index.test.ts +++ b/packages/rstack/tests/cli/fmt/index.test.ts @@ -585,16 +585,15 @@ test('writes nothing for empty stdin', () => { expect(result.stderr).toBe(''); }); -test('reports when no files match', () => { - const writeResult = runFmt(['missing/**/*.ts']); +test('returns exit code 2 when no files match', () => { + for (const modeArgs of [[], ['--check'], ['--list-different']]) { + const result = runFmt([...modeArgs, 'missing/**/*.ts']); - expect(writeResult.status).toBe(0); - expect(writeResult.stdout).toBe('info No files matched.\n'); - expect(writeResult.stderr).toBe(''); - - const checkResult = runFmt(['--check', 'missing/**/*.ts']); - - expect(checkResult.status).toBe(0); - expect(checkResult.stdout).toBe('info No files matched.\n'); - expect(checkResult.stderr).toBe(''); + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain( + 'No supported files matched "missing/**/*.ts", or all matching files were ignored.', + ); + expect(result.stderr).not.toContain('\n at '); + } }); From bb1ac5c91a94fd12be61606d1e2007fa3640c793 Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Tue, 4 Aug 2026 22:47:39 +0800 Subject: [PATCH 08/21] feat(fmt): add --no-error-on-unmatched-pattern (#178) --- packages/rstack/src/fmt/cli.ts | 34 ++++++++++++++----- packages/rstack/tests/cli/fmt/index.test.ts | 13 +++++++ .../tests/fmt/__snapshots__/cli.test.ts.snap | 15 ++++---- packages/rstack/tests/fmt/cli.test.ts | 14 ++++++++ website/docs/en/guide/cli/fmt.mdx | 20 ++++++++--- website/docs/zh/guide/cli/fmt.mdx | 20 ++++++++--- 6 files changed, 91 insertions(+), 25 deletions(-) diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index 9abd0dea..22e3ae86 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[]; + noErrorOnUnmatchedPattern: boolean; maxWorkers?: number; help: boolean; /** Path the stdin content is formatted as; it need not exist on disk. */ @@ -26,13 +27,14 @@ ${color.yellow(' $ rs fmt [options] [files/globs...]')} Format files with Prettier. ${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`; + --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) + --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 + -h, --help Display this help message`; const parseMaxWorkers = ( kebabValue: string | undefined, @@ -60,6 +62,8 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { 'list-different': { type: 'boolean' }, listDifferent: { type: 'boolean' }, 'ignore-path': { type: 'string', multiple: true }, + 'no-error-on-unmatched-pattern': { type: 'boolean' }, + noErrorOnUnmatchedPattern: { type: 'boolean' }, 'parallel-workers': { type: 'string' }, parallelWorkers: { type: 'string' }, 'stdin-filepath': { type: 'string' }, @@ -77,6 +81,8 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { } const mode = values.check ? 'check' : listDifferent ? 'list-different' : 'write'; + const noErrorOnUnmatchedPattern = + values['no-error-on-unmatched-pattern'] ?? values.noErrorOnUnmatchedPattern ?? false; const maxWorkers = parseMaxWorkers(values['parallel-workers'], values.parallelWorkers); const stdinFilepath = values['stdin-filepath'] ?? values.stdinFilepath; @@ -96,6 +102,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { mode, patterns: positionals, ignorePaths: values['ignore-path'] ?? [], + noErrorOnUnmatchedPattern, maxWorkers, help: values.help ?? false, stdinFilepath, @@ -214,7 +221,15 @@ 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, ignorePaths, maxWorkers, mode, patterns, stdinFilepath } = parseFmtCLIArgs(args); + const { + help, + ignorePaths, + maxWorkers, + mode, + noErrorOnUnmatchedPattern, + patterns, + stdinFilepath, + } = parseFmtCLIArgs(args); if (help) { logger.log(fmtHelpMessage); return; @@ -243,6 +258,9 @@ const runFmtCLI = async (args: string[]): Promise => { }); if (files.length === 0) { + if (noErrorOnUnmatchedPattern) { + return; + } const targets = (patterns.length ? patterns : ['.']) .map((pattern) => color.cyan(JSON.stringify(pattern))) .join(', '); diff --git a/packages/rstack/tests/cli/fmt/index.test.ts b/packages/rstack/tests/cli/fmt/index.test.ts index 285b5d20..e0489348 100644 --- a/packages/rstack/tests/cli/fmt/index.test.ts +++ b/packages/rstack/tests/cli/fmt/index.test.ts @@ -597,3 +597,16 @@ test('returns exit code 2 when no files match', () => { expect(result.stderr).not.toContain('\n at '); } }); + +test.each(['--no-error-on-unmatched-pattern', '--noErrorOnUnmatchedPattern'])( + 'allows no files to match with %s', + (option) => { + for (const modeArgs of [[], ['--check'], ['--list-different']]) { + const result = runFmt([...modeArgs, option, 'missing/**/*.ts']); + + expect(result.status).toBe(0); + expect(result.stdout).toBe(''); + expect(result.stderr).toBe(''); + } + }, +); diff --git a/packages/rstack/tests/fmt/__snapshots__/cli.test.ts.snap b/packages/rstack/tests/fmt/__snapshots__/cli.test.ts.snap index b22c2561..e794c9d1 100644 --- a/packages/rstack/tests/fmt/__snapshots__/cli.test.ts.snap +++ b/packages/rstack/tests/fmt/__snapshots__/cli.test.ts.snap @@ -7,11 +7,12 @@ exports[`provides command help 1`] = ` Format files with Prettier. 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" + --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) + --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 + -h, --help Display this help message" `; diff --git a/packages/rstack/tests/fmt/cli.test.ts b/packages/rstack/tests/fmt/cli.test.ts index 61cf2f40..bd6339ee 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: [], + noErrorOnUnmatchedPattern: false, maxWorkers: undefined, help: false, }); @@ -37,6 +38,7 @@ test.each([ mode, patterns: [], ignorePaths: [], + noErrorOnUnmatchedPattern: false, maxWorkers: undefined, help: false, }); @@ -49,6 +51,7 @@ test.each(['--parallel-workers', '--parallelWorkers'])( mode: 'write', patterns: [], ignorePaths: [], + noErrorOnUnmatchedPattern: false, maxWorkers: 3, help: false, }); @@ -75,6 +78,7 @@ test('preserves file paths and globs', () => { mode: 'check', patterns, ignorePaths: [], + noErrorOnUnmatchedPattern: false, maxWorkers: undefined, help: false, }); @@ -85,6 +89,7 @@ test('treats arguments after the terminator as paths', () => { mode: 'check', patterns: ['--write', '--help'], ignorePaths: [], + noErrorOnUnmatchedPattern: false, maxWorkers: undefined, help: false, }); @@ -101,11 +106,19 @@ test('collects repeated ignore paths', () => { ).toEqual(['.prettierignore', 'config/format.ignore']); }); +test.each(['--no-error-on-unmatched-pattern', '--noErrorOnUnmatchedPattern'])( + 'parses %s', + (option) => { + expect(parseFmtCLIArgs([option]).noErrorOnUnmatchedPattern).toBe(true); + }, +); + test.each(['--stdin-filepath', '--stdinFilepath'])('parses %s', (option) => { expect(parseFmtCLIArgs([option, 'src/index.ts'])).toEqual({ mode: 'write', patterns: [], ignorePaths: [], + noErrorOnUnmatchedPattern: false, maxWorkers: undefined, help: false, stdinFilepath: 'src/index.ts', @@ -117,6 +130,7 @@ test('accepts a worker count with --stdin-filepath', () => { mode: 'write', patterns: [], ignorePaths: [], + noErrorOnUnmatchedPattern: false, maxWorkers: 2, help: false, stdinFilepath: 'index.ts', diff --git a/website/docs/en/guide/cli/fmt.mdx b/website/docs/en/guide/cli/fmt.mdx index 4276ae22..43a99759 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. | -| `1` | One or more matched files have formatting issues. | -| `2` | `rs fmt` could not run or encountered a formatting error. | +| 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. | ### `-h, --help` @@ -97,6 +97,16 @@ rs fmt . --list-different The option uses the same exit codes as `--check` and cannot be combined with `--write` or `--check`. +### `--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: + +```bash +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. + ### `--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 a6c06a48..f3bd105c 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` | 所有匹配的文件均已格式化。 | -| `1` | 一个或多个匹配的文件存在格式问题。 | -| `2` | `rs fmt` 无法运行或在格式化过程中遇到错误。 | +| 状态码 | 含义 | +| ------ | -------------------------------------------------------------------------------------------------------------------------------- | +| `0` | 所有匹配的文件均已格式化;或未匹配到支持的文件,但指定了 [`--no-error-on-unmatched-pattern`](#--no-error-on-unmatched-pattern)。 | +| `1` | 一个或多个匹配的文件存在格式问题。 | +| `2` | `rs fmt` 无法运行或在格式化过程中遇到错误。 | ### `-h, --help` @@ -97,6 +97,16 @@ rs fmt . --list-different 此选项与 `--check` 使用相同的退出状态码,且不能与 `--write` 或 `--check` 同时使用。 +### `--no-error-on-unmatched-pattern` + +如果传入的路径或 glob 没有匹配任何支持的文件(包括所有匹配文件均被忽略的情况),则不输出诊断信息并成功退出: + +```bash +rs fmt --no-error-on-unmatched-pattern 'src/**/*.ts' +``` + +例如,pre-commit 脚本可能会始终运行 `rs fmt`,即使暂存的改动中没有支持的文件。此选项可让命令在这种情况下成功退出,避免阻止提交。 + ### `--parallel-workers ` 将格式化 worker 的最大数量设置为正整数: From afb7bbf9c15fa83c915c39765a467fbd10cae22d Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Tue, 4 Aug 2026 22:54:41 +0800 Subject: [PATCH 09/21] feat(fmt): support --ignorePath (#180) --- packages/rstack/src/fmt/cli.ts | 4 +++- packages/rstack/tests/cli/fmt/index.test.ts | 6 +++--- packages/rstack/tests/fmt/cli.test.ts | 10 ++++++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index 22e3ae86..47e3735b 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -62,6 +62,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { 'list-different': { type: 'boolean' }, listDifferent: { type: 'boolean' }, 'ignore-path': { type: 'string', multiple: true }, + ignorePath: { type: 'string', multiple: true }, 'no-error-on-unmatched-pattern': { type: 'boolean' }, noErrorOnUnmatchedPattern: { type: 'boolean' }, 'parallel-workers': { type: 'string' }, @@ -81,6 +82,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { } const mode = values.check ? 'check' : listDifferent ? 'list-different' : 'write'; + const ignorePaths = [...(values['ignore-path'] ?? []), ...(values.ignorePath ?? [])]; const noErrorOnUnmatchedPattern = values['no-error-on-unmatched-pattern'] ?? values.noErrorOnUnmatchedPattern ?? false; const maxWorkers = parseMaxWorkers(values['parallel-workers'], values.parallelWorkers); @@ -101,7 +103,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { return { mode, patterns: positionals, - ignorePaths: values['ignore-path'] ?? [], + ignorePaths, noErrorOnUnmatchedPattern, maxWorkers, help: values.help ?? false, diff --git a/packages/rstack/tests/cli/fmt/index.test.ts b/packages/rstack/tests/cli/fmt/index.test.ts index e0489348..07391cea 100644 --- a/packages/rstack/tests/cli/fmt/index.test.ts +++ b/packages/rstack/tests/cli/fmt/index.test.ts @@ -195,7 +195,7 @@ 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', () => { +test.each(['--ignore-path', '--ignorePath'])('applies repeated ignore paths with %s', (option) => { 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"'); @@ -203,9 +203,9 @@ test('applies repeated ignore paths to explicit files', () => { writeProjectFile('src/index.ts', 'const index="formatted"'); const result = runFmt([ - '--ignore-path', + option, '.prettierignore', - '--ignore-path=config/extra.ignore', + `${option}=config/extra.ignore`, 'src/ignored-by-root.ts', 'src/ignored-by-extra.ts', 'src/index.ts', diff --git a/packages/rstack/tests/fmt/cli.test.ts b/packages/rstack/tests/fmt/cli.test.ts index bd6339ee..3dcf5a97 100644 --- a/packages/rstack/tests/fmt/cli.test.ts +++ b/packages/rstack/tests/fmt/cli.test.ts @@ -99,9 +99,15 @@ test.each(['--help', '-h'])('parses %s', (option) => { expect(parseFmtCLIArgs([option]).help).toBe(true); }); -test('collects repeated ignore paths', () => { +test.each(['--ignore-path', '--ignorePath'])('collects repeated ignore paths with %s', (option) => { expect( - parseFmtCLIArgs(['--ignore-path', '.prettierignore', '--ignore-path=config/format.ignore']) + parseFmtCLIArgs([option, '.prettierignore', `${option}=config/format.ignore`]).ignorePaths, + ).toEqual(['.prettierignore', 'config/format.ignore']); +}); + +test('combines kebab-case and camel-case ignore paths', () => { + expect( + parseFmtCLIArgs(['--ignore-path', '.prettierignore', '--ignorePath', 'config/format.ignore']) .ignorePaths, ).toEqual(['.prettierignore', 'config/format.ignore']); }); From af69b59462d564f6fc2bfb59fafb2a4d27c6c73c Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Tue, 4 Aug 2026 22:54:48 +0800 Subject: [PATCH 10/21] fix(fmt): use ignore for pattern matching (#179) --- packages/rstack/THIRD_PARTY_NOTICES.md | 28 ------------------------ packages/rstack/package.json | 1 - packages/rstack/src/fmt/ignore.ts | 15 +++++++++---- packages/rstack/tests/fmt/ignore.test.ts | 8 +++++++ pnpm-lock.yaml | 24 -------------------- pnpm-workspace.yaml | 1 - 6 files changed, 19 insertions(+), 58 deletions(-) diff --git a/packages/rstack/THIRD_PARTY_NOTICES.md b/packages/rstack/THIRD_PARTY_NOTICES.md index 048171dc..7348005e 100644 --- a/packages/rstack/THIRD_PARTY_NOTICES.md +++ b/packages/rstack/THIRD_PARTY_NOTICES.md @@ -59,34 +59,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -## fast-ignore - -This package includes bundled code from [fast-ignore](https://github.com/fabiospampinato/fast-ignore). - -License: MIT - -The MIT License (MIT) - -Copyright (c) 2023-present Fabio Spampinato - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ## fresh-import This package includes bundled code from [fresh-import](https://github.com/sapphi-red/fresh-import). diff --git a/packages/rstack/package.json b/packages/rstack/package.json index 6bd9e14d..1c542aa8 100644 --- a/packages/rstack/package.json +++ b/packages/rstack/package.json @@ -69,7 +69,6 @@ "@rstest/adapter-rslib": "catalog:", "@types/micromatch": "catalog:", "@types/node": "catalog:", - "fast-ignore": "catalog:", "ignore": "catalog:", "import-meta-resolve": "catalog:", "is-binary-path": "catalog:", diff --git a/packages/rstack/src/fmt/ignore.ts b/packages/rstack/src/fmt/ignore.ts index 08130985..b27ce880 100644 --- a/packages/rstack/src/fmt/ignore.ts +++ b/packages/rstack/src/fmt/ignore.ts @@ -1,6 +1,6 @@ import { readFile } from 'node:fs/promises'; import path from 'node:path'; -import fastIgnore from 'fast-ignore'; +import createIgnore from 'ignore'; import type { ResolvedFmtConfig } from './types.ts'; /** @@ -21,10 +21,17 @@ interface CreateIgnoreMatcherOptions { } const createPatternMatcher = (rootPath: string, patterns: string): IgnoreMatcher => { - const matches = fastIgnore(patterns); + const matcher = createIgnore({ allowRelativePaths: true }).add(patterns); - return (filePath, isDirectory = false) => - matches(path.relative(rootPath, filePath), { isDirectory }); + return (filePath, isDirectory = false) => { + const relativePath = path.relative(rootPath, filePath); + if (relativePath === '') { + return false; + } + + const posixPath = path.sep === '\\' ? relativePath.replaceAll('\\', '/') : relativePath; + return matcher.ignores(isDirectory ? `${posixPath}/` : posixPath); + }; }; const loadIgnoreMatcher = async (cwd: string, ignorePath: string): Promise => { diff --git a/packages/rstack/tests/fmt/ignore.test.ts b/packages/rstack/tests/fmt/ignore.test.ts index 192ff6a0..5826767a 100644 --- a/packages/rstack/tests/fmt/ignore.test.ts +++ b/packages/rstack/tests/fmt/ignore.test.ts @@ -38,6 +38,14 @@ test('distinguishes directory-only patterns from files', async () => { expect(isIgnored(path.join(directoryPath, 'index.js'))).toBe(true); }); +test('does not apply negated directory patterns to files', async () => { + const isIgnored = await createMatcher(['fixtures/**/*', '!fixtures/**/']); + const directoryPath = path.join(rootPath, 'fixtures/case'); + + expect(isIgnored(directoryPath, true)).toBe(false); + expect(isIgnored(path.join(directoryPath, 'index.js'))).toBe(true); +}); + 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']); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c3c6232..b4613471 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -76,9 +76,6 @@ catalogs: cspell-ban-words: specifier: ^0.0.4 version: 0.0.4 - fast-ignore: - specifier: 2.0.0 - version: 2.0.0 happy-dom: specifier: ^20.11.1 version: 20.11.1 @@ -370,9 +367,6 @@ importers: '@types/node': specifier: 'catalog:' version: 24.13.3 - fast-ignore: - specifier: 'catalog:' - version: 2.0.0 ignore: specifier: 'catalog:' version: 7.0.6 @@ -1503,9 +1497,6 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - fast-ignore@2.0.0: - resolution: {integrity: sha512-41OOPBgTDyVjF2oytGXvmqm56a38znzWLTQhvD3gp3FPCs37+j7qYjWDnL2WEA0P/Z80W53MKAXNJxc0JT8Tpw==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1529,9 +1520,6 @@ packages: git-hooks-list@4.2.1: resolution: {integrity: sha512-WNvqJjOxxs/8ZP9+DWdwWJ7cDsd60NHf39XnD82pDVrKO5q7xfPqpkK6hwEAmBa/ZSEE4IOoR75EzbbIuwGlMw==} - grammex@3.1.13: - resolution: {integrity: sha512-LnPnhOBLEJEVKS8WFDVaA397L9Kq55Q9oSITJiVLHVdhAclfUkWzQv74KhvZHKL2Q09Pb1XdsrOsZ4LfTFFTEg==} - happy-dom@20.11.1: resolution: {integrity: sha512-XSt8tMzbW9ymE7687xztkO1ckR7qJNQ3LywY9vlYGhGi3zXrGBHuUo2Cl1ztZaICW+1eAGdkLbj6iwVqDT33kg==} engines: {node: '>=20.0.0'} @@ -2265,9 +2253,6 @@ packages: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} - string-escape-regex@1.0.1: - resolution: {integrity: sha512-cdSXOHSJ32K/T2dbj9t7rJwonujaOkaINpa1zsXT+PNFIv1zuPjtr0tXanCvUhN2bIu2IB0z/C7ksl+Qsy44nA==} - stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -3315,11 +3300,6 @@ snapshots: extend@3.0.2: {} - fast-ignore@2.0.0: - dependencies: - grammex: 3.1.13 - string-escape-regex: 1.0.1 - fdir@6.5.0(picomatch@4.0.5): optionalDependencies: picomatch: 4.0.5 @@ -3334,8 +3314,6 @@ snapshots: git-hooks-list@4.2.1: {} - grammex@3.1.13: {} - happy-dom@20.11.1: dependencies: '@types/node': 24.13.3 @@ -4424,8 +4402,6 @@ snapshots: string-argv@0.3.2: {} - string-escape-regex@1.0.1: {} - stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b19b7441..2d5bb31a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -35,7 +35,6 @@ catalog: '@types/react-dom': '^19.2.4' '@shikijs/transformers': '^4.3.1' 'cspell-ban-words': '^0.0.4' - 'fast-ignore': '2.0.0' 'happy-dom': '^20.11.1' 'heading-case': '^1.1.4' ignore: 7.0.6 From f351f20b5a3dd0d1d0b2df032ec0fcac934b6218 Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Wed, 5 Aug 2026 10:46:11 +0800 Subject: [PATCH 11/21] fix(fmt): handle unsupported files correctly (#181) --- packages/rstack/src/fmt/cli.ts | 35 ++++++++++------ packages/rstack/src/fmt/runner.ts | 41 +++++++++++++++---- packages/rstack/src/fmt/types.ts | 2 + packages/rstack/tests/cli/fmt/index.test.ts | 38 +++++++++++++++++ packages/rstack/tests/fmt/runner.test.ts | 6 ++- .../tests/fmt/runnerWorkerPreflight.test.ts | 1 + .../tests/fmt/runnerWriteFailure.test.ts | 1 + 7 files changed, 101 insertions(+), 23 deletions(-) diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index 47e3735b..cbbd7d39 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -147,11 +147,19 @@ const formatFileCount = (count: number, isError = false): string => { return `${isError ? color.red(formattedCount) : formattedCount} ${count === 1 ? 'file' : 'files'}`; }; +const reportNoSupportedFiles = (patterns: string[]): void => { + const targets = (patterns.length ? patterns : ['.']) + .map((pattern) => color.cyan(JSON.stringify(pattern))) + .join(', '); + logger.error(`No supported files matched ${targets}, or all matching files were ignored.`); + process.exitCode = 2; +}; + const logFmtResult = ( result: FmtRunResult, mode: FmtMode, cwd: string, - matchedFileCount: number, + processedFileCount: number, durationSeconds: number, ): void => { let writtenCount = 0; @@ -177,12 +185,12 @@ const logFmtResult = ( return; } - const matchedFiles = formatFileCount(matchedFileCount); + const processedFiles = formatFileCount(processedFileCount); const time = prettyTime(durationSeconds); const message = writtenCount > 0 - ? `Formatted ${formatCount(writtenCount)} of ${matchedFiles} in ${time}.` - : `Checked ${matchedFiles} in ${time}. No changes needed.`; + ? `Formatted ${formatCount(writtenCount)} of ${processedFiles} in ${time}.` + : `Checked ${processedFiles} in ${time}. No changes needed.`; logger[result.exitCode === 0 ? 'success' : 'info'](message); return; } @@ -193,15 +201,15 @@ const logFmtResult = ( if (differentCount > 0) { const differentFiles = formatFileCount(differentCount, true); - const matchedFiles = formatFileCount(matchedFileCount); + const processedFiles = formatFileCount(processedFileCount); const checkOption = color.cyan('--check'); logger.error( `Formatting issues found in ${differentFiles}. Run without ${checkOption} to fix.`, ); - logger.info(`Checked ${matchedFiles} in ${prettyTime(durationSeconds)}.`); + logger.info(`Checked ${processedFiles} in ${prettyTime(durationSeconds)}.`); } else if (result.exitCode === 0) { logger.success( - `Checked ${formatFileCount(matchedFileCount)} in ${prettyTime(durationSeconds)}. No issues found.`, + `Checked ${formatFileCount(processedFileCount)} in ${prettyTime(durationSeconds)}. No issues found.`, ); } }; @@ -263,11 +271,7 @@ const runFmtCLI = async (args: string[]): Promise => { if (noErrorOnUnmatchedPattern) { return; } - const targets = (patterns.length ? patterns : ['.']) - .map((pattern) => color.cyan(JSON.stringify(pattern))) - .join(', '); - logger.error(`No supported files matched ${targets}, or all matching files were ignored.`); - process.exitCode = 2; + reportNoSupportedFiles(patterns); return; } @@ -281,8 +285,13 @@ const runFmtCLI = async (args: string[]): Promise => { maxWorkers, }); + if (result.processedFileCount === 0) { + reportNoSupportedFiles(patterns); + return; + } + const durationSeconds = (performance.now() - startTime) / 1000; - logFmtResult(result, mode, cwd, files.length, durationSeconds); + logFmtResult(result, mode, cwd, result.processedFileCount, durationSeconds); process.exitCode = result.exitCode; } catch (error) { logger.error(error); diff --git a/packages/rstack/src/fmt/runner.ts b/packages/rstack/src/fmt/runner.ts index ec3192bb..aa0cc5c7 100644 --- a/packages/rstack/src/fmt/runner.ts +++ b/packages/rstack/src/fmt/runner.ts @@ -9,17 +9,23 @@ import type { FmtWorkerPool } from './workerPool.ts'; /** Formats one file and reports whether its contents differ. */ type FormatFile = FmtWorkerPool['formatFile']; +type FmtFileOutcome = FmtFileResult | 'unchanged' | 'unsupported'; + +interface FmtWorkerPoolResult { + files: FmtFileResult[]; + processedFileCount: number; +} /** Converts a formatter outcome into the shared per-file result. */ const runFmtFile = async ( file: FmtFileRequest, shouldWrite: boolean, formatFile: FormatFile, -): Promise => { +): Promise => { try { const result = await formatFile(file, shouldWrite); - if (result !== 'changed') { - return; + if (result === 'unchanged' || result === 'unsupported') { + return result; } return { @@ -40,7 +46,7 @@ const runFmtFilesInWorkerPool = async ( files: FmtFileRequest[], shouldWrite: boolean, maxWorkers?: number, -): Promise => { +): Promise => { const { createFmtWorkerPool } = await import('./workerPool.ts'); const workerPool = await createFmtWorkerPool(files.length, maxWorkers); @@ -48,7 +54,21 @@ const runFmtFilesInWorkerPool = async ( const results = await Promise.all( files.map((file) => runFmtFile(file, shouldWrite, workerPool.formatFile)), ); - return results.filter((result): result is FmtFileResult => result !== undefined); + const processedFiles: FmtFileResult[] = []; + let processedFileCount = 0; + + for (const result of results) { + if (result === 'unsupported') { + continue; + } + + processedFileCount++; + if (result !== 'unchanged') { + processedFiles.push(result); + } + } + + return { files: processedFiles, processedFileCount }; } finally { await workerPool.terminate(); } @@ -77,12 +97,15 @@ const runFmtFiles = async ({ maxWorkers, }: RunFmtFilesOptions): Promise => { const shouldWrite = mode === 'write'; - const results = - files.length === 0 ? [] : await runFmtFilesInWorkerPool(files, shouldWrite, maxWorkers); + const result = + files.length === 0 + ? { files: [], processedFileCount: 0 } + : await runFmtFilesInWorkerPool(files, shouldWrite, maxWorkers); return { - files: results, - exitCode: getFmtExitCode(results), + ...result, + exitCode: + files.length > 0 && result.processedFileCount === 0 ? 2 : getFmtExitCode(result.files), }; }; diff --git a/packages/rstack/src/fmt/types.ts b/packages/rstack/src/fmt/types.ts index 111ddb4d..db9b2d66 100644 --- a/packages/rstack/src/fmt/types.ts +++ b/packages/rstack/src/fmt/types.ts @@ -96,6 +96,8 @@ type FmtFileResult = SuccessfulFmtFileResult | FailedFmtFileResult; interface FmtRunResult { files: FmtFileResult[]; + /** Number of processed files, excluding files with no supported parser. */ + processedFileCount: number; /** Recommended CLI exit code. */ exitCode: FmtExitCode; } diff --git a/packages/rstack/tests/cli/fmt/index.test.ts b/packages/rstack/tests/cli/fmt/index.test.ts index 07391cea..36ee9268 100644 --- a/packages/rstack/tests/cli/fmt/index.test.ts +++ b/packages/rstack/tests/cli/fmt/index.test.ts @@ -610,3 +610,41 @@ test.each(['--no-error-on-unmatched-pattern', '--noErrorOnUnmatchedPattern'])( } }, ); + +test('counts only supported files', () => { + writeProjectFile('index.ts', 'const value = 1;\n'); + writeProjectFile('notes.unknown', 'plain text'); + + const result = runFmt(['--check', 'index.ts', 'notes.unknown']); + + expect(result.status).toBe(0); + expect(normalizeDuration(result.stdout)).toBe( + 'start Checking formatting...\nsuccess Checked 1 file in . No issues found.\n', + ); + expect(result.stderr).toBe(''); +}); + +test('returns exit code 2 when all matched files are unsupported', () => { + writeProjectFile('notes.unknown', 'plain text'); + + for (const modeArgs of [[], ['--check'], ['--list-different']]) { + const result = runFmt([...modeArgs, 'notes.unknown']); + + expect(result.status).toBe(2); + expect(result.stdout).not.toContain('success'); + expect(result.stderr).toContain( + 'No supported files matched "notes.unknown", or all matching files were ignored.', + ); + expect(result.stderr).not.toContain('\n at '); + } +}); + +test('does not treat unsupported files as unmatched patterns', () => { + writeProjectFile('notes.unknown', 'plain text'); + + const result = runFmt(['--no-error-on-unmatched-pattern', 'notes.unknown']); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain('No supported files matched "notes.unknown"'); +}); diff --git a/packages/rstack/tests/fmt/runner.test.ts b/packages/rstack/tests/fmt/runner.test.ts index 385ac5e3..7a31d387 100644 --- a/packages/rstack/tests/fmt/runner.test.ts +++ b/packages/rstack/tests/fmt/runner.test.ts @@ -31,6 +31,7 @@ test('does not rewrite unchanged files', async () => { expect(result).toMatchObject({ exitCode: 0, files: [], + processedFileCount: 1, }); expect(statSync(filePath).mtimeMs).toBe(mtimeMs); }); @@ -46,6 +47,7 @@ test('writes changed files', async () => { expect(result).toMatchObject({ exitCode: 0, files: [{ path: filePath, status: 'written' }], + processedFileCount: 1, }); expect(readFileSync(filePath, 'utf8')).toBe('const value = 1;\n'); }); @@ -75,6 +77,7 @@ for (const mode of ['check', 'list-different'] as const) { expect(result).toMatchObject({ exitCode: 1, files: [{ path: filePath, status: 'different' }], + processedFileCount: 1, }); expect(readFileSync(filePath, 'utf8')).toBe(source); }); @@ -96,6 +99,7 @@ test('continues after a file fails and gives errors exit-code precedence', async { path: invalidPath, status: 'error' }, { path: validPath, status: 'different' }, ], + processedFileCount: 2, }); expect(readFileSync(validPath, 'utf8')).toBe('const value=1'); }); @@ -113,7 +117,7 @@ test('omits unsupported files from the result', async () => { }, ]); - expect(result).toMatchObject({ exitCode: 0, files: [] }); + expect(result).toMatchObject({ exitCode: 2, files: [], processedFileCount: 0 }); expect(readFileSync(filePath, 'utf8')).toBe('plain text'); }); }); diff --git a/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts b/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts index 1b3871b0..127beaed 100644 --- a/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts +++ b/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts @@ -47,6 +47,7 @@ test('does not start the worker pool when there are no files', async () => { await expect(runFmtFiles({ files: [], mode: 'write' })).resolves.toMatchObject({ files: [], exitCode: 0, + processedFileCount: 0, }); expect(mocks.createFmtWorkerPoolCalls).toEqual([]); }); diff --git a/packages/rstack/tests/fmt/runnerWriteFailure.test.ts b/packages/rstack/tests/fmt/runnerWriteFailure.test.ts index db92d9a1..a7ea7b45 100644 --- a/packages/rstack/tests/fmt/runnerWriteFailure.test.ts +++ b/packages/rstack/tests/fmt/runnerWriteFailure.test.ts @@ -40,6 +40,7 @@ test('returns an error when a file write fails', async () => { error: { message: 'file write failed' }, }, ], + processedFileCount: 1, }); expect(mocks.terminateCalls).toBe(1); }); From f5901d7b0f62c11c26b9c9e8e1027ab1c84ce9be Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Wed, 5 Aug 2026 10:49:10 +0800 Subject: [PATCH 12/21] feat(cli): support camel-case option aliases (#182) --- packages/rstack/src/cli/args.ts | 71 +++++++++++++++++++++++++- packages/rstack/src/fmt/cli.ts | 34 +++++------- packages/rstack/src/setup/index.ts | 4 +- packages/rstack/src/staged.ts | 7 ++- packages/rstack/tests/cli/args.test.ts | 39 ++++++++++++++ 5 files changed, 128 insertions(+), 27 deletions(-) create mode 100644 packages/rstack/tests/cli/args.test.ts diff --git a/packages/rstack/src/cli/args.ts b/packages/rstack/src/cli/args.ts index a7b3eb09..27e3af92 100644 --- a/packages/rstack/src/cli/args.ts +++ b/packages/rstack/src/cli/args.ts @@ -1,4 +1,73 @@ -import { parseArgs } from 'node:util'; +import { + parseArgs as nodeParseArgs, + type ParseArgsConfig, + type ParseArgsOptionsConfig, +} from 'node:util'; + +type CamelCase = Value extends `${infer Head}-${infer Tail}` + ? `${Head}${Capitalize>}` + : Value; + +type NodeParseArgsResult = ReturnType>; + +type ParseArgsResult = Omit< + NodeParseArgsResult, + 'values' +> & { + values: { + [ + Name in keyof NodeParseArgsResult['values'] as CamelCase + ]: NodeParseArgsResult['values'][Name]; + }; +}; + +const KEBAB_CASE_REGEXP = /-([a-z])/g; + +const toCamelCase = (value: string): string => + value.includes('-') + ? value.replace(KEBAB_CASE_REGEXP, (_, character: string) => character.toUpperCase()) + : value; + +export function parseArgs( + config?: Config, +): ParseArgsResult { + const options: ParseArgsOptionsConfig = {}; + const optionNames: [originalName: string, camelName: string][] = []; + + for (const [originalName, descriptor] of Object.entries(config?.options ?? {})) { + const camelName = toCamelCase(originalName); + optionNames.push([originalName, camelName]); + options[originalName] = descriptor; + + if (camelName !== originalName) { + options[camelName] = descriptor; + } + } + + const parsed = nodeParseArgs({ + ...config, + options, + }); + const values: Record = {}; + + for (const [originalName, camelName] of optionNames) { + const originalValue = parsed.values[originalName]; + const camelValue = camelName === originalName ? undefined : parsed.values[camelName]; + const value = + Array.isArray(originalValue) && Array.isArray(camelValue) + ? [...originalValue, ...camelValue] + : (originalValue ?? camelValue); + + if (value !== undefined) { + values[camelName] = value; + } + } + + return { + ...parsed, + values, + } as unknown as ParseArgsResult; +} type ParsedRstackArgs = { args: string[]; diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index cbbd7d39..bbc9abaa 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -1,7 +1,7 @@ import path from 'node:path'; import { performance } from 'node:perf_hooks'; -import { parseArgs } from 'node:util'; import { color, logger } from 'rslog'; +import { parseArgs } from '../cli/args.ts'; import { loadRstackConfig } from '../config.ts'; import { resolveFmtConfig } from './config.ts'; import { discoverFmtFiles } from './discovery.ts'; @@ -36,11 +36,7 @@ ${color.cyan('Options')}: --stdin-filepath Format stdin as if it were saved at -h, --help Display this help message`; -const parseMaxWorkers = ( - kebabValue: string | undefined, - camelValue: string | undefined, -): number | undefined => { - const value = kebabValue ?? camelValue; +const parseMaxWorkers = (value: string | undefined): number | undefined => { if (value === undefined) { return undefined; } @@ -60,33 +56,31 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { write: { type: 'boolean' }, check: { type: 'boolean' }, 'list-different': { type: 'boolean' }, - listDifferent: { type: 'boolean' }, 'ignore-path': { type: 'string', multiple: true }, - ignorePath: { type: 'string', multiple: true }, 'no-error-on-unmatched-pattern': { type: 'boolean' }, - noErrorOnUnmatchedPattern: { type: 'boolean' }, 'parallel-workers': { type: 'string' }, - parallelWorkers: { type: 'string' }, 'stdin-filepath': { type: 'string' }, - stdinFilepath: { type: 'string' }, help: { type: 'boolean', short: 'h' }, }, allowPositionals: true, strict: true, }); - const listDifferent = values['list-different'] || values.listDifferent; - const modes = [values.write, values.check, listDifferent].filter(Boolean); + const write = values.write; + const check = values.check; + const listDifferent = values.listDifferent; + const modes = [write, check, listDifferent].filter(Boolean); if (modes.length > 1) { throw new Error('The --write, --check, and --list-different options cannot be used together.'); } - const mode = values.check ? 'check' : listDifferent ? 'list-different' : 'write'; - const ignorePaths = [...(values['ignore-path'] ?? []), ...(values.ignorePath ?? [])]; - const noErrorOnUnmatchedPattern = - values['no-error-on-unmatched-pattern'] ?? values.noErrorOnUnmatchedPattern ?? false; - const maxWorkers = parseMaxWorkers(values['parallel-workers'], values.parallelWorkers); - const stdinFilepath = values['stdin-filepath'] ?? values.stdinFilepath; + const mode = check ? 'check' : listDifferent ? 'list-different' : 'write'; + const ignorePaths = values.ignorePath ?? []; + const noErrorOnUnmatchedPattern = values.noErrorOnUnmatchedPattern ?? false; + const parallelWorkers = values.parallelWorkers; + const maxWorkers = parseMaxWorkers(parallelWorkers); + const help = values.help ?? false; + const stdinFilepath = values.stdinFilepath; if (stdinFilepath !== undefined) { if (modes.length > 0) { @@ -106,7 +100,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { ignorePaths, noErrorOnUnmatchedPattern, maxWorkers, - help: values.help ?? false, + help, stdinFilepath, }; }; diff --git a/packages/rstack/src/setup/index.ts b/packages/rstack/src/setup/index.ts index 0b35d06a..a8a039ac 100644 --- a/packages/rstack/src/setup/index.ts +++ b/packages/rstack/src/setup/index.ts @@ -1,5 +1,5 @@ -import { parseArgs } from 'node:util'; import { color, logger } from 'rslog'; +import { parseArgs } from '../cli/args.ts'; import { installHooks } from './install.ts'; const helpMessage = `Rstack v${RSTACK_VERSION} @@ -24,7 +24,7 @@ export const runSetupCLI = (args: string[]): void => { strict: true, }); - const hooksDirs = values['hooks-dir']; + const hooksDirs = values.hooksDir; if (hooksDirs && hooksDirs.length > 1) { throw new Error('The --hooks-dir option cannot be specified more than once.'); } diff --git a/packages/rstack/src/staged.ts b/packages/rstack/src/staged.ts index f08ade3f..da46b729 100644 --- a/packages/rstack/src/staged.ts +++ b/packages/rstack/src/staged.ts @@ -1,6 +1,6 @@ -import { parseArgs } from 'node:util'; import lintStaged from 'lint-staged'; import { color } from 'rslog'; +import { parseArgs } from './cli/args.ts'; import { loadRstackConfig } from './config.ts'; export type StagedSyncTaskGenerator = (stagedFileNames: readonly string[]) => string | string[]; @@ -44,7 +44,6 @@ export async function runStagedCLI(args: string[]): Promise { args, options: { 'allow-empty': { type: 'boolean' }, - allowEmpty: { type: 'boolean' }, concurrent: { type: 'string', short: 'p' }, cwd: { type: 'string' }, debug: { type: 'boolean', short: 'd' }, @@ -72,14 +71,14 @@ export async function runStagedCLI(args: string[]): Promise { } const success = await lintStaged({ - allowEmpty: values['allow-empty'] ?? values.allowEmpty, + allowEmpty: values.allowEmpty, concurrent: values.concurrent === undefined ? undefined : JSON.parse(values.concurrent), config: stagedConfig, cwd: values.cwd, debug: values.debug, quiet: values.quiet, relative: values.relative, - stash: values['no-stash'] ? false : undefined, + stash: values.noStash ? false : undefined, verbose: values.verbose, }); if (!success) { diff --git a/packages/rstack/tests/cli/args.test.ts b/packages/rstack/tests/cli/args.test.ts new file mode 100644 index 00000000..2063dce9 --- /dev/null +++ b/packages/rstack/tests/cli/args.test.ts @@ -0,0 +1,39 @@ +import { expect, test } from 'rstack/test'; +import { parseArgs } from '../../src/cli/args.ts'; + +test.each([ + ['--long-option', 'kebab'], + ['--longOption', 'camel'], +] as const)('accepts %s and returns only a camel-case value', (option, value) => { + const { values } = parseArgs({ + args: [option, value], + options: { + 'long-option': { type: 'string' }, + }, + }); + + expect(values).toEqual({ longOption: value }); + expect('long-option' in values).toBe(false); +}); + +test('combines repeated kebab-case and camel-case values', () => { + const { values } = parseArgs({ + args: ['--include-path', 'first', '--includePath', 'second'], + options: { + 'include-path': { type: 'string', multiple: true }, + }, + }); + + expect(values).toEqual({ includePath: ['first', 'second'] }); +}); + +test('omits undefined values', () => { + const { values } = parseArgs({ + args: [], + options: { + 'optional-value': { type: 'string' }, + }, + }); + + expect(values).toEqual({}); +}); From 1024d5c8010e59bad2ee8c20f925d27026df706a Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Wed, 5 Aug 2026 10:53:01 +0800 Subject: [PATCH 13/21] perf(fmt): avoid relative path resolution under root (#183) --- packages/rstack/src/fmt/ignore.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/rstack/src/fmt/ignore.ts b/packages/rstack/src/fmt/ignore.ts index b27ce880..c313ce39 100644 --- a/packages/rstack/src/fmt/ignore.ts +++ b/packages/rstack/src/fmt/ignore.ts @@ -22,9 +22,12 @@ interface CreateIgnoreMatcherOptions { const createPatternMatcher = (rootPath: string, patterns: string): IgnoreMatcher => { const matcher = createIgnore({ allowRelativePaths: true }).add(patterns); + const rootPrefix = rootPath.endsWith(path.sep) ? rootPath : `${rootPath}${path.sep}`; return (filePath, isDirectory = false) => { - const relativePath = path.relative(rootPath, filePath); + const relativePath = filePath.startsWith(rootPrefix) + ? filePath.slice(rootPrefix.length) + : path.relative(rootPath, filePath); if (relativePath === '') { return false; } From 32759876448cdc931a108abbd1c99d00fc0829ee Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Wed, 5 Aug 2026 11:02:39 +0800 Subject: [PATCH 14/21] fix(types): disallow parseArgs option defaults (#184) --- packages/rstack/src/cli/args.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/rstack/src/cli/args.ts b/packages/rstack/src/cli/args.ts index 27e3af92..cf55aa88 100644 --- a/packages/rstack/src/cli/args.ts +++ b/packages/rstack/src/cli/args.ts @@ -1,9 +1,18 @@ import { parseArgs as nodeParseArgs, - type ParseArgsConfig, + type ParseArgsConfig as NodeParseArgsConfig, + type ParseArgsOptionDescriptor as NodeParseArgsOptionDescriptor, type ParseArgsOptionsConfig, } from 'node:util'; +type ParseArgsOptionDescriptor = Omit & { + default?: never; +}; + +type ParseArgsConfig = Omit & { + options?: Record; +}; + type CamelCase = Value extends `${infer Head}-${infer Tail}` ? `${Head}${Capitalize>}` : Value; From a278f6a4b58e5cc008fb1da6a46ff4c738b04a21 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:08:41 +0800 Subject: [PATCH 15/21] chore(deps): update all non-major dependencies (#185) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: neverland --- .node-version | 2 +- package.json | 2 +- pnpm-lock.yaml | 511 ++++++++++++++++---------------------------- pnpm-workspace.yaml | 6 +- 4 files changed, 189 insertions(+), 332 deletions(-) diff --git a/.node-version b/.node-version index 8dfc5cb1..60ade1ae 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -24.18.1 +24.19.0 diff --git a/package.json b/package.json index 3953d1c0..93ea8261 100644 --- a/package.json +++ b/package.json @@ -23,5 +23,5 @@ "rstack": "workspace:*", "typescript": "catalog:" }, - "packageManager": "pnpm@11.18.0" + "packageManager": "pnpm@11.20.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4613471..b4c1e082 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,8 +8,8 @@ settings: catalogs: default: '@rsbuild/core': - specifier: ~2.1.9 - version: 2.1.9 + specifier: ~2.1.10 + version: 2.1.10 '@rsbuild/plugin-react': specifier: ^2.1.0 version: 2.1.0 @@ -50,8 +50,8 @@ catalogs: specifier: ~0.11.5 version: 0.11.5 '@shikijs/transformers': - specifier: ^4.3.1 - version: 4.3.1 + specifier: ^4.4.1 + version: 4.4.1 '@testing-library/dom': specifier: ^10.4.1 version: 10.4.1 @@ -92,8 +92,8 @@ catalogs: specifier: 3.0.0 version: 3.0.0 lint-staged: - specifier: ^17.2.0 - version: 17.2.0 + specifier: ^17.3.0 + version: 17.3.0 micromatch: specifier: 4.0.8 version: 4.0.8 @@ -165,7 +165,7 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: 'catalog:' - version: 2.1.0(@rsbuild/core@2.1.9)(@rspack/core@2.1.7) + version: 2.1.0(@rsbuild/core@2.1.10)(@rspack/core@2.1.8) '@testing-library/dom': specifier: 'catalog:' version: 10.4.1 @@ -253,7 +253,7 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: 'catalog:' - version: 2.1.0(@rsbuild/core@2.1.9)(@rspack/core@2.1.7) + version: 2.1.0(@rsbuild/core@2.1.10)(@rspack/core@2.1.8) '@testing-library/dom': specifier: 'catalog:' version: 10.4.1 @@ -296,7 +296,7 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: 'catalog:' - version: 2.1.0(@rsbuild/core@2.1.9)(@rspack/core@2.1.7) + version: 2.1.0(@rsbuild/core@2.1.10)(@rspack/core@2.1.8) '@testing-library/dom': specifier: 'catalog:' version: 10.4.1 @@ -326,7 +326,7 @@ importers: dependencies: '@rsbuild/core': specifier: 'catalog:' - version: 2.1.9 + version: 2.1.10 '@rslib/core': specifier: 'catalog:' version: 1.0.0-beta.1(typescript@7.0.2) @@ -357,7 +357,7 @@ importers: version: 0.2.0 '@rstest/adapter-rsbuild': specifier: 'catalog:' - version: 0.11.5(@rsbuild/core@2.1.9)(@rstest/core@0.11.5) + version: 0.11.5(@rsbuild/core@2.1.10)(@rstest/core@0.11.5) '@rstest/adapter-rslib': specifier: 'catalog:' version: 0.11.5(@rslib/core@1.0.0-beta.1)(@rstest/core@0.11.5)(typescript@7.0.2) @@ -378,7 +378,7 @@ importers: version: 3.0.0 lint-staged: specifier: 'catalog:' - version: 17.2.0 + version: 17.3.0 micromatch: specifier: 'catalog:' version: 4.0.8 @@ -399,7 +399,7 @@ importers: devDependencies: '@rsbuild/plugin-sass': specifier: 'catalog:' - version: 2.0.1(@rsbuild/core@2.1.9) + version: 2.0.1(@rsbuild/core@2.1.10) '@rspress/core': specifier: 'catalog:' version: 2.0.19(micromark-util-types@2.0.2)(micromark@4.0.2)(supports-color@8.1.1) @@ -414,7 +414,7 @@ importers: version: 1.14.7(@rspress/core@2.0.19) '@shikijs/transformers': specifier: 'catalog:' - version: 4.3.1 + version: 4.4.1 '@types/node': specifier: 'catalog:' version: 24.13.3 @@ -432,7 +432,7 @@ importers: version: 19.2.8(react@19.2.8) rsbuild-plugin-open-graph: specifier: 'catalog:' - version: 1.1.3(@rsbuild/core@2.1.9) + version: 1.1.3(@rsbuild/core@2.1.10) rspress-plugin-font-open-sans: specifier: 'catalog:' version: 1.0.4(@rspress/core@2.0.19) @@ -525,14 +525,14 @@ packages: '@bufbuild/protobuf@2.12.1': resolution: {integrity: sha512-BvAMfS6LrgZiryOAZ4pBYucu4wG/Ei/9o9DZ9akbREnMLbPJiom2i8b9C8IsKErQoiKqVhrerzt3kOT/RrzLHg==} - '@emnapi/core@1.11.2': - resolution: {integrity: sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==} + '@emnapi/core@1.11.3': + resolution: {integrity: sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==} - '@emnapi/runtime@1.11.2': - resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} - '@emnapi/wasi-threads@1.2.2': - resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@emnapi/wasi-threads@1.2.3': + resolution: {integrity: sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==} '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} @@ -637,18 +637,8 @@ packages: resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} engines: {node: '>= 10.0.0'} - '@rsbuild/core@2.1.8': - resolution: {integrity: sha512-Y70LMcCZspVoQ7Oip1W2Agu5wVhWZ2x3cYl4s9GLQG4VYphBud53DY/jkrqkyQF8ASYZDDDrW2KWNFj0kNLLuA==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - core-js: '>= 3.0.0' - peerDependenciesMeta: - core-js: - optional: true - - '@rsbuild/core@2.1.9': - resolution: {integrity: sha512-yqf1hFZ3wbMYI431LqsxLH3r0VZkfyarVKTf7kMeIiGe0YLwsrgsfp+sKpIyVkQkq60J0qyp6l/CoqqsQZqEwQ==} + '@rsbuild/core@2.1.10': + resolution: {integrity: sha512-lwxC5w88U2AMv6aNwG3VH7+AV33N4JJQjD//egVWFsMbV+OE/bnfCsurSpX8s3lGyRYkJMwUVN+YXMbmmYZFfw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -739,156 +729,76 @@ packages: cpu: [x64] os: [win32] - '@rspack/binding-darwin-arm64@2.1.5': - resolution: {integrity: sha512-XLAN6YSU36qkciJtV9DY9z57pdHOjKy/f1HyoqpZenRg8p46jnXPziV45lfrTZpG+FF6g/jtTUc4dKbeyoWqPw==} + '@rspack/binding-darwin-arm64@2.1.8': + resolution: {integrity: sha512-kia+eWtyWPvR4ntg1bWYoVU8nLPbUg2fG3zgBEocsTcsh5ZENSiEPxEKymDgMyIMONUqj611E0775cdUBoNmqw==} cpu: [arm64] os: [darwin] - '@rspack/binding-darwin-arm64@2.1.7': - resolution: {integrity: sha512-DwxzrXRctueP/3Pyom9JHcIsRShuEAlHb+mrE5OPT+4cdHI1UnJpbzEvEDLTo4IKJhDb3vjXdHLtjqtL0SYbeA==} - cpu: [arm64] - os: [darwin] - - '@rspack/binding-darwin-x64@2.1.5': - resolution: {integrity: sha512-KLW7PV86jyCOyqJSqrkZdvYUWYCFX/Q4LsGmVc8tyDA8jBYLMHJDewC/lNf9ot3rU2SoLDZKhDUh7q7dX0FRmg==} - cpu: [x64] - os: [darwin] - - '@rspack/binding-darwin-x64@2.1.7': - resolution: {integrity: sha512-kPbrYvR/XUHfAMgRVq3QnC71DW/qjwsPj+3hEUuEnRmlploPNy9u8Szf1IHKSVUSrVZBTgDyMoZQdxYLfhResw==} + '@rspack/binding-darwin-x64@2.1.8': + resolution: {integrity: sha512-08pBkFhlD3Y3Qzh94w/Fc3skaIE3e96kl2P14m8+tnYTcglpOfpA2OwS3iHt9fOqy0HjoAVe6/MW3cBgs5iabA==} cpu: [x64] os: [darwin] - '@rspack/binding-linux-arm64-gnu@2.1.5': - resolution: {integrity: sha512-KOooAC8L+Ljx5sY7ZuMeaXCQ310FNWaPWXcYQJpFPApF3qyLeSfuOftUGwxcxeo5U0mS5OY3zxp7/5CaHWKYjg==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rspack/binding-linux-arm64-gnu@2.1.7': - resolution: {integrity: sha512-VFB+YXM3kZ6IIuLV64H3vgnwqvQIIaqfR/aeGwuxYvwcZsrgblSBmXMeDULdgDjqP8Yr0VaFMBBiD9OtG5KdFw==} + '@rspack/binding-linux-arm64-gnu@2.1.8': + resolution: {integrity: sha512-KLniMc9GzhKpVqhPzaJo3KJwzdAllXVVqZIk/uL1QipXOxs57fgM4u7IexKPFVla0o/u1PQG/Ah2YLDmda24Ow==} cpu: [arm64] os: [linux] libc: [glibc] - '@rspack/binding-linux-arm64-musl@2.1.5': - resolution: {integrity: sha512-0GFc07gT71+uRbHtejDecBfSL0fs7dQAwCtXYieXboACauL7UvS3Hwr6v8A91aGtAcvLotnCaIa3CWWYwtYlYQ==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rspack/binding-linux-arm64-musl@2.1.7': - resolution: {integrity: sha512-Mzbxyg0aJ+ITj526Iuz0enEDYY6WxhFIwEKXqwjQh+Vpd5v/+aPzPo83sSQVX/3puBV1sbmviTURbh6N9e1fvA==} + '@rspack/binding-linux-arm64-musl@2.1.8': + resolution: {integrity: sha512-yUKAxHNGnICtw5RnxFWu4dHtsz/tdt7rbeFcsINNVre9HcrRxf5XP+FbOGL/SMxd9oM9XCo10paU2WckTKwbEA==} cpu: [arm64] os: [linux] libc: [musl] - '@rspack/binding-linux-riscv64-gnu@2.1.5': - resolution: {integrity: sha512-isQQDp3fBzPSZpLVFzPqVXIVA4I9b9mKs58TfUgOzDP/1g1582YSV3iFopgFogvEliihXDuuXvM6aAkP+w8Z+Q==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rspack/binding-linux-riscv64-gnu@2.1.7': - resolution: {integrity: sha512-mpazwgT/Pse1720mvEJsoXfPkJ+enj0xUqpbe/wL6aedwjGT+9jJNB8HTJXE4XBX0UO7umGqcJMeKA6YsD2CDA==} + '@rspack/binding-linux-riscv64-gnu@2.1.8': + resolution: {integrity: sha512-gg4S1jaitwYPHR9HZ3zNGH1EK2GXINm66p4kEpOP1gbc+akyOouVF/dMcu9NGPlRg58FbEhVRZYKu7Z/zcpKHg==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rspack/binding-linux-riscv64-musl@2.1.5': - resolution: {integrity: sha512-/SP6VknY4kH60BYplI2FDNAJCo4U4DUszLxhKsgVlgA5ImgHC8Ew2AsKgidk7ceiYV9OcKzYuWYe9tVpysBYVA==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rspack/binding-linux-riscv64-musl@2.1.7': - resolution: {integrity: sha512-oU/l3soPRsDEWn7KZic+npyTMM2N1kRdHjoJ+L5IUBXs8bjdTXPLoyTbTdIOza5ZSoT4+UeEiEryj4BB0tQE5w==} + '@rspack/binding-linux-riscv64-musl@2.1.8': + resolution: {integrity: sha512-b/aU5j1h368SLNyz5u+flqpZVhzSZ1UIslaj9sZJuAvqkGWv3xsjc/28/PTo/RYXCxd0FNVAxTxWHKvRiAAS8w==} cpu: [riscv64] os: [linux] libc: [musl] - '@rspack/binding-linux-x64-gnu@2.1.5': - resolution: {integrity: sha512-/GJmS+buA4pilT10gs5mfAhAZxSHXpDD8veZ3QpYvNUuoU9gvempAq9TgjbyNN/vc5pf76GdXPXKFMiehudYSA==} + '@rspack/binding-linux-x64-gnu@2.1.8': + resolution: {integrity: sha512-EyegohSx0BJRqieCg9f/caCqFARRWkqI5hwJt6k530MoOTLeq8I3vsbeg24/2MktwIC1dmJi8bl0+WhPKQs4eQ==} cpu: [x64] os: [linux] libc: [glibc] - '@rspack/binding-linux-x64-gnu@2.1.7': - resolution: {integrity: sha512-7Gtpl3h3jtnOpk1mYQE8mRndXAO2ibI8mnAbs7klevdKey+ZHneWMoMi2yOMQhhI/ifWEFxDzyGJ8bdxo0XTsA==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rspack/binding-linux-x64-musl@2.1.5': - resolution: {integrity: sha512-iwS3t75nZ2TnUjB05KtvpvjpdkOy/nLxbaOnwjbdnNZZXpUaMadLbllGAWayHPGCMsL2yKBEhVEESZpR7tK3SA==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rspack/binding-linux-x64-musl@2.1.7': - resolution: {integrity: sha512-w+whI2Uy+DYkGN+MVkzMFWweL7B/s1gMqX+nvTE1vhOy3hGV0VyA9H6lqWjSD3I+eGkpYhN9Pr244cYnLpZOUQ==} + '@rspack/binding-linux-x64-musl@2.1.8': + resolution: {integrity: sha512-I6E+goN+UQ297q4r1qdbiAyNCI3t0+a5Y0xDIAPOZfRDRxDTnH/LF8/y65gjsJoKRKyn7zxRC0T/NURTkRNQ9A==} cpu: [x64] os: [linux] libc: [musl] - '@rspack/binding-wasm32-wasi@2.1.5': - resolution: {integrity: sha512-jOhW69t1H/jcRaSMB5U8jVzWP18j0YQIlo0nT8W+KvNVScJeyOze4FFc+5RIS7VmuT8/jid7hUyydZOfm6UgqA==} - cpu: [wasm32] - - '@rspack/binding-wasm32-wasi@2.1.7': - resolution: {integrity: sha512-cDVgvzRdTgxaeM+a5Lx0+7/VAvunvwO0wNtQ3ATQGOtFCW5b7cUzhNPcytH5ZSJTnFWuxinlGwtar5yfcnkdZQ==} + '@rspack/binding-wasm32-wasi@2.1.8': + resolution: {integrity: sha512-om7GAKWAU3lcSvbCon2m7mzw8v9OTrO2LW2MZ1lGe/uVJJmwGGkl9HVoXFyWFLrN6YVFyx8iP+AkN4owDWB9Cw==} cpu: [wasm32] - '@rspack/binding-win32-arm64-msvc@2.1.5': - resolution: {integrity: sha512-OLfMXmtFBcrWr9BRGKXJYrWgYR0JSaoAWr3EVFDjKGi/YR5aUwQSGc3AJPBph9g0YsdSqRuUhtQGtCk84DeeEA==} + '@rspack/binding-win32-arm64-msvc@2.1.8': + resolution: {integrity: sha512-WDnsP/SUb9zbxyGX9XjPw5AXrX86u5oidn0MDdfJduOOqdCSpHwmRjlQ8NUJhbBq9WqVJMFlcab7NwZVWX/yyg==} cpu: [arm64] os: [win32] - '@rspack/binding-win32-arm64-msvc@2.1.7': - resolution: {integrity: sha512-JDd85+iYwUvaG9Zrt5X7oIxRZRiTW+76FwkRakoXNy/5VAWQW32Jq4ESjSVz6l6mh0KnZxPq3TLMugacCPnLjw==} - cpu: [arm64] - os: [win32] - - '@rspack/binding-win32-ia32-msvc@2.1.5': - resolution: {integrity: sha512-p4OSmnj21+AY8vpIADveLEBXfXJRXonOTYim2VLVWV4TbXfhYNMLiX3qESHCdNnn6lVJM0q6zxxzEUfTyAKydw==} - cpu: [ia32] - os: [win32] - - '@rspack/binding-win32-ia32-msvc@2.1.7': - resolution: {integrity: sha512-y9PKEs6v9BLHV0i/4eaIRtxpATvSgcf/VYQkMT8mp+qWlPjUwDQNwU2ueWVGpff6INO+YAa7zobzziNFRgO7Lg==} + '@rspack/binding-win32-ia32-msvc@2.1.8': + resolution: {integrity: sha512-QiMQMPNDiY3dhhaIdaFPzcPDC06cEYkNY89ea+EmDvNVgZq6V+2mFS/WnzZVMeEbGAYJCjsv/ABhhLT1hlYMvg==} cpu: [ia32] os: [win32] - '@rspack/binding-win32-x64-msvc@2.1.5': - resolution: {integrity: sha512-YCkLLDGMkHsw5CpcsFtFhzCu+JZEsNrcH9O1GpWMkyxR2ku2Fq1i2wEHYSm26HyW+PJ3+Fv347MPWMFZIz9q2g==} + '@rspack/binding-win32-x64-msvc@2.1.8': + resolution: {integrity: sha512-b7sA5eB64vo2mbsuc//MOYzVLeCKHPn0dfP/GmNEoHdWbhRgZ/orZLWurYMQj04ELTLW6YCJEy59g5KRzNYHfw==} cpu: [x64] os: [win32] - '@rspack/binding-win32-x64-msvc@2.1.7': - resolution: {integrity: sha512-BjkOzcPY/K8YlRRvyywz0mDWk89MMxqAMhDmgBXCWorh1IjgKTsWDJ2lCGIM8M9CZXUG3khom8AfrOGwRT2I+g==} - cpu: [x64] - os: [win32] + '@rspack/binding@2.1.8': + resolution: {integrity: sha512-tmAyHzDbPiy8V7HvQqtuPsbs6dPgwV0YjzW5XrPRV9gzf+Hdm7pvsZJKE1QKO9WV5RuvGYav98xIX6O+abZxzQ==} - '@rspack/binding@2.1.5': - resolution: {integrity: sha512-lF3ZLeeyV0AN3BL0m2jAmNZD5pP9IHsQ8gUXN7mo0g4xsW6nf6hsN7o9CnEHECz5uUZb+EoWsuWzAAmnA9Ip8Q==} - - '@rspack/binding@2.1.7': - resolution: {integrity: sha512-wYqi8TY30hsIzLry503o/Uqu7y9Ec7pEwN5TVmB7Pb3xHrR2eHsQPzdpF/GkCLUjQSgD2Es3CDVV1mr6zO/78g==} - - '@rspack/core@2.1.5': - resolution: {integrity: sha512-YHjL7xVXycAWsjJtF39cRZ2u+ddiNFxY+FcNgEBe6Y8OaLeUfMfjba3gK+B1Oj32UcKD6BmXGsPfu9p+OII/Yw==} - engines: {node: ^20.19.0 || >=22.12.0} - peerDependencies: - '@module-federation/runtime-tools': ^0.24.1 || ^2.0.0 - '@swc/helpers': ^0.5.23 - peerDependenciesMeta: - '@module-federation/runtime-tools': - optional: true - '@swc/helpers': - optional: true - - '@rspack/core@2.1.7': - resolution: {integrity: sha512-d5Ju3zXzGgbqQWvlMlLUtek2eFPIzsFe2QOF4nwTAknxo/4OZ64t+kPT9nM6fr3aZX93VK0R3v02/kZYIRrV9Q==} + '@rspack/core@2.1.8': + resolution: {integrity: sha512-na1kyA6Mj8/LWw9O3A8NsrG9rNKN3Iq2WiXrEuIwsU5r/Nl/evm3hO7bWKHxgsRyydI6W7okwx3MXgf8rzel6g==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@module-federation/runtime-tools': ^0.24.1 || ^2.0.0 @@ -980,6 +890,10 @@ packages: resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==} engines: {node: '>=20'} + '@shikijs/core@4.4.1': + resolution: {integrity: sha512-VeR2CY6Nn9/WbisoYLOQZ7HZOnwTrpBuOw4wExjqLnBCi62BNWynBUO6K2uPIASPFJwAv7cX1fUu+LrPlSstcw==} + engines: {node: '>=20'} + '@shikijs/engine-javascript@4.3.1': resolution: {integrity: sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==} engines: {node: '>=20'} @@ -996,6 +910,10 @@ packages: resolution: {integrity: sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==} engines: {node: '>=20'} + '@shikijs/primitive@4.4.1': + resolution: {integrity: sha512-ko2OfDoG89YuQ7xL5LtcQiWKb7NIv1Ephb7g48TVU198OzAMLC8lXVEwaJGHK4sUMYrfAGJDqYmNLOLiW/Kz8w==} + engines: {node: '>=20'} + '@shikijs/rehype@4.3.1': resolution: {integrity: sha512-oshrlfUF3VPUJfnp5K1lLwsS/SRBKrIxONpdWebSKZXdBE3UsZnxgqpvRUA8UsofS7vmjFOCAHIT71ECbmOxTw==} engines: {node: '>=20'} @@ -1004,14 +922,18 @@ packages: resolution: {integrity: sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==} engines: {node: '>=20'} - '@shikijs/transformers@4.3.1': - resolution: {integrity: sha512-z6ir0bGDgWcF2FduktEfPgIsdOtIlDiLAjFBgBzE42Q9xHbkkIXZtORHzlLVB71iZP9elEcqKg6keajvOUwE2A==} + '@shikijs/transformers@4.4.1': + resolution: {integrity: sha512-Sb9Eehas+5EhClpFgNuklwY3aWf354FLaKRCiAWmjdNbHAjoQUpv6WmSj+N19eTXO6GLIWh1dIOH9dxyauhVWw==} engines: {node: '>=20'} '@shikijs/types@4.3.1': resolution: {integrity: sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==} engines: {node: '>=20'} + '@shikijs/types@4.4.1': + resolution: {integrity: sha512-GOwCLQDHM5EjGUWNPrhzJbr6JP8V/Dx/CDVkWvbZ1Avw5JFnNUckrgbLmE07qtg4WlW7Q7QFndhjIkeU9XMPvw==} + engines: {node: '>=20'} + '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -1067,8 +989,8 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -1090,9 +1012,6 @@ packages: peerDependencies: '@types/react': ^19.2.0 - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} - '@types/react@19.2.18': resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} @@ -1635,8 +1554,8 @@ packages: engines: {node: '>=6'} hasBin: true - lint-staged@17.2.0: - resolution: {integrity: sha512-FchGnFe4i4B1C/a35SPU9bNGPEHSC1+1iV0plLjzBmKVe9klZrlRfSgK6Cw4VeHyqOXbJUXP0vON61uRftNQ0A==} + lint-staged@17.3.0: + resolution: {integrity: sha512-woZS3vNe3UKqBaLPvbLOtKRY4tLANpWQhom12MGWqC8Mh1lCOO+WgSwmX2amjJAqTY9BkXYW87fCUH5H9Ph6xw==} engines: {node: '>=22.22.1'} hasBin: true @@ -2444,18 +2363,18 @@ snapshots: '@bufbuild/protobuf@2.12.1': {} - '@emnapi/core@1.11.2': + '@emnapi/core@1.11.3': dependencies: - '@emnapi/wasi-threads': 1.2.2 + '@emnapi/wasi-threads': 1.2.3 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.11.2': + '@emnapi/runtime@1.11.3': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.2': + '@emnapi/wasi-threads@1.2.3': dependencies: tslib: 2.8.1 optional: true @@ -2464,7 +2383,7 @@ snapshots: dependencies: '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdx': 2.0.14 acorn: 8.17.0 collapse-white-space: 2.1.0 @@ -2490,16 +2409,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8)': + '@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8)': dependencies: '@types/mdx': 2.0.14 - '@types/react': 19.2.17 + '@types/react': 19.2.18 react: 19.2.8 - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: - '@emnapi/core': 1.11.2 - '@emnapi/runtime': 1.11.2 + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 '@tybys/wasm-util': 0.10.3 optional: true @@ -2564,39 +2483,23 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.6 optional: true - '@rsbuild/core@2.1.8': - dependencies: - '@rspack/core': 2.1.5(@swc/helpers@0.5.23) - '@swc/helpers': 0.5.23 - transitivePeerDependencies: - - '@module-federation/runtime-tools' - - '@rsbuild/core@2.1.9': + '@rsbuild/core@2.1.10': dependencies: - '@rspack/core': 2.1.7(@swc/helpers@0.5.23) + '@rspack/core': 2.1.8(@swc/helpers@0.5.23) '@swc/helpers': 0.5.23 transitivePeerDependencies: - '@module-federation/runtime-tools' - '@rsbuild/plugin-react@2.1.0(@rsbuild/core@2.1.8)': - dependencies: - '@rspack/plugin-react-refresh': 2.0.2(@rspack/core@2.1.7)(react-refresh@0.18.0) - react-refresh: 0.18.0 - optionalDependencies: - '@rsbuild/core': 2.1.8 - transitivePeerDependencies: - - '@rspack/core' - - '@rsbuild/plugin-react@2.1.0(@rsbuild/core@2.1.9)(@rspack/core@2.1.7)': + '@rsbuild/plugin-react@2.1.0(@rsbuild/core@2.1.10)(@rspack/core@2.1.8)': dependencies: - '@rspack/plugin-react-refresh': 2.0.2(@rspack/core@2.1.7)(react-refresh@0.18.0) + '@rspack/plugin-react-refresh': 2.0.2(@rspack/core@2.1.8)(react-refresh@0.18.0) react-refresh: 0.18.0 optionalDependencies: - '@rsbuild/core': 2.1.9 + '@rsbuild/core': 2.1.10 transitivePeerDependencies: - '@rspack/core' - '@rsbuild/plugin-sass@2.0.1(@rsbuild/core@2.1.9)': + '@rsbuild/plugin-sass@2.0.1(@rsbuild/core@2.1.10)': dependencies: deepmerge: 4.3.1 loader-utils: 2.0.4 @@ -2604,12 +2507,12 @@ snapshots: reduce-configs: 2.0.1 sass-embedded: 1.100.0 optionalDependencies: - '@rsbuild/core': 2.1.9 + '@rsbuild/core': 2.1.10 '@rslib/core@1.0.0-beta.1(typescript@7.0.2)': dependencies: - '@rsbuild/core': 2.1.9 - rsbuild-plugin-dts: 1.0.0-beta.1(@rsbuild/core@2.1.9)(typescript@7.0.2) + '@rsbuild/core': 2.1.10 + rsbuild-plugin-dts: 1.0.0-beta.1(@rsbuild/core@2.1.10)(typescript@7.0.2) optionalDependencies: typescript: 7.0.2 transitivePeerDependencies: @@ -2653,144 +2556,83 @@ snapshots: '@rslint/native-win32-x64-msvc@0.7.2': optional: true - '@rspack/binding-darwin-arm64@2.1.5': - optional: true - - '@rspack/binding-darwin-arm64@2.1.7': - optional: true - - '@rspack/binding-darwin-x64@2.1.5': - optional: true - - '@rspack/binding-darwin-x64@2.1.7': - optional: true - - '@rspack/binding-linux-arm64-gnu@2.1.5': - optional: true - - '@rspack/binding-linux-arm64-gnu@2.1.7': - optional: true - - '@rspack/binding-linux-arm64-musl@2.1.5': - optional: true - - '@rspack/binding-linux-arm64-musl@2.1.7': - optional: true - - '@rspack/binding-linux-riscv64-gnu@2.1.5': + '@rspack/binding-darwin-arm64@2.1.8': optional: true - '@rspack/binding-linux-riscv64-gnu@2.1.7': + '@rspack/binding-darwin-x64@2.1.8': optional: true - '@rspack/binding-linux-riscv64-musl@2.1.5': + '@rspack/binding-linux-arm64-gnu@2.1.8': optional: true - '@rspack/binding-linux-riscv64-musl@2.1.7': + '@rspack/binding-linux-arm64-musl@2.1.8': optional: true - '@rspack/binding-linux-x64-gnu@2.1.5': + '@rspack/binding-linux-riscv64-gnu@2.1.8': optional: true - '@rspack/binding-linux-x64-gnu@2.1.7': + '@rspack/binding-linux-riscv64-musl@2.1.8': optional: true - '@rspack/binding-linux-x64-musl@2.1.5': + '@rspack/binding-linux-x64-gnu@2.1.8': optional: true - '@rspack/binding-linux-x64-musl@2.1.7': + '@rspack/binding-linux-x64-musl@2.1.8': optional: true - '@rspack/binding-wasm32-wasi@2.1.5': + '@rspack/binding-wasm32-wasi@2.1.8': dependencies: - '@emnapi/core': 1.11.2 - '@emnapi/runtime': 1.11.2 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) optional: true - '@rspack/binding-wasm32-wasi@2.1.7': - dependencies: - '@emnapi/core': 1.11.2 - '@emnapi/runtime': 1.11.2 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) - optional: true - - '@rspack/binding-win32-arm64-msvc@2.1.5': - optional: true - - '@rspack/binding-win32-arm64-msvc@2.1.7': - optional: true - - '@rspack/binding-win32-ia32-msvc@2.1.5': - optional: true - - '@rspack/binding-win32-ia32-msvc@2.1.7': + '@rspack/binding-win32-arm64-msvc@2.1.8': optional: true - '@rspack/binding-win32-x64-msvc@2.1.5': + '@rspack/binding-win32-ia32-msvc@2.1.8': optional: true - '@rspack/binding-win32-x64-msvc@2.1.7': + '@rspack/binding-win32-x64-msvc@2.1.8': optional: true - '@rspack/binding@2.1.5': - optionalDependencies: - '@rspack/binding-darwin-arm64': 2.1.5 - '@rspack/binding-darwin-x64': 2.1.5 - '@rspack/binding-linux-arm64-gnu': 2.1.5 - '@rspack/binding-linux-arm64-musl': 2.1.5 - '@rspack/binding-linux-riscv64-gnu': 2.1.5 - '@rspack/binding-linux-riscv64-musl': 2.1.5 - '@rspack/binding-linux-x64-gnu': 2.1.5 - '@rspack/binding-linux-x64-musl': 2.1.5 - '@rspack/binding-wasm32-wasi': 2.1.5 - '@rspack/binding-win32-arm64-msvc': 2.1.5 - '@rspack/binding-win32-ia32-msvc': 2.1.5 - '@rspack/binding-win32-x64-msvc': 2.1.5 - - '@rspack/binding@2.1.7': + '@rspack/binding@2.1.8': optionalDependencies: - '@rspack/binding-darwin-arm64': 2.1.7 - '@rspack/binding-darwin-x64': 2.1.7 - '@rspack/binding-linux-arm64-gnu': 2.1.7 - '@rspack/binding-linux-arm64-musl': 2.1.7 - '@rspack/binding-linux-riscv64-gnu': 2.1.7 - '@rspack/binding-linux-riscv64-musl': 2.1.7 - '@rspack/binding-linux-x64-gnu': 2.1.7 - '@rspack/binding-linux-x64-musl': 2.1.7 - '@rspack/binding-wasm32-wasi': 2.1.7 - '@rspack/binding-win32-arm64-msvc': 2.1.7 - '@rspack/binding-win32-ia32-msvc': 2.1.7 - '@rspack/binding-win32-x64-msvc': 2.1.7 - - '@rspack/core@2.1.5(@swc/helpers@0.5.23)': - dependencies: - '@rspack/binding': 2.1.5 - optionalDependencies: - '@swc/helpers': 0.5.23 - - '@rspack/core@2.1.7(@swc/helpers@0.5.23)': - dependencies: - '@rspack/binding': 2.1.7 + '@rspack/binding-darwin-arm64': 2.1.8 + '@rspack/binding-darwin-x64': 2.1.8 + '@rspack/binding-linux-arm64-gnu': 2.1.8 + '@rspack/binding-linux-arm64-musl': 2.1.8 + '@rspack/binding-linux-riscv64-gnu': 2.1.8 + '@rspack/binding-linux-riscv64-musl': 2.1.8 + '@rspack/binding-linux-x64-gnu': 2.1.8 + '@rspack/binding-linux-x64-musl': 2.1.8 + '@rspack/binding-wasm32-wasi': 2.1.8 + '@rspack/binding-win32-arm64-msvc': 2.1.8 + '@rspack/binding-win32-ia32-msvc': 2.1.8 + '@rspack/binding-win32-x64-msvc': 2.1.8 + + '@rspack/core@2.1.8(@swc/helpers@0.5.23)': + dependencies: + '@rspack/binding': 2.1.8 optionalDependencies: '@swc/helpers': 0.5.23 - '@rspack/plugin-react-refresh@2.0.2(@rspack/core@2.1.7)(react-refresh@0.18.0)': + '@rspack/plugin-react-refresh@2.0.2(@rspack/core@2.1.8)(react-refresh@0.18.0)': dependencies: react-refresh: 0.18.0 optionalDependencies: - '@rspack/core': 2.1.7(@swc/helpers@0.5.23) + '@rspack/core': 2.1.8(@swc/helpers@0.5.23) '@rspress/core@2.0.19(micromark-util-types@2.0.2)(micromark@4.0.2)(supports-color@8.1.1)': dependencies: '@mdx-js/mdx': 3.1.1(supports-color@8.1.1) - '@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.8) - '@rsbuild/core': 2.1.8 - '@rsbuild/plugin-react': 2.1.0(@rsbuild/core@2.1.8) + '@mdx-js/react': 3.1.1(@types/react@19.2.18)(react@19.2.8) + '@rsbuild/core': 2.1.10 + '@rsbuild/plugin-react': 2.1.0(@rsbuild/core@2.1.10)(@rspack/core@2.1.8) '@rspress/shared': 2.0.19(supports-color@8.1.1) '@shikijs/rehype': 4.3.1 '@types/mdast': 4.0.4 - '@types/react': 19.2.17 + '@types/react': 19.2.18 '@unhead/react': 2.1.16(react@19.2.8) body-scroll-lock: 4.0.0-beta.0 clsx: 2.1.1 @@ -2839,9 +2681,9 @@ snapshots: '@rspress/shared@2.0.19(supports-color@8.1.1)': dependencies: - '@rsbuild/core': 2.1.8 + '@rsbuild/core': 2.1.10 '@shikijs/rehype': 4.3.1 - '@types/react': 19.2.17 + '@types/react': 19.2.18 mdast-util-mdx-jsx: 3.2.0(supports-color@8.1.1) unified: 11.0.5 transitivePeerDependencies: @@ -2857,9 +2699,9 @@ snapshots: '@rstackjs/test-utils@0.2.0': {} - '@rstest/adapter-rsbuild@0.11.5(@rsbuild/core@2.1.9)(@rstest/core@0.11.5)': + '@rstest/adapter-rsbuild@0.11.5(@rsbuild/core@2.1.10)(@rstest/core@0.11.5)': dependencies: - '@rsbuild/core': 2.1.9 + '@rsbuild/core': 2.1.10 '@rstest/core': 0.11.5(happy-dom@20.11.1) '@rstest/adapter-rslib@0.11.5(@rslib/core@1.0.0-beta.1)(@rstest/core@0.11.5)(typescript@7.0.2)': @@ -2871,7 +2713,7 @@ snapshots: '@rstest/core@0.11.5(happy-dom@20.11.1)': dependencies: - '@rsbuild/core': 2.1.8 + '@rsbuild/core': 2.1.10 '@types/chai': 5.2.3 optionalDependencies: happy-dom: 20.11.1 @@ -2884,7 +2726,15 @@ snapshots: '@shikijs/primitive': 4.3.1 '@shikijs/types': 4.3.1 '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 + hast-util-to-html: 9.0.5 + + '@shikijs/core@4.4.1': + dependencies: + '@shikijs/primitive': 4.4.1 + '@shikijs/types': 4.4.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 '@shikijs/engine-javascript@4.3.1': @@ -2906,12 +2756,18 @@ snapshots: dependencies: '@shikijs/types': 4.3.1 '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 + + '@shikijs/primitive@4.4.1': + dependencies: + '@shikijs/types': 4.4.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 '@shikijs/rehype@4.3.1': dependencies: '@shikijs/types': 4.3.1 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-to-string: 3.0.1 shiki: 4.3.1 unified: 11.0.5 @@ -2921,15 +2777,20 @@ snapshots: dependencies: '@shikijs/types': 4.3.1 - '@shikijs/transformers@4.3.1': + '@shikijs/transformers@4.4.1': dependencies: - '@shikijs/core': 4.3.1 - '@shikijs/types': 4.3.1 + '@shikijs/core': 4.4.1 + '@shikijs/types': 4.4.1 '@shikijs/types@4.3.1': dependencies: '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 + + '@shikijs/types@4.4.1': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 '@shikijs/vscode-textmate@10.0.2': {} @@ -2994,7 +2855,7 @@ snapshots: '@types/estree@1.0.9': {} - '@types/hast@3.0.4': + '@types/hast@3.0.5': dependencies: '@types/unist': 3.0.3 @@ -3018,10 +2879,6 @@ snapshots: dependencies: '@types/react': 19.2.18 - '@types/react@19.2.17': - dependencies: - csstype: 3.2.3 - '@types/react@19.2.18': dependencies: csstype: 3.2.3 @@ -3331,7 +3188,7 @@ snapshots: hast-util-from-parse5@8.0.3: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 devlop: 1.1.0 hastscript: 9.0.1 @@ -3342,19 +3199,19 @@ snapshots: hast-util-heading-rank@3.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-is-element@3.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-parse-selector@4.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-raw@9.1.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 '@ungap/structured-clone': 1.3.2 hast-util-from-parse5: 8.0.3 @@ -3372,7 +3229,7 @@ snapshots: dependencies: '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 comma-separated-tokens: 2.0.3 devlop: 1.1.0 estree-util-attach-comments: 3.0.0 @@ -3391,7 +3248,7 @@ snapshots: hast-util-to-html@9.0.5: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 ccount: 2.0.1 comma-separated-tokens: 2.0.3 @@ -3406,7 +3263,7 @@ snapshots: hast-util-to-jsx-runtime@2.3.6(supports-color@8.1.1): dependencies: '@types/estree': 1.0.9 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 comma-separated-tokens: 2.0.3 devlop: 1.1.0 @@ -3425,7 +3282,7 @@ snapshots: hast-util-to-parse5@8.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 comma-separated-tokens: 2.0.3 devlop: 1.1.0 property-information: 7.2.0 @@ -3435,15 +3292,15 @@ snapshots: hast-util-to-string@3.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-whitespace@3.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hastscript@9.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 comma-separated-tokens: 2.0.3 hast-util-parse-selector: 4.0.0 property-information: 7.2.0 @@ -3498,7 +3355,7 @@ snapshots: json5@2.2.3: {} - lint-staged@17.2.0: + lint-staged@17.3.0: dependencies: picomatch: 4.0.5 string-argv: 0.3.2 @@ -3604,7 +3461,7 @@ snapshots: mdast-util-mdx-expression@2.0.1(supports-color@8.1.1): dependencies: '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) @@ -3615,7 +3472,7 @@ snapshots: mdast-util-mdx-jsx@3.2.0(supports-color@8.1.1): dependencies: '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 '@types/unist': 3.0.3 ccount: 2.0.1 @@ -3642,7 +3499,7 @@ snapshots: mdast-util-mdxjs-esm@2.0.1(supports-color@8.1.1): dependencies: '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) @@ -3657,7 +3514,7 @@ snapshots: mdast-util-to-hast@13.2.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 '@ungap/structured-clone': 1.3.2 devlop: 1.1.0 @@ -4159,7 +4016,7 @@ snapshots: rehype-external-links@3.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@ungap/structured-clone': 1.3.2 hast-util-is-element: 3.0.0 is-absolute-url: 4.0.1 @@ -4168,14 +4025,14 @@ snapshots: rehype-raw@7.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-raw: 9.1.0 vfile: 6.0.3 rehype-recma@1.0.0(supports-color@8.1.1): dependencies: '@types/estree': 1.0.9 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-to-estree: 3.1.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -4232,7 +4089,7 @@ snapshots: remark-rehype@11.1.2: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 mdast-util-to-hast: 13.2.1 unified: 11.0.5 @@ -4244,16 +4101,16 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 - rsbuild-plugin-dts@1.0.0-beta.1(@rsbuild/core@2.1.9)(typescript@7.0.2): + rsbuild-plugin-dts@1.0.0-beta.1(@rsbuild/core@2.1.10)(typescript@7.0.2): dependencies: '@ast-grep/napi': 0.37.0 - '@rsbuild/core': 2.1.9 + '@rsbuild/core': 2.1.10 optionalDependencies: typescript: 7.0.2 - rsbuild-plugin-open-graph@1.1.3(@rsbuild/core@2.1.9): + rsbuild-plugin-open-graph@1.1.3(@rsbuild/core@2.1.10): optionalDependencies: - '@rsbuild/core': 2.1.9 + '@rsbuild/core': 2.1.10 rslog@2.3.0: {} @@ -4380,7 +4237,7 @@ snapshots: '@shikijs/themes': 4.3.1 '@shikijs/types': 4.3.1 '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 sort-object-keys@2.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2d5bb31a..555ddb92 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -12,7 +12,7 @@ catalogMode: prefer cleanupUnusedCatalogs: true catalog: - '@rsbuild/core': '~2.1.9' + '@rsbuild/core': '~2.1.10' '@rsbuild/plugin-react': '^2.1.0' '@rsbuild/plugin-sass': '^2.0.1' '@rslib/core': '~1.0.0-beta.1' @@ -33,14 +33,14 @@ catalog: '@types/node': '^24.13.3' '@types/react': '^19.2.18' '@types/react-dom': '^19.2.4' - '@shikijs/transformers': '^4.3.1' + '@shikijs/transformers': '^4.4.1' 'cspell-ban-words': '^0.0.4' 'happy-dom': '^20.11.1' 'heading-case': '^1.1.4' ignore: 7.0.6 'import-meta-resolve': '4.2.0' is-binary-path: 3.0.0 - 'lint-staged': '^17.2.0' + 'lint-staged': '^17.3.0' 'micromatch': '4.0.8' prettier: '3.9.6' 'react': '^19.2.8' From f6d91892079373958de5dfe156a8ed67cca91f05 Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Wed, 5 Aug 2026 11:38:49 +0800 Subject: [PATCH 16/21] test(cli): simplify option alias coverage (#186) --- packages/rstack/tests/cli/fmt/index.test.ts | 33 ++++------- .../rstack/tests/cli/staged/index.test.ts | 6 +- packages/rstack/tests/fmt/cli.test.ts | 56 +++++++------------ 3 files changed, 33 insertions(+), 62 deletions(-) diff --git a/packages/rstack/tests/cli/fmt/index.test.ts b/packages/rstack/tests/cli/fmt/index.test.ts index 36ee9268..bb47ddfc 100644 --- a/packages/rstack/tests/cli/fmt/index.test.ts +++ b/packages/rstack/tests/cli/fmt/index.test.ts @@ -195,7 +195,7 @@ test('does not load Prettier config or ignore files', () => { expect(readProjectFile('index.ts')).toBe('function getMessage() {\n return "hello";\n}\n'); }); -test.each(['--ignore-path', '--ignorePath'])('applies repeated ignore paths with %s', (option) => { +test('applies repeated ignore paths', () => { 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"'); @@ -203,9 +203,9 @@ test.each(['--ignore-path', '--ignorePath'])('applies repeated ignore paths with writeProjectFile('src/index.ts', 'const index="formatted"'); const result = runFmt([ - option, + '--ignore-path', '.prettierignore', - `${option}=config/extra.ignore`, + '--ignore-path=config/extra.ignore', 'src/ignored-by-root.ts', 'src/ignored-by-extra.ts', 'src/index.ts', @@ -437,14 +437,6 @@ test('formats stdin for the given filepath', () => { 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', @@ -598,18 +590,15 @@ test('returns exit code 2 when no files match', () => { } }); -test.each(['--no-error-on-unmatched-pattern', '--noErrorOnUnmatchedPattern'])( - 'allows no files to match with %s', - (option) => { - for (const modeArgs of [[], ['--check'], ['--list-different']]) { - const result = runFmt([...modeArgs, option, 'missing/**/*.ts']); +test('allows no files to match with --no-error-on-unmatched-pattern', () => { + for (const modeArgs of [[], ['--check'], ['--list-different']]) { + const result = runFmt([...modeArgs, '--no-error-on-unmatched-pattern', 'missing/**/*.ts']); - expect(result.status).toBe(0); - expect(result.stdout).toBe(''); - expect(result.stderr).toBe(''); - } - }, -); + expect(result.status).toBe(0); + expect(result.stdout).toBe(''); + expect(result.stderr).toBe(''); + } +}); test('counts only supported files', () => { writeProjectFile('index.ts', 'const value = 1;\n'); diff --git a/packages/rstack/tests/cli/staged/index.test.ts b/packages/rstack/tests/cli/staged/index.test.ts index 93376ccd..a5176b25 100644 --- a/packages/rstack/tests/cli/staged/index.test.ts +++ b/packages/rstack/tests/cli/staged/index.test.ts @@ -89,11 +89,11 @@ test('should pass long options to lint-staged', async ({ expect }) => { }); }); -test('should pass short options and aliases to lint-staged', async ({ expect }) => { - await runStagedCLI(['--allowEmpty', '-p', '1', '-d', '-q', '-r', '-v']); +test('should pass short options to lint-staged', async ({ expect }) => { + await runStagedCLI(['-p', '1', '-d', '-q', '-r', '-v']); expect(mocks.lintStaged).toHaveBeenCalledWith({ - allowEmpty: true, + allowEmpty: undefined, concurrent: 1, config: stagedConfig, cwd: undefined, diff --git a/packages/rstack/tests/fmt/cli.test.ts b/packages/rstack/tests/fmt/cli.test.ts index 3dcf5a97..f4142c2f 100644 --- a/packages/rstack/tests/fmt/cli.test.ts +++ b/packages/rstack/tests/fmt/cli.test.ts @@ -32,7 +32,6 @@ test.each([ ['--write', 'write'], ['--check', 'check'], ['--list-different', 'list-different'], - ['--listDifferent', 'list-different'], ] as const)('parses %s mode', (option, mode) => { expect(parseFmtCLIArgs([option])).toEqual({ mode, @@ -44,19 +43,16 @@ test.each([ }); }); -test.each(['--parallel-workers', '--parallelWorkers'])( - 'configures parallel worker count with %s', - (option) => { - expect(parseFmtCLIArgs([option, '3'])).toEqual({ - mode: 'write', - patterns: [], - ignorePaths: [], - noErrorOnUnmatchedPattern: false, - maxWorkers: 3, - help: false, - }); - }, -); +test('configures parallel worker count', () => { + expect(parseFmtCLIArgs(['--parallel-workers', '3'])).toEqual({ + mode: 'write', + patterns: [], + ignorePaths: [], + noErrorOnUnmatchedPattern: false, + maxWorkers: 3, + help: false, + }); +}); test.each(['0', '-1', '1.5', 'invalid', '9007199254740992'])( 'rejects invalid parallel worker count %s', @@ -67,10 +63,6 @@ test.each(['0', '-1', '1.5', 'invalid', '9007199254740992'])( }, ); -test('prefers the kebab-case parallel worker option', () => { - expect(parseFmtCLIArgs(['--parallel-workers', '2', '--parallelWorkers', '3']).maxWorkers).toBe(2); -}); - test('preserves file paths and globs', () => { const patterns = ['src/file with spaces.ts', 'src/**/*.{js,ts}', '!src/generated/**']; @@ -99,28 +91,19 @@ test.each(['--help', '-h'])('parses %s', (option) => { expect(parseFmtCLIArgs([option]).help).toBe(true); }); -test.each(['--ignore-path', '--ignorePath'])('collects repeated ignore paths with %s', (option) => { - expect( - parseFmtCLIArgs([option, '.prettierignore', `${option}=config/format.ignore`]).ignorePaths, - ).toEqual(['.prettierignore', 'config/format.ignore']); -}); - -test('combines kebab-case and camel-case ignore paths', () => { +test('collects repeated ignore paths', () => { expect( - parseFmtCLIArgs(['--ignore-path', '.prettierignore', '--ignorePath', 'config/format.ignore']) + parseFmtCLIArgs(['--ignore-path', '.prettierignore', '--ignore-path=config/format.ignore']) .ignorePaths, ).toEqual(['.prettierignore', 'config/format.ignore']); }); -test.each(['--no-error-on-unmatched-pattern', '--noErrorOnUnmatchedPattern'])( - 'parses %s', - (option) => { - expect(parseFmtCLIArgs([option]).noErrorOnUnmatchedPattern).toBe(true); - }, -); +test('parses --no-error-on-unmatched-pattern', () => { + expect(parseFmtCLIArgs(['--no-error-on-unmatched-pattern']).noErrorOnUnmatchedPattern).toBe(true); +}); -test.each(['--stdin-filepath', '--stdinFilepath'])('parses %s', (option) => { - expect(parseFmtCLIArgs([option, 'src/index.ts'])).toEqual({ +test('parses --stdin-filepath', () => { + expect(parseFmtCLIArgs(['--stdin-filepath', 'src/index.ts'])).toEqual({ mode: 'write', patterns: [], ignorePaths: [], @@ -143,7 +126,7 @@ test('accepts a worker count with --stdin-filepath', () => { }); }); -test.each(['--write', '--check', '--list-different', '--listDifferent'])( +test.each(['--write', '--check', '--list-different'])( 'rejects %s with --stdin-filepath', (option) => { expect(() => parseFmtCLIArgs(['--stdin-filepath', 'index.ts', option])).toThrow( @@ -168,7 +151,6 @@ test('provides command help', () => { test.each([ ['--write', '--check'], ['--write', '--list-different'], - ['--write', '--listDifferent'], ['--check', '--list-different'], ['--write', '--check', '--list-different'], ])('rejects conflicting modes: %s', (...args) => { @@ -177,7 +159,7 @@ test.each([ ); }); -test.each(['--unknown', '--no-cache', '--no-parallel', '--noParallel'])( +test.each(['--unknown', '--no-cache', '--no-parallel'])( 'rejects unsupported option %s', (option) => { expect(() => parseFmtCLIArgs([option])).toThrow(); From 83e3d7d1e74507ffc00eb67f839ba625d3139681 Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Wed, 5 Aug 2026 11:50:42 +0800 Subject: [PATCH 17/21] perf(fmt): skip unused ignore matcher wrapper (#187) --- packages/rstack/src/fmt/ignore.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/rstack/src/fmt/ignore.ts b/packages/rstack/src/fmt/ignore.ts index c313ce39..bc814a5c 100644 --- a/packages/rstack/src/fmt/ignore.ts +++ b/packages/rstack/src/fmt/ignore.ts @@ -62,6 +62,10 @@ const createIgnoreMatcher = async ({ config.rootPath, [...defaultIgnorePatterns, ...config.ignorePatterns].join('\n'), ); + if (ignorePaths.length === 0) { + return configMatcher; + } + const ignoreMatchers = await Promise.all( ignorePaths.map((ignorePath) => loadIgnoreMatcher(cwd, ignorePath)), ); From 3525ebc83eea41106c48a62cbdfd59c03617e6a6 Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Wed, 5 Aug 2026 13:02:45 +0800 Subject: [PATCH 18/21] feat(staged): allow unmatched rs fmt tasks (#188) --- packages/rstack/src/fmt/cli.ts | 4 +++- packages/rstack/src/staged.ts | 3 +++ packages/rstack/tests/cli/staged/fmt.test.ts | 23 +++++++++++++++++++ .../rstack/tests/cli/staged/index.test.ts | 18 ++++++++++++++- website/docs/en/guide/cli/fmt.mdx | 2 ++ website/docs/zh/guide/cli/fmt.mdx | 2 ++ 6 files changed, 50 insertions(+), 2 deletions(-) diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index bbc9abaa..a2addad5 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 da46b729..7aee8839 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 e9c48296..3b892bb9 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 a5176b25..d7c0146d 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 43a99759..78187c7c 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 f3bd105c..1c58ea54 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 的最大数量设置为正整数: From 05a31d5777abfbe58deb5ce6ba4d52c3f4d28afc Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Wed, 5 Aug 2026 13:27:38 +0800 Subject: [PATCH 19/21] feat(fmt): add --ignore-unknown (#190) --- packages/rstack/src/fmt/cli.ts | 13 ++++++++ packages/rstack/src/fmt/stdin.ts | 6 ++++ packages/rstack/tests/cli/fmt/index.test.ts | 32 +++++++++++++++++++ packages/rstack/tests/cli/staged/fmt.test.ts | 19 +++++++++++ .../tests/fmt/__snapshots__/cli.test.ts.snap | 1 + packages/rstack/tests/fmt/cli.test.ts | 11 +++++++ website/docs/en/guide/cli/fmt.mdx | 24 ++++++++++---- website/docs/zh/guide/cli/fmt.mdx | 24 ++++++++++---- 8 files changed, 118 insertions(+), 12 deletions(-) diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index a2addad5..cff04bbc 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 3534d54d..ec69200d 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 bb47ddfc..33ba3ca7 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 3b892bb9..fa156f59 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 e794c9d1..62ae1697 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 f4142c2f..cf5e1908 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 78187c7c..818140e1 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 1c58ea54..3c810f3e 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' From 039ae53165b236e4998d8e3a813c068fb9251104 Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Wed, 5 Aug 2026 13:28:16 +0800 Subject: [PATCH 20/21] perf(fmt): optimize default lock file matching (#189) --- packages/rstack/src/fmt/ignore.ts | 18 +++++++++++++----- packages/rstack/tests/fmt/ignore.test.ts | 3 +++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/rstack/src/fmt/ignore.ts b/packages/rstack/src/fmt/ignore.ts index bc814a5c..1405de29 100644 --- a/packages/rstack/src/fmt/ignore.ts +++ b/packages/rstack/src/fmt/ignore.ts @@ -9,7 +9,7 @@ import type { ResolvedFmtConfig } from './types.ts'; * Prettier already skips other generated lock files when it cannot infer a parser, so this list * contains only the additional defaults owned by `rs fmt`. */ -const defaultIgnorePatterns = ['package-lock.json', 'pnpm-lock.yaml']; +const defaultIgnoreNames = ['package-lock.json', 'pnpm-lock.yaml']; type IgnoreMatcher = (filePath: string, isDirectory?: boolean) => boolean; @@ -20,6 +20,12 @@ interface CreateIgnoreMatcherOptions { ignorePaths?: string[]; } +const createDefaultIgnoreMatcher = (): IgnoreMatcher => { + const suffixes = defaultIgnoreNames.map((name) => `${path.sep}${name}`); + + return (filePath) => suffixes.some((suffix) => filePath.endsWith(suffix)); +}; + const createPatternMatcher = (rootPath: string, patterns: string): IgnoreMatcher => { const matcher = createIgnore({ allowRelativePaths: true }).add(patterns); const rootPrefix = rootPath.endsWith(path.sep) ? rootPath : `${rootPath}${path.sep}`; @@ -58,10 +64,12 @@ const createIgnoreMatcher = async ({ cwd, ignorePaths = [], }: CreateIgnoreMatcherOptions): Promise => { - const configMatcher = createPatternMatcher( - config.rootPath, - [...defaultIgnorePatterns, ...config.ignorePatterns].join('\n'), - ); + const configMatcher = config.ignorePatterns.length + ? createPatternMatcher( + config.rootPath, + [...defaultIgnoreNames, ...config.ignorePatterns].join('\n'), + ) + : createDefaultIgnoreMatcher(); if (ignorePaths.length === 0) { return configMatcher; } diff --git a/packages/rstack/tests/fmt/ignore.test.ts b/packages/rstack/tests/fmt/ignore.test.ts index 5826767a..1d033618 100644 --- a/packages/rstack/tests/fmt/ignore.test.ts +++ b/packages/rstack/tests/fmt/ignore.test.ts @@ -64,6 +64,9 @@ test('ignores common lock files by default and allows explicit negation', async expect(isIgnored(path.join(rootPath, 'package-lock.json'))).toBe(true); expect(isIgnored(path.join(rootPath, 'packages/app/pnpm-lock.yaml'))).toBe(true); + expect(isIgnored(path.join(rootPath, 'packages/app/PNPM-LOCK.YAML'))).toBe(false); + expect(isIgnored(path.join(rootPath, '../shared/pnpm-lock.yaml'))).toBe(true); + expect(isIgnored(path.join(rootPath, 'pnpm-lock.yaml.backup'))).toBe(false); expect(isIgnoredAfterReinclude(path.join(rootPath, 'pnpm-lock.yaml'))).toBe(false); }); From 80114979771d72ba95efca947d31e134c7a419a9 Mon Sep 17 00:00:00 2001 From: Jiahan Chen Date: Wed, 5 Aug 2026 13:37:58 +0800 Subject: [PATCH 21/21] release: v0.3.3 (#191) --- packages/rstack/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rstack/package.json b/packages/rstack/package.json index 1c542aa8..ba0b3322 100644 --- a/packages/rstack/package.json +++ b/packages/rstack/package.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.3.2", + "version": "0.3.3", "repository": "https://github.com/rstackjs/rstack-cli", "license": "MIT", "type": "module",