Skip to content

Commit a9a3d66

Browse files
committed
fix(core): refactor pre-compiling glob patterns into a reusable GlobMatcher closure, eliminating repeated regex compilation in hot paths
1 parent 416f44c commit a9a3d66

9 files changed

Lines changed: 87 additions & 35 deletions

packages/core/src/lib/main/parse-project.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { TsData } from '../file-info/ts-data';
99
import { Module } from '../modules/module';
1010
import { Configuration } from '../config/configuration';
1111
import { findModulePaths } from '../modules/find-module-paths';
12+
import { createGlobMatcher } from '../util/match-glob';
1213

1314
export type ParsedResult = {
1415
fileInfo: FileInfo;
@@ -39,12 +40,18 @@ export const parseProject = (
3940
const getFileInfo = (path: FsPath) =>
4041
throwIfNull(fileInfoMap.get(path), `cannot find FileInfo for ${path}`);
4142

42-
const modulePaths = findModulePaths(projectDirs, rootDir, config);
43+
const isBarrelMatch = createGlobMatcher(config.barrelFileName);
44+
const modulePaths = findModulePaths(
45+
projectDirs,
46+
rootDir,
47+
config,
48+
isBarrelMatch,
49+
);
4350

4451
const modules = createModules(modulePaths, fileInfoMap, getFileInfo, {
4552
entryFileInfo: unassignedFileInfo,
4653
rootDir,
47-
barrelFile: config.barrelFileName,
54+
isBarrelMatch,
4855
});
4956
fillFileInfoMap(fileInfoMap, modules);
5057

packages/core/src/lib/modules/create-modules.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,19 @@ import {
1111
keys,
1212
values,
1313
} from '../util/typed-object-functions';
14+
import { GlobMatcher } from '../util/match-glob';
1415

1516
interface CreateModulesContext {
1617
entryFileInfo: UnassignedFileInfo;
1718
rootDir: FsPath;
18-
barrelFile: string[];
19+
isBarrelMatch: GlobMatcher;
1920
}
2021

2122
export function createModules(
2223
modulePathMap: ModulePathMap,
2324
fileInfoMap: Map<FsPath, FileInfo>,
2425
getFileInfo: (path: FsPath) => FileInfo,
25-
{ entryFileInfo, rootDir, barrelFile }: CreateModulesContext,
26+
{ entryFileInfo, rootDir, isBarrelMatch }: CreateModulesContext,
2627
): Module[] {
2728
const moduleMap = fromEntries(
2829
entries(modulePathMap).map(([path, hasBarrel]) => [
@@ -33,7 +34,7 @@ export function createModules(
3334
getFileInfo,
3435
false,
3536
hasBarrel,
36-
barrelFile,
37+
isBarrelMatch,
3738
),
3839
]),
3940
);
@@ -44,7 +45,7 @@ export function createModules(
4445
getFileInfo,
4546
true,
4647
false,
47-
barrelFile,
48+
isBarrelMatch,
4849
);
4950

5051
const modulePaths = keys(moduleMap);

packages/core/src/lib/modules/find-module-paths.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { FsPath } from '../file-info/fs-path';
22
import { findModulePathsWithBarrel } from './internal/find-module-paths-with-barrel';
33
import { findModulePathsWithoutBarrel } from './internal/find-module-paths-without-barrel';
44
import { Configuration } from '../config/configuration';
5+
import { GlobMatcher } from '../util/match-glob';
56

67
export type ModulePathMap = Record<FsPath, boolean>;
78

@@ -15,14 +16,11 @@ export function findModulePaths(
1516
projectDirs: FsPath[],
1617
rootDir: FsPath,
1718
sheriffConfig: Configuration,
19+
isBarrelMatch: GlobMatcher,
1820
): ModulePathMap {
19-
const {
20-
modules,
21-
enableBarrelLess,
22-
barrelFileName
23-
} = sheriffConfig;
21+
const { modules, enableBarrelLess, barrelFileName } = sheriffConfig;
2422
const modulesWithoutBarrel = enableBarrelLess
25-
? findModulePathsWithoutBarrel(modules, rootDir, barrelFileName)
23+
? findModulePathsWithoutBarrel(modules, rootDir, isBarrelMatch)
2624
: [];
2725
const modulesWithBarrel = findModulePathsWithBarrel(
2826
projectDirs,

packages/core/src/lib/modules/internal/find-module-paths-without-barrel.ts

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as path from 'path';
88
import getFs from '../../fs/getFs';
99
import { FOLDER_CHARACTERS_REGEX_STRING } from '../../tags/calc-tags-for-module';
1010
import { flattenModules } from './flatten-modules';
11-
import { matchGlob } from '../../util/match-glob';
11+
import { GlobMatcher } from '../../util/match-glob';
1212

1313
/**
1414
* The current criterion for finding modules is via
@@ -20,11 +20,15 @@ import { matchGlob } from '../../util/match-glob';
2020
export function findModulePathsWithoutBarrel(
2121
moduleConfig: ModuleConfig,
2222
rootDir: FsPath,
23-
barrelFileNames: string[]
23+
isBarrelMatch: GlobMatcher,
2424
): Set<FsPath> {
2525
const paths = flattenModules(moduleConfig, '');
2626
const modulePathsPatternTree = createModulePathPatternsTree(paths);
27-
const modules = traverseAndMatch(modulePathsPatternTree, rootDir, barrelFileNames);
27+
const modules = traverseAndMatch(
28+
modulePathsPatternTree,
29+
rootDir,
30+
isBarrelMatch,
31+
);
2832
return new Set<FsPath>(modules);
2933
}
3034

@@ -34,14 +38,14 @@ export function findModulePathsWithoutBarrel(
3438
function traverseAndMatch(
3539
groupedPatterns: ModulePathPatternsTree,
3640
basePath: FsPath,
37-
barrelFileNames: string[]
41+
isBarrelMatch: GlobMatcher,
3842
): FsPath[] {
3943
const fs = getFs();
4044
const matchedDirectories: FsPath[] = [];
4145

4246
// Check if the current directory should be matched
4347
if ('' in groupedPatterns) {
44-
addAsModuleIfWithoutBarrel(matchedDirectories, basePath, barrelFileNames);
48+
addAsModuleIfWithoutBarrel(matchedDirectories, basePath, isBarrelMatch);
4549
}
4650

4751
const subDirectories = fs.readDirectory(basePath, 'directory');
@@ -55,11 +59,23 @@ function traverseAndMatch(
5559

5660
if (matchingPattern) {
5761
if (Object.keys(groupedPatterns[matchingPattern]).length === 0) {
58-
addAsModuleIfWithoutBarrel(matchedDirectories, subDirectory, barrelFileNames);
62+
addAsModuleIfWithoutBarrel(
63+
matchedDirectories,
64+
subDirectory,
65+
isBarrelMatch,
66+
);
5967
} else {
60-
const newDirectories = traverseAndMatch(groupedPatterns[matchingPattern], subDirectory, barrelFileNames);
68+
const newDirectories = traverseAndMatch(
69+
groupedPatterns[matchingPattern],
70+
subDirectory,
71+
isBarrelMatch,
72+
);
6173
for (const newDirectory of newDirectories) {
62-
addAsModuleIfWithoutBarrel(matchedDirectories, newDirectory, barrelFileNames);
74+
addAsModuleIfWithoutBarrel(
75+
matchedDirectories,
76+
newDirectory,
77+
isBarrelMatch,
78+
);
6379
}
6480
}
6581
}
@@ -91,22 +107,21 @@ function matchPattern(pattern: string, pathSegment: string): boolean {
91107
function addAsModuleIfWithoutBarrel(
92108
modulePaths: FsPath[],
93109
directory: FsPath,
94-
barrelFileNames: string[],
110+
isBarrelMatch: GlobMatcher,
95111
) {
96-
if (hasBarrelFile(directory, barrelFileNames)) {
112+
if (hasBarrelFile(directory, isBarrelMatch)) {
97113
return;
98114
}
99115

100116
modulePaths.push(directory);
101117
}
102118

103-
function hasBarrelFile(directory: FsPath, barrelFileNames: string[]): boolean {
119+
function hasBarrelFile(directory: FsPath, isBarrelMatch: GlobMatcher): boolean {
104120
const fs = getFs();
105121
const children = fs.readDirectory(directory);
106122
return children.some((child) => {
107123
if (fs.isFile(child)) {
108-
const fileName = path.basename(child);
109-
return barrelFileNames.some((pattern) => matchGlob(pattern, fileName));
124+
return isBarrelMatch(path.basename(child));
110125
}
111126
return false;
112127
});

packages/core/src/lib/modules/module.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { FileInfo } from './file.info';
33
import * as path from 'path';
44
import { FsPath } from '../file-info/fs-path';
55
import getFs from '../fs/getFs';
6-
import { matchGlob } from '../util/match-glob';
6+
import { GlobMatcher } from '../util/match-glob';
77

88
/**
99
* Since modules are constructed incrementally with in-place
@@ -19,7 +19,7 @@ export class Module {
1919
private readonly getFileInfo: (fsPath: FsPath) => FileInfo,
2020
public readonly isRoot: boolean,
2121
public readonly hasBarrel: boolean,
22-
private readonly barrelFilePatterns: string[],
22+
private readonly isBarrelMatch: GlobMatcher,
2323
) {
2424
}
2525

@@ -40,14 +40,11 @@ export class Module {
4040
*/
4141
isBarrelFile(filePath: FsPath): boolean {
4242
const fileDir = getFs().getParent(filePath);
43-
const fileName = path.basename(filePath);
4443

4544
if (fileDir !== this.path) {
4645
return false;
4746
}
4847

49-
return this.barrelFilePatterns.some((pattern) =>
50-
matchGlob(pattern, fileName),
51-
);
48+
return this.isBarrelMatch(path.basename(filePath));
5249
}
5350
}

packages/core/src/lib/modules/tests/create-module.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { FileInfo } from '../file.info';
1616
import { buildFileInfo } from '../../test/build-file-info';
1717
import { fromEntries } from '../../util/typed-object-functions';
1818
import { ModulePathMap } from '../find-module-paths';
19+
import { createGlobMatcher } from '../../util/match-glob';
1920

2021
interface TestParameter {
2122
fileInfo: UnassignedFileInfo;
@@ -226,10 +227,11 @@ function assertModule(createTestParams: () => TestParameter) {
226227
const modulePathMap: ModulePathMap = fromEntries(
227228
barrelFiles.map((path) => [toFsPath(path.replace('/index.ts', '')), true]),
228229
);
230+
const isBarrelMatch = createGlobMatcher(['index.ts']);
229231
const modules = createModules(modulePathMap, fileInfoMap, getFileInfo, {
230232
entryFileInfo: fileInfo,
231233
rootDir: toFsPath('/'),
232-
barrelFile: ['index.ts'],
234+
isBarrelMatch,
233235
});
234236

235237
const expectedModules = testParams.expectedModules.map((mi) => {
@@ -245,7 +247,7 @@ function assertModule(createTestParams: () => TestParameter) {
245247
getFileInfo,
246248
mi.path === '/',
247249
mi.path !== '/',
248-
['index.ts'],
250+
isBarrelMatch,
249251
);
250252
for (const fi of fileInfos) {
251253
module.addFileInfo(fi);

packages/core/src/lib/modules/tests/find-module-paths-without-barrel.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { createProject } from '../../test/project-creator';
55
import { findModulePathsWithoutBarrel } from '../internal/find-module-paths-without-barrel';
66
import { useVirtualFs } from '../../fs/getFs';
77
import { toFsPath } from '../../file-info/fs-path';
8+
import { createGlobMatcher } from '../../util/match-glob';
89

910
function assertProject(fileTree: FileTree) {
1011
return {
@@ -18,7 +19,7 @@ function assertProject(fileTree: FileTree) {
1819
const actualModulePaths = findModulePathsWithoutBarrel(
1920
moduleConfig,
2021
toFsPath('/project'),
21-
['index.ts'],
22+
createGlobMatcher(['index.ts']),
2223
);
2324
expect(Array.from(actualModulePaths)).toEqual(absoluteModulePaths);
2425
},

packages/core/src/lib/modules/tests/module-is-barrel-file.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Module } from '../module';
33
import { FsPath, toFsPath } from '../../file-info/fs-path';
44
import { FileInfo } from '../file.info';
55
import getFs, { useVirtualFs } from '../../fs/getFs';
6+
import { createGlobMatcher } from '../../util/match-glob';
67

78
describe('Module.isBarrelFile', () => {
89
beforeAll(() => {
@@ -34,7 +35,7 @@ describe('Module.isBarrelFile', () => {
3435
getFileInfo,
3536
false,
3637
true,
37-
barrelFilePatterns,
38+
createGlobMatcher(barrelFilePatterns),
3839
);
3940
}
4041

packages/core/src/lib/util/match-glob.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,33 @@
1+
export type GlobMatcher = (text: string) => boolean;
2+
3+
/**
4+
* Creates a pre-compiled matcher function for the given glob patterns.
5+
*
6+
* Supported wildcards:
7+
* - `*` matches zero or more characters
8+
* - `?` matches exactly one character
9+
*
10+
* All other characters are matched literally.
11+
* The matching is **case-insensitive**.
12+
*
13+
* @param patterns - The glob patterns to match against.
14+
* @returns A function that returns `true` if the text matches any pattern.
15+
*/
16+
export function createGlobMatcher(patterns: string[]): GlobMatcher {
17+
const literals = patterns
18+
.filter((p) => !p.includes('*') && !p.includes('?'))
19+
.map((p) => p.toLowerCase());
20+
21+
const regexes = patterns
22+
.filter((p) => p.includes('*') || p.includes('?'))
23+
.map((p) => new RegExp(`^${globToRegexString(p)}$`, 'i'));
24+
25+
return (text: string): boolean => {
26+
const lower = text.toLowerCase();
27+
return literals.includes(lower) || regexes.some((re) => re.test(text));
28+
};
29+
}
30+
131
/**
232
* Matches a text string against a glob pattern.
333
*

0 commit comments

Comments
 (0)