Skip to content
Closed
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
48 changes: 48 additions & 0 deletions packages/pluginutils/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,54 @@ export default function myPlugin(options = {}) {
}
```

### createHookFilter

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
returned object is not guaranteed to be stable between versions.

Parameters: `(include?: <picomatch>, exclude?: <picomatch>, options?: Object)`<br>
Returns: `StringFilter`

_Note: Please see the TypeScript definition for complete documentation of the return type_

#### `include` and `exclude`

Type: `String | RegExp | Array[...String|RegExp]`<br>

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.

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.

#### `options`

##### `resolve`

Type: `String | Boolean | null`

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.

#### Usage

```js
import { createHookFilter } from '@rollup/pluginutils';

export default function myPlugin(options = {}) {
return {
transform: {
filter: {
// assume that the myPlugin accepts options of `options.include` and `options.exclude`
id: createHookFilter(options.include, options.exclude, {
resolve: '/my/base/dir'
})
},
handler(code) {
// implement the transformation...
}
}
};
}
```

### dataToEsm

Transforms objects into tree-shakable ES Module imports.
Expand Down
65 changes: 36 additions & 29 deletions packages/pluginutils/src/createFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { resolve, posix, isAbsolute } from 'path';

import pm from 'picomatch';

import type { CreateFilter } from '../types';
import type { CreateFilter, CreateHookFilter } from '../types';

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

const createFilter: CreateFilter = function createFilter(include?, exclude?, options?) {
const createHookFilter: CreateHookFilter = function createHookFilter(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) {
// Copy to avoid modifying user-provided regexps
return new RegExp(id);
}
// Convert the pattern to a RegExp, so that rollup doesn't resolve and
// normalize patterns. We want to control that logic ourselves via the user
// provided options.resolve value.
return pm.makeRe(getMatcherString(id, resolutionBase), { dot: true });
};

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

if (!includeMatchers.length && !excludeMatchers.length)
return (id) => typeof id === 'string' && !id.includes('\0');
excludeMatchers.push(/\0/);

return {
include: includeMatchers,
exclude: excludeMatchers
};
};

const createFilter: CreateFilter = function createFilter(include?, exclude?, options?) {
const { include: includeMatchers, exclude: excludeMatchers } = createHookFilter(
include,
exclude,
options
) as {
include: RegExp[];
exclude: RegExp[];
};

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

const pathId = normalizePath(id);

for (let i = 0; i < excludeMatchers.length; ++i) {
const matcher = excludeMatchers[i];
if (matcher instanceof RegExp) {
matcher.lastIndex = 0;
}
if (matcher.test(pathId)) return false;
const excludeMatcher = excludeMatchers[i];
excludeMatcher.lastIndex = 0;
if (excludeMatcher.test(pathId)) return false;
}

for (let i = 0; i < includeMatchers.length; ++i) {
const matcher = includeMatchers[i];
if (matcher instanceof RegExp) {
matcher.lastIndex = 0;
}
if (matcher.test(pathId)) return true;
const includeMatcher = includeMatchers[i];
includeMatcher.lastIndex = 0;
if (includeMatcher.test(pathId)) return true;
}

return !includeMatchers.length;
};
};

export { createFilter as default };
export { createFilter, createHookFilter };
4 changes: 3 additions & 1 deletion packages/pluginutils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import addExtension from './addExtension';
import attachScopes from './attachScopes';
import createFilter from './createFilter';
import { createFilter, createHookFilter } from './createFilter';
import dataToEsm from './dataToEsm';
import extractAssignedNames from './extractAssignedNames';
import makeLegalIdentifier from './makeLegalIdentifier';
Expand All @@ -11,6 +11,7 @@ export {
addExtension,
attachScopes,
createFilter,
createHookFilter,
dataToEsm,
exactRegex,
extractAssignedNames,
Expand All @@ -25,6 +26,7 @@ export default {
addExtension,
attachScopes,
createFilter,
createHookFilter,
dataToEsm,
exactRegex,
extractAssignedNames,
Expand Down
47 changes: 46 additions & 1 deletion packages/pluginutils/test/createFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { resolve as rawResolve } from 'path';

import test from 'ava';

import { createFilter, normalizePath } from '../';
import { createFilter, normalizePath, createHookFilter } from '../';

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

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

test('pass a regular expression to the include parameter with y flag', (t) => {
const filter = createFilter([/zxcvbnmasdfg/y]);
t.truthy(filter('zxcvbnmasdfg'));
t.truthy(filter('zxcvbnmasdfg'));
});

test('pass a regular expression to the exclude parameter', (t) => {
const filter = createFilter(null, [/zxcvbnmasdfg/]);
t.falsy(filter(resolve('zxcvbnmasdfg')));
Expand All @@ -199,3 +205,42 @@ test('pass a regular expression to the exclude parameter with g flag', (t) => {
t.falsy(filter(resolve('zxcvbnmasdfg')));
t.falsy(filter(resolve('zxcvbnmasdfg')));
});

test('pass a regular expression to the exclude parameter with y flag', (t) => {
const filter = createFilter(null, [/zxcvbnmasdfg/y]);
t.falsy(filter('zxcvbnmasdfg'));
t.falsy(filter('zxcvbnmasdfg'));
});

interface StringFilterSubset {
include: RegExp[];
exclude: RegExp[];
}

test('createHookFilter always returns an object with include and exclude arrays', (t) => {
const filter = createHookFilter() as StringFilterSubset;
t.true(Array.isArray((filter as any).include));
t.is((filter as any).include.length, 0);
t.true(Array.isArray((filter as any).exclude));
});

test('createHookFilter populates include and exclude from provided patterns', (t) => {
const filter = createHookFilter(['*.js'], ['*.test.js'], {
resolve: false
}) as StringFilterSubset;
t.true(filter.include.some((re) => re.test('foo.js')));
t.true(filter.exclude.some((re) => re.test('foo.test.js')));
});

test('createHookFilter resolves patterns using resolve base path', (t) => {
const filter = createHookFilter(['*.js'], ['*.test.js'], {
resolve: '/basedir'
}) as StringFilterSubset;
t.true(filter.include.some((re) => re.test('/basedir/foo.js')));
t.true(filter.exclude.some((re) => re.test('/basedir/foo.test.js')));
});

test('createHookFilter passes through RegExp', (t) => {
const filter = createHookFilter([/zxcvbnmasdfg/]) as StringFilterSubset;
t.true(filter.include.some((re) => re.source === 'zxcvbnmasdfg'));
});
28 changes: 28 additions & 0 deletions packages/pluginutils/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,32 @@ export function createFilter(
options?: { resolve?: string | false | null }
): (id: string | unknown) => boolean;

export type StringFilter<Value = string | RegExp> =
| (Value | Value[])
| {
include?: Value | Value[];
exclude?: Value | Value[];
};

/**
* 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.
*
* @param include If omitted or empty, all modules are included by default.
* @param exclude ID must not match any of the `exclude` patterns.
* @param options Optionally resolves patterns against a directory other than `process.cwd()`.
* If a `string` is specified, it will be used as the base directory.
* Relative paths are resolved against `process.cwd()` first.
* If `false`, patterns will not be resolved (useful for virtual module names).
* @returns A StringFilter object compatible with Rollup's filter configuration.
*/
export function createHookFilter(
include?: FilterPattern,
exclude?: FilterPattern,
options?: { resolve?: string | false | null }
): StringFilter;

/**
* Transforms objects into tree-shakable ES Module imports.
* @param data An object to transform into an ES module.
Expand Down Expand Up @@ -102,6 +128,7 @@ export function suffixRegex(str: string | string[], flags?: string): RegExp;
export type AddExtension = typeof addExtension;
export type AttachScopes = typeof attachScopes;
export type CreateFilter = typeof createFilter;
export type CreateHookFilter = typeof createHookFilter;
export type ExactRegex = typeof exactRegex;
export type ExtractAssignedNames = typeof extractAssignedNames;
export type MakeLegalIdentifier = typeof makeLegalIdentifier;
Expand All @@ -114,6 +141,7 @@ declare const defaultExport: {
addExtension: AddExtension;
attachScopes: AttachScopes;
createFilter: CreateFilter;
createHookFilter: CreateHookFilter;
dataToEsm: DataToEsm;
exactRegex: ExactRegex;
extractAssignedNames: ExtractAssignedNames;
Expand Down