Rollup
的插件体系
初始化用户配置项
Rollup
在入口处会对用户的配置项 rawInputOptions
进行处理,得到 inputOptions
和 unsetInputOptions
。
async function rollupInternal(
rawInputOptions: RollupOptions,
watcher: RollupWatcher | null
): Promise<RollupBuild> {
const { options: inputOptions, unsetOptions: unsetInputOptions } = await getInputOptions(
rawInputOptions,
watcher !== null
);
}
其中 getInputOptions
函数会调用 getProcessedInputOptions
来处理用户配置项,得到 ProcessedInputOptions
对象。
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
};
}
处理用户配置项是通过用户传递的插件的 options
钩子来实现的。这是 Rollup
中第一个执行的钩子。rollup 会遍历所有具有 options
钩子的插件,同时按照优先级(pre
、normal
、post
)排序具有 options
钩子的用户插件。
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];
}
之后调用已排序好的插件的 options
钩子,将用户配置项(即 inputOptions
)传递给插件,插件可以在这个钩子中对配置项(即 inputOptions
)进行修改。
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;
}
至此,用户配置项已经处理完毕,得到的 inputOptions
即为 processedInputOptions
对象。之后通过 normalizeInputOptions
方法来简单整理一下后续需要用的配置项,对于插件没有 name
属性的,会自动生成一个(即 ${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> }> {
// 省略其他逻辑
const { options, unsetOptions } = await normalizeInputOptions(
processedInputOptions,
watchMode
);
normalizePlugins(options.plugins, ANONYMOUS_PLUGIN_PREFIX);
return { options, unsetOptions };
}
Rollup 内部处理的配置项也就是上述最终处理结束的 options
,对应的别名为 inputOptions
。
插件驱动程序
PluginDriver
类是 Rollup
的核心类之一,负责管理插件系统。在初始化用户配置项,紧接着就是实例化 Graph
类。
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);
// 省略其他逻辑
}
Graph
类在初始化时会创建一个唯一的 PluginDriver
实例。
class Graphs {
constructor(
private readonly options: NormalizedInputOptions,
watcher: RollupWatcher | null
) {
this.pluginDriver = new PluginDriver(
this,
options,
options.plugins,
this.pluginCache
);
}
}
PluginDriver
类在初始化会设置文件发射器,绑定相关方法,合并插件,创建插件上下文,并检查用户插件中的输入钩子。
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)
);
}
}
}
}
}
}
可以看到 PluginDriver
会为每一个用户插件构建插件与插件上下文(即 PluginContext
)的映射关系,并存储在 pluginContexts
中。
插件上下文
插件上下文是插件的运行时环境,包含了插件的配置项、文件发射器、插件缓存等信息。
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
对象贯穿着插件的整个生命周期,在插件的各个钩子中都可以访问到。其中介绍几个较有意思的方法。
this.load
方法tsfunction getPluginContext( plugin: Plugin, pluginCache: Record<string, SerializablePluginCache> | void, graph: Graph, options: NormalizedInputOptions, fileEmitter: FileEmitter, existingPluginNames: Set<string> ): PluginContext { return { // 省略其他函数 load(resolvedId) { return graph.moduleLoader.preloadModule(resolvedId); } }; }
this.load
方法会通过preloadModule
方法来预加载获取指定模块自身的一些信息(即module.info
)。需要注意的是,此时获取到的模块的时机只是解析了依赖项模块的resolvedId
,还未开始正式解析依赖项模块。tsclass ModuleLoader { public async preloadModule( resolvedId: { id: string; resolveDependencies?: boolean } & Partial< PartialNull<ModuleOptions> > ): Promise<ModuleInfo> { const module = await this.fetchModule( this.getResolvedIdWithDefaults(resolvedId, EMPTY_OBJECT)!, undefined, false, resolvedId.resolveDependencies ? RESOLVE_DEPENDENCIES : true ); return module.info; } }
resolvedId.resolveDependencies
参数,当值为true
时,this.load
方法会返回已经解析完成的模块,同时该模块的所有依赖项模块的路径(resolvedId)也已经解析完成;当值为false
时(默认),this.load
方法会返回还未开始解析依赖项模块的模块。tsasync 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; } // 省略其他代码 } 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; } // 省略其他代码 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; } }
实现细节说明
await loadPromise
代表着已经解析完当前模块,但还未开始解析当前模块的依赖项模块(包括动态依赖项和静态依赖项)。await loadAndResolveDependenciesPromise
代表着已经解析完当前模块的所有依赖项模块(包括动态依赖项和静态依赖项)的路径(resolvedId),同时触发moduleParsed
钩子,通过moduleParsed
钩子来 并发 发布当前模块的信息(即module.info
) 给订阅的插件。换句话说当模块的所有子依赖模块(包括动态依赖项模块和静态依赖项模块)的路径(resolvedId
)解析完成时,就会触发moduleParsed
钩子,发布当前模块的信息(即module.info
)。fetchModuleDependencies
方法中会解析当前模块的所有依赖项模块(包括动态依赖项和静态依赖项)的路径(resolvedId),通过fetchResolvedDependency
方法来解析依赖项模块。await fetchModuleDependencies
代表着已经解析完当前模块的所有依赖项模块(包括动态依赖项模块和静态依赖项模块)。
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
方法的作用
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
插件的执行方式
PluginDriver
类中提供了多种插件执行方式,这些执行方式都是基于 hook
钩子来实现的。
runHook
这个方法是执行指定插件的指定钩子,并返回钩子的执行结果,是插件执行方式的基础方法。
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 })
);
});
}
}
上述的 PluginContext
插件上下文实例就用在这里。
const hookResult = (handler as Function).apply(context, parameters);
当然,从上述逻辑中可以看到,插件上下文实例还可以通过 replaceContext
方法来替换。
if (replaceContext) {
context = replaceContext(context, plugin);
}
那么具体在什么地方会用到 replaceContext
方法呢?
这是提供给 Rollup
内部使用的,可以发现在 resolveIdViaPlugins
方法中会用到。
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
);
}
该方法重写了 pluginContext.resolve
方法,当在插件中调用 this.resolve
方法时,会使用重写后的 resolve
方法。再进一步分析 loadEntryModule
方法,在解析模块路径(resolvedId)时,会重写插件上下文中的 resolve
方法。
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
);
}
}
对比一下插件上下文中的 this.resolve
方法和重写后的 resolveId
方法有哪些变化,可以发现:
// 插件上下文中的 resolve 方法
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
);
};
// 重写后的 resolveId 方法
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
);
};
看上去只是透传了 skip
参数。
runHookSync
与 runHook
方法类似,只是该方法执行的是同步钩子。
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
适用的插件钩子:
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;
}
}
可以看到 hookFirstAndGetPlugin
对通过 getSortedPlugins
方法整理指定类型(hookName)的插件优先级(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];
}
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)
);
}
}
链式执行已排序好的插件,执行指定钩子,把第一个有返回结果的插件(即 result != null
)的执行结果及其当前的插件(即 [result, plugin]
)进行返回。
该执行方式常用于 resolveId
钩子中,当在插件中调用 this.resolve
方法时,会使用 hookFirstAndGetPlugin
方法来解析模块路径(resolvedId),当解析到第一个有返回结果的插件时,就会停止解析,并返回该插件的执行结果及其当前的插件。
hookFirst
适用的插件钩子:
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]);
}
}
该执行方式是对于 hookFirstAndGetPlugin
方法的补充,仅返回钩子的执行结果,不返回所解析的插件。
hookFirstSync
适用的插件钩子:
renderDynamicImport
、resolveFileUrl
、resolveImportMeta
。
与 hookFirst
方法类似,只是该方法执行的是同步钩子,依旧是同步执行已排序好的插件,执行指定钩子,把第一个有返回结果的插件(即 result != null
)的执行结果进行返回。
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
适用的插件钩子:
renderStart
、renderError
、watchChange
、closeWatcher
、moduleParsed
、buildStart
、buildEnd
、closeBundle
。
并行执行已排序好的插件,执行指定钩子,忽略钩子的返回结果。这里还支持 sequential
属性,当 sequential
属性为 true
时,会由这个插件为止先并行执行,当并行完成插件集合后再并行后续的插件集合。
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
适用的插件钩子:
transform
、renderChunk
。
链式执行已排序好的插件,执行指定钩子。每次执行的时候会将 第一个参数
和 执行结果
传递给 reduce
方法来确认下一个插件的 第一个参数
值,直到最后一个插件执行完毕,返回最终的钩子执行结果。
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
适用的插件钩子:
outputOptions
。
该方法执行逻辑与 hookReduceArg0
方法类似,只是该方法执行的是同步钩子。
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
适用的插件钩子:
banner
、footer
、intro
、outro
。
与 hookParallel
执行方式类似,并行执行已排序好的插件。不同的点在于每一个插件的执行结果均会通过 reducer
方法处理,初始值为 await initialValue
,最终返回一个累加后的结果。
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
适用的插件钩子:
augmentChunkHash
。
与 hookReduceValue
方法类似,只是该方法执行的是同步钩子。
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
适用的插件钩子:
generateBundle
。
链式执行完所有已排序好的插件,执行指定钩子,忽略钩子的返回结果。
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);
}
}
插件执行方式小结
可以发现每一个执行方式中,第一步要做的就是用户插件通过 getSortedPlugins
方法按照用户提供的优先级(pre
、normal
、post
)进行排序。
runHook
: 异步执行单个插件的指定钩子。这是最基础的执行方法,其他方法都是基于它的封装。runHookSync
: 与上述方法类似,同步执行单个插件的指定钩子。是基础的执行方法,其他同步性质的执行时机也是基于它的封装。hookFirstAndGetPlugin
: 按顺序执行插件,返回第一个有结果的插件及其结果。应用于resolveId
钩子,需要确定性的结果。hookFirst
: 与上述的hookFirstAndGetPlugin
方法类似,但只返回结果不返回执行的插件。应用于resolveDynamicImport
、load
、shouldTransformCachedModule
钩子。hookFirstSync
: 是hookFirst
的同步版本。应用于renderDynamicImport
、resolveFileUrl
、resolveImportMeta
同步钩子。hookParallel
: 并行执行所有插件的指定钩子,忽略返回值。可以通过sequential
选项控制并发执行。应用于renderStart
、renderError
、watchChange
、closeWatcher
、moduleParsed
、buildStart
、buildEnd
、closeBundle
不需要返回值的钩子。hookReduceArg0
: 链式执行插件,每次通过reduce
方法执行结果作为下一个插件的第一个参数。应用于transform
、renderChunk
需要累积处理的钩子。hookReduceArg0Sync
: 是上述hookReduceArg0
方法的同步版本。应用于outputOptions
的同步钩子。hookReduceValue
: 与hookParallel
执行方式类似,并行执行已排序好的插件。不同的点在于每一个插件的执行结果均会通过reducer
方法处理,初始值为await initialValue
,最终返回一个累加后的结果。应用于banner
、footer
、intro
、outro
钩子。hookReduceValueSync
: 是上述hookReduceValue
方法的同步版本。应用于augmentChunkHash
钩子。hookSeq
: 链式执行所有插件,忽略返回值。应用于generateBundle
需要按顺序执行但不需要返回值的钩子。
插件钩子的执行时机
通过上述的描述,可以发现 Rollup
定义的插件钩子有以下几种:
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;
}