Skip to content

Commit 8b574dc

Browse files
authored
feat(fmt): resolve format file requests (#119)
1 parent 630a6c0 commit 8b574dc

3 files changed

Lines changed: 160 additions & 0 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { getFileInfo, type FileInfoOptions } from 'prettier';
2+
import { resolveFmtOptions } from './config.ts';
3+
import { discoverFmtPaths } from './discoverPaths.ts';
4+
import { createFmtIgnoreMatcher } from './ignore.ts';
5+
import type { DiscoverFmtFilesOptions, FmtFileRequest, ResolvedFmtConfig } from './types.ts';
6+
7+
const fileInfoOptions = {
8+
ignorePath: [],
9+
resolveConfig: false,
10+
withNodeModules: true,
11+
} satisfies FileInfoOptions;
12+
13+
const resolveFileRequest = async (
14+
filePath: string,
15+
config: ResolvedFmtConfig,
16+
): Promise<FmtFileRequest | undefined> => {
17+
const options = resolveFmtOptions(filePath, config);
18+
19+
if (options.plugins?.length) {
20+
throw new Error('Prettier plugins are not supported yet.');
21+
}
22+
23+
const parser = options.parser ?? (await getFileInfo(filePath, fileInfoOptions)).inferredParser;
24+
if (!parser) {
25+
return;
26+
}
27+
28+
return {
29+
path: filePath,
30+
options: {
31+
...options,
32+
filepath: filePath,
33+
parser,
34+
},
35+
};
36+
};
37+
38+
/** Discovers format-ready files without reading Prettier config files or `.prettierignore`. */
39+
const discoverFmtFiles = async ({
40+
cwd,
41+
patterns,
42+
config,
43+
}: DiscoverFmtFilesOptions): Promise<FmtFileRequest[]> => {
44+
const candidates = await discoverFmtPaths({ cwd, patterns });
45+
if (candidates.length === 0) {
46+
return [];
47+
}
48+
49+
const isFmtIgnored = config.ignorePatterns.length ? createFmtIgnoreMatcher(config) : undefined;
50+
const filePaths = isFmtIgnored
51+
? candidates.filter((filePath) => !isFmtIgnored(filePath))
52+
: candidates;
53+
const files = await Promise.all(
54+
filePaths.map((filePath) => resolveFileRequest(filePath, config)),
55+
);
56+
57+
return files.filter((file): file is FmtFileRequest => file !== undefined);
58+
};
59+
60+
export { discoverFmtFiles };

packages/rstack/src/fmt/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,22 @@ interface FormatTextOptions {
2828
config: ResolvedFmtConfig;
2929
}
3030

31+
interface DiscoverFmtFilesOptions {
32+
/** Absolute directory used to resolve input paths. */
33+
cwd: string;
34+
/** Files, directories, and positive or negative globs. Defaults to the current directory. */
35+
patterns?: string[];
36+
/** Resolved project config applied to discovered files. */
37+
config: ResolvedFmtConfig;
38+
}
39+
40+
interface FmtFileRequest {
41+
/** Absolute path to the file. */
42+
path: string;
43+
/** Final Prettier options with the parser and file path resolved. */
44+
options: PrettierOptions & Required<Pick<PrettierOptions, 'filepath' | 'parser'>>;
45+
}
46+
3147
interface FormattedTextResult {
3248
status: 'formatted';
3349
formatted: string;
@@ -42,8 +58,10 @@ interface SkippedTextResult {
4258
type FormatTextResult = FormattedTextResult | SkippedTextResult;
4359

4460
export type {
61+
DiscoverFmtFilesOptions,
4562
FmtConfig,
4663
FmtConfigDefinition,
64+
FmtFileRequest,
4765
FormatTextOptions,
4866
FormatTextResult,
4967
ResolvedFmtConfig,
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2+
import { tmpdir } from 'node:os';
3+
import path from 'node:path';
4+
import { expect, test } from 'rstack/test';
5+
import { normalizeFmtConfig } from '../../src/fmt/config.ts';
6+
import { discoverFmtFiles } from '../../src/fmt/discovery.ts';
7+
import type { FmtConfig } from '../../src/fmt/types.ts';
8+
9+
const withProject = async (callback: (rootPath: string) => Promise<void>): Promise<void> => {
10+
const rootPath = mkdtempSync(path.join(tmpdir(), 'rstack fmt '));
11+
12+
try {
13+
await callback(rootPath);
14+
} finally {
15+
rmSync(rootPath, { force: true, recursive: true });
16+
}
17+
};
18+
19+
const writeProjectFile = (rootPath: string, filePath: string, content = ''): string => {
20+
const absolutePath = path.join(rootPath, filePath);
21+
mkdirSync(path.dirname(absolutePath), { recursive: true });
22+
writeFileSync(absolutePath, content);
23+
return absolutePath;
24+
};
25+
26+
const discover = async (cwd: string, patterns?: string[], config?: FmtConfig, configRoot = cwd) =>
27+
discoverFmtFiles({
28+
cwd,
29+
patterns,
30+
config: normalizeFmtConfig(config, configRoot),
31+
});
32+
33+
const relativePaths = (rootPath: string, files: Awaited<ReturnType<typeof discover>>): string[] =>
34+
files.map((file) => path.relative(rootPath, file.path));
35+
36+
test('applies config ignore patterns to discovered and explicit files', async () => {
37+
await withProject(async (rootPath) => {
38+
const keepPath = writeProjectFile(rootPath, 'generated/keep.ts');
39+
const blockedPath = writeProjectFile(rootPath, 'generated/blocked.ts');
40+
writeProjectFile(rootPath, 'src/index.ts');
41+
const config = { ignorePatterns: ['generated/blocked.ts'] };
42+
43+
const discoveredFiles = await discover(rootPath, undefined, config);
44+
const explicitFiles = await discover(rootPath, [keepPath, blockedPath], config);
45+
46+
expect(relativePaths(rootPath, discoveredFiles)).toEqual([
47+
path.join('generated', 'keep.ts'),
48+
path.join('src', 'index.ts'),
49+
]);
50+
expect(relativePaths(rootPath, explicitFiles)).toEqual([path.join('generated', 'keep.ts')]);
51+
});
52+
});
53+
54+
test('applies config ignore patterns outside the config root', async () => {
55+
await withProject(async (rootPath) => {
56+
const configRoot = path.join(rootPath, 'project');
57+
const filePath = writeProjectFile(rootPath, 'shared/index.ts');
58+
mkdirSync(configRoot);
59+
60+
await expect(
61+
discover(configRoot, [filePath], { ignorePatterns: ['../shared/*.ts'] }, configRoot),
62+
).resolves.toEqual([]);
63+
});
64+
});
65+
66+
test('resolves parsers and accepts unknown extensions with an explicit parser', async () => {
67+
await withProject(async (rootPath) => {
68+
writeProjectFile(rootPath, 'index.ts');
69+
writeProjectFile(rootPath, 'source.custom');
70+
writeProjectFile(rootPath, 'unknown.extension');
71+
72+
const inferredFiles = await discover(rootPath);
73+
const configuredFiles = await discover(rootPath, ['source.custom'], { parser: 'babel' });
74+
75+
expect(relativePaths(rootPath, inferredFiles)).toEqual(['index.ts']);
76+
expect(inferredFiles[0].options.parser).toBe('typescript');
77+
expect(configuredFiles[0].options).toMatchObject({
78+
filepath: path.join(rootPath, 'source.custom'),
79+
parser: 'babel',
80+
});
81+
});
82+
});

0 commit comments

Comments
 (0)