From fdaa3f1f2dd20c74f773196ca073aba1b16b97e1 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 28 Sep 2026 03:09:15 +0100 Subject: [PATCH] perf(pluginutils): compile createFilter patterns once The matcher returned by getMatcher rebuilt the glob string and compiled a new picomatch matcher inside every test() call. Compile it when the filter is created and only recompile if the working directory has changed since, so relative patterns still resolve against the cwd at call time. --- packages/pluginutils/src/createFilter.ts | 40 +++++++++++------ packages/pluginutils/test/createFilter.ts | 54 +++++++++++++++++++++++ 2 files changed, 81 insertions(+), 13 deletions(-) diff --git a/packages/pluginutils/src/createFilter.ts b/packages/pluginutils/src/createFilter.ts index 86f9d3be5..c5d506fc5 100755 --- a/packages/pluginutils/src/createFilter.ts +++ b/packages/pluginutils/src/createFilter.ts @@ -26,19 +26,33 @@ function getMatcherString(id: string, resolutionBase: string | false | null | un const createFilter: CreateFilter = function createFilter(include?, exclude?, options?) { const resolutionBase = options && options.resolve; - const getMatcher = (id: string | RegExp) => - id instanceof RegExp - ? id - : { - test: (what: string) => { - // this refactor is a tad overly verbose but makes for easy debugging - const pattern = getMatcherString(id, resolutionBase); - const fn = pm(pattern, { dot: true }); - const result = fn(what); - - return result; - } - }; + const getMatcher = (id: string | RegExp) => { + if (id instanceof RegExp) { + return id; + } + + // Compiling a pattern costs far more than matching against it, so only recompile when the cwd it resolves against changes + const compile = () => pm(getMatcherString(id, resolutionBase), { dot: true }); + + // These patterns never resolve against the cwd, so there is no need to read or track it + if (resolutionBase === false || isAbsolute(id) || id.startsWith('**')) { + return { test: compile() }; + } + + let compiledCwd = process.cwd(); + let fn = compile(); + + return { + test: (what: string) => { + const cwd = process.cwd(); + if (cwd !== compiledCwd) { + compiledCwd = cwd; + fn = compile(); + } + return fn(what); + } + }; + }; const includeMatchers = ensureArray(include).map(getMatcher); const excludeMatchers = ensureArray(exclude).map(getMatcher); diff --git a/packages/pluginutils/test/createFilter.ts b/packages/pluginutils/test/createFilter.ts index fbbb27f23..72f77dfc1 100755 --- a/packages/pluginutils/test/createFilter.ts +++ b/packages/pluginutils/test/createFilter.ts @@ -2,10 +2,64 @@ import { resolve as rawResolve } from 'path'; import { createFilter, normalizePath } from '../'; +const picomatchCalls = vi.hoisted(() => { + return { count: 0 }; +}); + +vi.mock('picomatch', async (importOriginal) => { + const actual = await importOriginal<{ default: (...args: unknown[]) => unknown }>(); + const countingPicomatch = Object.assign((...args: unknown[]) => { + picomatchCalls.count += 1; + return actual.default(...args); + }, actual.default); + return { ...actual, default: countingPicomatch }; +}); + const resolve = (...parts: string[]) => normalizePath(rawResolve(...parts)); beforeEach(() => process.chdir(__dirname)); +test('compiles each pattern once when the filter is created, not on every call', () => { + picomatchCalls.count = 0; + const filter = createFilter(['a/**', 'b/**'], ['a/excluded/**']); + expect(picomatchCalls.count).toBe(3); + + expect(filter(resolve('a/file.js'))).toBeTruthy(); + expect(filter(resolve('a/excluded/file.js'))).toBeFalsy(); + expect(filter(resolve('b/file.js'))).toBeTruthy(); + expect(filter(resolve('c/file.js'))).toBeFalsy(); + expect(picomatchCalls.count).toBe(3); +}); + +test.sequential('resolves relative patterns against the cwd at call time', () => { + picomatchCalls.count = 0; + const filter = createFilter(['*.ts']); + const fileInOriginalCwd = resolve('main.ts'); + expect(filter(fileInOriginalCwd)).toBeTruthy(); + + process.chdir(resolve(__dirname, 'fixtures')); + expect(filter(resolve('main.ts'))).toBeTruthy(); + expect(filter(fileInOriginalCwd)).toBeFalsy(); + expect(picomatchCalls.count).toBe(2); +}); + +test.sequential('does not read the cwd for patterns that do not resolve against it', () => { + const unresolved = createFilter(['*.ts'], null, { resolve: false }); + const absolute = createFilter([resolve('*.ts')]); + const globstar = createFilter(['**/*.ts']); + + const cwdSpy = vi.spyOn(process, 'cwd').mockImplementation(() => { + throw new Error('ENOENT'); + }); + try { + expect(unresolved('main.ts')).toBeTruthy(); + expect(absolute(resolve(__dirname, 'main.ts'))).toBeTruthy(); + expect(globstar('/any/where/main.ts')).toBeTruthy(); + } finally { + cwdSpy.mockRestore(); + } +}); + test('includes by default ', () => { const filter = createFilter(); expect(filter(resolve('x'))).toBeTruthy();