Skip to content
Open
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
40 changes: 27 additions & 13 deletions packages/pluginutils/src/createFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Comment thread
greptile-apps[bot] marked this conversation as resolved.
if (cwd !== compiledCwd) {
compiledCwd = cwd;
fn = compile();
}
return fn(what);
}
};
};

const includeMatchers = ensureArray(include).map(getMatcher);
const excludeMatchers = ensureArray(exclude).map(getMatcher);
Expand Down
54 changes: 54 additions & 0 deletions packages/pluginutils/test/createFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down