Skip to content

Commit 3363f86

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 3363f86

File tree

5 files changed

+156
-31
lines changed

5 files changed

+156
-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: 43 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,67 @@ 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+
return new RegExp(id);
32+
}
33+
return getMatcherString(id, resolutionBase);
34+
};
4235

4336
const includeMatchers = ensureArray(include).map(getMatcher);
4437
const excludeMatchers = ensureArray(exclude).map(getMatcher);
4538

46-
if (!includeMatchers.length && !excludeMatchers.length)
47-
return (id) => typeof id === 'string' && !id.includes('\0');
39+
excludeMatchers.push(/\0/);
40+
41+
return {
42+
include: includeMatchers,
43+
exclude: excludeMatchers
44+
};
45+
};
46+
47+
const createFilter: CreateFilter = function createFilter(include?, exclude?, options?) {
48+
const { include: includeFilters, exclude: excludeFilters } = createHookFilter(
49+
include,
50+
exclude,
51+
options
52+
) as {
53+
include: (string | RegExp)[];
54+
exclude: (string | RegExp)[];
55+
};
56+
57+
const getMatcher = (id: string | RegExp) => {
58+
if (id instanceof RegExp) {
59+
// Ensure the global flag is not set, so that state is not shared across
60+
// test() calls
61+
return new RegExp(id.source, id.flags.replace('g', ''));
62+
}
63+
const fn = pm(id, { dot: true });
64+
return {
65+
test: (what: string) => fn(what)
66+
};
67+
};
68+
69+
const includeMatchers = includeFilters.map((id) => getMatcher(id));
70+
const excludeMatchers = excludeFilters.map((id) => getMatcher(id));
4871

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

5375
const pathId = normalizePath(id);
5476

5577
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;
78+
if (excludeMatchers[i].test(pathId)) return false;
6179
}
6280

6381
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;
82+
if (includeMatchers[i].test(pathId)) return true;
6983
}
7084

7185
return !includeMatchers.length;
7286
};
7387
};
7488

75-
export { createFilter as default };
89+
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: 34 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

@@ -199,3 +199,36 @@ test('pass a regular expression to the exclude parameter with g flag', (t) => {
199199
t.falsy(filter(resolve('zxcvbnmasdfg')));
200200
t.falsy(filter(resolve('zxcvbnmasdfg')));
201201
});
202+
203+
interface StringFilterSubset {
204+
include: (string | RegExp)[];
205+
exclude: (string | RegExp)[];
206+
}
207+
208+
test('createHookFilter always returns an object with include and exclude arrays', (t) => {
209+
const filter = createHookFilter() as StringFilterSubset;
210+
t.true(Array.isArray((filter as any).include));
211+
t.is((filter as any).include.length, 0);
212+
t.true(Array.isArray((filter as any).exclude));
213+
});
214+
215+
test('createHookFilter populates include and exclude from provided patterns', (t) => {
216+
const filter = createHookFilter(['*.js'], ['*.test.js'], {
217+
resolve: false
218+
}) as StringFilterSubset;
219+
t.true(filter.include.some((pattern) => pattern === '*.js'));
220+
t.true(filter.exclude.some((pattern) => pattern === '*.test.js'));
221+
});
222+
223+
test('createHookFilter resolves patterns using resolve base path', (t) => {
224+
const filter = createHookFilter(['*.js'], ['*.test.js'], {
225+
resolve: '/basedir'
226+
}) as StringFilterSubset;
227+
t.true(filter.include.some((pattern) => pattern === '/basedir/*.js'));
228+
t.true(filter.exclude.some((pattern) => pattern === '/basedir/*.test.js'));
229+
});
230+
231+
test('createHookFilter passes through RegExp', (t) => {
232+
const filter = createHookFilter([/zxcvbnmasdfg/]) as StringFilterSubset;
233+
t.true(filter.include.some((pattern) => (pattern as RegExp).source === 'zxcvbnmasdfg'));
234+
});

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)