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
17 changes: 4 additions & 13 deletions packages/rstack/src/fmt/discovery.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
import { resolveFmtOptions } from './config.ts';
import { discoverFmtPaths } from './discoverPaths.ts';
import { createFmtIgnoreMatcher } from './ignore.ts';
import { resolveFmtParser } from './parser.ts';
import { createFmtPluginResolver, type FmtPluginResolver } from './plugins.ts';
import type { DiscoverFmtFilesOptions, FmtFileRequest, ResolvedFmtConfig } from './types.ts';

const resolveFileRequest = async (
const createFileRequest = (
filePath: string,
config: ResolvedFmtConfig,
resolvePlugins: FmtPluginResolver,
): Promise<FmtFileRequest | undefined> => {
): FmtFileRequest => {
const options = resolvePlugins(resolveFmtOptions(filePath, config));
const parser = await resolveFmtParser(filePath, options);
if (!parser) {
return;
}

return {
path: filePath,
options: {
...options,
filepath: filePath,
parser,
},
};
};

/** Discovers format-ready files without reading Prettier config files or `.prettierignore`. */
/** Discovers worker-ready files without reading Prettier config files or `.prettierignore`. */
const discoverFmtFiles = async ({
cwd,
patterns,
Expand All @@ -42,11 +36,8 @@ const discoverFmtFiles = async ({
? candidates.filter((filePath) => !isFmtIgnored(filePath))
: candidates;
const resolvePlugins = createFmtPluginResolver(config.rootPath);
const files = await Promise.all(
filePaths.map((filePath) => resolveFileRequest(filePath, config, resolvePlugins)),
);

return files.filter((file): file is FmtFileRequest => file !== undefined);
return filePaths.map((filePath) => createFileRequest(filePath, config, resolvePlugins));
};

export { discoverFmtFiles };
19 changes: 9 additions & 10 deletions packages/rstack/src/fmt/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ const formatText = async (
{ filePath, cursorOffset, config }: FormatTextOptions,
): Promise<FormatTextResult> => {
const options = createFmtPluginResolver(config.rootPath)(resolveFmtOptions(filePath, config));
const parser = await resolveFmtParser(filePath, options);
const formatOptions = {
...options,
filepath: filePath,
};
const plugins = await getPrettierPlugins(formatOptions);
const parser = await resolveFmtParser(filePath, formatOptions, plugins);

if (!parser) {
return {
Expand All @@ -20,24 +25,18 @@ const formatText = async (
};
}

const formatOptions = {
...options,
filepath: filePath,
parser,
};
const plugins = await getPrettierPlugins(formatOptions);
const resolvedOptions = { ...formatOptions, parser, plugins };

if (cursorOffset === undefined) {
return {
status: 'formatted',
formatted: await format(source, { ...formatOptions, plugins }),
formatted: await format(source, resolvedOptions),
};
}

const result = await formatWithCursor(source, {
...formatOptions,
...resolvedOptions,
cursorOffset,
plugins,
});

return {
Expand Down
6 changes: 4 additions & 2 deletions packages/rstack/src/fmt/parser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getFileInfo, type FileInfoOptions, type Options as PrettierOptions } from 'prettier';
import { getPrettierPlugins } from './prettierPlugins.ts';

type PrettierPlugins = NonNullable<PrettierOptions['plugins']>;

const fileInfoOptions = {
ignorePath: [],
Expand All @@ -11,12 +12,13 @@ const fileInfoOptions = {
const resolveFmtParser = async (
filePath: string,
options: PrettierOptions,
plugins: PrettierPlugins,
): Promise<PrettierOptions['parser'] | null> =>
options.parser ??
(
await getFileInfo(filePath, {
...fileInfoOptions,
plugins: await getPrettierPlugins(options),
plugins,
})
).inferredParser;

Expand Down
17 changes: 12 additions & 5 deletions packages/rstack/src/fmt/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,30 @@ import type {
FmtFileRequest,
FmtFileResult,
FmtRunResult,
FmtWorkerFileResult,
RunFmtFilesOptions,
} from './types.ts';

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

/** Converts a formatter outcome into the shared per-file result. */
const runFmtFile = async (
file: FmtFileRequest,
shouldWrite: boolean,
formatFile: FormatFile,
): Promise<FmtFileResult> => {
): Promise<FmtFileResult | undefined> => {
const startTime = performance.now();

try {
const changed = await formatFile(file, shouldWrite);
const result = await formatFile(file, shouldWrite);
if (result === 'unsupported') {
return;
}

return {
path: file.path,
status: changed ? (shouldWrite ? 'written' : 'different') : 'unchanged',
status: result === 'changed' ? (shouldWrite ? 'written' : 'different') : 'unchanged',
durationMs: performance.now() - startTime,
};
} catch (error) {
Expand All @@ -45,7 +49,10 @@ const runFmtFilesWithWorkers = async (
const worker = await createFmtWorker(files.length, maxWorkers);

try {
return await Promise.all(files.map((file) => runFmtFile(file, shouldWrite, worker.formatFile)));
const results = await Promise.all(
files.map((file) => runFmtFile(file, shouldWrite, worker.formatFile)),
);
return results.filter((result): result is FmtFileResult => result !== undefined);
} finally {
worker.terminate();
}
Expand Down
6 changes: 4 additions & 2 deletions packages/rstack/src/fmt/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,11 @@ interface DiscoverFmtFilesOptions {
interface FmtFileRequest {
/** Absolute path to the file. */
path: string;
/** Final Prettier options with the parser and file path resolved. */
options: ResolvedFmtOptions & Required<Pick<PrettierOptions, 'filepath' | 'parser'>>;
/** Final per-file options with project plugins and the file path resolved. */
options: ResolvedFmtOptions & Required<Pick<PrettierOptions, 'filepath'>>;
}

type FmtWorkerFileResult = 'changed' | 'unchanged' | 'unsupported';
type FmtMode = 'write' | 'check' | 'list-different';
type FmtExitCode = 0 | 1 | 2;

Expand Down Expand Up @@ -129,6 +130,7 @@ export type {
FmtMode,
FmtPluginSpecifier,
FmtRunResult,
FmtWorkerFileResult,
FormatTextOptions,
FormatTextResult,
ResolvedFmtConfig,
Expand Down
18 changes: 13 additions & 5 deletions packages/rstack/src/fmt/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import { readFileSync, writeFileSync } from 'node:fs';
import { format } from 'prettier';
import { resolveFmtParser } from './parser.ts';
import { getPrettierPlugins } from './prettierPlugins.ts';
import type { FmtFileRequest } from './types.ts';
import type { FmtFileRequest, FmtWorkerFileResult } from './types.ts';

/**
* Use synchronous direct I/O inside the dedicated worker to avoid libuv
Expand All @@ -12,22 +13,29 @@ import type { FmtFileRequest } from './types.ts';
const formatFile = async (
{ path, options }: FmtFileRequest,
shouldWrite: boolean,
): Promise<boolean> => {
): Promise<FmtWorkerFileResult> => {
const plugins = await getPrettierPlugins(options);
const parser = await resolveFmtParser(path, options, plugins);
if (!parser) {
return 'unsupported';
}

const source = readFileSync(path, 'utf8');
const formatted = await format(source, {
...options,
plugins: await getPrettierPlugins(options),
parser,
plugins,
});

if (source === formatted) {
return false;
return 'unchanged';
}

if (shouldWrite) {
writeFileSync(path, formatted, 'utf8');
}

return true;
return 'changed';
};

/** Confirms that the worker module and its runtime dependencies are ready. */
Expand Down
13 changes: 8 additions & 5 deletions packages/rstack/tests/fmt/discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ test('applies config ignore patterns outside the config root', async () => {
});
});

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

expect(relativePaths(rootPath, inferredFiles)).toEqual(['index.js', 'index.ts']);
expect(inferredFiles.map((file) => file.options.parser)).toEqual(['yuku', 'yuku-ts']);
expect(relativePaths(rootPath, inferredFiles)).toEqual([
'index.js',
'index.ts',
'source.custom',
'unknown.extension',
]);
expect(inferredFiles.every((file) => file.options.parser === undefined)).toBe(true);
expect(configuredFiles[0].options).toMatchObject({
filepath: path.join(rootPath, 'source.custom'),
parser: 'babel',
Expand Down Expand Up @@ -108,13 +113,11 @@ test('resolves plugins after applying matching overrides', async () => {
expect(files).toHaveLength(2);
expect(files[0]).toMatchObject({
options: {
parser: 'json',
plugins: [pathToFileURL(pluginEntry).href],
},
});
expect(files[1]).toMatchObject({
options: {
parser: 'babel',
plugins: [pathToFileURL(pluginEntry).href],
},
});
Expand Down
17 changes: 17 additions & 0 deletions packages/rstack/tests/fmt/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,20 @@ test('continues after a file fails and gives errors exit-code precedence', async
expect(readFileSync(validPath, 'utf8')).toBe('const value=1');
});
});

test('omits unsupported files from the result', async () => {
await withTempProject(async (rootPath) => {
const filePath = path.join(rootPath, 'example.unknown');
writeFileSync(filePath, 'plain text');

const result = await run([
{
path: filePath,
options: { filepath: filePath },
},
]);

expect(result).toMatchObject({ exitCode: 0, files: [] });
expect(readFileSync(filePath, 'utf8')).toBe('plain text');
});
});
38 changes: 37 additions & 1 deletion packages/rstack/tests/fmt/worker.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { expect, test } from 'rstack/test';
import { formatFile } from '../../src/fmt/worker.ts';
import { withTempProject, writeProjectFile } from './helpers.ts';
Expand All @@ -18,8 +19,43 @@ test('writes formatted files', async () => {
},
true,
),
).resolves.toBe(true);
).resolves.toBe('changed');

expect(readFileSync(filePath, 'utf8')).toBe('const value = 1;\n');
});
});

test('infers the parser before formatting', async () => {
await withTempProject(async (rootPath) => {
const source = 'const value=1';
const filePath = writeProjectFile(rootPath, 'example.ts', source);

await expect(
formatFile(
{
path: filePath,
options: { filepath: filePath },
},
false,
),
).resolves.toBe('changed');

expect(readFileSync(filePath, 'utf8')).toBe(source);
});
});

test('skips unsupported files before reading them', async () => {
await withTempProject(async (rootPath) => {
const filePath = path.join(rootPath, 'missing.unknown');

await expect(
formatFile(
{
path: filePath,
options: { filepath: filePath },
},
true,
),
).resolves.toBe('unsupported');
});
});