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
39 changes: 23 additions & 16 deletions packages/rstack/src/fmt/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { dirname, relative } from 'node:path';
import { dirname } from 'node:path';
import micromatch from 'micromatch';
import { createRelativePathResolver } from './relativePath.ts';
import type {
FmtConfig,
FmtConfigDefinition,
Expand All @@ -14,6 +15,7 @@ type ResolveFmtConfigOptions = {
};

type PathMatcher = (filePath: string) => boolean;
type FmtOptionsResolver = (filePath: string) => ResolvedFmtOptions;

const neverMatches: PathMatcher = () => false;

Expand Down Expand Up @@ -87,26 +89,30 @@ const normalizeFmtConfig = (config: FmtConfig | undefined, rootPath: string): Re
};
};

/** Applies matching overrides to the shared formatter options. */
const resolveFmtOptions = (filePath: string, config: ResolvedFmtConfig): ResolvedFmtOptions => {
/** Creates a reusable resolver for applying per-file formatter overrides. */
const createFmtOptionsResolver = (config: ResolvedFmtConfig): FmtOptionsResolver => {
if (config.overrides.length === 0) {
return config.baseOptions;
return () => config.baseOptions;
}

let options = config.baseOptions;
const relativeFilePath = relative(config.rootPath, filePath);
const resolveRelativePath = createRelativePathResolver(config.rootPath);

for (const override of config.overrides) {
if (!override.options || !override.matches(relativeFilePath)) {
continue;
}
if (options === config.baseOptions) {
options = { ...options };
return (filePath) => {
let options = config.baseOptions;
const relativeFilePath = resolveRelativePath(filePath);

for (const override of config.overrides) {
if (!override.options || !override.matches(relativeFilePath)) {
continue;
}
if (options === config.baseOptions) {
options = { ...options };
}
Object.assign(options, override.options);
}
Object.assign(options, override.options);
}

return options;
return options;
};
};

/** Resolves a formatter config definition and its project root. */
Expand All @@ -121,4 +127,5 @@ const resolveFmtConfig = async ({
return normalizeFmtConfig(config, rootPath);
};

export { normalizeFmtConfig, resolveFmtConfig, resolveFmtOptions };
export { createFmtOptionsResolver, normalizeFmtConfig, resolveFmtConfig };
export type { FmtOptionsResolver };
14 changes: 9 additions & 5 deletions packages/rstack/src/fmt/discovery.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { resolveFmtOptions } from './config.ts';
import { createFmtOptionsResolver, type FmtOptionsResolver } from './config.ts';
import { discoverFmtPaths } from './discoverPaths.ts';
import { createIgnoreMatcher } from './ignore.ts';
import type { DiscoverFmtFilesOptions, FmtFileRequest, ResolvedFmtConfig } from './types.ts';
import type { DiscoverFmtFilesOptions, FmtFileRequest } from './types.ts';

const createFileRequest = (filePath: string, config: ResolvedFmtConfig): FmtFileRequest => ({
const createFileRequest = (
filePath: string,
resolveOptions: FmtOptionsResolver,
): FmtFileRequest => ({
path: filePath,
options: resolveFmtOptions(filePath, config),
options: resolveOptions(filePath),
});

/** Discovers worker-ready files without automatically reading Prettier config or ignore files. */
Expand All @@ -28,7 +31,8 @@ const discoverFmtFiles = async ({
}

const filePaths = candidates.filter((filePath) => !isIgnored(filePath));
const files = filePaths.map((filePath) => createFileRequest(filePath, config));
const resolveOptions = createFmtOptionsResolver(config);
const files = filePaths.map((filePath) => createFileRequest(filePath, resolveOptions));
if (!files.some((file) => file.options.plugins?.length)) {
return files;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/rstack/src/fmt/stdin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { resolve } from 'node:path';
import { createFmtOptionsResolver } from './config.ts';
import { createFileRequest } from './discovery.ts';
import { formatFmtSource } from './format.ts';
import { createIgnoreMatcher } from './ignore.ts';
Expand Down Expand Up @@ -78,7 +79,7 @@ const runFmtStdin = async ({
return;
}

let file = createFileRequest(absolutePath, config);
let file = createFileRequest(absolutePath, createFmtOptionsResolver(config));
if (file.options.plugins?.length) {
const { createFmtPluginResolver } = await import(
/* rspackChunkName: 'fmtPlugins' */
Expand Down
22 changes: 18 additions & 4 deletions packages/rstack/tests/fmt/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'node:path';
import { expect, test } from 'rstack/test';
import { normalizeFmtConfig, resolveFmtOptions } from '../../src/fmt/config.ts';
import { createFmtOptionsResolver, normalizeFmtConfig } from '../../src/fmt/config.ts';

const rootPath = path.join(import.meta.dirname, 'project');

Expand All @@ -12,8 +12,9 @@ test('reuses base options when no override matches', () => {
},
rootPath,
);
const resolveOptions = createFmtOptionsResolver(config);

expect(resolveFmtOptions(path.join(rootPath, 'index.js'), config)).toBe(config.baseOptions);
expect(resolveOptions(path.join(rootPath, 'index.js'))).toBe(config.baseOptions);
});

test('applies basename and path overrides in declaration order', () => {
Expand All @@ -38,12 +39,25 @@ test('applies basename and path overrides in declaration order', () => {
},
rootPath,
);
const resolveOptions = createFmtOptionsResolver(config);

const options = resolveFmtOptions(path.join(rootPath, 'src/index.ts'), config);
const testOptions = resolveFmtOptions(path.join(rootPath, 'src/index.test.ts'), config);
const options = resolveOptions(path.join(rootPath, 'src/index.ts'));
const testOptions = resolveOptions(path.join(rootPath, 'src/index.test.ts'));

expect(options).not.toBe(config.baseOptions);
expect(options).toEqual({ semi: true, singleQuote: true, tabWidth: 4 });
expect(testOptions).toEqual({ singleQuote: true });
expect(config.baseOptions).toEqual({ singleQuote: false });
});

test('applies overrides outside the config root', () => {
const config = normalizeFmtConfig(
{
overrides: [{ files: '../shared/*.ts', options: { semi: false } }],
},
rootPath,
);
const resolveOptions = createFmtOptionsResolver(config);

expect(resolveOptions(path.join(rootPath, '../shared/index.ts'))).toEqual({ semi: false });
});