Rollup's Plugin System
Initializing User Configuration
Rollup
processes the user's configuration rawInputOptions
at the entry point to obtain inputOptions
and unsetInputOptions
.
async function rollupInternal(
rawInputOptions: RollupOptions,
watcher: RollupWatcher | null
): Promise<RollupBuild> {
const { options: inputOptions, unsetOptions: unsetInputOptions } = await getInputOptions(
rawInputOptions,
watcher !== null
);
}
The getInputOptions
function calls getProcessedInputOptions
to process the user configuration, resulting in a ProcessedInputOptions
object.
async function getInputOptions(
initialInputOptions: InputOptions,
watchMode: boolean
): Promise<{ options: NormalizedInputOptions; unsetOptions: Set<string> }> {
if (!initialInputOptions) {
throw new Error('You must supply an options object to rollup');
}
const processedInputOptions = await getProcessedInputOptions(
initialInputOptions,
watchMode
);
const { options, unsetOptions } = await normalizeInputOptions(
processedInputOptions,
watchMode
);
normalizePlugins(options.plugins, ANONYMOUS_PLUGIN_PREFIX);
return {
options,
unsetOptions
};
}
The processing of user configuration is implemented through the options
hook of the plugins provided by the user. This is the first hook executed in Rollup
. Rollup will traverse all plugins that have the options
hook, while sorting user plugins with the options
hook according to priority (pre
, normal
, post
).
function getSortedValidatedPlugins(
hookName: keyof FunctionPluginHooks | AddonHooks,
plugins: readonly Plugin[],
validateHandler = validateFunctionPluginHandler
): Plugin[] {
const pre: Plugin[] = [];
const normal: Plugin[] = [];
const post: Plugin[] = [];
for (const plugin of plugins) {
const hook = plugin[hookName];
if (hook) {
if (typeof hook === 'object') {
validateHandler(hook.handler, hookName, plugin);
if (hook.order === 'pre') {
pre.push(plugin);
continue;
}
if (hook.order === 'post') {
post.push(plugin);
continue;
}
} else {
validateHandler(hook, hookName, plugin);
}
normal.push(plugin);
}
}
return [...pre, ...normal, ...post];
}
Then it calls the options
hook of the sorted plugins, passing the user configuration (i.e., inputOptions
) to the plugins. Plugins can modify the configuration (i.e., inputOptions
) in this hook.
async function getProcessedInputOptions(
inputOptions: InputOptions,
watchMode: boolean
): Promise<InputOptions> {
const plugins = getSortedValidatedPlugins(
'options',
await normalizePluginOption(inputOptions.plugins)
);
const logLevel = inputOptions.logLevel || LOGLEVEL_INFO;
const logger = getLogger(
plugins,
getOnLog(inputOptions, logLevel),
watchMode,
logLevel
);
for (const plugin of plugins) {
const { name, options } = plugin;
const handler = 'handler' in options! ? options.handler : options!;
const processedOptions = await handler.call(
{
debug: getLogHandler(
LOGLEVEL_DEBUG,
'PLUGIN_LOG',
logger,
name,
logLevel
),
error: (error_): never =>
error(
logPluginError(normalizeLog(error_), name, { hook: 'onLog' })
),
info: getLogHandler(
LOGLEVEL_INFO,
'PLUGIN_LOG',
logger,
name,
logLevel
),
meta: { rollupVersion, watchMode },
warn: getLogHandler(
LOGLEVEL_WARN,
'PLUGIN_WARNING',
logger,
name,
logLevel
)
},
inputOptions
);
if (processedOptions) {
inputOptions = processedOptions;
}
}
return inputOptions;
}
At this point, the user configuration has been processed, and the resulting inputOptions
is the processedInputOptions
object. Then, through the normalizeInputOptions
method, it simply organizes the configuration items needed later. For plugins without the name
property, one will be automatically generated (i.e., ${anonymousPrefix}${index + 1}
).
const normalizePluginOption: {
(plugins: InputPluginOption): Promise<Plugin[]>;
(plugins: OutputPluginOption): Promise<OutputPlugin[]>;
(plugins: unknown): Promise<any[]>;
} = async (plugins: any) => (await asyncFlatten([plugins])).filter(Boolean);
async function normalizeInputOptions(
config: InputOptions,
watchMode: boolean
): Promise<{
options: NormalizedInputOptions;
unsetOptions: Set<string>;
}> {
// These are options that may trigger special warnings or behaviour later
// if the user did not select an explicit value
const unsetOptions = new Set<string>();
const context = config.context ?? 'undefined';
const plugins = await normalizePluginOption(config.plugins);
const logLevel = config.logLevel || LOGLEVEL_INFO;
const onLog = getLogger(
plugins,
getOnLog(config, logLevel),
watchMode,
logLevel
);
const strictDeprecations = config.strictDeprecations || false;
const maxParallelFileOps = getMaxParallelFileOps(config);
const options: NormalizedInputOptions & InputOptions = {
cache: getCache(config),
context,
experimentalCacheExpiry: config.experimentalCacheExpiry ?? 10,
experimentalLogSideEffects: config.experimentalLogSideEffects || false,
external: getIdMatcher(config.external),
input: getInput(config),
logLevel,
makeAbsoluteExternalsRelative:
config.makeAbsoluteExternalsRelative ?? 'ifRelativeSource',
maxParallelFileOps,
moduleContext: getModuleContext(config, context),
onLog,
perf: config.perf || false,
plugins,
preserveEntrySignatures:
config.preserveEntrySignatures ?? 'exports-only',
preserveSymlinks: config.preserveSymlinks || false,
shimMissingExports: config.shimMissingExports || false,
strictDeprecations,
treeshake: getTreeshake(config)
};
warnUnknownOptions(
config,
[...Object.keys(options), 'onwarn', 'watch'],
'input options',
onLog,
/^(output)$/
);
return { options, unsetOptions };
}
function normalizePlugins(
plugins: readonly Plugin[],
anonymousPrefix: string
): void {
for (const [index, plugin] of plugins.entries()) {
if (!plugin.name) {
plugin.name = `${anonymousPrefix}${index + 1}`;
}
}
}
async function getInputOptions(
initialInputOptions: InputOptions,
watchMode: boolean
): Promise<{ options: NormalizedInputOptions; unsetOptions: Set<string> }> {
// Omitted other logic
const { options, unsetOptions } = await normalizeInputOptions(
processedInputOptions,
watchMode
);
normalizePlugins(options.plugins, ANONYMOUS_PLUGIN_PREFIX);
return { options, unsetOptions };
}
The configuration items processed internally by Rollup are the options
processed above, with the alias inputOptions
.
Plugin Driver Program
PluginDriver
class is one of the core classes of Rollup
. It is responsible for managing the plugin system. After initializing user configuration, the next step is to instantiate the Graph
class.
async function rollupInternal(
rawInputOptions: RollupOptions,
watcher: RollupWatcher | null
): Promise<RollupBuild> {
const { options: inputOptions, unsetOptions: unsetInputOptions } =
await getInputOptions(rawInputOptions, watcher !== null);
initialiseTimers(inputOptions);
await initWasm();
const graph = new Graph(inputOptions, watcher);
// Omitted other logic
}
Graph
class will create a unique instance of PluginDriver
when it is initialized.
class Graphs {
constructor(
private readonly options: NormalizedInputOptions,
watcher: RollupWatcher | null
) {
this.pluginDriver = new PluginDriver(
this,
options,
options.plugins,
this.pluginCache
);
}
}
PluginDriver
class will set up the file emitter, bind related methods, merge plugins, create plugin contexts, and check the input hooks in user plugins.
class PluginDriver {
constructor(
private readonly graph: Graph,
private readonly options: NormalizedInputOptions,
userPlugins: readonly Plugin[],
private readonly pluginCache:
| Record<string, SerializablePluginCache>
| undefined,
basePluginDriver?: PluginDriver
) {
this.fileEmitter = new FileEmitter(
graph,
options,
basePluginDriver && basePluginDriver.fileEmitter
);
this.emitFile = this.fileEmitter.emitFile.bind(this.fileEmitter);
this.getFileName = this.fileEmitter.getFileName.bind(this.fileEmitter);
this.finaliseAssets = this.fileEmitter.finaliseAssets.bind(
this.fileEmitter
);
this.setChunkInformation = this.fileEmitter.setChunkInformation.bind(
this.fileEmitter
);
this.setOutputBundle = this.fileEmitter.setOutputBundle.bind(
this.fileEmitter
);
this.plugins = [
...(basePluginDriver ? basePluginDriver.plugins : []),
...userPlugins
];
const existingPluginNames = new Set<string>();
this.pluginContexts = new Map(
this.plugins.map(plugin => [
plugin,
getPluginContext(
plugin,
pluginCache,
graph,
options,
this.fileEmitter,
existingPluginNames
)
])
);
if (basePluginDriver) {
for (const plugin of userPlugins) {
for (const hook of inputHooks) {
if (hook in plugin) {
options.onLog(
LOGLEVEL_WARN,
logInputHookInOutputPlugin(plugin.name, hook)
);
}
}
}
}
}
}
We can see that PluginDriver
will build a mapping relationship between the plugin and the plugin context (i.e., PluginContext
) for each user plugin and store it in pluginContexts
.
Plugin Context
Plugin context is the runtime environment for the plugin, which contains the plugin configuration, file emitter, plugin cache, etc.
export function getPluginContext(
plugin: Plugin,
pluginCache: Record<string, SerializablePluginCache> | void,
graph: Graph,
options: NormalizedInputOptions,
fileEmitter: FileEmitter,
existingPluginNames: Set<string>
): PluginContext {
const { logLevel, onLog } = options;
let cacheable = true;
if (typeof plugin.cacheKey !== 'string') {
if (
plugin.name.startsWith(ANONYMOUS_PLUGIN_PREFIX) ||
plugin.name.startsWith(ANONYMOUS_OUTPUT_PLUGIN_PREFIX) ||
existingPluginNames.has(plugin.name)
) {
cacheable = false;
} else {
existingPluginNames.add(plugin.name);
}
}
let cacheInstance: PluginCache;
if (!pluginCache) {
cacheInstance = NO_CACHE;
} else if (cacheable) {
const cacheKey = plugin.cacheKey || plugin.name;
cacheInstance = createPluginCache(
pluginCache[cacheKey] || (pluginCache[cacheKey] = Object.create(null))
);
} else {
cacheInstance = getCacheForUncacheablePlugin(plugin.name);
}
return {
addWatchFile(id) {
graph.watchFiles[id] = true;
},
cache: cacheInstance,
debug: getLogHandler(
LOGLEVEL_DEBUG,
'PLUGIN_LOG',
onLog,
plugin.name,
logLevel
),
emitFile: fileEmitter.emitFile.bind(fileEmitter),
error(error_): never {
return error(logPluginError(normalizeLog(error_), plugin.name));
},
getFileName: fileEmitter.getFileName,
getModuleIds: () => graph.modulesById.keys(),
getModuleInfo: graph.getModuleInfo,
getWatchFiles: () => Object.keys(graph.watchFiles),
info: getLogHandler(
LOGLEVEL_INFO,
'PLUGIN_LOG',
onLog,
plugin.name,
logLevel
),
load(resolvedId) {
return graph.moduleLoader.preloadModule(resolvedId);
},
meta: {
rollupVersion,
watchMode: graph.watchMode
},
parse: parseAst,
resolve(
source,
importer,
{ attributes, custom, isEntry, skipSelf } = BLANK
) {
skipSelf ??= true;
return graph.moduleLoader.resolveId(
source,
importer,
custom,
isEntry,
attributes || EMPTY_OBJECT,
skipSelf ? [{ importer, plugin, source }] : null
);
},
setAssetSource: fileEmitter.setAssetSource,
warn: getLogHandler(
LOGLEVEL_WARN,
'PLUGIN_WARNING',
onLog,
plugin.name,
logLevel
)
};
}
PluginContext
object is a part of the plugin's entire lifecycle, and can be accessed in various hooks of the plugin. Among them, we introduce several interesting methods.
this.load
methodtsfunction getPluginContext( plugin: Plugin, pluginCache: Record<string, SerializablePluginCache> | void, graph: Graph, options: NormalizedInputOptions, fileEmitter: FileEmitter, existingPluginNames: Set<string> ): PluginContext { return { // Omitted other functions load(resolvedId) { return graph.moduleLoader.preloadModule(resolvedId); } }; }
this.load
method will load the specified module instance through thepreloadModule
method, but it should be noted that:When
resolvedId.resolveDependencies = true
, it means that the path of the current module's dependency module has been resolved.this.load
method will return the current module instance that has been parsed and completed, and the path of all dependency modules of the current module (resolvedId) has also been resolved (i.e.,moduleParsed
hook has been triggered).When
resolvedId.resolveDependencies = false
is not specified (default), it means that the path of the current module's dependency module has not started to be resolved.this.load
method will return the current module instance that has been parsed and completed, but all paths of its dependency modules (resolvedId) are not resolved.
This helps to avoid deadlocks when waiting for this.load
in resolveId
hook. Pre-loading the specified module can be done without additional overhead if the specified module later becomes part of the graph because of the cache, so the specified module will not be parsed again.
async function waitForDependencyResolution(
loadPromise: LoadModulePromise
) {
const [resolveStaticDependencyPromises, resolveDynamicImportPromises] =
await loadPromise;
return Promise.all([
...resolveStaticDependencyPromises,
...resolveDynamicImportPromises
]);
}
class ModuleLoader {
private async handleExistingModule(
module: Module,
isEntry: boolean,
isPreload: PreloadType
) {
const loadPromise = this.moduleLoadPromises.get(module)!;
if (isPreload) {
return isPreload === RESOLVE_DEPENDENCIES
? waitForDependencyResolution(loadPromise)
: loadPromise;
}
// Omitted other code
}
private async fetchModule(
{
attributes,
id,
meta,
moduleSideEffects,
syntheticNamedExports
}: ResolvedId,
importer: string | undefined,
isEntry: boolean,
isPreload: PreloadType
): Promise<Module> {
const existingModule = this.modulesById.get(id);
if (existingModule instanceof Module) {
if (
importer &&
doAttributesDiffer(attributes, existingModule.info.attributes)
) {
this.options.onLog(
LOGLEVEL_WARN,
logInconsistentImportAttributes(
existingModule.info.attributes,
attributes,
id,
importer
)
);
}
await this.handleExistingModule(
existingModule,
isEntry,
isPreload
);
return existingModule;
}
// Omitted other code
const loadPromise: LoadModulePromise = this.addModuleSource(
id,
importer,
module
).then(() => [
this.getResolveStaticDependencyPromises(module),
this.getResolveDynamicImportPromises(module),
loadAndResolveDependenciesPromise
]);
const loadAndResolveDependenciesPromise =
waitForDependencyResolution(loadPromise).then(() =>
this.pluginDriver.hookParallel('moduleParsed', [module.info])
);
loadAndResolveDependenciesPromise.catch(() => {
/* avoid unhandled promise rejections */
});
this.moduleLoadPromises.set(module, loadPromise);
const resolveDependencyPromises = await loadPromise;
if (!isPreload) {
await this.fetchModuleDependencies(
module,
...resolveDependencyPromises
);
} else if (isPreload === RESOLVE_DEPENDENCIES) {
await loadAndResolveDependenciesPromise;
}
return module;
}
}
Implementation Details
await loadPromise
represents that the current module has been parsed but not started to parse the current module's dependency modules (including dynamic dependency modules and static dependency modules).await loadAndResolveDependenciesPromise
represents that all paths of the current module's dependency modules (including dynamic dependency modules and static dependency modules) have been resolved, and themoduleParsed
hook has been triggered through themoduleParsed
hook to concurrently publish the information of the current module (i.e.,module.info
) to the subscribed plugins. In other words, when the paths of all sub-dependency modules (including dynamic dependency modules and static dependency modules) of the module are resolved, themoduleParsed
hook will be triggered to publish the information of the current module (i.e.,module.info
).fetchModuleDependencies
method will parse the paths of all dependency modules (including dynamic dependency modules and static dependency modules) of the current module, and through thefetchResolvedDependency
method to parse the dependency modules.await fetchModuleDependencies
represents that all dependency modules (including dynamic dependency modules and static dependency modules) of the current module have been parsed.
class ModuleLoader {
private async fetchModuleDependencies(
module: Module,
resolveStaticDependencyPromises: readonly ResolveStaticDependencyPromise[],
resolveDynamicDependencyPromises: readonly ResolveDynamicDependencyPromise[],
loadAndResolveDependenciesPromise: Promise<void>
): Promise<void> {
if (this.modulesWithLoadedDependencies.has(module)) {
return;
}
this.modulesWithLoadedDependencies.add(module);
await Promise.all([
this.fetchStaticDependencies(module, resolveStaticDependencyPromises),
this.fetchDynamicDependencies(
module,
resolveDynamicDependencyPromises
)
]);
module.linkImports();
// To handle errors when resolving dependencies or in moduleParsed
await loadAndResolveDependenciesPromise;
}
private async fetchStaticDependencies(
module: Module,
resolveStaticDependencyPromises: readonly ResolveStaticDependencyPromise[]
): Promise<void> {
for (const dependency of await Promise.all(
resolveStaticDependencyPromises.map(resolveStaticDependencyPromise =>
resolveStaticDependencyPromise.then(([source, resolvedId]) =>
this.fetchResolvedDependency(source, module.id, resolvedId)
)
)
)) {
module.dependencies.add(dependency);
dependency.importers.push(module.id);
}
if (
!this.options.treeshake ||
module.info.moduleSideEffects === 'no-treeshake'
) {
for (const dependency of module.dependencies) {
if (dependency instanceof Module) {
dependency.importedFromNotTreeshaken = true;
}
}
}
}
private async fetchDynamicDependencies(
module: Module,
resolveDynamicImportPromises: readonly ResolveDynamicDependencyPromise[]
): Promise<void> {
const dependencies = await Promise.all(
resolveDynamicImportPromises.map(resolveDynamicImportPromise =>
resolveDynamicImportPromise.then(
async ([dynamicImport, resolvedId]) => {
if (resolvedId === null) return null;
if (typeof resolvedId === 'string') {
dynamicImport.resolution = resolvedId;
return null;
}
return (dynamicImport.resolution =
await this.fetchResolvedDependency(
relativeId(resolvedId.id),
module.id,
resolvedId
));
}
)
)
);
for (const dependency of dependencies) {
if (dependency) {
module.dynamicDependencies.add(dependency);
dependency.dynamicImporters.push(module.id);
}
}
}
}
this.load
method的作用
rollup
为每一个插件提供插件执行上下文,也就是在插件中可以通过 this
来访问到插件上下文中的方法。this.load
方法就是插件上下文中的方法之一,当在插件中调用 this.load
方法时,会加载指定路径(resolvedId
)的模块实例,不过需要注意的是:
- 当
resolvedId.resolveDependencies = true
时,意味着完成当前模块的依赖项模块的路径解析。this.load
方法会返回已经解析完成的当前模块,同时该模块的所有依赖项模块的路径(resolvedId)也已经解析完成(即moduleParsed
钩子已经触发)。 - 当
未指定 resolvedId.resolveDependencies = false
时(默认),意味着还未开始解析当前模块的依赖项模块的路径。this.load
方法会返回解析完成的当前模块实例,但其所有依赖项模块的路径(resolvedId
)均还未解析完成。
这有助于避免在 resolveId
钩子中等待 this.load
时出现死锁情况。预先加载指定模块,如果指定模块稍后成为图的一部分,则使用此上下文函数并不会带来额外的开销,因为由于缓存,指定模块不会再次解析。
emitFile
resolve
Plugin Execution Method
PluginDriver
class provides various plugin execution methods, all of which are based on hook
hooks.
runHook
This method executes the specified hook of the specified plugin and returns the execution result of the hook. It is the basic method for plugin execution.
class PluginDriver {
private runHook<H extends AsyncPluginHooks | AddonHooks>(
hookName: H,
parameters: unknown[],
plugin: Plugin,
replaceContext?: ReplaceContext | null
): Promise<unknown> {
// We always filter for plugins that support the hook before running it
const hook = plugin[hookName];
const handler = typeof hook === 'object' ? hook.handler : hook;
let context = this.pluginContexts.get(plugin)!;
if (replaceContext) {
context = replaceContext(context, plugin);
}
let action: [string, string, Parameters<any>] | null = null;
return Promise.resolve()
.then(() => {
if (typeof handler !== 'function') {
return handler;
}
const hookResult = (handler as Function).apply(context, parameters);
if (!hookResult?.then) {
// short circuit for non-thenables and non-Promises
return hookResult;
}
// Track pending hook actions to properly error out when
// unfulfilled promises cause rollup to abruptly and confusingly
// exit with a successful 0 return code but without producing any
// output, errors or warnings.
action = [plugin.name, hookName, parameters];
this.unfulfilledActions.add(action);
// Although it would be more elegant to just return hookResult here
// and put the .then() handler just above the .catch() handler below,
// doing so would subtly change the defacto async event dispatch order
// which at least one test and some plugins in the wild may depend on.
return Promise.resolve(hookResult).then(result => {
// action was fulfilled
this.unfulfilledActions.delete(action!);
return result;
});
})
.catch(error_ => {
if (action !== null) {
// action considered to be fulfilled since error being handled
this.unfulfilledActions.delete(action);
}
return error(
logPluginError(error_, plugin.name, { hook: hookName })
);
});
}
}
The above PluginContext
plugin context instance is used here.
const hookResult = (handler as Function).apply(context, parameters);
Of course, the PluginContext
instance can also be replaced through the replaceContext
method.
if (replaceContext) {
context = replaceContext(context, plugin);
}
Then, where exactly will the replaceContext
method be used?
This is provided for Rollup
internal use, and can be found in the resolveIdViaPlugins
method.
function resolveIdViaPlugins(
source: string,
importer: string | undefined,
pluginDriver: PluginDriver,
moduleLoaderResolveId: ModuleLoaderResolveId,
skip:
| readonly {
importer: string | undefined;
plugin: Plugin;
source: string;
}[]
| null,
customOptions: CustomPluginOptions | undefined,
isEntry: boolean,
attributes: Record<string, string>
): Promise<[NonNullable<ResolveIdResult>, Plugin] | null> {
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,
{ attributes, custom, isEntry, skipSelf } = BLANK
) => {
skipSelf ??= true;
return moduleLoaderResolveId(
source,
importer,
custom,
isEntry,
attributes || EMPTY_OBJECT,
skipSelf ? [...skip, { importer, plugin, source }] : skip
);
}
});
}
return pluginDriver.hookFirstAndGetPlugin(
'resolveId',
[source, importer, { attributes, custom: customOptions, isEntry }],
replaceContext,
skipped
);
}
This method rewrites the pluginContext.resolve
method, when the this.resolve
method is called in the plugin, the rewritten resolve
method will be used. Then further analyze the loadEntryModule
method, when parsing the module path (resolvedId), the resolve
method of the plugin context will be rewritten.
class ModuleLoader {
resolveId: ModuleLoaderResolveId = async (
source,
importer,
customOptions,
isEntry,
attributes,
skip = null
) =>
this.getResolvedIdWithDefaults(
this.getNormalizedResolvedIdWithoutDefaults(
this.options.external(source, importer, false)
? false
: await resolveId(
source,
importer,
this.options.preserveSymlinks,
this.pluginDriver,
this.resolveId,
skip,
customOptions,
typeof isEntry === 'boolean' ? isEntry : !importer,
attributes
),
importer,
source
),
attributes
);
private async loadEntryModule(
unresolvedId: string,
isEntry: boolean,
importer: string | undefined,
implicitlyLoadedBefore: string | null,
isLoadForManualChunks = false
): Promise<Module> {
const resolveIdResult = await resolveId(
unresolvedId,
importer,
this.options.preserveSymlinks,
this.pluginDriver,
this.resolveId,
null,
EMPTY_OBJECT,
true,
EMPTY_OBJECT
);
}
}
Compare the this.resolve
method in the plugin context and the rewritten resolveId
method, we can find:
// plugin context resolve method
this.resolve = (
source,
importer,
{ attributes, custom, isEntry, skipSelf } = BLANK
) => {
skipSelf ??= true;
return graph.moduleLoader.resolveId(
source,
importer,
custom,
isEntry,
attributes || EMPTY_OBJECT,
skipSelf ? [{ importer, plugin, source }] : null
);
};
// rewritten resolveId method
const overrideResolveId = (
source,
importer,
{ attributes, custom, isEntry, skipSelf } = BLANK
) => {
skipSelf ??= true;
return graph.moduleLoader.resolveId(
source,
importer,
custom,
isEntry,
attributes || EMPTY_OBJECT,
skipSelf ? [...skip, { importer, plugin, source }] : skip
);
};
It looks like it just passes the skip
parameter.
runHookSync
Similar to the runHook
method, except that this method executes synchronous hooks.
class PluginDriver {
/**
* Run a sync plugin hook and return the result.
* @param hookName Name of the plugin hook. Must be in `PluginHooks`.
* @param args Arguments passed to the plugin hook.
* @param plugin The acutal plugin
* @param replaceContext When passed, the plugin context can be overridden.
*/
private runHookSync<H extends SyncPluginHooks>(
hookName: H,
parameters: Parameters<FunctionPluginHooks[H]>,
plugin: Plugin,
replaceContext?: ReplaceContext
): ReturnType<FunctionPluginHooks[H]> {
const hook = plugin[hookName]!;
const handler = typeof hook === 'object' ? hook.handler : hook;
let context = this.pluginContexts.get(plugin)!;
if (replaceContext) {
context = replaceContext(context, plugin);
}
try {
return (handler as Function).apply(context, parameters);
} catch (error_: any) {
return error(logPluginError(error_, plugin.name, { hook: hookName }));
}
}
}
hookFirstAndGetPlugin
Applicable plugin hooks:
resolveId
。
class PluginDriver {
async hookFirstAndGetPlugin<
H extends AsyncPluginHooks & FirstPluginHooks
>(
hookName: H,
parameters: Parameters<FunctionPluginHooks[H]>,
replaceContext?: ReplaceContext | null,
skipped?: ReadonlySet<Plugin> | null
): Promise<
[NonNullable<ReturnType<FunctionPluginHooks[H]>>, Plugin] | null
> {
for (const plugin of this.getSortedPlugins(hookName)) {
if (skipped?.has(plugin)) continue;
const result = await this.runHook(
hookName,
parameters,
plugin,
replaceContext
);
if (result != null) return [result, plugin];
}
return null;
}
}
We can see that hookFirstAndGetPlugin
sorts the specified type (hookName) plugins according to the priority (pre
, normal
, post
) provided by the user.
function getSortedValidatedPlugins(
hookName: keyof FunctionPluginHooks | AddonHooks,
plugins: readonly Plugin[],
validateHandler = validateFunctionPluginHandler
): Plugin[] {
const pre: Plugin[] = [];
const normal: Plugin[] = [];
const post: Plugin[] = [];
for (const plugin of plugins) {
const hook = plugin[hookName];
if (hook) {
if (typeof hook === 'object') {
validateHandler(hook.handler, hookName, plugin);
if (hook.order === 'pre') {
pre.push(plugin);
continue;
}
if (hook.order === 'post') {
post.push(plugin);
continue;
}
} else {
validateHandler(hook, hookName, plugin);
}
normal.push(plugin);
}
}
return [...pre, ...normal, ...post];
}
class PluginDriver {
private getSortedPlugins(
hookName: keyof FunctionPluginHooks | AddonHooks,
validateHandler?: (
handler: unknown,
hookName: string,
plugin: Plugin
) => void
): Plugin[] {
return getOrCreate(this.sortedPlugins, hookName, () =>
getSortedValidatedPlugins(hookName, this.plugins, validateHandler)
);
}
}
Chain execution of sorted plugins, execute specified hooks, and return the first plugin with a result (i.e., result != null
) and its result.
This execution method is often used in the resolveId
hook, when calling the this.resolve
method in the plugin, the hookFirstAndGetPlugin
method will be used to parse the module path (resolvedId), when the first plugin with a result is parsed, the execution will stop and return the execution result and the current plugin.
hookFirst
Applicable plugin hooks:
resolveDynamicImport
,load
,shouldTransformCachedModule
.
class PluginDriver {
// chains, first non-null result stops and returns
hookFirst<H extends AsyncPluginHooks & FirstPluginHooks>(
hookName: H,
parameters: Parameters<FunctionPluginHooks[H]>,
replaceContext?: ReplaceContext | null,
skipped?: ReadonlySet<Plugin> | null
): Promise<ReturnType<FunctionPluginHooks[H]> | null> {
return this.hookFirstAndGetPlugin(
hookName,
parameters,
replaceContext,
skipped
).then(result => result && result[0]);
}
}
This execution method is a supplement to the hookFirstAndGetPlugin
method, only returning the execution result without returning the executed plugin.
hookFirstSync
Applicable plugin hooks:
renderDynamicImport
,resolveFileUrl
,resolveImportMeta
.
Similar to the hookFirst
method, except that this method executes synchronous hooks.
class PluginDriver {
// chains synchronously, first non-null result stops and returns
hookFirstSync<H extends SyncPluginHooks & FirstPluginHooks>(
hookName: H,
parameters: Parameters<FunctionPluginHooks[H]>,
replaceContext?: ReplaceContext
): ReturnType<FunctionPluginHooks[H]> | null {
for (const plugin of this.getSortedPlugins(hookName)) {
const result = this.runHookSync(
hookName,
parameters,
plugin,
replaceContext
);
if (result != null) return result;
}
return null;
}
}
hookParallel
Applicable plugin hooks:
renderStart
,renderError
,watchChange
,closeWatcher
,moduleParsed
,buildStart
,buildEnd
,closeBundle
.
Parallel execution of sorted plugins, execute specified hooks, and ignore the return value of the hook. Here, the sequential
attribute is also supported, when sequential
attribute is true
, it will be executed by this plugin first, and when all plugins in the plugin set are completed, it will be executed in parallel.
class PluginDriver {
// parallel, ignores returns
async hookParallel<H extends AsyncPluginHooks & ParallelPluginHooks>(
hookName: H,
parameters: Parameters<FunctionPluginHooks[H]>,
replaceContext?: ReplaceContext
): Promise<void> {
const parallelPromises: Promise<unknown>[] = [];
for (const plugin of this.getSortedPlugins(hookName)) {
if ((plugin[hookName] as { sequential?: boolean }).sequential) {
await Promise.all(parallelPromises);
parallelPromises.length = 0;
await this.runHook(hookName, parameters, plugin, replaceContext);
} else {
parallelPromises.push(
this.runHook(hookName, parameters, plugin, replaceContext)
);
}
}
await Promise.all(parallelPromises);
}
}
hookReduceArg0
Applicable plugin hooks:
transform
,renderChunk
.
Chain execution of sorted plugins, execute specified hooks. Each execution will pass the first parameter
and execution result
to the reduce
method to confirm the first parameter
value of the next plugin until the last plugin execution is completed, and the final hook execution result is returned.
class PluginDriver {
// chains, reduces returned value, handling the reduced value as the first hook argument
hookReduceArg0<H extends AsyncPluginHooks & SequentialPluginHooks>(
hookName: H,
[argument0, ...rest]: Parameters<FunctionPluginHooks[H]>,
reduce: (
reduction: Argument0<H>,
result: ReturnType<FunctionPluginHooks[H]>,
plugin: Plugin
) => Argument0<H>,
replaceContext?: ReplaceContext
): Promise<Argument0<H>> {
let promise = Promise.resolve(argument0);
for (const plugin of this.getSortedPlugins(hookName)) {
promise = promise.then(argument0 =>
this.runHook(
hookName,
[argument0, ...rest] as Parameters<FunctionPluginHooks[H]>,
plugin,
replaceContext
).then(result =>
reduce.call(
this.pluginContexts.get(plugin),
argument0,
result,
plugin
)
)
);
}
return promise;
}
}
hookReduceArg0Sync
Applicable plugin hooks:
outputOptions
.
This method executes logic similar to the hookReduceArg0
method, except that this method executes synchronous hooks.
class PluginDriver {
// chains synchronously, reduces returned value, handling the reduced value as the first hook argument
hookReduceArg0Sync<H extends SyncPluginHooks & SequentialPluginHooks>(
hookName: H,
[argument0, ...rest]: Parameters<FunctionPluginHooks[H]>,
reduce: (
reduction: Argument0<H>,
result: ReturnType<FunctionPluginHooks[H]>,
plugin: Plugin
) => Argument0<H>,
replaceContext?: ReplaceContext
): Argument0<H> {
for (const plugin of this.getSortedPlugins(hookName)) {
const parameters = [argument0, ...rest] as Parameters<
FunctionPluginHooks[H]
>;
const result = this.runHookSync(
hookName,
parameters,
plugin,
replaceContext
);
argument0 = reduce.call(
this.pluginContexts.get(plugin),
argument0,
result,
plugin
);
}
return argument0;
}
}
hookReduceValue
Applicable plugin hooks:
banner
,footer
,intro
,outro
.
Similar to the hookParallel
execution method, parallel execution of sorted plugins. The different point is that the execution result of each plugin will be processed through the reducer
method, the initial value is await initialValue
, and the final result is a cumulative result.
class PluginDriver {
// chains, reduces returned value to type string, handling the reduced value separately. permits hooks as values.
async hookReduceValue<H extends AddonHooks>(
hookName: H,
initialValue: string | Promise<string>,
parameters: Parameters<AddonHookFunction>,
reducer: (result: string, next: string) => string
): Promise<string> {
const results: string[] = [];
const parallelResults: (string | Promise<string>)[] = [];
for (const plugin of this.getSortedPlugins(
hookName,
validateAddonPluginHandler
)) {
if ((plugin[hookName] as { sequential?: boolean }).sequential) {
results.push(...(await Promise.all(parallelResults)));
parallelResults.length = 0;
results.push(await this.runHook(hookName, parameters, plugin));
} else {
parallelResults.push(this.runHook(hookName, parameters, plugin));
}
}
results.push(...(await Promise.all(parallelResults)));
return results.reduce(reducer, await initialValue);
}
}
hookReduceValueSync
Applicable plugin hooks:
augmentChunkHash
.
Similar to the hookReduceValue
method, except that this method executes synchronous hooks.
class PluginDriver {
// chains synchronously, reduces returned value to type T, handling the reduced value separately. permits hooks as values.
hookReduceValueSync<H extends SyncPluginHooks & SequentialPluginHooks, T>(
hookName: H,
initialValue: T,
parameters: Parameters<FunctionPluginHooks[H]>,
reduce: (
reduction: T,
result: ReturnType<FunctionPluginHooks[H]>,
plugin: Plugin
) => T,
replaceContext?: ReplaceContext
): T {
let accumulator = initialValue;
for (const plugin of this.getSortedPlugins(hookName)) {
const result = this.runHookSync(
hookName,
parameters,
plugin,
replaceContext
);
accumulator = reduce.call(
this.pluginContexts.get(plugin),
accumulator,
result,
plugin
);
}
return accumulator;
}
}
hookSeq
Applicable plugin hooks:
generateBundle
.
Chain execution of all sorted plugins, execute specified hooks, and ignore the return value of the hook.
class PluginDriver {
// chains, ignores returns
hookSeq<H extends AsyncPluginHooks & SequentialPluginHooks>(
hookName: H,
parameters: Parameters<FunctionPluginHooks[H]>,
replaceContext?: ReplaceContext
): Promise<void> {
let promise: Promise<unknown> = Promise.resolve();
for (const plugin of this.getSortedPlugins(hookName)) {
promise = promise.then(() =>
this.runHook(hookName, parameters, plugin, replaceContext)
);
}
return promise.then(noReturn);
}
}
Plugin Execution Method Summary
We can find that the first step in each execution method is to sort the user plugins through the getSortedPlugins
method according to the priority (pre
, normal
, post
) provided by the user.
runHook
: Asynchronous execution of a single plugin's specified hook. This is the most basic execution method, and other methods are based on its encapsulation.runHookSync
: Similar to the above method, synchronous execution of a single plugin's specified hook. It is also the basic execution method, and other synchronous execution methods are based on its encapsulation.hookFirstAndGetPlugin
: Execute plugins in sequence and return the first plugin with a result. It is applied toresolveId
hook, which requires deterministic results.hookFirst
: Similar to the abovehookFirstAndGetPlugin
method, but only returns the result without returning the executed plugin. It is applied toresolveDynamicImport
,load
,shouldTransformCachedModule
hooks.hookFirstSync
: It is the synchronous version ofhookFirst
. It is applied torenderDynamicImport
,resolveFileUrl
,resolveImportMeta
synchronous hooks.hookParallel
: Parallel execution of all plugins' specified hooks, ignoring the return value. This method supportssequential
attribute to control concurrent execution. It is applied torenderStart
,renderError
,watchChange
,closeWatcher
,moduleParsed
,buildStart
,buildEnd
,closeBundle
hooks that do not require return values.hookReduceArg0
: Chain execution of plugins, execute specified hooks. Each execution will pass thefirst parameter
andexecution result
to thereduce
method to confirm thefirst parameter
value of the next plugin until the last plugin execution is completed, and the final hook execution result is returned. It is applied totransform
,renderChunk
hooks that need to accumulate processing.hookReduceArg0Sync
: It is the synchronous version of the abovehookReduceArg0
method. It is applied tooutputOptions
synchronous hooks.hookReduceValue
: Similar to thehookParallel
execution method, parallel execution of sorted plugins. The different point is that the execution result of each plugin will be processed through thereducer
method, the initial value isawait initialValue
, and the final result is a cumulative result. It is applied tobanner
,footer
,intro
,outro
hooks.hookReduceValueSync
: It is the synchronous version of the abovehookReduceValue
method. It is applied toaugmentChunkHash
hook.hookSeq
: Chain execution of all sorted plugins, execute specified hooks, and ignore the return value. It is applied togenerateBundle
hooks that need to execute in sequence but do not require return values.
Plugin Hook Execution Timing
Through the above description, we can find that Rollup
defines the following plugin hooks:
type AddonHooks = 'banner' | 'footer' | 'intro' | 'outro';
interface FunctionPluginHooks {
augmentChunkHash: (
this: PluginContext,
chunk: RenderedChunk
) => string | void;
buildEnd: (this: PluginContext, error?: 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;
load: LoadHook;
moduleParsed: ModuleParsedHook;
onLog: (
this: MinimalPluginContext,
level: LogLevel,
log: RollupLog
) => boolean | NullValue;
options: (
this: MinimalPluginContext,
options: InputOptions
) => InputOptions | NullValue;
outputOptions: (
this: PluginContext,
options: OutputOptions
) => OutputOptions | NullValue;
renderChunk: RenderChunkHook;
renderDynamicImport: (
this: PluginContext,
options: {
customResolution: string | null;
format: InternalModuleFormat;
moduleId: string;
targetModuleId: string | null;
}
) => { left: string; right: string } | NullValue;
renderError: (this: PluginContext, error?: Error) => void;
renderStart: (
this: PluginContext,
outputOptions: NormalizedOutputOptions,
inputOptions: NormalizedInputOptions
) => void;
resolveDynamicImport: ResolveDynamicImportHook;
resolveFileUrl: ResolveFileUrlHook;
resolveId: ResolveIdHook;
resolveImportMeta: ResolveImportMetaHook;
shouldTransformCachedModule: ShouldTransformCachedModuleHook;
transform: TransformHook;
watchChange: WatchChangeHook;
writeBundle: (
this: PluginContext,
options: NormalizedOutputOptions,
bundle: OutputBundle
) => void;
}