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/.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/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..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", @@ -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/cli/args.ts b/packages/rstack/src/cli/args.ts index a7b3eb09..cf55aa88 100644 --- a/packages/rstack/src/cli/args.ts +++ b/packages/rstack/src/cli/args.ts @@ -1,4 +1,82 @@ -import { parseArgs } from 'node:util'; +import { + parseArgs as nodeParseArgs, + 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; + +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 06fda1d1..cff04bbc 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'; @@ -11,6 +11,9 @@ import type { FmtMode, FmtRunResult, ResolvedFmtConfig } from './types.ts'; interface ParsedFmtCLIArgs { mode: FmtMode; patterns: string[]; + ignorePaths: string[]; + ignoreUnknown: boolean; + noErrorOnUnmatchedPattern: boolean; maxWorkers?: number; help: boolean; /** Path the stdin content is formatted as; it need not exist on disk. */ @@ -25,18 +28,17 @@ ${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 - --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, - camelValue: string | undefined, -): number | undefined => { - const value = kebabValue ?? camelValue; + --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) + --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 + -h, --help Display this help message`; + +const parseMaxWorkers = (value: string | undefined): number | undefined => { if (value === undefined) { return undefined; } @@ -56,26 +58,33 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { write: { type: 'boolean' }, check: { type: 'boolean' }, 'list-different': { type: 'boolean' }, - listDifferent: { type: 'boolean' }, + 'ignore-path': { type: 'string', multiple: true }, + 'ignore-unknown': { type: 'boolean' }, + 'no-error-on-unmatched-pattern': { 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 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 ignoreUnknown = values.ignoreUnknown ?? false; + 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) { @@ -92,8 +101,11 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => { return { mode, patterns: positionals, + ignorePaths, + ignoreUnknown, + noErrorOnUnmatchedPattern, maxWorkers, - help: values.help ?? false, + help, stdinFilepath, }; }; @@ -134,11 +146,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; @@ -164,12 +184,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; } @@ -180,15 +200,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.`, ); } }; @@ -210,7 +230,16 @@ 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, + ignoreUnknown, + maxWorkers, + mode, + noErrorOnUnmatchedPattern, + patterns, + stdinFilepath, + } = parseFmtCLIArgs(args); if (help) { logger.log(fmtHelpMessage); return; @@ -221,17 +250,31 @@ const runFmtCLI = async (args: string[]): Promise => { /* rspackChunkName: 'fmtStdin' */ './stdin.ts' ); - await runFmtStdin({ filepath: stdinFilepath, cwd, loadConfig: () => loadFmtConfig(cwd) }); + await runFmtStdin({ + filepath: stdinFilepath, + cwd, + ignorePaths, + ignoreUnknown, + 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') { - logger.info('No files matched.'); + // Staged tasks may pass only paths excluded by formatter ignore rules. + const allowUnmatched = noErrorOnUnmatchedPattern || process.env.RSTACK_STAGED === '1'; + if (allowUnmatched) { + return; } + reportNoSupportedFiles(patterns); return; } @@ -245,8 +288,19 @@ const runFmtCLI = async (args: string[]): Promise => { maxWorkers, }); + if (result.processedFileCount === 0) { + if (ignoreUnknown) { + if (mode === 'check') { + logger.success('No supported files to check.'); + } + return; + } + 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/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 9a90a287..47dc1572 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 => ({ @@ -8,19 +8,24 @@ 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 = await discoverFmtPaths({ cwd, patterns }); + const isIgnored = await createIgnoreMatcher({ config, cwd, ignorePaths }); + const candidates = await discoverFmtPaths({ + cwd, + patterns, + isDirectoryIgnored: (directoryPath) => isIgnored(directoryPath, true), + }); 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 +37,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..1405de29 100644 --- a/packages/rstack/src/fmt/ignore.ts +++ b/packages/rstack/src/fmt/ignore.ts @@ -1,5 +1,6 @@ -import { relative } from 'node:path'; -import fastIgnore from 'fast-ignore'; +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import createIgnore from 'ignore'; import type { ResolvedFmtConfig } from './types.ts'; /** @@ -8,13 +9,78 @@ 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']; -/** 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, isDirectory?: boolean) => boolean; - return (filePath) => matches(relative(config.rootPath, filePath)); +interface CreateIgnoreMatcherOptions { + config: ResolvedFmtConfig; + /** Base directory for relative ignore paths. */ + cwd: string; + 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}`; + + return (filePath, isDirectory = false) => { + const relativePath = filePath.startsWith(rootPrefix) + ? filePath.slice(rootPrefix.length) + : 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 => { + 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 = config.ignorePatterns.length + ? createPatternMatcher( + config.rootPath, + [...defaultIgnoreNames, ...config.ignorePatterns].join('\n'), + ) + : createDefaultIgnoreMatcher(); + if (ignorePaths.length === 0) { + return configMatcher; + } + + const ignoreMatchers = await Promise.all( + ignorePaths.map((ignorePath) => loadIgnoreMatcher(cwd, ignorePath)), + ); + + return (filePath, isDirectory = false) => + configMatcher(filePath, isDirectory) || + ignoreMatchers.some((matches) => matches(filePath, isDirectory)); }; -export { createFmtIgnoreMatcher }; +export { createIgnoreMatcher }; 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/stdin.ts b/packages/rstack/src/fmt/stdin.ts index dc946dc3..ec69200d 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,10 @@ interface RunFmtStdinOptions { filepath: string; /** Absolute directory used to resolve the path. */ 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; } @@ -45,7 +49,13 @@ 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, + ignoreUnknown, + 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 +64,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,12 +84,18 @@ 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); if (result.status === 'unsupported') { + if (ignoreUnknown) { + return; + } throw new Error(`No parser could be inferred for "${filepath}".`); } diff --git a/packages/rstack/src/fmt/types.ts b/packages/rstack/src/fmt/types.ts index 451f65d8..db9b2d66 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; } @@ -94,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/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..7aee8839 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' }, @@ -71,15 +70,18 @@ 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['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({}); +}); diff --git a/packages/rstack/tests/cli/fmt/index.test.ts b/packages/rstack/tests/cli/fmt/index.test.ts index bcfbba51..33ba3ca7 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', () => { + 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', @@ -402,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', @@ -469,6 +496,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); @@ -486,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 = ;'); @@ -536,16 +585,87 @@ 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(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 '); + } +}); + +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(''); + } +}); + +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'); - expect(writeResult.status).toBe(0); - expect(writeResult.stdout).toBe('info No files matched.\n'); - expect(writeResult.stderr).toBe(''); + for (const modeArgs of [[], ['--check'], ['--list-different']]) { + const result = runFmt([...modeArgs, 'notes.unknown']); - const checkResult = runFmt(['--check', 'missing/**/*.ts']); + 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('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(''); + } +}); - expect(checkResult.status).toBe(0); - expect(checkResult.stdout).toBe('info No files matched.\n'); - expect(checkResult.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'); + + 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/cli/staged/fmt.test.ts b/packages/rstack/tests/cli/staged/fmt.test.ts index e9c48296..fa156f59 100644 --- a/packages/rstack/tests/cli/staged/fmt.test.ts +++ b/packages/rstack/tests/cli/staged/fmt.test.ts @@ -81,6 +81,48 @@ 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('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/cli/staged/index.test.ts b/packages/rstack/tests/cli/staged/index.test.ts index 93376ccd..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', @@ -89,11 +105,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/__snapshots__/cli.test.ts.snap b/packages/rstack/tests/fmt/__snapshots__/cli.test.ts.snap new file mode 100644 index 00000000..62ae1697 --- /dev/null +++ b/packages/rstack/tests/fmt/__snapshots__/cli.test.ts.snap @@ -0,0 +1,19 @@ +// 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) + --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 + -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 e9639fbc..cf5e1908 100644 --- a/packages/rstack/tests/fmt/cli.test.ts +++ b/packages/rstack/tests/fmt/cli.test.ts @@ -21,6 +21,9 @@ test('uses write mode by default', () => { expect(parseFmtCLIArgs([])).toEqual({ mode: 'write', patterns: [], + ignorePaths: [], + ignoreUnknown: false, + noErrorOnUnmatchedPattern: false, maxWorkers: undefined, help: false, }); @@ -30,27 +33,29 @@ 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, patterns: [], + ignorePaths: [], + ignoreUnknown: false, + noErrorOnUnmatchedPattern: false, maxWorkers: undefined, help: false, }); }); -test.each(['--parallel-workers', '--parallelWorkers'])( - 'configures parallel worker count with %s', - (option) => { - expect(parseFmtCLIArgs([option, '3'])).toEqual({ - mode: 'write', - patterns: [], - maxWorkers: 3, - help: false, - }); - }, -); +test('configures parallel worker count', () => { + expect(parseFmtCLIArgs(['--parallel-workers', '3'])).toEqual({ + mode: 'write', + patterns: [], + ignorePaths: [], + ignoreUnknown: false, + noErrorOnUnmatchedPattern: false, + maxWorkers: 3, + help: false, + }); +}); test.each(['0', '-1', '1.5', 'invalid', '9007199254740992'])( 'rejects invalid parallel worker count %s', @@ -61,16 +66,15 @@ 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/**']; expect(parseFmtCLIArgs([patterns[0], '--check', ...patterns.slice(1)])).toEqual({ mode: 'check', patterns, + ignorePaths: [], + ignoreUnknown: false, + noErrorOnUnmatchedPattern: false, maxWorkers: undefined, help: false, }); @@ -80,6 +84,9 @@ test('treats arguments after the terminator as paths', () => { expect(parseFmtCLIArgs(['--check', '--', '--write', '--help'])).toEqual({ mode: 'check', patterns: ['--write', '--help'], + ignorePaths: [], + ignoreUnknown: false, + noErrorOnUnmatchedPattern: false, maxWorkers: undefined, help: false, }); @@ -89,10 +96,28 @@ test.each(['--help', '-h'])('parses %s', (option) => { expect(parseFmtCLIArgs([option]).help).toBe(true); }); -test.each(['--stdin-filepath', '--stdinFilepath'])('parses %s', (option) => { - expect(parseFmtCLIArgs([option, 'src/index.ts'])).toEqual({ +test('collects repeated ignore paths', () => { + expect( + parseFmtCLIArgs(['--ignore-path', '.prettierignore', '--ignore-path=config/format.ignore']) + .ignorePaths, + ).toEqual(['.prettierignore', 'config/format.ignore']); +}); + +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, stdinFilepath: 'src/index.ts', @@ -103,13 +128,16 @@ test('accepts a worker count with --stdin-filepath', () => { expect(parseFmtCLIArgs(['--stdin-filepath', 'index.ts', '--parallel-workers', '2'])).toEqual({ mode: 'write', patterns: [], + ignorePaths: [], + ignoreUnknown: false, + noErrorOnUnmatchedPattern: false, maxWorkers: 2, help: false, stdinFilepath: 'index.ts', }); }); -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( @@ -125,19 +153,15 @@ 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('--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([ ['--write', '--check'], ['--write', '--list-different'], - ['--write', '--listDifferent'], ['--check', '--list-different'], ['--write', '--check', '--list-different'], ])('rejects conflicting modes: %s', (...args) => { @@ -146,7 +170,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(); 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 63501dd9..1d033618 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,27 @@ 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('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('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']); + const isIgnoredAfterReinclude = await createMatcher(['dist', '!dist']); const filePath = path.join(rootPath, 'src/keep.js'); expect(isIgnored(filePath)).toBe(false); @@ -31,31 +58,63 @@ 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(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); }); -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/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); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c3c6232..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 @@ -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 @@ -95,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 @@ -168,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 @@ -256,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 @@ -299,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 @@ -329,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) @@ -360,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) @@ -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 @@ -384,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 @@ -405,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) @@ -420,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 @@ -438,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) @@ -531,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==} @@ -643,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: @@ -745,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==} + '@rspack/binding-linux-arm64-musl@2.1.8': + resolution: {integrity: sha512-yUKAxHNGnICtw5RnxFWu4dHtsz/tdt7rbeFcsINNVre9HcrRxf5XP+FbOGL/SMxd9oM9XCo10paU2WckTKwbEA==} cpu: [arm64] os: [linux] libc: [musl] - '@rspack/binding-linux-arm64-musl@2.1.7': - resolution: {integrity: sha512-Mzbxyg0aJ+ITj526Iuz0enEDYY6WxhFIwEKXqwjQh+Vpd5v/+aPzPo83sSQVX/3puBV1sbmviTURbh6N9e1fvA==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rspack/binding-linux-riscv64-gnu@2.1.5': - resolution: {integrity: sha512-isQQDp3fBzPSZpLVFzPqVXIVA4I9b9mKs58TfUgOzDP/1g1582YSV3iFopgFogvEliihXDuuXvM6aAkP+w8Z+Q==} + '@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-gnu@2.1.7': - resolution: {integrity: sha512-mpazwgT/Pse1720mvEJsoXfPkJ+enj0xUqpbe/wL6aedwjGT+9jJNB8HTJXE4XBX0UO7umGqcJMeKA6YsD2CDA==} - 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==} + '@rspack/binding-wasm32-wasi@2.1.8': + resolution: {integrity: sha512-om7GAKWAU3lcSvbCon2m7mzw8v9OTrO2LW2MZ1lGe/uVJJmwGGkl9HVoXFyWFLrN6YVFyx8iP+AkN4owDWB9Cw==} cpu: [wasm32] - '@rspack/binding-wasm32-wasi@2.1.7': - resolution: {integrity: sha512-cDVgvzRdTgxaeM+a5Lx0+7/VAvunvwO0wNtQ3ATQGOtFCW5b7cUzhNPcytH5ZSJTnFWuxinlGwtar5yfcnkdZQ==} - 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.5': - resolution: {integrity: sha512-lF3ZLeeyV0AN3BL0m2jAmNZD5pP9IHsQ8gUXN7mo0g4xsW6nf6hsN7o9CnEHECz5uUZb+EoWsuWzAAmnA9Ip8Q==} - - '@rspack/binding@2.1.7': - resolution: {integrity: sha512-wYqi8TY30hsIzLry503o/Uqu7y9Ec7pEwN5TVmB7Pb3xHrR2eHsQPzdpF/GkCLUjQSgD2Es3CDVV1mr6zO/78g==} + '@rspack/binding@2.1.8': + resolution: {integrity: sha512-tmAyHzDbPiy8V7HvQqtuPsbs6dPgwV0YjzW5XrPRV9gzf+Hdm7pvsZJKE1QKO9WV5RuvGYav98xIX6O+abZxzQ==} - '@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 @@ -986,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'} @@ -1002,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'} @@ -1010,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==} @@ -1073,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==} @@ -1096,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==} @@ -1503,9 +1416,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 +1439,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'} @@ -1647,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 @@ -2265,9 +2172,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==} @@ -2459,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 @@ -2479,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 @@ -2505,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 @@ -2579,39 +2483,23 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.6 optional: true - '@rsbuild/core@2.1.8': + '@rsbuild/core@2.1.10': dependencies: - '@rspack/core': 2.1.5(@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/core@2.1.9': + '@rsbuild/plugin-react@2.1.0(@rsbuild/core@2.1.10)(@rspack/core@2.1.8)': dependencies: - '@rspack/core': 2.1.7(@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)': - 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 @@ -2619,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: @@ -2668,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': + '@rspack/binding-darwin-arm64@2.1.8': optional: true - '@rspack/binding-linux-arm64-musl@2.1.5': + '@rspack/binding-darwin-x64@2.1.8': optional: true - '@rspack/binding-linux-arm64-musl@2.1.7': + '@rspack/binding-linux-arm64-gnu@2.1.8': optional: true - '@rspack/binding-linux-riscv64-gnu@2.1.5': + '@rspack/binding-linux-arm64-musl@2.1.8': optional: true - '@rspack/binding-linux-riscv64-gnu@2.1.7': + '@rspack/binding-linux-riscv64-gnu@2.1.8': optional: true - '@rspack/binding-linux-riscv64-musl@2.1.5': + '@rspack/binding-linux-riscv64-musl@2.1.8': optional: true - '@rspack/binding-linux-riscv64-musl@2.1.7': + '@rspack/binding-linux-x64-gnu@2.1.8': optional: true - '@rspack/binding-linux-x64-gnu@2.1.5': + '@rspack/binding-linux-x64-musl@2.1.8': optional: true - '@rspack/binding-linux-x64-gnu@2.1.7': - optional: true - - '@rspack/binding-linux-x64-musl@2.1.5': - optional: true - - '@rspack/binding-linux-x64-musl@2.1.7': - 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) + '@rspack/binding-win32-arm64-msvc@2.1.8': optional: true - '@rspack/binding-win32-arm64-msvc@2.1.5': + '@rspack/binding-win32-ia32-msvc@2.1.8': optional: true - '@rspack/binding-win32-arm64-msvc@2.1.7': + '@rspack/binding-win32-x64-msvc@2.1.8': optional: true - '@rspack/binding-win32-ia32-msvc@2.1.5': - optional: true - - '@rspack/binding-win32-ia32-msvc@2.1.7': - optional: true - - '@rspack/binding-win32-x64-msvc@2.1.5': - optional: true - - '@rspack/binding-win32-x64-msvc@2.1.7': - optional: true - - '@rspack/binding@2.1.5': + '@rspack/binding@2.1.8': 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': - 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 + '@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/core@2.1.7(@swc/helpers@0.5.23)': - dependencies: - '@rspack/binding': 2.1.7 - 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 @@ -2854,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: @@ -2872,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)': @@ -2886,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 @@ -2899,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': @@ -2921,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 @@ -2936,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': {} @@ -3009,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 @@ -3033,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 @@ -3315,11 +3157,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 +3171,6 @@ snapshots: git-hooks-list@4.2.1: {} - grammex@3.1.13: {} - happy-dom@20.11.1: dependencies: '@types/node': 24.13.3 @@ -3353,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 @@ -3364,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 @@ -3394,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 @@ -3413,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 @@ -3428,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 @@ -3447,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 @@ -3457,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 @@ -3520,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 @@ -3626,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) @@ -3637,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 @@ -3664,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) @@ -3679,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 @@ -4181,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 @@ -4190,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 @@ -4254,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 @@ -4266,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: {} @@ -4402,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: {} @@ -4424,8 +4259,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..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,15 +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' - 'fast-ignore': '2.0.0' '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' diff --git a/website/docs/en/guide/cli/fmt.mdx b/website/docs/en/guide/cli/fmt.mdx index f6a59b18..818140e1 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,124 @@ 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` | The command completed successfully. | +| `1` | One or more files have formatting issues. | +| `2` | The command could not run or encountered an error. | + +### `-h, --help` + +Display usage and option information without formatting files: + +```bash +rs fmt --help +``` + +### `--ignore-path ` + +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/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: + +```bash +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 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. + +> [`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: + +```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 ``, 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 +``` + +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` + +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/en/guide/formatting.mdx b/website/docs/en/guide/formatting.mdx index 9fdddb51..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). @@ -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). ::: @@ -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: @@ -109,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`**: 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. + +> 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} 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 689a9cef..3c810f3e 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,124 @@ 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` | 命令无法运行或执行过程中遇到错误。 | + +### `-h, --help` + +显示命令用法和选项信息,但不格式化文件: + +```bash +rs fmt --help +``` + +### `--ignore-path ` + +使用 `--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/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` 的汇总信息。需要将结果交给其他命令处理时,可以使用此选项: + +```bash +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`,即使暂存的改动中没有支持的文件。此选项可让命令在这种情况下成功退出,避免阻止提交。 + +> [`rs staged`](./staged) 会为其中的 `rs fmt` 任务自动启用此行为。 + +### `--parallel-workers ` + +将格式化 worker 的最大数量设置为正整数: + +```bash +rs fmt . --parallel-workers 4 +``` + +省略此选项时,`rs fmt` 会根据可用的 CPU 并行度和匹配的文件数量,自动选择最多 8 个 worker。在资源受限的环境中,可以设置较小的值来限制 CPU 或内存用量。 + +### `--stdin-filepath ` + +将 stdin 传入的内容按保存在 `` 的文件进行格式化,例如用于编辑器集成。该路径用于确定 parser 和匹配的[覆盖配置](../formatting#overrides),但不需要在磁盘上真实存在: + +```bash +cat src/index.ts | rs fmt --stdin-filepath src/index.ts +``` + +格式化结果写入 stdout,诊断信息写入 stderr。若输入路径被忽略,`rs fmt` 会跳过格式化并原样输出内容;若无法根据路径推断 parser 或内容解析失败,则输出错误并以状态码 `2` 退出。 + +> `--stdin-filepath` 不能与文件参数或 `--write`、`--check`、`--list-different` 同时使用。 + +### `--write` + +将格式化结果写回文件。这是默认模式,因此可以省略 `--write`: + +```bash +rs fmt src --write +``` + +`--write` 不能与 `--check` 或 `--list-different` 同时使用。 diff --git a/website/docs/zh/guide/formatting.mdx b/website/docs/zh/guide/formatting.mdx index 9f04b823..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)。 @@ -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)。 ::: @@ -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` 排除不需要格式化的文件: @@ -109,6 +99,16 @@ define.fmt({ }); ``` +### 忽略顺序 \{#ignore-order} + +`rs fmt` 会通过以下三个步骤,决定需要格式化哪些路径: + +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` 的排除结果会叠加:只要任一来源忽略某个路径,该路径就会保持排除,即使其他来源尝试重新包含它。 + +> 即使命令行直接指定了某个文件,默认忽略规则、`ignorePatterns` 和 `--ignore-path` 中的规则仍然有效。通过 [`--stdin-filepath`](./cli/fmt#--stdin-filepath-path) 指定的路径也是如此。 + ## 排序 package.json 字段 \{#sort-package-json} 启用 `sortPackageJson` 后,`rs fmt` 会使用 [`sort-package-json`](https://github.com/keithamus/sort-package-json) 对每个待格式化的 `package.json` 中的字段排序: