Skip to content

Commit 7d3c005

Browse files
committed
feat(pluginutils): add createHookFilter()
When trying to port a plugin that uses `createFilter()` to hook filters it's hard keep the functionality 100% the same, as `createFilter()` does various pre-processing, like normalising paths and, resolving relative paths, exluding paths which contain \0, etc. To make porting easier, provide a `createHookFilter()` which instead of returning a function, returns a filter object that can be assigned to "filter.id" or "filter.code", and behaves the same as if createFilter() was used and the returned function used in a hook. To make sure that the implementation of `createHookFilter()` matches `createFilter()`, `createFilter()` now calls `createHookFilter()` internally and uses the same object to create the test function.
1 parent 232dcf8 commit 7d3c005

File tree

5 files changed

+161
-31
lines changed

5 files changed

+161
-31
lines changed

packages/pluginutils/README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,54 @@ export default function myPlugin(options = {}) {
139139
}
140140
```
141141

142+
### createHookFilter
143+
144+
Constructs a StringFilter object which can be passed to Rollup for module filtering. Provides the same filtering behavior as `createFilter()` but returns a declarative filter object instead of a function. The content of the
145+
returned object is not guaranteed to be stable between versions.
146+
147+
Parameters: `(include?: <picomatch>, exclude?: <picomatch>, options?: Object)`<br>
148+
Returns: `StringFilter`
149+
150+
_Note: Please see the TypeScript definition for complete documentation of the return type_
151+
152+
#### `include` and `exclude`
153+
154+
Type: `String | RegExp | Array[...String|RegExp]`<br>
155+
156+
A valid [`picomatch`](https://github.com/micromatch/picomatch#globbing-features) pattern, or array of patterns. If `options.include` is omitted or has zero length, filter will return `true` by default. Otherwise, an ID must match one or more of the `picomatch` patterns, and must not match any of the `options.exclude` patterns.
157+
158+
Note that `picomatch` patterns are very similar to [`minimatch`](https://github.com/isaacs/minimatch#readme) patterns, and in most use cases, they are interchangeable. If you have more specific pattern matching needs, you can view [this comparison table](https://github.com/micromatch/picomatch#library-comparisons) to learn more about where the libraries differ.
159+
160+
#### `options`
161+
162+
##### `resolve`
163+
164+
Type: `String | Boolean | null`
165+
166+
Optionally resolves the patterns against a directory other than `process.cwd()`. If a `String` is specified, then the value will be used as the base directory. Relative paths will be resolved against `process.cwd()` first. If `false`, then the patterns will not be resolved against any directory. This can be useful if you want to create a filter for virtual module names.
167+
168+
#### Usage
169+
170+
```js
171+
import { createHookFilter } from '@rollup/pluginutils';
172+
173+
export default function myPlugin(options = {}) {
174+
return {
175+
transform: {
176+
filter: {
177+
// assume that the myPlugin accepts options of `options.include` and `options.exclude`
178+
id: createHookFilter(options.include, options.exclude, {
179+
resolve: '/my/base/dir'
180+
})
181+
},
182+
handler(code) {
183+
// implement the transformation...
184+
}
185+
}
186+
};
187+
}
188+
```
189+
142190
### dataToEsm
143191

144192
Transforms objects into tree-shakable ES Module imports.

packages/pluginutils/src/createFilter.ts

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { resolve, posix, isAbsolute } from 'path';
22

33
import pm from 'picomatch';
44

5-
import type { CreateFilter } from '../types';
5+
import type { CreateFilter, CreateHookFilter } from '../types';
66

77
import ensureArray from './utils/ensureArray';
88
import normalizePath from './normalizePath';
@@ -23,53 +23,60 @@ function getMatcherString(id: string, resolutionBase: string | false | null | un
2323
return posix.join(basePath, normalizePath(id));
2424
}
2525

26-
const createFilter: CreateFilter = function createFilter(include?, exclude?, options?) {
26+
const createHookFilter: CreateHookFilter = function createHookFilter(include?, exclude?, options?) {
2727
const resolutionBase = options && options.resolve;
2828

29-
const getMatcher = (id: string | RegExp) =>
30-
id instanceof RegExp
31-
? id
32-
: {
33-
test: (what: string) => {
34-
// this refactor is a tad overly verbose but makes for easy debugging
35-
const pattern = getMatcherString(id, resolutionBase);
36-
const fn = pm(pattern, { dot: true });
37-
const result = fn(what);
38-
39-
return result;
40-
}
41-
};
29+
const getMatcher = (id: string | RegExp) => {
30+
if (id instanceof RegExp) {
31+
// Copy to avoid modifying user-provided regexps
32+
return new RegExp(id);
33+
}
34+
// Convert the pattern to a RegExp, so that rollup doesn't resolve and
35+
// normalize patterns. We want to control that logic ourselves via the user
36+
// provided options.resolve value.
37+
return pm.makeRe(getMatcherString(id, resolutionBase), { dot: true });
38+
};
4239

4340
const includeMatchers = ensureArray(include).map(getMatcher);
4441
const excludeMatchers = ensureArray(exclude).map(getMatcher);
4542

46-
if (!includeMatchers.length && !excludeMatchers.length)
47-
return (id) => typeof id === 'string' && !id.includes('\0');
43+
excludeMatchers.push(/\0/);
44+
45+
return {
46+
include: includeMatchers,
47+
exclude: excludeMatchers
48+
};
49+
};
50+
51+
const createFilter: CreateFilter = function createFilter(include?, exclude?, options?) {
52+
const { include: includeMatchers, exclude: excludeMatchers } = createHookFilter(
53+
include,
54+
exclude,
55+
options
56+
) as {
57+
include: RegExp[];
58+
exclude: RegExp[];
59+
};
4860

4961
return function result(id: string | unknown): boolean {
5062
if (typeof id !== 'string') return false;
51-
if (id.includes('\0')) return false;
5263

5364
const pathId = normalizePath(id);
5465

5566
for (let i = 0; i < excludeMatchers.length; ++i) {
56-
const matcher = excludeMatchers[i];
57-
if (matcher instanceof RegExp) {
58-
matcher.lastIndex = 0;
59-
}
60-
if (matcher.test(pathId)) return false;
67+
const excludeMatcher = excludeMatchers[i];
68+
excludeMatcher.lastIndex = 0;
69+
if (excludeMatcher.test(pathId)) return false;
6170
}
6271

6372
for (let i = 0; i < includeMatchers.length; ++i) {
64-
const matcher = includeMatchers[i];
65-
if (matcher instanceof RegExp) {
66-
matcher.lastIndex = 0;
67-
}
68-
if (matcher.test(pathId)) return true;
73+
const includeMatcher = includeMatchers[i];
74+
includeMatcher.lastIndex = 0;
75+
if (includeMatcher.test(pathId)) return true;
6976
}
7077

7178
return !includeMatchers.length;
7279
};
7380
};
7481

75-
export { createFilter as default };
82+
export { createFilter, createHookFilter };

packages/pluginutils/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import addExtension from './addExtension';
22
import attachScopes from './attachScopes';
3-
import createFilter from './createFilter';
3+
import { createFilter, createHookFilter } from './createFilter';
44
import dataToEsm from './dataToEsm';
55
import extractAssignedNames from './extractAssignedNames';
66
import makeLegalIdentifier from './makeLegalIdentifier';
@@ -11,6 +11,7 @@ export {
1111
addExtension,
1212
attachScopes,
1313
createFilter,
14+
createHookFilter,
1415
dataToEsm,
1516
exactRegex,
1617
extractAssignedNames,
@@ -25,6 +26,7 @@ export default {
2526
addExtension,
2627
attachScopes,
2728
createFilter,
29+
createHookFilter,
2830
dataToEsm,
2931
exactRegex,
3032
extractAssignedNames,

packages/pluginutils/test/createFilter.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { resolve as rawResolve } from 'path';
22

33
import test from 'ava';
44

5-
import { createFilter, normalizePath } from '../';
5+
import { createFilter, normalizePath, createHookFilter } from '../';
66

77
const resolve = (...parts: string[]) => normalizePath(rawResolve(...parts));
88

@@ -188,6 +188,12 @@ test('pass a regular expression to the include parameter with g flag', (t) => {
188188
t.truthy(filter(resolve('zxcvbnmasdfg')));
189189
});
190190

191+
test('pass a regular expression to the include parameter with y flag', (t) => {
192+
const filter = createFilter([/zxcvbnmasdfg/y]);
193+
t.truthy(filter('zxcvbnmasdfg'));
194+
t.truthy(filter('zxcvbnmasdfg'));
195+
});
196+
191197
test('pass a regular expression to the exclude parameter', (t) => {
192198
const filter = createFilter(null, [/zxcvbnmasdfg/]);
193199
t.falsy(filter(resolve('zxcvbnmasdfg')));
@@ -199,3 +205,42 @@ test('pass a regular expression to the exclude parameter with g flag', (t) => {
199205
t.falsy(filter(resolve('zxcvbnmasdfg')));
200206
t.falsy(filter(resolve('zxcvbnmasdfg')));
201207
});
208+
209+
test('pass a regular expression to the exclude parameter with y flag', (t) => {
210+
const filter = createFilter(null, [/zxcvbnmasdfg/y]);
211+
t.falsy(filter('zxcvbnmasdfg'));
212+
t.falsy(filter('zxcvbnmasdfg'));
213+
});
214+
215+
interface StringFilterSubset {
216+
include: RegExp[];
217+
exclude: RegExp[];
218+
}
219+
220+
test('createHookFilter always returns an object with include and exclude arrays', (t) => {
221+
const filter = createHookFilter() as StringFilterSubset;
222+
t.true(Array.isArray((filter as any).include));
223+
t.is((filter as any).include.length, 0);
224+
t.true(Array.isArray((filter as any).exclude));
225+
});
226+
227+
test('createHookFilter populates include and exclude from provided patterns', (t) => {
228+
const filter = createHookFilter(['*.js'], ['*.test.js'], {
229+
resolve: false
230+
}) as StringFilterSubset;
231+
t.true(filter.include.some((re) => re.test('foo.js')));
232+
t.true(filter.exclude.some((re) => re.test('foo.test.js')));
233+
});
234+
235+
test('createHookFilter resolves patterns using resolve base path', (t) => {
236+
const filter = createHookFilter(['*.js'], ['*.test.js'], {
237+
resolve: '/basedir'
238+
}) as StringFilterSubset;
239+
t.true(filter.include.some((re) => re.test('/basedir/foo.js')));
240+
t.true(filter.exclude.some((re) => re.test('/basedir/foo.test.js')));
241+
});
242+
243+
test('createHookFilter passes through RegExp', (t) => {
244+
const filter = createHookFilter([/zxcvbnmasdfg/]) as StringFilterSubset;
245+
t.true(filter.include.some((re) => re.source === 'zxcvbnmasdfg'));
246+
});

packages/pluginutils/types/index.d.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,32 @@ export function createFilter(
5656
options?: { resolve?: string | false | null }
5757
): (id: string | unknown) => boolean;
5858

59+
export type StringFilter<Value = string | RegExp> =
60+
| (Value | Value[])
61+
| {
62+
include?: Value | Value[];
63+
exclude?: Value | Value[];
64+
};
65+
66+
/**
67+
* Constructs a StringFilter object which can be passed to Rollup for module filtering.
68+
* Provides the same filtering behavior as `createFilter()` but returns a declarative
69+
* filter object instead of a function.
70+
*
71+
* @param include If omitted or empty, all modules are included by default.
72+
* @param exclude ID must not match any of the `exclude` patterns.
73+
* @param options Optionally resolves patterns against a directory other than `process.cwd()`.
74+
* If a `string` is specified, it will be used as the base directory.
75+
* Relative paths are resolved against `process.cwd()` first.
76+
* If `false`, patterns will not be resolved (useful for virtual module names).
77+
* @returns A StringFilter object compatible with Rollup's filter configuration.
78+
*/
79+
export function createHookFilter(
80+
include?: FilterPattern,
81+
exclude?: FilterPattern,
82+
options?: { resolve?: string | false | null }
83+
): StringFilter;
84+
5985
/**
6086
* Transforms objects into tree-shakable ES Module imports.
6187
* @param data An object to transform into an ES module.
@@ -102,6 +128,7 @@ export function suffixRegex(str: string | string[], flags?: string): RegExp;
102128
export type AddExtension = typeof addExtension;
103129
export type AttachScopes = typeof attachScopes;
104130
export type CreateFilter = typeof createFilter;
131+
export type CreateHookFilter = typeof createHookFilter;
105132
export type ExactRegex = typeof exactRegex;
106133
export type ExtractAssignedNames = typeof extractAssignedNames;
107134
export type MakeLegalIdentifier = typeof makeLegalIdentifier;
@@ -114,6 +141,7 @@ declare const defaultExport: {
114141
addExtension: AddExtension;
115142
attachScopes: AttachScopes;
116143
createFilter: CreateFilter;
144+
createHookFilter: CreateHookFilter;
117145
dataToEsm: DataToEsm;
118146
exactRegex: ExactRegex;
119147
extractAssignedNames: ExtractAssignedNames;

0 commit comments

Comments
 (0)