Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion packages/rstack/src/fmt/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { FmtMode, FmtRunResult, ResolvedFmtConfig } from './types.ts';

interface ParsedFmtCLIArgs {
cache: boolean;
cacheLocation?: string;
mode: FmtMode;
patterns: string[];
ignorePaths: string[];
Expand All @@ -39,6 +40,7 @@ ${color.cyan('Options')}:
--ignore-path <path> Path to an additional ignore file (repeatable)
-u, --ignore-unknown Ignore unknown files
--no-cache Disable the formatting cache
--cache-location <path> Path to the formatting cache directory
--no-error-on-unmatched-pattern Do not error when no files match
--with-node-modules Process files inside node_modules
--parallel-workers <count> Number of parallel workers
Expand Down Expand Up @@ -68,6 +70,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => {
'ignore-path': { type: 'string', multiple: true },
'ignore-unknown': { type: 'boolean', short: 'u' },
'no-cache': { type: 'boolean' },
'cache-location': { type: 'string' },
'no-error-on-unmatched-pattern': { type: 'boolean' },
'with-node-modules': { type: 'boolean' },
'parallel-workers': { type: 'string' },
Expand All @@ -88,6 +91,11 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => {

const mode = check ? 'check' : listDifferent ? 'list-different' : 'write';
const cache = !(values.noCache ?? false);
const cacheLocation = cache ? values.cacheLocation : undefined;
if (cacheLocation === '') {
throw new Error('The --cache-location option requires a path.');
}

const ignorePaths = values.ignorePath ?? [];
const ignoreUnknown = values.ignoreUnknown ?? false;
const noErrorOnUnmatchedPattern = values.noErrorOnUnmatchedPattern ?? false;
Expand All @@ -111,6 +119,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => {

return {
cache,
cacheLocation,
mode,
patterns: positionals,
ignorePaths,
Expand Down Expand Up @@ -247,6 +256,7 @@ const runFmtCLI = async (args: string[]): Promise<void> => {
try {
const {
cache,
cacheLocation,
help,
ignorePaths,
ignoreUnknown,
Expand Down Expand Up @@ -277,11 +287,13 @@ const runFmtCLI = async (args: string[]): Promise<void> => {
return;
}

const cacheDirPath = cacheLocation ? path.resolve(cwd, cacheLocation) : undefined;
const config = await loadFmtConfig(cwd);
const files = await discoverFmtFiles({
cwd,
patterns,
config,
excludedDirPath: cacheDirPath,
ignorePaths,
withNodeModules,
});
Expand All @@ -297,7 +309,12 @@ const runFmtCLI = async (args: string[]): Promise<void> => {
}

let cacheContext;
if (cache) {
if (cacheDirPath) {
cacheContext = {
filePath: path.join(cacheDirPath, fmtCacheFileName),
rootPath: config.rootPath,
};
} else if (cache) {
const cacheDir = await ensureProjectCacheDir(config.rootPath);
if (cacheDir.status === 'available') {
cacheContext = {
Expand Down
16 changes: 14 additions & 2 deletions packages/rstack/src/fmt/discovery.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import path from 'node:path';
import { createFmtOptionsResolver, type FmtOptionsResolver } from './config.ts';
import { discoverFmtPaths } from './discoverPaths.ts';
import { createIgnoreMatcher } from './ignore.ts';
Expand All @@ -11,26 +12,37 @@ const createFileRequest = (
options: resolveOptions(filePath),
});

const createDirMatcher = (dirPath: string): ((filePath: string) => boolean) => {
const prefix = dirPath.endsWith(path.sep) ? dirPath : `${dirPath}${path.sep}`;
return (filePath) => filePath === dirPath || filePath.startsWith(prefix);
Comment thread
chenjiahan marked this conversation as resolved.
};

/** Discovers worker-ready files without automatically reading Prettier config or ignore files. */
const discoverFmtFiles = async ({
cwd,
excludedDirPath,
patterns,
ignorePaths,
withNodeModules,
config,
}: DiscoverFmtFilesOptions): Promise<FmtFileRequest[]> => {
const isIgnored = await createIgnoreMatcher({ config, cwd, ignorePaths });
const isExcluded = excludedDirPath ? createDirMatcher(excludedDirPath) : undefined;
const shouldIgnore = isExcluded
? (filePath: string, isDirectory = false) =>
isExcluded(filePath) || isIgnored(filePath, isDirectory)
: isIgnored;
const candidates = await discoverFmtPaths({
cwd,
patterns,
withNodeModules,
isIgnored,
isIgnored: shouldIgnore,
});
if (candidates.length === 0) {
return [];
}

const filePaths = candidates.filter((filePath) => !isIgnored(filePath));
const filePaths = candidates.filter((filePath) => !shouldIgnore(filePath));
const resolveOptions = createFmtOptionsResolver(config);
const files = filePaths.map((filePath) => createFileRequest(filePath, resolveOptions));
if (!files.some((file) => file.options.plugins?.length)) {
Expand Down
2 changes: 2 additions & 0 deletions packages/rstack/src/fmt/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ interface ResolvedFmtConfig {
interface DiscoverFmtFilesOptions {
/** Absolute directory used to resolve input paths. */
cwd: string;
/** Absolute directory to exclude from formatting. */
excludedDirPath?: 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. */
Expand Down
44 changes: 42 additions & 2 deletions packages/rstack/tests/cli/fmt/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,18 @@ test.each([

test('--no-cache bypasses cache reads and writes', () => {
writeProjectFile('index.ts', 'const value=1');

const first = runFmt(['--no-cache', 'index.ts']);
writeProjectFile('custom-cache/v1.json', '{"value":true}');

const first = runFmt([
'--no-cache',
'--cache-location',
'custom-cache',
'index.ts',
'custom-cache/v1.json',
]);

expect(first.status).toBe(0);
expect(readProjectFile('custom-cache/v1.json')).toBe('{ "value": true }\n');
expect(existsSync(path.join(projectPath, '.rstack'))).toBe(false);

writeProjectFile('.rstack/cache/fmt-v1.json', 'stale');
Expand All @@ -203,6 +211,38 @@ test('--no-cache bypasses cache reads and writes', () => {
expect(existsSync(path.join(projectPath, '.rstack/cache/.gitignore'))).toBe(false);
});

test.each(['relative', 'absolute'] as const)('uses a %s custom cache location', (kind) => {
const cacheDir = path.join(projectPath, 'custom-cache');
const cacheLocation = kind === 'relative' ? path.relative(projectPath, cacheDir) : cacheDir;
const cachePath = path.join(cacheDir, 'v1.json');
writeProjectFile('index.ts', 'const value = 1;\n');

const result = runFmt(['--cache-location', cacheLocation, 'index.ts']);

expect(result.status).toBe(0);
expect(JSON.parse(readFileSync(cachePath, 'utf8'))).toMatchObject({
version: 1,
files: {
'index.ts': [expect.any(String), expect.any(String), 'clean'],
},
});
expect(existsSync(path.join(projectPath, 'custom-cache/.gitignore'))).toBe(false);
expect(existsSync(path.join(projectPath, '.rstack'))).toBe(false);
});

test('excludes the custom cache directory from formatting', () => {
const cacheLocation = 'custom-cache';
writeProjectFile('index.ts', 'const value = 1;\n');
writeProjectFile('custom-cache/nested/ignored.ts', 'const value=2');
expect(runFmt(['--cache-location', cacheLocation, 'index.ts']).status).toBe(0);

const result = runFmt(['--cache-location', cacheLocation, '.']);

expect(result.status).toBe(0);
expectWriteSummary(result.stdout, 2, 0);
expect(readProjectFile('custom-cache/nested/ignored.ts')).toBe('const value=2');
});

test('uses an explicit config root cache from a subdirectory', () => {
const appPath = path.join(projectPath, 'packages/app');
writeProjectFile('packages/app/index.ts', 'const value=1');
Expand Down
1 change: 1 addition & 0 deletions packages/rstack/tests/fmt/__snapshots__/cli.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Options:
--ignore-path <path> Path to an additional ignore file (repeatable)
-u, --ignore-unknown Ignore unknown files
--no-cache Disable the formatting cache
--cache-location <path> Path to the formatting cache directory
--no-error-on-unmatched-pattern Do not error when no files match
--with-node-modules Process files inside node_modules
--parallel-workers <count> Number of parallel workers
Expand Down
17 changes: 17 additions & 0 deletions packages/rstack/tests/fmt/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,23 @@ test('parses --no-cache', () => {
expect(parseFmtCLIArgs(['--no-cache']).cache).toBe(false);
});

test('parses --cache-location', () => {
expect(parseFmtCLIArgs(['--cache-location', '.cache/fmt']).cacheLocation).toBe('.cache/fmt');
});

test('--no-cache ignores --cache-location', () => {
expect(parseFmtCLIArgs(['--no-cache', '--cache-location='])).toMatchObject({
cache: false,
cacheLocation: undefined,
});
});

test('rejects an empty cache location', () => {
expect(() => parseFmtCLIArgs(['--cache-location='])).toThrow(
'The --cache-location option requires a path.',
);
});

test('parses --with-node-modules', () => {
expect(parseFmtCLIArgs(['--with-node-modules']).withNodeModules).toBe(true);
});
Expand Down
24 changes: 24 additions & 0 deletions packages/rstack/tests/fmt/discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,30 @@ test('excludes .rstack from discovery', async () => {
});
});

test('excludes a custom cache directory', async () => {
await withTempProject(async (rootPath) => {
const cacheDir = path.join(rootPath, 'custom-cache');
const cacheFile = writeProjectFile(rootPath, 'custom-cache/v1.json', '{}');
writeProjectFile(rootPath, 'custom-cache/nested/ignored.ts');
writeProjectFile(rootPath, 'index.ts');

const discoveredFiles = await discoverFmtFiles({
cwd: rootPath,
excludedDirPath: cacheDir,
config: normalizeFmtConfig(undefined, rootPath),
});
const explicitFile = await discoverFmtFiles({
cwd: rootPath,
excludedDirPath: cacheDir,
patterns: [cacheFile],
config: normalizeFmtConfig(undefined, rootPath),
});

expect(relativePaths(rootPath, discoveredFiles)).toEqual(['index.ts']);
expect(explicitFile).toEqual([]);
});
});

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');
Expand Down
12 changes: 12 additions & 0 deletions website/docs/en/guide/cli/fmt.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,18 @@ Without this option, `rs fmt` stores cache data in `.rstack/cache/fmt` under the

See [Cache](../formatting#cache) for cache behavior and cleanup guidance.

### `--cache-location <path>`

Store the persistent cache in a custom directory:

```bash
rs fmt --cache-location .cache/rs-fmt
```

Relative paths are resolved from the current working directory, while absolute paths are used as-is. The directory is created as needed and excluded from file discovery. Unlike the default cache location, a custom directory does not receive an automatic `.gitignore`; exclude it from version control or manage it through your CI cache configuration.

When both options are provided, `--no-cache` takes precedence and the custom directory is not excluded from file discovery.

### `--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:
Expand Down
2 changes: 2 additions & 0 deletions website/docs/en/guide/formatting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ define.fmt({

The default cache directory is `.rstack/cache/fmt` under the Rstack configuration root. When a command runs from a subdirectory, it continues to use the cache next to the resolved `rstack.config.*` file. Stdin formatting does not use this cache.

Use [`--cache-location <path>`](./cli/fmt#--cache-location-path) to store the cache in a different directory. Relative paths are resolved from the current working directory. Custom directories are excluded from file discovery but are not automatically ignored by Git.

Use [`--no-cache`](./cli/fmt#--no-cache) to run without reading, creating, or updating the cache:

```bash
Expand Down
12 changes: 12 additions & 0 deletions website/docs/zh/guide/cli/fmt.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,18 @@ rs fmt --no-cache

缓存行为和清理方式请参考[缓存](../formatting#cache)。

### `--cache-location <path>`

将持久化缓存保存到自定义目录:

```bash
rs fmt --cache-location .cache/rs-fmt
```

相对路径基于当前工作目录解析,绝对路径则原样使用。目录会在需要时自动创建,并从文件发现中排除。与默认缓存位置不同,自定义目录不会自动生成 `.gitignore`;请将其排除在版本控制之外,或通过 CI 缓存配置进行管理。

同时使用两个选项时,优先使用 `--no-cache`,且不会从文件发现中排除自定义目录。

### `--no-error-on-unmatched-pattern`

如果传入的路径或 glob 没有匹配任何文件(包括所有匹配文件均被忽略的情况),则不输出诊断信息并成功退出:
Expand Down
2 changes: 2 additions & 0 deletions website/docs/zh/guide/formatting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ define.fmt({

默认缓存目录位于 Rstack 配置根目录下的 `.rstack/cache/fmt`。从子目录运行命令时,仍会使用解析到的 `rstack.config.*` 文件旁的缓存。stdin 格式化不会使用该缓存。

使用 [`--cache-location <path>`](./cli/fmt#--cache-location-path) 可以将缓存保存到其他目录。相对路径基于当前工作目录解析。自定义目录会从文件发现中排除,但不会被 Git 自动忽略。

使用 [`--no-cache`](./cli/fmt#--no-cache) 可以在运行时跳过缓存读取、创建和更新:

```bash
Expand Down