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
7 changes: 7 additions & 0 deletions packages/rstack/src/fmt/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface ParsedFmtCLIArgs {
ignorePaths: string[];
ignoreUnknown: boolean;
noErrorOnUnmatchedPattern: boolean;
withNodeModules: boolean;
maxWorkers?: number;
help: boolean;
/** Path the stdin content is formatted as; it need not exist on disk. */
Expand All @@ -34,6 +35,7 @@ ${color.cyan('Options')}:
--ignore-path <path> Path to an additional ignore file (repeatable)
-u, --ignore-unknown Ignore unknown files
--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
--stdin-filepath <path> Format stdin as if it were saved at <path>
-h, --help Display this help message`;
Expand Down Expand Up @@ -61,6 +63,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => {
'ignore-path': { type: 'string', multiple: true },
'ignore-unknown': { type: 'boolean', short: 'u' },
'no-error-on-unmatched-pattern': { type: 'boolean' },
'with-node-modules': { type: 'boolean' },
'parallel-workers': { type: 'string' },
'stdin-filepath': { type: 'string' },
help: { type: 'boolean', short: 'h' },
Expand All @@ -81,6 +84,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => {
const ignorePaths = values.ignorePath ?? [];
const ignoreUnknown = values.ignoreUnknown ?? false;
const noErrorOnUnmatchedPattern = values.noErrorOnUnmatchedPattern ?? false;
const withNodeModules = values.withNodeModules ?? false;
const parallelWorkers = values.parallelWorkers;
const maxWorkers = parseMaxWorkers(parallelWorkers);
const help = values.help ?? false;
Expand All @@ -104,6 +108,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => {
ignorePaths,
ignoreUnknown,
noErrorOnUnmatchedPattern,
withNodeModules,
maxWorkers,
help,
stdinFilepath,
Expand Down Expand Up @@ -239,6 +244,7 @@ const runFmtCLI = async (args: string[]): Promise<void> => {
noErrorOnUnmatchedPattern,
patterns,
stdinFilepath,
withNodeModules,
} = parseFmtCLIArgs(args);
if (help) {
logger.log(fmtHelpMessage);
Expand Down Expand Up @@ -266,6 +272,7 @@ const runFmtCLI = async (args: string[]): Promise<void> => {
patterns,
config,
ignorePaths,
withNodeModules,
});

if (files.length === 0) {
Expand Down
39 changes: 31 additions & 8 deletions packages/rstack/src/fmt/discoverPaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import isBinaryPath from 'is-binary-path';
import micromatch from 'micromatch';
import readdir, { type Dirent } from 'tiny-readdir';

const alwaysIgnoredNames = new Set(['.git', '.sl', '.svn', '.hg', '.jj', 'node_modules']);
const defaultIgnoredDirNames = new Set(['.git', '.sl', '.svn', '.hg', '.jj', 'node_modules']);

interface DiscoverFmtPathsOptions {
/** Absolute directory used to resolve input paths. */
cwd: string;
patterns?: string[];
/** Whether files inside node_modules may be discovered. */
withNodeModules?: boolean;
/** Returns whether a scanned directory can be pruned before traversal. */
isDirectoryIgnored?: (directoryPath: string) => boolean;
}
Expand Down Expand Up @@ -47,11 +49,15 @@ const getDirentParentPath = (dirent: Dirent): string =>
const getDirentPath = (dirent: Dirent, parentPath: string): string =>
`${parentPath}${parentPath === path.sep ? '' : path.sep}${dirent.name}`;

const hasAlwaysIgnoredSegment = (cwd: string, filePath: string): boolean =>
const hasBuiltInIgnoredSegment = (
cwd: string,
filePath: string,
ignoredDirNames: ReadonlySet<string>,
): boolean =>
path
.relative(cwd, filePath)
.split(path.sep)
.some((segment) => alwaysIgnoredNames.has(segment));
.some((segment) => ignoredDirNames.has(segment));

const findGitRoot = async (cwd: string): Promise<string> => {
let directoryPath = cwd;
Expand Down Expand Up @@ -202,6 +208,7 @@ class GitIgnoreMatcher {

const createTraversalOptions = (
gitIgnore: GitIgnoreMatcher,
ignoredDirNames: ReadonlySet<string>,
isIncluded?: (filePath: string) => boolean,
isDirectoryIgnored?: (directoryPath: string) => boolean,
) => {
Expand All @@ -212,7 +219,7 @@ const createTraversalOptions = (
followSymlinks: false,
ignore: (targetPath: string) => {
const isDirectory = directories.delete(targetPath);
if (alwaysIgnoredNames.has(path.basename(targetPath))) {
if (ignoredDirNames.has(path.basename(targetPath))) {
return true;
}

Expand Down Expand Up @@ -265,15 +272,19 @@ type ClassifiedPatterns = {
negativeGlobs: string[];
};

const classifyPatterns = async (cwd: string, patterns: string[]): Promise<ClassifiedPatterns> => {
const classifyPatterns = async (
cwd: string,
patterns: string[],
ignoredDirNames: ReadonlySet<string>,
): Promise<ClassifiedPatterns> => {
const entries = await Promise.all(
patterns.map(async (pattern): Promise<PatternEntry | undefined> => {
if (pattern.startsWith('!')) {
return { kind: 'negative-glob', value: normalizeGlob(cwd, pattern.slice(1)) };
}

const filePath = path.resolve(cwd, pattern);
if (hasAlwaysIgnoredSegment(cwd, filePath)) {
if (hasBuiltInIgnoredSegment(cwd, filePath, ignoredDirNames)) {
return;
}

Expand Down Expand Up @@ -346,15 +357,24 @@ const getTraversalRoots = (cwd: string, directories: string[], globs: string[]):
const discoverFmtPaths = async ({
cwd,
patterns: inputPatterns,
withNodeModules = false,
isDirectoryIgnored,
}: DiscoverFmtPathsOptions): Promise<string[]> => {
const patterns = inputPatterns?.length ? inputPatterns : ['.'];
const ignoredDirNames = withNodeModules
? new Set(defaultIgnoredDirNames)
: defaultIgnoredDirNames;

if (withNodeModules) {
ignoredDirNames.delete('node_modules');
}

const {
files: explicitFiles,
directories,
globs,
negativeGlobs,
} = await classifyPatterns(cwd, patterns);
} = await classifyPatterns(cwd, patterns, ignoredDirNames);
const directoryRoots = getOutermostPaths(directories);
const globMatchers = globs.map((pattern) => micromatch.matcher(pattern, { dot: true }));
const candidates = new Set(explicitFiles);
Expand Down Expand Up @@ -389,7 +409,10 @@ const discoverFmtPaths = async ({
};

return (
await readdir(rootPath, createTraversalOptions(gitIgnore, isIncluded, isDirectoryIgnored))
await readdir(
rootPath,
createTraversalOptions(gitIgnore, ignoredDirNames, isIncluded, isDirectoryIgnored),
)
).files;
}),
);
Expand Down
2 changes: 2 additions & 0 deletions packages/rstack/src/fmt/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ const discoverFmtFiles = async ({
cwd,
patterns,
ignorePaths,
withNodeModules,
config,
}: DiscoverFmtFilesOptions): Promise<FmtFileRequest[]> => {
const isIgnored = await createIgnoreMatcher({ config, cwd, ignorePaths });
const candidates = await discoverFmtPaths({
cwd,
patterns,
withNodeModules,
isDirectoryIgnored: (directoryPath) => isIgnored(directoryPath, true),
});
if (candidates.length === 0) {
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 @@ -58,6 +58,8 @@ interface DiscoverFmtFilesOptions {
patterns?: string[];
/** Ignore files resolved from `cwd`; each file's patterns are relative to its own directory. */
ignorePaths?: string[];
/** Whether files inside node_modules may be discovered. */
withNodeModules?: boolean;
/** Resolved project config applied to discovered files. */
config: ResolvedFmtConfig;
}
Expand Down
15 changes: 15 additions & 0 deletions packages/rstack/tests/cli/fmt/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,21 @@ test('formats the current directory with Prettier defaults', () => {
expect(readProjectFile('index.ts')).toBe('const message = "hello";\n');
});

test('formats files in node_modules with --with-node-modules', () => {
const source = 'const message="hello"';
writeProjectFile('node_modules/example/index.ts', source);

const skipped = runFmt(['node_modules/example']);
expect(skipped.status).toBe(2);
expect(readProjectFile('node_modules/example/index.ts')).toBe(source);

const result = runFmt(['--with-node-modules', 'node_modules/example']);
expect(result.status).toBe(0);
expectWriteSummary(result.stdout, 1, 1);
expect(result.stderr).toBe('');
expect(readProjectFile('node_modules/example/index.ts')).toBe('const message = "hello";\n');
});

test('summarizes write mode when no files change', () => {
writeProjectFile('index.ts', 'const message = "hello";\n');

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-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
--stdin-filepath <path> Format stdin as if it were saved at <path>
-h, --help Display this help message"
Expand Down
11 changes: 11 additions & 0 deletions packages/rstack/tests/fmt/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ test('uses write mode by default', () => {
ignorePaths: [],
ignoreUnknown: false,
noErrorOnUnmatchedPattern: false,
withNodeModules: false,
maxWorkers: undefined,
help: false,
});
Expand All @@ -40,6 +41,7 @@ test.each([
ignorePaths: [],
ignoreUnknown: false,
noErrorOnUnmatchedPattern: false,
withNodeModules: false,
maxWorkers: undefined,
help: false,
});
Expand All @@ -52,6 +54,7 @@ test('configures parallel worker count', () => {
ignorePaths: [],
ignoreUnknown: false,
noErrorOnUnmatchedPattern: false,
withNodeModules: false,
maxWorkers: 3,
help: false,
});
Expand All @@ -75,6 +78,7 @@ test('preserves file paths and globs', () => {
ignorePaths: [],
ignoreUnknown: false,
noErrorOnUnmatchedPattern: false,
withNodeModules: false,
maxWorkers: undefined,
help: false,
});
Expand All @@ -87,6 +91,7 @@ test('treats arguments after the terminator as paths', () => {
ignorePaths: [],
ignoreUnknown: false,
noErrorOnUnmatchedPattern: false,
withNodeModules: false,
maxWorkers: undefined,
help: false,
});
Expand All @@ -111,13 +116,18 @@ test.each(['-u', '--ignore-unknown', '--ignoreUnknown'])('parses %s', (option) =
expect(parseFmtCLIArgs([option]).ignoreUnknown).toBe(true);
});

test('parses --with-node-modules', () => {
expect(parseFmtCLIArgs(['--with-node-modules']).withNodeModules).toBe(true);
});

test('parses --stdin-filepath', () => {
expect(parseFmtCLIArgs(['--stdin-filepath', 'src/index.ts'])).toEqual({
mode: 'write',
patterns: [],
ignorePaths: [],
ignoreUnknown: false,
noErrorOnUnmatchedPattern: false,
withNodeModules: false,
maxWorkers: undefined,
help: false,
stdinFilepath: 'src/index.ts',
Expand All @@ -131,6 +141,7 @@ test('accepts a worker count with --stdin-filepath', () => {
ignorePaths: [],
ignoreUnknown: false,
noErrorOnUnmatchedPattern: false,
withNodeModules: false,
maxWorkers: 2,
help: false,
stdinFilepath: 'index.ts',
Expand Down
27 changes: 27 additions & 0 deletions packages/rstack/tests/fmt/discoverPaths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,43 @@ test('discovers non-binary files in stable order and skips hard-ignored paths',
writeProjectFile(rootPath, '.jj/internal.js');

const files = await discoverFmtPaths({ cwd: rootPath });
const filesWithNodeModules = await discoverFmtPaths({ cwd: rootPath, withNodeModules: true });

expect(relativePaths(rootPath, files)).toEqual([
'a.js',
'b.ts',
path.join('folder with spaces', 'c.ts'),
'unknown.extension',
]);
expect(relativePaths(rootPath, filesWithNodeModules)).toEqual([
'a.js',
'b.ts',
path.join('folder with spaces', 'c.ts'),
path.join('node_modules', 'package', 'index.js'),
'unknown.extension',
]);
await expect(
discoverFmtPaths({ cwd: rootPath, patterns: ['node_modules/package/index.js'] }),
).resolves.toEqual([]);
await expect(
discoverFmtPaths({
cwd: rootPath,
patterns: ['node_modules/package/index.js'],
withNodeModules: true,
}),
).resolves.toEqual([path.join(rootPath, 'node_modules/package/index.js')]);
});
});

test('keeps node_modules excluded by gitignore when built-in exclusion is disabled', async () => {
await withTempProject(async (rootPath) => {
writeProjectFile(rootPath, '.gitignore', 'node_modules/\n');
writeProjectFile(rootPath, 'node_modules/package/index.js');
writeProjectFile(rootPath, 'index.js');

const files = await discoverFmtPaths({ cwd: rootPath, withNodeModules: true });

expect(relativePaths(rootPath, files)).toEqual(['.gitignore', 'index.js']);
});
});

Expand Down
10 changes: 10 additions & 0 deletions website/docs/en/guide/cli/fmt.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,16 @@ Formatted output is written to stdout and diagnostics to stderr. If the input pa

> `--stdin-filepath` cannot be combined with file arguments or with `--write`, `--check`, or `--list-different`.

### `--with-node-modules`

Process files inside `node_modules`, which `rs fmt` excludes by default:

```bash
rs fmt --with-node-modules node_modules/example/index.js
```

This option only disables the built-in `node_modules` exclusion. Directory and glob scans still follow `.gitignore`, while `ignorePatterns` and `--ignore-path` continue to apply to every input.

### `--write`

Write formatted files in place. This is the default mode, so specifying `--write` is optional:
Expand Down
10 changes: 10 additions & 0 deletions website/docs/zh/guide/cli/fmt.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,16 @@ cat src/index.ts | rs fmt --stdin-filepath src/index.ts

> `--stdin-filepath` 不能与文件参数或 `--write`、`--check`、`--list-different` 同时使用。

### `--with-node-modules`

处理 `node_modules` 中的文件。默认情况下,`rs fmt` 会排除这些文件:

```bash
rs fmt --with-node-modules node_modules/example/index.js
```

此选项只会关闭内置的 `node_modules` 排除规则。目录和 glob 扫描仍然遵循 `.gitignore`,`ignorePatterns` 和 `--ignore-path` 也会继续作用于所有输入。

### `--write`

将格式化结果写回文件。这是默认模式,因此可以省略 `--write`:
Expand Down