Introduction to Plugin Mechanisms and Their Comparison
Differences between Vite Plugin Mechanism and Rollup Plugin Mechanism
The conventions for Rollup
plugins are as follows:
- Plugins should have a clear and understandable name with the
rollup-plugin-
prefix. - Include the
rollup-plugin
keyword inpackage.json
. - Plugins should be tested. We recommend using
mocha
orava
which provide out-of-the-box promise capabilities. - Use asynchronous methods whenever possible. For example, use
fs.readFile
instead offs.readFileSync
. - Plugin documentation should be written in English.
- If possible, ensure the plugin outputs correct
source mappings
. - If your plugin uses virtual modules (e.g., for helper functions), prefix module IDs with
\0
. This prevents other plugins from attempting to process virtual modules.
Rollup
plugins are driven by the PluginDriver
function, which provides the following hooks
:
hookFirst
Description: Executes the corresponding plugin hooks in a chained Promise manner while keeping the call parameters unchanged.
hookFirst
returns the first non-null or non-undefined value from plugin calls.Use Cases:
- Chain calls return the first value processed by a plugin.
- Supports asynchronous plugins.
- Parameters remain unchanged, plugins are independent.
Related plugin hooks:
load
,resolveDynamicImport
,resolveId
,shouldTransformCachedModule
resolveId
Specifically used for path resolution. Converting relative paths to absolute paths usually only requires one plugin.
ts// rollup/src/utils/resolveIdViaPlugins.ts export function resolveIdViaPlugins( source: string, importer: string | undefined, pluginDriver: PluginDriver, moduleLoaderResolveId: ( source: string, importer: string | undefined, customOptions: CustomPluginOptions | undefined, isEntry: boolean | undefined, skip: | readonly { importer: string | undefined; plugin: Plugin; source: string; }[] | null ) => Promise<ResolvedId | null>, skip: | readonly { importer: string | undefined; plugin: Plugin; source: string; }[] | null, customOptions: CustomPluginOptions | undefined, isEntry: boolean ): Promise<ResolveIdResult> { let skipped: Set<Plugin> | null = null; let replaceContext: ReplaceContext | null = null; if (skip) { skipped = new Set(); for (const skippedCall of skip) { if ( source === skippedCall.source && importer === skippedCall.importer ) { skipped.add(skippedCall.plugin); } } replaceContext = (pluginContext, plugin): PluginContext => ({ ...pluginContext, resolve: ( source, importer, { custom, isEntry, skipSelf } = BLANK ) => { return moduleLoaderResolveId( source, importer, custom, isEntry, skipSelf ? [...skip, { importer, plugin, source }] : skip ); } }); } return pluginDriver.hookFirst( 'resolveId', [source, importer, { custom: customOptions, isEntry }], replaceContext, skipped ); }
load
load
is strongly associated with virtual module loading. Usually, there is a one-to-one relationship between a virtual module and a plugin, so it only needs to be processed by one plugin.ts// rollup/src/ModuleLoader.ts source = await this.readQueue.run( async () => (await this.pluginDriver.hookFirst('load', [id])) ?? (await fs.readFile(id, 'utf8')) );
hookFirstSync
Description:
Synchronously executes the corresponding plugin hooks while keeping the call parameters unchanged.
hookFirstSync
returns the first non-null or non-undefined value from plugin calls.Use Cases:
- Chain calls return the first value processed by a plugin.
- Does not support asynchronous plugins.
- Parameters remain unchanged, plugins are independent.
Related plugin hooks:
renderDynamicImport
,resolveAssetUrl
,resolveFileUrl
,resolveImportMeta
hookParallel
Description: Executes with the same parameters, directly executes for those without return values and collects and executes in parallel for those with return values. Does not wait for the current plugin to complete execution, no return value.
Use Cases:
- Tasks need to be completed as much as possible.
- Parameters remain unchanged, does not affect plugins.
- Supports both synchronous and asynchronous plugin hooks.
Related plugin hooks:
buildEnd
,buildStart
,moduleParsed
,renderError
,renderStart
,writeBundle
,closeBundle
,closeWatcher
,watchChange
hookReduceArg0
Description: Only modifies the first parameter, chains asynchronous calls to the corresponding plugin hook, uses a reduce function to decide modifications to the first parameter, with strong sequential dependencies between plugins.
Use Cases:
- Need to chain-modify the first parameter of plugins.
- Supports asynchronous plugin calls.
- Chain calls.
Related plugin hooks:
options
,generateBundle
,renderChunk
,transform
hookReduceArg0Sync
Description: Only modifies the first parameter, chains synchronous calls to the corresponding plugin hook, uses a reduce function to decide modifications to the first parameter, with strong sequential dependencies between plugins.
Use Cases:
- Need to chain-modify the first parameter of plugins.
- Only supports synchronous plugin calls.
- Chain calls.
Related plugin hooks:
augmentChunkHash
,outputOptions
hookReduceValue
Description: Plugin parameters remain unchanged, plugins are unaware of changes to
initialValue
. Determines the value ofinitialValue
through plugin return values and thereduce
function, with chained asynchronous calls.Use Cases:
- Specifically used to handle user-defined variables (
initialValue
), meaning it can be considered when variables are affected by plugin return values. - Plugin parameters remain unchanged, does not affect plugin calls.
- Asynchronous plugins exist.
Related plugin hooks:
banner
,footer
,intro
,outro
- Specifically used to handle user-defined variables (
hookReduceValueSync
Description: Plugin parameters remain unchanged, plugins are unaware of changes to
initialValue
. Determines the value ofinitialValue
through plugin return values and thereduce
function, with chained synchronous calls.Use Cases:
- Specifically used to handle user-defined variables (
initialValue
), meaning it can be considered when variables are affected by plugin return values. - Plugin parameters remain unchanged, does not affect plugin calls.
- No asynchronous plugins exist.
Related plugin hooks:
augmentChunkHash
,outputOptions
- Specifically used to handle user-defined variables (
hookSeq
Description: Plugin parameters remain unchanged, chains calls to various plugins.
Use Cases:
- Strong plugin order requirements.
- Plugins are independent of each other.
- Asynchronous plugins exist.
Related plugin hooks:
options
,generateBundle
,renderChunk
,transform
Rollup Plugin Execution Diagram: Note that Rollup
injects context
when executing plugins to provide additional capabilities.
this.pluginContexts = new Map(
this.plugins.map(plugin => [
plugin,
getPluginContext(
plugin,
pluginCache,
graph,
options,
this.fileEmitter,
existingPluginNames
)
])
);
function runHook<H extends AsyncPluginHooks>(
hookName: H,
args: Parameters<PluginHooks[H]>,
plugin: Plugin,
permitValues: boolean,
hookContext?: ReplaceContext | null
): EnsurePromise<ReturnType<PluginHooks[H]>> {
const hook = plugin[hookName];
if (!hook) return undefined as any;
let context = this.pluginContexts.get(plugin)!;
if (hookContext) {
context = hookContext(context, plugin);
}
return Promise.resolve().then(() => {
// ...
});
}
The injected context
capabilities include:
addWatchFile: (id: string) => void
Adds other files to be watched in
watch
mode, so that when these files change, the rebuild process will be triggered.id
can be an absolute path or a relative path to the current working directory. This context method can only be used during the build phase, such asbuildStart
,load
,resolveId
,transform
.Note: Usually used to improve rebuild speed in
watch
mode. Thetransform
hook will only be triggered when the content of a given module actually changes. Usingthis.addWatchFile
intransform
, if a file change is detected, thetransform
hook will re-parse this module (whether rebuild is needed).cache
emitAsset
emitChunk
emitFile
Generates new modules that need to be included in the build output. The method returns a
referenceId
that users can use in various places to reference the newly generated module.emitFile
supports two formats:tsinterface EmittedChunk { type: 'chunk'; id: string; name?: string; fileName?: string; implicitlyLoadedAfterOneOf?: string[]; importer?: string; preserveSignature?: | 'strict' | 'allow-extension' | 'exports-only' | false; } interface EmittedAsset { type: 'asset'; name?: string; fileName?: string; source?: string | Uint8Array; }
In both formats above, either
fileName
orname
can be provided. IffileName
is provided,error
getAssetFileName
getChunkFileName
getFileName
getModuleIds
getModuleInfo
getWatchFiles
isExternal
load
meta
moduleIds
parse
resolve
resolveId
setAssetSource
warnv
Rollup
plugins call various hook functions during the build phase and output generation phase to trigger plugin hooks
.
The execution flow diagram is as follows:
Rollup Plugin Mechanism Summary
Advantages: Rollup
's plugins are similar to other large frameworks, providing unified interfaces and following the principle of convention over configuration. The 8
types of hook
loading functions make Rollup
's plugin development very flexible, though this also brings a learning curve.
Compared to Webpack
, Rollup
's plugin system is unique and does not distinguish between plugin
and loader
. The core of Rollup
's plugin mechanism is the various hook functions during the build phase and output generation phase. Internally, it implements asynchronous hook
scheduling based on Promise
.
Disadvantages:
All source code is mixed in one library, with seemingly arbitrary management of modules and utility functions.
Cannot directly transplant any of its tools to our projects. In comparison, webpack's plugin system is encapsulated into a plugin
tapable
, which is more conducive to our learning and use.
Vite's Role
Vite
leverages Rollup
's capabilities during the build phase, therefore it needs to be compatible with Rollup
's plugin ecosystem (compatibility of dev
phase plugins to the build
phase). It implements a similar plugin system by drawing inspiration from Rollup
's plugin mechanism.
Therefore, for Vite
, it implements the following capabilities:
- Implements scheduling of
Rollup
plugin hooks. - Implements a plugin context mechanism similar to
Rollup
. - Processes hook return values accordingly.
- Implements hook types.
Webpack Plugin Mechanism
Role of Tapable
Tapable
is a library similar to EventEmitter
in Node.js
, but it focuses more on custom event triggering and handling. Through Tapable
, we can register custom events and then execute them at appropriate times.