Skip to content

Commit 9e57744

Browse files
authored
feat(fmt): add --with-node-modules (#197)
1 parent 9b9c1b3 commit 9e57744

10 files changed

Lines changed: 116 additions & 8 deletions

File tree

‎packages/rstack/src/fmt/cli.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface ParsedFmtCLIArgs {
1414
ignorePaths: string[];
1515
ignoreUnknown: boolean;
1616
noErrorOnUnmatchedPattern: boolean;
17+
withNodeModules: boolean;
1718
maxWorkers?: number;
1819
help: boolean;
1920
/** Path the stdin content is formatted as; it need not exist on disk. */
@@ -34,6 +35,7 @@ ${color.cyan('Options')}:
3435
--ignore-path <path> Path to an additional ignore file (repeatable)
3536
-u, --ignore-unknown Ignore unknown files
3637
--no-error-on-unmatched-pattern Do not error when no files match
38+
--with-node-modules Process files inside node_modules
3739
--parallel-workers <count> Number of parallel workers
3840
--stdin-filepath <path> Format stdin as if it were saved at <path>
3941
-h, --help Display this help message`;
@@ -61,6 +63,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => {
6163
'ignore-path': { type: 'string', multiple: true },
6264
'ignore-unknown': { type: 'boolean', short: 'u' },
6365
'no-error-on-unmatched-pattern': { type: 'boolean' },
66+
'with-node-modules': { type: 'boolean' },
6467
'parallel-workers': { type: 'string' },
6568
'stdin-filepath': { type: 'string' },
6669
help: { type: 'boolean', short: 'h' },
@@ -81,6 +84,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => {
8184
const ignorePaths = values.ignorePath ?? [];
8285
const ignoreUnknown = values.ignoreUnknown ?? false;
8386
const noErrorOnUnmatchedPattern = values.noErrorOnUnmatchedPattern ?? false;
87+
const withNodeModules = values.withNodeModules ?? false;
8488
const parallelWorkers = values.parallelWorkers;
8589
const maxWorkers = parseMaxWorkers(parallelWorkers);
8690
const help = values.help ?? false;
@@ -104,6 +108,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => {
104108
ignorePaths,
105109
ignoreUnknown,
106110
noErrorOnUnmatchedPattern,
111+
withNodeModules,
107112
maxWorkers,
108113
help,
109114
stdinFilepath,
@@ -239,6 +244,7 @@ const runFmtCLI = async (args: string[]): Promise<void> => {
239244
noErrorOnUnmatchedPattern,
240245
patterns,
241246
stdinFilepath,
247+
withNodeModules,
242248
} = parseFmtCLIArgs(args);
243249
if (help) {
244250
logger.log(fmtHelpMessage);
@@ -266,6 +272,7 @@ const runFmtCLI = async (args: string[]): Promise<void> => {
266272
patterns,
267273
config,
268274
ignorePaths,
275+
withNodeModules,
269276
});
270277

271278
if (files.length === 0) {

‎packages/rstack/src/fmt/discoverPaths.ts‎

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import isBinaryPath from 'is-binary-path';
55
import micromatch from 'micromatch';
66
import readdir, { type Dirent } from 'tiny-readdir';
77

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

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

50-
const hasAlwaysIgnoredSegment = (cwd: string, filePath: string): boolean =>
52+
const hasBuiltInIgnoredSegment = (
53+
cwd: string,
54+
filePath: string,
55+
ignoredDirNames: ReadonlySet<string>,
56+
): boolean =>
5157
path
5258
.relative(cwd, filePath)
5359
.split(path.sep)
54-
.some((segment) => alwaysIgnoredNames.has(segment));
60+
.some((segment) => ignoredDirNames.has(segment));
5561

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

203209
const createTraversalOptions = (
204210
gitIgnore: GitIgnoreMatcher,
211+
ignoredDirNames: ReadonlySet<string>,
205212
isIncluded?: (filePath: string) => boolean,
206213
isDirectoryIgnored?: (directoryPath: string) => boolean,
207214
) => {
@@ -212,7 +219,7 @@ const createTraversalOptions = (
212219
followSymlinks: false,
213220
ignore: (targetPath: string) => {
214221
const isDirectory = directories.delete(targetPath);
215-
if (alwaysIgnoredNames.has(path.basename(targetPath))) {
222+
if (ignoredDirNames.has(path.basename(targetPath))) {
216223
return true;
217224
}
218225

@@ -265,15 +272,19 @@ type ClassifiedPatterns = {
265272
negativeGlobs: string[];
266273
};
267274

268-
const classifyPatterns = async (cwd: string, patterns: string[]): Promise<ClassifiedPatterns> => {
275+
const classifyPatterns = async (
276+
cwd: string,
277+
patterns: string[],
278+
ignoredDirNames: ReadonlySet<string>,
279+
): Promise<ClassifiedPatterns> => {
269280
const entries = await Promise.all(
270281
patterns.map(async (pattern): Promise<PatternEntry | undefined> => {
271282
if (pattern.startsWith('!')) {
272283
return { kind: 'negative-glob', value: normalizeGlob(cwd, pattern.slice(1)) };
273284
}
274285

275286
const filePath = path.resolve(cwd, pattern);
276-
if (hasAlwaysIgnoredSegment(cwd, filePath)) {
287+
if (hasBuiltInIgnoredSegment(cwd, filePath, ignoredDirNames)) {
277288
return;
278289
}
279290

@@ -346,15 +357,24 @@ const getTraversalRoots = (cwd: string, directories: string[], globs: string[]):
346357
const discoverFmtPaths = async ({
347358
cwd,
348359
patterns: inputPatterns,
360+
withNodeModules = false,
349361
isDirectoryIgnored,
350362
}: DiscoverFmtPathsOptions): Promise<string[]> => {
351363
const patterns = inputPatterns?.length ? inputPatterns : ['.'];
364+
const ignoredDirNames = withNodeModules
365+
? new Set(defaultIgnoredDirNames)
366+
: defaultIgnoredDirNames;
367+
368+
if (withNodeModules) {
369+
ignoredDirNames.delete('node_modules');
370+
}
371+
352372
const {
353373
files: explicitFiles,
354374
directories,
355375
globs,
356376
negativeGlobs,
357-
} = await classifyPatterns(cwd, patterns);
377+
} = await classifyPatterns(cwd, patterns, ignoredDirNames);
358378
const directoryRoots = getOutermostPaths(directories);
359379
const globMatchers = globs.map((pattern) => micromatch.matcher(pattern, { dot: true }));
360380
const candidates = new Set(explicitFiles);
@@ -389,7 +409,10 @@ const discoverFmtPaths = async ({
389409
};
390410

391411
return (
392-
await readdir(rootPath, createTraversalOptions(gitIgnore, isIncluded, isDirectoryIgnored))
412+
await readdir(
413+
rootPath,
414+
createTraversalOptions(gitIgnore, ignoredDirNames, isIncluded, isDirectoryIgnored),
415+
)
393416
).files;
394417
}),
395418
);

‎packages/rstack/src/fmt/discovery.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ const discoverFmtFiles = async ({
1313
cwd,
1414
patterns,
1515
ignorePaths,
16+
withNodeModules,
1617
config,
1718
}: DiscoverFmtFilesOptions): Promise<FmtFileRequest[]> => {
1819
const isIgnored = await createIgnoreMatcher({ config, cwd, ignorePaths });
1920
const candidates = await discoverFmtPaths({
2021
cwd,
2122
patterns,
23+
withNodeModules,
2224
isDirectoryIgnored: (directoryPath) => isIgnored(directoryPath, true),
2325
});
2426
if (candidates.length === 0) {

‎packages/rstack/src/fmt/types.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ interface DiscoverFmtFilesOptions {
5858
patterns?: string[];
5959
/** Ignore files resolved from `cwd`; each file's patterns are relative to its own directory. */
6060
ignorePaths?: string[];
61+
/** Whether files inside node_modules may be discovered. */
62+
withNodeModules?: boolean;
6163
/** Resolved project config applied to discovered files. */
6264
config: ResolvedFmtConfig;
6365
}

‎packages/rstack/tests/cli/fmt/index.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,21 @@ test('formats the current directory with Prettier defaults', () => {
128128
expect(readProjectFile('index.ts')).toBe('const message = "hello";\n');
129129
});
130130

131+
test('formats files in node_modules with --with-node-modules', () => {
132+
const source = 'const message="hello"';
133+
writeProjectFile('node_modules/example/index.ts', source);
134+
135+
const skipped = runFmt(['node_modules/example']);
136+
expect(skipped.status).toBe(2);
137+
expect(readProjectFile('node_modules/example/index.ts')).toBe(source);
138+
139+
const result = runFmt(['--with-node-modules', 'node_modules/example']);
140+
expect(result.status).toBe(0);
141+
expectWriteSummary(result.stdout, 1, 1);
142+
expect(result.stderr).toBe('');
143+
expect(readProjectFile('node_modules/example/index.ts')).toBe('const message = "hello";\n');
144+
});
145+
131146
test('summarizes write mode when no files change', () => {
132147
writeProjectFile('index.ts', 'const message = "hello";\n');
133148

‎packages/rstack/tests/fmt/__snapshots__/cli.test.ts.snap‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Options:
1313
--ignore-path <path> Path to an additional ignore file (repeatable)
1414
-u, --ignore-unknown Ignore unknown files
1515
--no-error-on-unmatched-pattern Do not error when no files match
16+
--with-node-modules Process files inside node_modules
1617
--parallel-workers <count> Number of parallel workers
1718
--stdin-filepath <path> Format stdin as if it were saved at <path>
1819
-h, --help Display this help message"

‎packages/rstack/tests/fmt/cli.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ test('uses write mode by default', () => {
2424
ignorePaths: [],
2525
ignoreUnknown: false,
2626
noErrorOnUnmatchedPattern: false,
27+
withNodeModules: false,
2728
maxWorkers: undefined,
2829
help: false,
2930
});
@@ -40,6 +41,7 @@ test.each([
4041
ignorePaths: [],
4142
ignoreUnknown: false,
4243
noErrorOnUnmatchedPattern: false,
44+
withNodeModules: false,
4345
maxWorkers: undefined,
4446
help: false,
4547
});
@@ -52,6 +54,7 @@ test('configures parallel worker count', () => {
5254
ignorePaths: [],
5355
ignoreUnknown: false,
5456
noErrorOnUnmatchedPattern: false,
57+
withNodeModules: false,
5558
maxWorkers: 3,
5659
help: false,
5760
});
@@ -75,6 +78,7 @@ test('preserves file paths and globs', () => {
7578
ignorePaths: [],
7679
ignoreUnknown: false,
7780
noErrorOnUnmatchedPattern: false,
81+
withNodeModules: false,
7882
maxWorkers: undefined,
7983
help: false,
8084
});
@@ -87,6 +91,7 @@ test('treats arguments after the terminator as paths', () => {
8791
ignorePaths: [],
8892
ignoreUnknown: false,
8993
noErrorOnUnmatchedPattern: false,
94+
withNodeModules: false,
9095
maxWorkers: undefined,
9196
help: false,
9297
});
@@ -111,13 +116,18 @@ test.each(['-u', '--ignore-unknown', '--ignoreUnknown'])('parses %s', (option) =
111116
expect(parseFmtCLIArgs([option]).ignoreUnknown).toBe(true);
112117
});
113118

119+
test('parses --with-node-modules', () => {
120+
expect(parseFmtCLIArgs(['--with-node-modules']).withNodeModules).toBe(true);
121+
});
122+
114123
test('parses --stdin-filepath', () => {
115124
expect(parseFmtCLIArgs(['--stdin-filepath', 'src/index.ts'])).toEqual({
116125
mode: 'write',
117126
patterns: [],
118127
ignorePaths: [],
119128
ignoreUnknown: false,
120129
noErrorOnUnmatchedPattern: false,
130+
withNodeModules: false,
121131
maxWorkers: undefined,
122132
help: false,
123133
stdinFilepath: 'src/index.ts',
@@ -131,6 +141,7 @@ test('accepts a worker count with --stdin-filepath', () => {
131141
ignorePaths: [],
132142
ignoreUnknown: false,
133143
noErrorOnUnmatchedPattern: false,
144+
withNodeModules: false,
134145
maxWorkers: 2,
135146
help: false,
136147
stdinFilepath: 'index.ts',

‎packages/rstack/tests/fmt/discoverPaths.test.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,43 @@ test('discovers non-binary files in stable order and skips hard-ignored paths',
1919
writeProjectFile(rootPath, '.jj/internal.js');
2020

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

2324
expect(relativePaths(rootPath, files)).toEqual([
2425
'a.js',
2526
'b.ts',
2627
path.join('folder with spaces', 'c.ts'),
2728
'unknown.extension',
2829
]);
30+
expect(relativePaths(rootPath, filesWithNodeModules)).toEqual([
31+
'a.js',
32+
'b.ts',
33+
path.join('folder with spaces', 'c.ts'),
34+
path.join('node_modules', 'package', 'index.js'),
35+
'unknown.extension',
36+
]);
2937
await expect(
3038
discoverFmtPaths({ cwd: rootPath, patterns: ['node_modules/package/index.js'] }),
3139
).resolves.toEqual([]);
40+
await expect(
41+
discoverFmtPaths({
42+
cwd: rootPath,
43+
patterns: ['node_modules/package/index.js'],
44+
withNodeModules: true,
45+
}),
46+
).resolves.toEqual([path.join(rootPath, 'node_modules/package/index.js')]);
47+
});
48+
});
49+
50+
test('keeps node_modules excluded by gitignore when built-in exclusion is disabled', async () => {
51+
await withTempProject(async (rootPath) => {
52+
writeProjectFile(rootPath, '.gitignore', 'node_modules/\n');
53+
writeProjectFile(rootPath, 'node_modules/package/index.js');
54+
writeProjectFile(rootPath, 'index.js');
55+
56+
const files = await discoverFmtPaths({ cwd: rootPath, withNodeModules: true });
57+
58+
expect(relativePaths(rootPath, files)).toEqual(['.gitignore', 'index.js']);
3259
});
3360
});
3461

‎website/docs/en/guide/cli/fmt.mdx‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,16 @@ Formatted output is written to stdout and diagnostics to stderr. If the input pa
149149

150150
> `--stdin-filepath` cannot be combined with file arguments or with `--write`, `--check`, or `--list-different`.
151151
152+
### `--with-node-modules`
153+
154+
Process files inside `node_modules`, which `rs fmt` excludes by default:
155+
156+
```bash
157+
rs fmt --with-node-modules node_modules/example/index.js
158+
```
159+
160+
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.
161+
152162
### `--write`
153163

154164
Write formatted files in place. This is the default mode, so specifying `--write` is optional:

‎website/docs/zh/guide/cli/fmt.mdx‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,16 @@ cat src/index.ts | rs fmt --stdin-filepath src/index.ts
149149

150150
> `--stdin-filepath` 不能与文件参数或 `--write`、`--check`、`--list-different` 同时使用。
151151
152+
### `--with-node-modules`
153+
154+
处理 `node_modules` 中的文件。默认情况下,`rs fmt` 会排除这些文件:
155+
156+
```bash
157+
rs fmt --with-node-modules node_modules/example/index.js
158+
```
159+
160+
此选项只会关闭内置的 `node_modules` 排除规则。目录和 glob 扫描仍然遵循 `.gitignore`,`ignorePatterns` 和 `--ignore-path` 也会继续作用于所有输入。
161+
152162
### `--write`
153163

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

0 commit comments

Comments
 (0)