Skip to content

Commit 71ce5ba

Browse files
authored
perf(fmt): infer parsers in workers (#143)
1 parent a3ef9f9 commit 71ce5ba

9 files changed

Lines changed: 108 additions & 43 deletions

File tree

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,26 @@
11
import { resolveFmtOptions } from './config.ts';
22
import { discoverFmtPaths } from './discoverPaths.ts';
33
import { createFmtIgnoreMatcher } from './ignore.ts';
4-
import { resolveFmtParser } from './parser.ts';
54
import { createFmtPluginResolver, type FmtPluginResolver } from './plugins.ts';
65
import type { DiscoverFmtFilesOptions, FmtFileRequest, ResolvedFmtConfig } from './types.ts';
76

8-
const resolveFileRequest = async (
7+
const createFileRequest = (
98
filePath: string,
109
config: ResolvedFmtConfig,
1110
resolvePlugins: FmtPluginResolver,
12-
): Promise<FmtFileRequest | undefined> => {
11+
): FmtFileRequest => {
1312
const options = resolvePlugins(resolveFmtOptions(filePath, config));
14-
const parser = await resolveFmtParser(filePath, options);
15-
if (!parser) {
16-
return;
17-
}
1813

1914
return {
2015
path: filePath,
2116
options: {
2217
...options,
2318
filepath: filePath,
24-
parser,
2519
},
2620
};
2721
};
2822

29-
/** Discovers format-ready files without reading Prettier config files or `.prettierignore`. */
23+
/** Discovers worker-ready files without reading Prettier config files or `.prettierignore`. */
3024
const discoverFmtFiles = async ({
3125
cwd,
3226
patterns,
@@ -42,11 +36,8 @@ const discoverFmtFiles = async ({
4236
? candidates.filter((filePath) => !isFmtIgnored(filePath))
4337
: candidates;
4438
const resolvePlugins = createFmtPluginResolver(config.rootPath);
45-
const files = await Promise.all(
46-
filePaths.map((filePath) => resolveFileRequest(filePath, config, resolvePlugins)),
47-
);
4839

49-
return files.filter((file): file is FmtFileRequest => file !== undefined);
40+
return filePaths.map((filePath) => createFileRequest(filePath, config, resolvePlugins));
5041
};
5142

5243
export { discoverFmtFiles };

packages/rstack/src/fmt/format.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ const formatText = async (
1111
{ filePath, cursorOffset, config }: FormatTextOptions,
1212
): Promise<FormatTextResult> => {
1313
const options = createFmtPluginResolver(config.rootPath)(resolveFmtOptions(filePath, config));
14-
const parser = await resolveFmtParser(filePath, options);
14+
const formatOptions = {
15+
...options,
16+
filepath: filePath,
17+
};
18+
const plugins = await getPrettierPlugins(formatOptions);
19+
const parser = await resolveFmtParser(filePath, formatOptions, plugins);
1520

1621
if (!parser) {
1722
return {
@@ -20,24 +25,18 @@ const formatText = async (
2025
};
2126
}
2227

23-
const formatOptions = {
24-
...options,
25-
filepath: filePath,
26-
parser,
27-
};
28-
const plugins = await getPrettierPlugins(formatOptions);
28+
const resolvedOptions = { ...formatOptions, parser, plugins };
2929

3030
if (cursorOffset === undefined) {
3131
return {
3232
status: 'formatted',
33-
formatted: await format(source, { ...formatOptions, plugins }),
33+
formatted: await format(source, resolvedOptions),
3434
};
3535
}
3636

3737
const result = await formatWithCursor(source, {
38-
...formatOptions,
38+
...resolvedOptions,
3939
cursorOffset,
40-
plugins,
4140
});
4241

4342
return {

packages/rstack/src/fmt/parser.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getFileInfo, type FileInfoOptions, type Options as PrettierOptions } from 'prettier';
2-
import { getPrettierPlugins } from './prettierPlugins.ts';
2+
3+
type PrettierPlugins = NonNullable<PrettierOptions['plugins']>;
34

45
const fileInfoOptions = {
56
ignorePath: [],
@@ -11,12 +12,13 @@ const fileInfoOptions = {
1112
const resolveFmtParser = async (
1213
filePath: string,
1314
options: PrettierOptions,
15+
plugins: PrettierPlugins,
1416
): Promise<PrettierOptions['parser'] | null> =>
1517
options.parser ??
1618
(
1719
await getFileInfo(filePath, {
1820
...fileInfoOptions,
19-
plugins: await getPrettierPlugins(options),
21+
plugins,
2022
})
2123
).inferredParser;
2224

packages/rstack/src/fmt/runner.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,30 @@ import type {
33
FmtFileRequest,
44
FmtFileResult,
55
FmtRunResult,
6+
FmtWorkerFileResult,
67
RunFmtFilesOptions,
78
} from './types.ts';
89

910
/** Formats one file and reports whether its contents differ. */
10-
type FormatFile = (file: FmtFileRequest, shouldWrite: boolean) => Promise<boolean>;
11+
type FormatFile = (file: FmtFileRequest, shouldWrite: boolean) => Promise<FmtWorkerFileResult>;
1112

1213
/** Converts a formatter outcome into the shared per-file result. */
1314
const runFmtFile = async (
1415
file: FmtFileRequest,
1516
shouldWrite: boolean,
1617
formatFile: FormatFile,
17-
): Promise<FmtFileResult> => {
18+
): Promise<FmtFileResult | undefined> => {
1819
const startTime = performance.now();
1920

2021
try {
21-
const changed = await formatFile(file, shouldWrite);
22+
const result = await formatFile(file, shouldWrite);
23+
if (result === 'unsupported') {
24+
return;
25+
}
2226

2327
return {
2428
path: file.path,
25-
status: changed ? (shouldWrite ? 'written' : 'different') : 'unchanged',
29+
status: result === 'changed' ? (shouldWrite ? 'written' : 'different') : 'unchanged',
2630
durationMs: performance.now() - startTime,
2731
};
2832
} catch (error) {
@@ -45,7 +49,10 @@ const runFmtFilesWithWorkers = async (
4549
const worker = await createFmtWorker(files.length, maxWorkers);
4650

4751
try {
48-
return await Promise.all(files.map((file) => runFmtFile(file, shouldWrite, worker.formatFile)));
52+
const results = await Promise.all(
53+
files.map((file) => runFmtFile(file, shouldWrite, worker.formatFile)),
54+
);
55+
return results.filter((result): result is FmtFileResult => result !== undefined);
4956
} finally {
5057
worker.terminate();
5158
}

packages/rstack/src/fmt/types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,11 @@ interface DiscoverFmtFilesOptions {
6666
interface FmtFileRequest {
6767
/** Absolute path to the file. */
6868
path: string;
69-
/** Final Prettier options with the parser and file path resolved. */
70-
options: ResolvedFmtOptions & Required<Pick<PrettierOptions, 'filepath' | 'parser'>>;
69+
/** Final per-file options with project plugins and the file path resolved. */
70+
options: ResolvedFmtOptions & Required<Pick<PrettierOptions, 'filepath'>>;
7171
}
7272

73+
type FmtWorkerFileResult = 'changed' | 'unchanged' | 'unsupported';
7374
type FmtMode = 'write' | 'check' | 'list-different';
7475
type FmtExitCode = 0 | 1 | 2;
7576

@@ -129,6 +130,7 @@ export type {
129130
FmtMode,
130131
FmtPluginSpecifier,
131132
FmtRunResult,
133+
FmtWorkerFileResult,
132134
FormatTextOptions,
133135
FormatTextResult,
134136
ResolvedFmtConfig,

packages/rstack/src/fmt/worker.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
import { readFileSync, writeFileSync } from 'node:fs';
44
import { format } from 'prettier';
5+
import { resolveFmtParser } from './parser.ts';
56
import { getPrettierPlugins } from './prettierPlugins.ts';
6-
import type { FmtFileRequest } from './types.ts';
7+
import type { FmtFileRequest, FmtWorkerFileResult } from './types.ts';
78

89
/**
910
* Use synchronous direct I/O inside the dedicated worker to avoid libuv
@@ -12,22 +13,29 @@ import type { FmtFileRequest } from './types.ts';
1213
const formatFile = async (
1314
{ path, options }: FmtFileRequest,
1415
shouldWrite: boolean,
15-
): Promise<boolean> => {
16+
): Promise<FmtWorkerFileResult> => {
17+
const plugins = await getPrettierPlugins(options);
18+
const parser = await resolveFmtParser(path, options, plugins);
19+
if (!parser) {
20+
return 'unsupported';
21+
}
22+
1623
const source = readFileSync(path, 'utf8');
1724
const formatted = await format(source, {
1825
...options,
19-
plugins: await getPrettierPlugins(options),
26+
parser,
27+
plugins,
2028
});
2129

2230
if (source === formatted) {
23-
return false;
31+
return 'unchanged';
2432
}
2533

2634
if (shouldWrite) {
2735
writeFileSync(path, formatted, 'utf8');
2836
}
2937

30-
return true;
38+
return 'changed';
3139
};
3240

3341
/** Confirms that the worker module and its runtime dependencies are ready. */

packages/rstack/tests/fmt/discovery.test.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ test('applies config ignore patterns outside the config root', async () => {
4747
});
4848
});
4949

50-
test('uses Yuku parsers by default and accepts an explicit parser', async () => {
50+
test('defers parser inference to workers and preserves an explicit parser', async () => {
5151
await withTempProject(async (rootPath) => {
5252
writeProjectFile(rootPath, 'index.js');
5353
writeProjectFile(rootPath, 'index.ts');
@@ -57,8 +57,13 @@ test('uses Yuku parsers by default and accepts an explicit parser', async () =>
5757
const inferredFiles = await discover(rootPath);
5858
const configuredFiles = await discover(rootPath, ['source.custom'], { parser: 'babel' });
5959

60-
expect(relativePaths(rootPath, inferredFiles)).toEqual(['index.js', 'index.ts']);
61-
expect(inferredFiles.map((file) => file.options.parser)).toEqual(['yuku', 'yuku-ts']);
60+
expect(relativePaths(rootPath, inferredFiles)).toEqual([
61+
'index.js',
62+
'index.ts',
63+
'source.custom',
64+
'unknown.extension',
65+
]);
66+
expect(inferredFiles.every((file) => file.options.parser === undefined)).toBe(true);
6267
expect(configuredFiles[0].options).toMatchObject({
6368
filepath: path.join(rootPath, 'source.custom'),
6469
parser: 'babel',
@@ -108,13 +113,11 @@ test('resolves plugins after applying matching overrides', async () => {
108113
expect(files).toHaveLength(2);
109114
expect(files[0]).toMatchObject({
110115
options: {
111-
parser: 'json',
112116
plugins: [pathToFileURL(pluginEntry).href],
113117
},
114118
});
115119
expect(files[1]).toMatchObject({
116120
options: {
117-
parser: 'babel',
118121
plugins: [pathToFileURL(pluginEntry).href],
119122
},
120123
});

packages/rstack/tests/fmt/runner.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,20 @@ test('continues after a file fails and gives errors exit-code precedence', async
104104
expect(readFileSync(validPath, 'utf8')).toBe('const value=1');
105105
});
106106
});
107+
108+
test('omits unsupported files from the result', async () => {
109+
await withTempProject(async (rootPath) => {
110+
const filePath = path.join(rootPath, 'example.unknown');
111+
writeFileSync(filePath, 'plain text');
112+
113+
const result = await run([
114+
{
115+
path: filePath,
116+
options: { filepath: filePath },
117+
},
118+
]);
119+
120+
expect(result).toMatchObject({ exitCode: 0, files: [] });
121+
expect(readFileSync(filePath, 'utf8')).toBe('plain text');
122+
});
123+
});

packages/rstack/tests/fmt/worker.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { readFileSync } from 'node:fs';
2+
import path from 'node:path';
23
import { expect, test } from 'rstack/test';
34
import { formatFile } from '../../src/fmt/worker.ts';
45
import { withTempProject, writeProjectFile } from './helpers.ts';
@@ -18,8 +19,43 @@ test('writes formatted files', async () => {
1819
},
1920
true,
2021
),
21-
).resolves.toBe(true);
22+
).resolves.toBe('changed');
2223

2324
expect(readFileSync(filePath, 'utf8')).toBe('const value = 1;\n');
2425
});
2526
});
27+
28+
test('infers the parser before formatting', async () => {
29+
await withTempProject(async (rootPath) => {
30+
const source = 'const value=1';
31+
const filePath = writeProjectFile(rootPath, 'example.ts', source);
32+
33+
await expect(
34+
formatFile(
35+
{
36+
path: filePath,
37+
options: { filepath: filePath },
38+
},
39+
false,
40+
),
41+
).resolves.toBe('changed');
42+
43+
expect(readFileSync(filePath, 'utf8')).toBe(source);
44+
});
45+
});
46+
47+
test('skips unsupported files before reading them', async () => {
48+
await withTempProject(async (rootPath) => {
49+
const filePath = path.join(rootPath, 'missing.unknown');
50+
51+
await expect(
52+
formatFile(
53+
{
54+
path: filePath,
55+
options: { filepath: filePath },
56+
},
57+
true,
58+
),
59+
).resolves.toBe('unsupported');
60+
});
61+
});

0 commit comments

Comments
 (0)