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
58 changes: 58 additions & 0 deletions packages/rstack/src/fmt/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { format, formatWithCursor, getFileInfo } from 'prettier';
import { resolveFmtOptions } from './config.ts';
import type { FormatTextOptions, FormatTextResult } from './types.ts';

/** Formats source text without reading formatter config or ignore files. */
const formatText = async (
source: string,
{ filePath, cursorOffset, config }: FormatTextOptions,
): Promise<FormatTextResult> => {
const options = resolveFmtOptions(filePath, config);

if (options.plugins?.length) {
throw new Error('Prettier plugins are not supported yet.');
}

const parser =
options.parser ??
(
await getFileInfo(filePath, {
ignorePath: [],
resolveConfig: false,
withNodeModules: true,
})
).inferredParser;

if (!parser) {
return {
status: 'skipped',
reason: 'unsupported',
};
}

const formatOptions = {
...options,
filepath: filePath,
parser,
};

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

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

return {
status: 'formatted',
formatted: result.formatted,
cursorOffset: result.cursorOffset,
};
};

export { formatText };
30 changes: 29 additions & 1 deletion packages/rstack/src/fmt/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,32 @@ interface ResolvedFmtConfig {
ignorePatterns: string[];
}

export type { FmtConfig, FmtConfigDefinition, ResolvedFmtConfig };
interface FormatTextOptions {
/** File path used to resolve per-file options and infer the parser. */
filePath: string;
/** Cursor offset in the source to preserve across formatting. */
cursorOffset?: number;
/** Resolved project config used to derive per-file options. */
config: ResolvedFmtConfig;
}

interface FormattedTextResult {
status: 'formatted';
formatted: string;
cursorOffset?: number;
}

interface SkippedTextResult {
status: 'skipped';
reason: 'unsupported';
}

type FormatTextResult = FormattedTextResult | SkippedTextResult;

export type {
FmtConfig,
FmtConfigDefinition,
FormatTextOptions,
FormatTextResult,
ResolvedFmtConfig,
};
77 changes: 77 additions & 0 deletions packages/rstack/tests/fmt/format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import path from 'node:path';
import { expect, test } from 'rstack/test';
import { normalizeFmtConfig } from '../../src/fmt/config.ts';
import { formatText } from '../../src/fmt/format.ts';

const rootPath = import.meta.dirname;

test('applies per-file overrides and maps the cursor', async () => {
const source = 'const value={message:"hello"}';
const config = normalizeFmtConfig(
{
overrides: [
{
files: '*.ts',
options: {
singleQuote: true,
},
},
],
},
rootPath,
);

const result = await formatText(source, {
config,
cursorOffset: source.indexOf('message'),
filePath: path.join(rootPath, 'example.ts'),
});

expect(result).toEqual({
status: 'formatted',
formatted: "const value = { message: 'hello' };\n",
cursorOffset: 16,
});
});

test('returns unsupported when no parser can be inferred', async () => {
const config = normalizeFmtConfig(undefined, rootPath);

await expect(
formatText('plain text', {
config,
filePath: path.join(rootPath, 'unknown.extension'),
}),
).resolves.toEqual({
status: 'skipped',
reason: 'unsupported',
});
});

test('uses an explicit parser for unknown file extensions', async () => {
const config = normalizeFmtConfig({ parser: 'babel' }, rootPath);

const result = await formatText('const value={nested:true}', {
config,
filePath: path.join(rootPath, 'unknown.extension'),
});

expect(result).toEqual({
status: 'formatted',
formatted: 'const value = { nested: true };\n',
});
});

test('formats an explicitly provided node_modules file', async () => {
const config = normalizeFmtConfig(undefined, rootPath);

const result = await formatText('const value={nested:true}', {
config,
filePath: path.join(rootPath, 'node_modules', 'example', 'index.js'),
});

expect(result).toEqual({
status: 'formatted',
formatted: 'const value = { nested: true };\n',
});
});