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();