Skip to content

Commit d7cea9b

Browse files
authored
perf(fmt): precompile override matchers (#151)
1 parent e8eedc0 commit d7cea9b

3 files changed

Lines changed: 127 additions & 29 deletions

File tree

‎packages/rstack/src/fmt/config.ts‎

Lines changed: 70 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,54 +13,97 @@ type ResolveFmtConfigOptions = {
1313
cwd: string;
1414
};
1515

16+
type PathMatcher = (filePath: string) => boolean;
17+
18+
const neverMatches: PathMatcher = () => false;
19+
20+
const compileMatchers = (
21+
patterns: string[],
22+
excludedPatterns: string | string[] | undefined,
23+
basename: boolean,
24+
): PathMatcher | undefined => {
25+
if (patterns.length === 0) {
26+
return;
27+
}
28+
29+
const options = {
30+
ignore: excludedPatterns,
31+
basename,
32+
dot: true,
33+
};
34+
35+
if (patterns.length === 1) {
36+
return micromatch.matcher(patterns[0], options);
37+
}
38+
39+
const matchers = patterns.map((pattern) => micromatch.matcher(pattern, options));
40+
41+
return (filePath) => {
42+
for (const matches of matchers) {
43+
if (matches(filePath)) {
44+
return true;
45+
}
46+
}
47+
return false;
48+
};
49+
};
50+
51+
const createPathMatcher = (
52+
patterns: string | string[],
53+
excludedPatterns?: string | string[],
54+
): PathMatcher => {
55+
const pathPatterns: string[] = [];
56+
const basenamePatterns: string[] = [];
57+
58+
for (const pattern of Array.isArray(patterns) ? patterns : [patterns]) {
59+
if (pattern.includes('/')) {
60+
pathPatterns.push(pattern);
61+
} else {
62+
basenamePatterns.push(pattern);
63+
}
64+
}
65+
66+
const basenameMatcher = compileMatchers(basenamePatterns, excludedPatterns, true);
67+
const pathMatcher = compileMatchers(pathPatterns, excludedPatterns, false);
68+
69+
if (!basenameMatcher || !pathMatcher) {
70+
return basenameMatcher ?? pathMatcher ?? neverMatches;
71+
}
72+
return (filePath) => basenameMatcher(filePath) || pathMatcher(filePath);
73+
};
74+
1675
/** Splits a flat config into project-level formatting options and rules. */
1776
const normalizeFmtConfig = (config: FmtConfig | undefined, rootPath: string): ResolvedFmtConfig => {
1877
const { ignorePatterns = [], overrides = [], ...baseOptions } = config ?? {};
1978

2079
return {
2180
rootPath,
2281
baseOptions,
23-
overrides,
82+
overrides: overrides.map(({ files, excludeFiles, options }) => ({
83+
matches: createPathMatcher(files, excludeFiles),
84+
options,
85+
})),
2486
ignorePatterns,
2587
};
2688
};
2789

28-
const pathMatchesGlobs = (
29-
filePath: string,
30-
patterns: string | string[],
31-
excludedPatterns?: string | string[],
32-
): boolean => {
33-
const patternList = Array.isArray(patterns) ? patterns : [patterns];
34-
const withSlashes = patternList.filter((pattern) => pattern.includes('/'));
35-
const withoutSlashes = patternList.filter((pattern) => !pattern.includes('/'));
36-
37-
return (
38-
micromatch.isMatch(filePath, withoutSlashes, {
39-
ignore: excludedPatterns,
40-
basename: true,
41-
dot: true,
42-
}) ||
43-
micromatch.isMatch(filePath, withSlashes, {
44-
ignore: excludedPatterns,
45-
basename: false,
46-
dot: true,
47-
})
48-
);
49-
};
50-
5190
/** Applies matching overrides to the shared formatter options. */
5291
const resolveFmtOptions = (filePath: string, config: ResolvedFmtConfig): ResolvedFmtOptions => {
5392
if (config.overrides.length === 0) {
5493
return config.baseOptions;
5594
}
5695

57-
const options = { ...config.baseOptions };
96+
let options = config.baseOptions;
5897
const relativeFilePath = relative(config.rootPath, filePath);
5998

6099
for (const override of config.overrides) {
61-
if (pathMatchesGlobs(relativeFilePath, override.files, override.excludeFiles)) {
62-
Object.assign(options, override.options);
100+
if (!override.options || !override.matches(relativeFilePath)) {
101+
continue;
102+
}
103+
if (options === config.baseOptions) {
104+
options = { ...options };
63105
}
106+
Object.assign(options, override.options);
64107
}
65108

66109
return options;

‎packages/rstack/src/fmt/types.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,20 @@ interface FmtConfig extends Omit<PrettierConfig, 'plugins' | 'overrides'>, FmtBu
3333

3434
type FmtConfigDefinition = FmtConfig | (() => FmtConfig | Promise<FmtConfig>);
3535

36+
interface ResolvedFmtOverride {
37+
/** Matches a path relative to the config root. */
38+
matches: (relativeFilePath: string) => boolean;
39+
options?: ResolvedFmtOptions;
40+
}
41+
3642
/** Internal project config before per-file rules are applied. */
3743
interface ResolvedFmtConfig {
3844
/** Root for relative patterns and plugin paths. */
3945
rootPath: string;
4046
/** Shared Prettier options before per-file overrides. */
4147
baseOptions: ResolvedFmtOptions;
42-
/** Per-file override rules. */
43-
overrides: NonNullable<PrettierConfig['overrides']>;
48+
/** Precompiled per-file override rules. */
49+
overrides: ResolvedFmtOverride[];
4450
/** Root-relative ignore patterns. */
4551
ignorePatterns: string[];
4652
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import path from 'node:path';
2+
import { expect, test } from 'rstack/test';
3+
import { normalizeFmtConfig, resolveFmtOptions } from '../../src/fmt/config.ts';
4+
5+
const rootPath = path.join(import.meta.dirname, 'project');
6+
7+
test('reuses base options when no override matches', () => {
8+
const config = normalizeFmtConfig(
9+
{
10+
singleQuote: true,
11+
overrides: [{ files: '*.ts', options: { semi: false } }],
12+
},
13+
rootPath,
14+
);
15+
16+
expect(resolveFmtOptions(path.join(rootPath, 'index.js'), config)).toBe(config.baseOptions);
17+
});
18+
19+
test('applies basename and path overrides in declaration order', () => {
20+
const config = normalizeFmtConfig(
21+
{
22+
singleQuote: false,
23+
overrides: [
24+
{
25+
files: '*.ts',
26+
excludeFiles: '*.test.ts',
27+
options: { semi: false },
28+
},
29+
{
30+
files: 'src/**/*.{ts,tsx}',
31+
options: { singleQuote: true },
32+
},
33+
{
34+
files: 'src/**/index.ts',
35+
options: { semi: true, tabWidth: 4 },
36+
},
37+
],
38+
},
39+
rootPath,
40+
);
41+
42+
const options = resolveFmtOptions(path.join(rootPath, 'src/index.ts'), config);
43+
const testOptions = resolveFmtOptions(path.join(rootPath, 'src/index.test.ts'), config);
44+
45+
expect(options).not.toBe(config.baseOptions);
46+
expect(options).toEqual({ semi: true, singleQuote: true, tabWidth: 4 });
47+
expect(testOptions).toEqual({ singleQuote: true });
48+
expect(config.baseOptions).toEqual({ singleQuote: false });
49+
});

0 commit comments

Comments
 (0)