8000 Add support for hook filters by sapphi-red · Pull Request #5882 · rollup/rollup · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add support for hook filters #5882

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 29, 2025
Merged
29 changes: 29 additions & 0 deletions browser/LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,32 @@ Repository: https://github.com/rich-harris/magic-string
> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

---------------------------------------

## picomatch
License: MIT
By: Jon Schlinkert
Repository: micromatch/picomatch

> The MIT License (MIT)
>
> Copyright (c) 2017-present, Jon Schlinkert.
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in
> all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
> THE SOFTWARE.
38 changes: 37 additions & 1 deletion docs/plugin-development/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ To interact with the build process, your plugin object includes "hooks". Hooks a
- `sequential`: If several plugins implement this hook, all of them will be run in the specified plugin order. If a hook is `async`, subsequent hooks of this kind will wait until the current hook is resolved.
- `parallel`: If several plugins implement this hook, all of them will be run in the specified plugin order. If a hook is `async`, subsequent hooks of this kind will be run in parallel and not wait for the current hook.

Instead of a function, hooks can also be objects. In that case, the actual hook function (or value for `banner/footer/intro/outro`) must be specified as `handler`. This allows you to provide additional optional properties that change hook execution:
Instead of a function, hooks can also be objects. In that case, the actual hook function (or value for `banner/footer/intro/outro`) must be specified as `handler`. This allows you to provide additional optional properties that change hook execution or skip hook execution:

- `order: "pre" | "post" | null`<br> If there are several plugins implementing this hook, either run this plugin first (`"pre"`), last (`"post"`), or in the user-specified position (no value or `null`).

Expand Down Expand Up @@ -145,6 +145,42 @@ Instead of a function, hooks can also be objects. In that case, the actual hook
}
```

- `filter`<br> Run this plugin hook only when the specified filter returns true. This property is only available for `resolveId`, `load`, `transform`. The `code` filter is only available for `transform` hook. The `id` filter supports [picomatch patterns](https://github.com/micromatch/picomatch#globbing-features) except for `resolveId` hook.

```ts
type StringOrRegExp = string | RegExp;
type StringFilter<Value = StringOrRegExp> =
| MaybeArray<Value>
| {
include?: MaybeArray<Value>;
exclude?: MaybeArray<Value>;
};

interface HookFilter {
id?: StringFilter;
code?: StringFilter;
}
```

```js twoslash
/** @returns {import('rollup').Plugin} */
// ---cut---
export default function jsxAdditionalTransform() {
return {
name: 'jsxAdditionalTransform' 8000 ;,
transform: {
filter: {
id: '*.jsx',
code: '<Custom'
},
handler(code) {
// transform <Custom /> here
}
}
};
}
```

Build hooks are run during the build phase, which is triggered by `rollup.rollup(inputOptions)`. They are mainly concerned with locating, providing and transforming input files before they are processed by Rollup. The first hook of the build phase is [`options`](#options), the last one is always [`buildEnd`](#buildend). If there is a build error, [`closeBundle`](#closebundle) will be called after that.

<style>
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
"@shikijs/vitepress-twoslash": "^3.2.1",
"@types/mocha": "^10.0.10",
"@types/node": "^18.19.83",
"@types/picomatch": "^3.0.2",
"@types/semver": "^7.5.8",
"@types/yargs-parser": "^21.0.3",
"@vue/language-server": "^2.2.8",
Expand Down Expand Up @@ -177,6 +178,7 @@
"npm-audit-resolver": "^3.0.0-RC.0",
"nyc": "^17.1.0",
"picocolors": "^1.1.1",
"picomatch": "^4.0.2",
"pinia": "^3.0.1",
"prettier": "^3.5.3",
"prettier-plugin-organize-imports": "^4.1.0",
Expand Down
25 changes: 24 additions & 1 deletion src/rollup/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,20 @@ export interface PluginContextMeta {
watchMode: boolean;
}

export type StringOrRegExp = string | RegExp;

export type StringFilter<Value = StringOrRegExp> =
| MaybeArray<Value>
| {
include?: MaybeArray<Value>;
exclude?: MaybeArray<Value>;
};

export interface HookFilter {
id?: StringFilter;
code?: StringFilter;
}

export interface ResolvedId extends ModuleOptions {
external: boolean | 'absolute';
id: string;
Expand Down Expand Up @@ -526,11 +540,20 @@ type MakeAsync<Function_> = Function_ extends (
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export type ObjectHook<T, O = {}> = T | ({ handler: T; order?: 'pre' | 'post' | null } & O);

export type HookFilterExtension<K extends keyof FunctionPluginHooks> = K extends 'transform'
? { filter?: HookFilter }
: K extends 'load'
? { filter?: Pick<HookFilter, 'id'> }
: K extends 'resolveId'
? { filter?: { id: StringFilter<RegExp> } }
: // eslint-disable-next-line @typescript-eslint/no-empty-object-type
{};

export type PluginHooks = {
[K in keyof FunctionPluginHooks]: ObjectHook<
K extends AsyncPluginHooks ? MakeAsync<FunctionPluginHooks[K]> : FunctionPluginHooks[K],
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
K extends ParallelPluginHooks ? { sequential?: boolean } : {}
HookFilterExtension<K> & (K extends ParallelPluginHooks ? { sequential?: boolean } : {})
>;
};

Expand Down
33 changes: 33 additions & 0 deletions src/utils/PluginDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
EmitFile,
FirstPluginHooks,
FunctionPluginHooks,
HookFilter,
InputPluginHooks,
NormalizedInputOptions,
NormalizedOutputOptions,
Expand All @@ -30,6 +31,12 @@ import {
logPluginError
} from './logs';
import type { OutputBundleWithPlaceholders } from './outputBundle';
import {
createFilterForId,
createFilterForTransform,
type PluginFilter,
type TransformHookFilter
} from './pluginFilter';

/**
* Coerce a promise union to always be a promise.
Expand Down Expand Up @@ -79,6 +86,10 @@ export class PluginDriver {
private readonly plugins: readonly Plugin[];
private readonly sortedPlugins = new Map<AsyncPluginHooks, Plugin[]>();
private readonly unfulfilledActions = new Set<HookAction>();
private readonly compiledPluginFilters = {
idOnlyFilter: new WeakMap<Pick<HookFilter, 'id'>, PluginFilter | undefined>(),
transformFilter: new WeakMap<HookFilter, TransformHookFilter | undefined>()
};

constructor(
private readonly graph: Graph,
Expand Down Expand Up @@ -319,6 +330,28 @@ export class PluginDriver {
const hook = plugin[hookName];
const handler = typeof hook === 'object' ? hook.handler : hook;

if (typeof hook === 'object' && 'filter' in hook && hook.filter) {
if (hookName === 'transform') {
const filter = hook.filter as HookFilter;
const hookParameters = parameters as Parameters<FunctionPluginHooks['transform']>;
const compiledFilter = getOrCreate(this.compiledPluginFilters.transformFilter, filter, () =>
createFilterForTransform(filter.id, filter.code)
);
if (compiledFilter && !compiledFilter(hookParameters[1], hookParameters[0])) {
return Promise.resolve();
}
} else if (hookName === 'resolveId' || hookName === 'load') {
const filter = hook.filter;
const hookParameters = parameters as Parameters<FunctionPluginHooks['load' | 'resolveId']>;
const compiledFilter = getOrCreate(this.compiledPluginFilters.idOnlyFilter, filter, () =>
createFilterForId(filter.id)
);
if (compiledFilter && !compiledFilter(hookParameters[0])) {
return Promise.resolve();
}
}
}

let context = this.pluginContexts.get(plugin)!;
if (replaceContext) {
context = replaceContext(context, plugin);
Expand Down
12 changes: 11 additions & 1 deletion src/utils/getOrCreate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
export function getOrCreate<K, V>(map: Map<K, V>, key: K, init: () => V): V {
export function getOrCreate<K extends object, V>(
map: Map<K, V> | WeakMap<K, V>,
key: K,
init: () => V
): V;
export function getOrCreate<K, V>(map: Map<K, V>, key: K, init: () => V): V;
export function getOrCreate<K extends object, V>(
map: Map<K, V> | WeakMap<K, V>,
key: K,
init: () => V
): V {
const existing = map.get(key);
if (existing !== undefined) {
return existing;
Expand Down
140 changes: 140 additions & 0 deletions src/utils/pluginFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import picomatch from 'picomatch';
import type { StringFilter, StringOrRegExp } from '../rollup/types';
import { ensureArray } from './ensureArray';
import { isAbsolute, normalize, resolve } from './path';

const FALLBACK_TRUE = 1;
const FALLBACK_FALSE = 0;
type FallbackValues = typeof FALLBACK_TRUE | typeof FALLBACK_FALSE;

type PluginFilterWithFallback = (input: string) => boolean | FallbackValues;

export type PluginFilter = (input: string) => boolean;
export type TransformHookFilter = (id: string, code: string) => boolean;

interface NormalizedStringFilter {
include?: StringOrRegExp[];
exclude?: StringOrRegExp[];
}

function getMatcherString(glob: string, cwd: string) {
if (glob.startsWith('**') || isAbsolute(glob)) {
return normalize(glob);
}

const resolved = resolve(cwd, glob);
return normalize(resolved);
}

function patternToIdFilter(pattern: StringOrRegExp): PluginFilter {
if (pattern instanceof RegExp) {
return (id: string) => {
const normalizedId = normalize(id);
const result = pattern.test(normalizedId);
pattern.lastIndex = 0;
return result;
};
}
const cwd = process.cwd();
const glob = getMatcherString(pattern, cwd);
const matcher = picomatch(glob, { dot: true });
return (id: string) => {
const normalizedId = normalize(id);
return matcher(normalizedId);
};
}

function patternToCodeFilter(pattern: StringOrRegExp): PluginFilter {
if (pattern instanceof RegExp) {
return (code: string) => {
const result = pattern.test(code);
pattern.lastIndex = 0;
return result;
};
}
return (code: string) => code.includes(pattern);
}

function createFilter(
exclude: PluginFilter[] | undefined,
include: PluginFilter[] | undefined
): PluginFilterWithFallback | undefined {
if (!exclude && !include) {
return;
}

return input => {
if (exclude?.some(filter => filter(input))) {
return false;
}
if (include?.some(filter => filter(input))) {
return true;
}
return !!include && include.length > 0 ? FALLBACK_FALSE : FALLBACK_TRUE;
};
}

function normalizeFilter(filter: StringFilter): NormalizedStringFilter {
if (typeof filter === 'string' || filter instanceof RegExp) {
return {
include: [filter]
};
}
if (Array.isArray(filter)) {
return {
include: ensureArray(filter)
};
}
return {
exclude: filter.exclude ? ensureArray(filter.exclude) : undefined,
include: filter.include ? ensureArray(filter.include) : undefined
};
}

function createIdFilter(filter: StringFilter | undefined): PluginFilterWithFallback | undefined {
if (!filter) return;
const { exclude, include } = normalizeFilter(filter);
const excludeFilter = exclude?.map(patternToIdFilter);
const includeFilter = include?.map(patternToIdFilter);
return createFilter(excludeFilter, includeFilter);
}

function createCodeFilter(filter: StringFilter | undefined): PluginFilterWithFallback | undefined {
if (!filter) return;
const { exclude, include } = normalizeFilter(filter);
const excludeFilter = exclude?.map(patternToCodeFilter);
const includeFilter = include?.map(patternToCodeFilter);
return createFilter(excludeFilter, includeFilter);
}

export function createFilterForId(filter: StringFilter | undefined): PluginFilter | undefined {
const filterFunction = createIdFilter(filter);
return filterFunction ? id => !!filterFunction(id) : undefined;
}

export function createFilterForTransform(
idFilter: StringFilter | undefined,
codeFilter: StringFilter | undefined
): TransformHookFilter | undefined {
if (!idFilter && !codeFilter) return;
const idFilterFunction = createIdFilter(idFilter);
const codeFilterFunction = createCodeFilter(codeFilter);
return (id, code) => {
let fallback = true;
if (idFilterFunction) {
const idResult = idFilterFunction(id);
if (typeof idResult === 'boolean') {
return idResult;
}
fallback &&= !!idResult;
}
if (codeFilterFunction) {
const codeResult = codeFilterFunction(code);
if (typeof codeResult === 'boolean') {
return codeResult;
}
fallback &&= !!codeResult;
}
return fallback;
};
}
Loading
0