Skip to content
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

Allow using objects as hooks to change execution order #4600

Merged
merged 16 commits into from Aug 14, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
54 changes: 50 additions & 4 deletions docs/05-plugin-development.md
Expand Up @@ -51,7 +51,7 @@ export default ({
- Plugins should have a clear name with `rollup-plugin-` prefix.
- Include `rollup-plugin` keyword in `package.json`.
- Plugins should be tested. We recommend [mocha](https://github.com/mochajs/mocha) or [ava](https://github.com/avajs/ava) which support Promises out of the box.
- Use asynchronous methods when it is possible.
- Use asynchronous methods when it is possible, e.g. `fs.readFile` instead of `fs.readFileSync`.
- Document your plugin in English.
- Make sure your plugin outputs correct source mappings if appropriate.
- If your plugin uses 'virtual modules' (e.g. for helper functions), prefix the module ID with `\0`. This prevents other plugins from trying to process it.
Expand All @@ -66,12 +66,58 @@ The name of the plugin, for use in error messages and warnings.

### Build Hooks

To interact with the build process, your plugin object includes 'hooks'. Hooks are functions which are called at various stages of the build. Hooks can affect how a build is run, provide information about a build, or modify a build once complete. There are different kinds of hooks:
To interact with the build process, your plugin object includes "hooks". Hooks are functions which are called at various stages of the build. Hooks can affect how a build is run, provide information about a build, or modify a build once complete. There are different kinds of hooks:

- `async`: The hook may also return a Promise resolving to the same type of value; otherwise, the hook is marked as `sync`.
- `first`: If several plugins implement this hook, the hooks are run sequentially until a hook returns a value other than `null` or `undefined`.
- `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.
- `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:

- `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`).

```js
export default function resolveFirst() {
return {
name: 'resolve-first',
resolveId: {
order: 'pre',
handler(source) {
if (source === 'external') {
return { id: source, external: true };
}
return null;
}
}
};
}
```

If several plugins use `"pre"` or `"post"`, Rollup runs them in the user-specified order. This option can be used for all plugin hooks. For parallel hooks, it changes the order in which the synchronous part of the hook is run.

- `sequential: boolean`<br> Do not run this hook in parallel with the same hook of other plugins. Can only be used for `parallel` hooks. Using this option will make Rollup await the results of all previous plugins, then execute the plugin hook, and then run the remaining plugins in parallel again. E.g. when you have plugins `A`, `B`, `C`, `D`, `E` that all implement the same parallel hook and the middle plugin `C` has `sequential: true`, then Rollup will first run `A + B` in parallel, then `C` on its own, then `D + E` in parallel.

This can be useful when you need to run several command line tools in different [`writeBundle`](guide/en/#writebundle) hooks that depend on each other (note that if possible, it is recommended to add/remove files in the sequential [`generateBundle`](guide/en/#generatebundle) hook, though, which is faster, works with pure in-memory builds and permits other in-memory build plugins to see the files). You can combine this option with `order` for additional sorting.

```js
import { resolve } from 'node:path';
import { readdir } from 'node:fs/promises';

export default function getFilesOnDisk() {
return {
name: 'getFilesOnDisk',
writeBundle: {
sequential: true,
order: 'post',
async handler({ dir }) {
const topLevelFiles = await readdir(resolve(dir));
console.log(topLevelFiles);
}
}
};
}
```

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`](guide/en/#options), the last one is always [`buildEnd`](guide/en/#buildend). If there is a build error, [`closeBundle`](guide/en/#closebundle) will be called after that.

Expand Down
23 changes: 12 additions & 11 deletions src/rollup/rollup.ts
@@ -1,6 +1,7 @@
import { version as rollupVersion } from 'package.json';
import Bundle from '../Bundle';
import Graph from '../Graph';
import { getSortedValidatedPlugins } from '../utils/PluginDriver';
import type { PluginDriver } from '../utils/PluginDriver';
import { ensureArray } from '../utils/ensureArray';
import { errAlreadyClosed, errCannotEmitFromOptionsHook, error } from '../utils/error';
Expand Down Expand Up @@ -112,7 +113,10 @@ async function getInputOptions(
if (!rawInputOptions) {
throw new Error('You must supply an options object to rollup');
}
const rawPlugins = ensureArray(rawInputOptions.plugins) as Plugin[];
const rawPlugins = getSortedValidatedPlugins(
'options',
ensureArray(rawInputOptions.plugins) as Plugin[]
);
const { options, unsetOptions } = normalizeInputOptions(
await rawPlugins.reduce(applyOptionHook(watchMode), Promise.resolve(rawInputOptions))
);
Expand All @@ -125,16 +129,13 @@ function applyOptionHook(watchMode: boolean) {
inputOptions: Promise<GenericConfigObject>,
plugin: Plugin
): Promise<GenericConfigObject> => {
if (plugin.options) {
return (
((await plugin.options.call(
{ meta: { rollupVersion, watchMode } },
await inputOptions
)) as GenericConfigObject) || inputOptions
);
}

return inputOptions;
const handler = 'handler' in plugin.options! ? plugin.options.handler : plugin.options!;
return (
((await handler.call(
{ meta: { rollupVersion, watchMode } },
await inputOptions
)) as GenericConfigObject) || inputOptions
);
};
}

Expand Down
150 changes: 71 additions & 79 deletions src/rollup/types.d.ts
Expand Up @@ -244,7 +244,7 @@ export type ResolveIdHook = (
source: string,
importer: string | undefined,
options: { custom?: CustomPluginOptions; isEntry: boolean }
) => Promise<ResolveIdResult> | ResolveIdResult;
) => ResolveIdResult;

export type ShouldTransformCachedModuleHook = (
this: PluginContext,
Expand All @@ -257,7 +257,7 @@ export type ShouldTransformCachedModuleHook = (
resolvedSources: ResolvedIdMap;
syntheticNamedExports: boolean | string;
}
) => Promise<boolean> | boolean;
) => boolean;

export type IsExternal = (
source: string,
Expand All @@ -269,9 +269,9 @@ export type IsPureModule = (id: string) => boolean | null | void;

export type HasModuleSideEffects = (id: string, external: boolean) => boolean;

type LoadResult = SourceDescription | string | null | void;
export type LoadResult = SourceDescription | string | null | void;

export type LoadHook = (this: PluginContext, id: string) => Promise<LoadResult> | LoadResult;
export type LoadHook = (this: PluginContext, id: string) => LoadResult;

export interface TransformPluginContext extends PluginContext {
getCombinedSourcemap: () => SourceMap;
Expand All @@ -283,27 +283,22 @@ export type TransformHook = (
this: TransformPluginContext,
code: string,
id: string
) => Promise<TransformResult> | TransformResult;
) => TransformResult;

export type ModuleParsedHook = (this: PluginContext, info: ModuleInfo) => Promise<void> | void;
export type ModuleParsedHook = (this: PluginContext, info: ModuleInfo) => void;

export type RenderChunkHook = (
this: PluginContext,
code: string,
chunk: RenderedChunk,
options: NormalizedOutputOptions
) =>
| Promise<{ code: string; map?: SourceMapInput } | null>
| { code: string; map?: SourceMapInput }
| string
| null
| undefined;
) => { code: string; map?: SourceMapInput } | string | null | undefined;

export type ResolveDynamicImportHook = (
this: PluginContext,
specifier: string | AcornNode,
importer: string
) => Promise<ResolveIdResult> | ResolveIdResult;
) => ResolveIdResult;

export type ResolveImportMetaHook = (
this: PluginContext,
Expand Down Expand Up @@ -344,7 +339,7 @@ export type WatchChangeHook = (
this: PluginContext,
id: string,
change: { event: ChangeEvent }
) => Promise<void> | void;
) => void;

/**
* use this type for plugin annotation
Expand All @@ -371,32 +366,21 @@ export interface OutputBundleWithPlaceholders {
[fileName: string]: OutputAsset | OutputChunk | FilePlaceholder;
}

export interface PluginHooks extends OutputPluginHooks {
buildEnd: (this: PluginContext, err?: Error) => Promise<void> | void;
buildStart: (this: PluginContext, options: NormalizedInputOptions) => Promise<void> | void;
closeBundle: (this: PluginContext) => Promise<void> | void;
closeWatcher: (this: PluginContext) => Promise<void> | void;
load: LoadHook;
moduleParsed: ModuleParsedHook;
options: (
this: MinimalPluginContext,
options: InputOptions
) => Promise<InputOptions | null | void> | InputOptions | null | void;
resolveDynamicImport: ResolveDynamicImportHook;
resolveId: ResolveIdHook;
shouldTransformCachedModule: ShouldTransformCachedModuleHook;
transform: TransformHook;
watchChange: WatchChangeHook;
}

interface OutputPluginHooks {
export interface FunctionPluginHooks {
augmentChunkHash: (this: PluginContext, chunk: PreRenderedChunk) => string | void;
buildEnd: (this: PluginContext, err?: Error) => void;
buildStart: (this: PluginContext, options: NormalizedInputOptions) => void;
closeBundle: (this: PluginContext) => void;
closeWatcher: (this: PluginContext) => void;
generateBundle: (
this: PluginContext,
options: NormalizedOutputOptions,
bundle: OutputBundle,
isWrite: boolean
) => void | Promise<void>;
) => void;
load: LoadHook;
moduleParsed: ModuleParsedHook;
options: (this: MinimalPluginContext, options: InputOptions) => InputOptions | null | void;
outputOptions: (this: PluginContext, options: OutputOptions) => OutputOptions | null | void;
renderChunk: RenderChunkHook;
renderDynamicImport: (
Expand All @@ -408,45 +392,52 @@ interface OutputPluginHooks {
targetModuleId: string | null;
}
) => { left: string; right: string } | null | void;
renderError: (this: PluginContext, err?: Error) => Promise<void> | void;
renderError: (this: PluginContext, err?: Error) => void;
renderStart: (
this: PluginContext,
outputOptions: NormalizedOutputOptions,
inputOptions: NormalizedInputOptions
) => Promise<void> | void;
) => void;
/** @deprecated Use `resolveFileUrl` instead */
resolveAssetUrl: ResolveAssetUrlHook;
resolveDynamicImport: ResolveDynamicImportHook;
resolveFileUrl: ResolveFileUrlHook;
resolveId: ResolveIdHook;
resolveImportMeta: ResolveImportMetaHook;
shouldTransformCachedModule: ShouldTransformCachedModuleHook;
transform: TransformHook;
watchChange: WatchChangeHook;
writeBundle: (
this: PluginContext,
options: NormalizedOutputOptions,
bundle: OutputBundle
) => void | Promise<void>;
) => void;
}

export type AsyncPluginHooks =
| 'options'
| 'buildEnd'
| 'buildStart'
export type OutputPluginHooks =
| 'augmentChunkHash'
| 'generateBundle'
| 'load'
| 'moduleParsed'
| 'outputOptions'
| 'renderChunk'
| 'renderDynamicImport'
| 'renderError'
| 'renderStart'
| 'resolveDynamicImport'
| 'resolveId'
| 'shouldTransformCachedModule'
| 'transform'
| 'writeBundle'
| 'closeBundle'
| 'closeWatcher'
| 'watchChange';
| 'resolveAssetUrl'
| 'resolveFileUrl'
| 'resolveImportMeta'
| 'writeBundle';

export type InputPluginHooks = Exclude<keyof FunctionPluginHooks, OutputPluginHooks>;

export type PluginValueHooks = 'banner' | 'footer' | 'intro' | 'outro';
export type SyncPluginHooks =
| 'augmentChunkHash'
| 'outputOptions'
| 'renderDynamicImport'
| 'resolveAssetUrl'
| 'resolveFileUrl'
| 'resolveImportMeta';

export type SyncPluginHooks = Exclude<keyof PluginHooks, AsyncPluginHooks>;
export type AsyncPluginHooks = Exclude<keyof FunctionPluginHooks, SyncPluginHooks>;

export type FirstPluginHooks =
| 'load'
Expand All @@ -466,37 +457,38 @@ export type SequentialPluginHooks =
| 'renderChunk'
| 'transform';

export type ParallelPluginHooks =
| 'banner'
| 'buildEnd'
| 'buildStart'
| 'footer'
| 'intro'
| 'moduleParsed'
| 'outro'
| 'renderError'
| 'renderStart'
| 'writeBundle'
| 'closeBundle'
| 'closeWatcher'
| 'watchChange';
export type ParallelPluginHooks = Exclude<
keyof FunctionPluginHooks | AddonHooks,
FirstPluginHooks | SequentialPluginHooks
>;

interface OutputPluginValueHooks {
banner: AddonHook;
cacheKey: string;
footer: AddonHook;
intro: AddonHook;
outro: AddonHook;
}
export type AddonHooks = 'banner' | 'footer' | 'intro' | 'outro';

export interface Plugin extends Partial<PluginHooks>, Partial<OutputPluginValueHooks> {
// for inter-plugin communication
api?: any;
type MakeAsync<T extends (...a: any) => any> = (
...a: Parameters<T>
) => ReturnType<T> | Promise<ReturnType<T>>;
lukastaegert marked this conversation as resolved.
Show resolved Hide resolved

type ObjectHook<T, O = Record<string, never>> =
| T
| ({ handler: T; order?: 'pre' | 'post' | null } & O);
lukastaegert marked this conversation as resolved.
Show resolved Hide resolved

export type PluginHooks = {
[K in keyof FunctionPluginHooks]: ObjectHook<
K extends AsyncPluginHooks ? MakeAsync<FunctionPluginHooks[K]> : FunctionPluginHooks[K],
K extends ParallelPluginHooks ? { sequential?: boolean } : Record<string, never>
lukastaegert marked this conversation as resolved.
Show resolved Hide resolved
>;
};

export interface OutputPlugin
extends Partial<{ [K in OutputPluginHooks]: PluginHooks[K] }>,
Partial<{ [K in AddonHooks]: ObjectHook<AddonHook> }> {
cacheKey?: string;
name: string;
}

export interface OutputPlugin extends Partial<OutputPluginHooks>, Partial<OutputPluginValueHooks> {
name: string;
export interface Plugin extends OutputPlugin, Partial<PluginHooks> {
// for inter-plugin communication
api?: any;
}

type TreeshakingPreset = 'smallest' | 'safest' | 'recommended';
Expand Down
3 changes: 1 addition & 2 deletions src/utils/PluginContext.ts
Expand Up @@ -80,7 +80,7 @@ export function getPluginContext(
cacheInstance = getCacheForUncacheablePlugin(plugin.name);
}

const context: PluginContext = {
return {
addWatchFile(id) {
if (graph.phase >= BuildPhase.GENERATE) {
return this.error(errInvalidRollupPhaseForAddWatchFile());
Expand Down Expand Up @@ -193,5 +193,4 @@ export function getPluginContext(
options.onwarn(warning);
}
};
return context;
}