Skip to content

Rollup 的插件体系

初始化用户配置项

Rollup 在入口处会对用户的配置项 rawInputOptions 进行处理,得到 inputOptionsunsetInputOptions

ts
async function rollupInternal(
  rawInputOptions: RollupOptions,
  watcher: RollupWatcher | null
  ): Promise<RollupBuild> {
  const { options: inputOptions, unsetOptions: unsetInputOptions } = await getInputOptions(
    rawInputOptions,
    watcher !== null
  );
}

其中 getInputOptions 函数会调用 getProcessedInputOptions 来处理用户配置项,得到 ProcessedInputOptions 对象。

ts
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 钩子的插件,同时按照优先级(prenormalpost)排序具有 options 钩子的用户插件。

ts
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)进行修改。

ts
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})。

ts
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 类。

ts
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 实例。

ts
class Graphs {
  constructor(
    private readonly options: NormalizedInputOptions,
    watcher: RollupWatcher | null
  ) {
    this.pluginDriver = new PluginDriver(
      this,
      options,
      options.plugins,
      this.pluginCache
    );
  }
}

PluginDriver 类在初始化会设置文件发射器,绑定相关方法,合并插件,创建插件上下文,并检查用户插件中的输入钩子。

ts
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 中。

插件上下文

插件上下文是插件的运行时环境,包含了插件的配置项、文件发射器、插件缓存等信息。

ts
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 对象贯穿着插件的整个生命周期,在插件的各个钩子中都可以访问到。其中介绍几个较有意思的方法。

  1. this.load 方法

    ts
    function 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,还未开始正式解析依赖项模块。

    ts
    class 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 方法会返回还未开始解析依赖项模块的模块。

    ts
    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;
        }
        // 省略其他代码
      }
      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;
      }
    }

实现细节说明

  1. await loadPromise 代表着已经解析完当前模块,但还未开始解析当前模块的依赖项模块(包括动态依赖项和静态依赖项)。
  2. await loadAndResolveDependenciesPromise 代表着已经解析完当前模块的所有依赖项模块(包括动态依赖项和静态依赖项)的路径(resolvedId),同时触发 moduleParsed 钩子,通过 moduleParsed 钩子来 并发 发布当前模块的信息(即 module.info) 给订阅的插件。换句话说当模块的所有子依赖模块(包括动态依赖项模块和静态依赖项模块)的路径(resolvedId)解析完成时,就会触发 moduleParsed 钩子,发布当前模块的信息(即 module.info)。
  3. fetchModuleDependencies 方法中会解析当前模块的所有依赖项模块(包括动态依赖项和静态依赖项)的路径(resolvedId),通过 fetchResolvedDependency 方法来解析依赖项模块。await fetchModuleDependencies 代表着已经解析完当前模块的所有依赖项模块(包括动态依赖项模块和静态依赖项模块)。
ts
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)的模块实例,不过需要注意的是:

  1. resolvedId.resolveDependencies = true 时,意味着完成当前模块的依赖项模块的路径解析。this.load 方法会返回已经解析完成的当前模块,同时该模块的所有依赖项模块的路径(resolvedId)也已经解析完成(即 moduleParsed 钩子已经触发)。
  2. 未指定 resolvedId.resolveDependencies = false 时(默认),意味着还未开始解析当前模块的依赖项模块的路径。this.load 方法会返回解析完成的当前模块实例,但其所有依赖项模块的路径(resolvedId)均还未解析完成。

这有助于避免在 resolveId 钩子中等待 this.load 时出现死锁情况。预先加载指定模块,如果指定模块稍后成为图的一部分,则使用此上下文函数并不会带来额外的开销,因为由于缓存,指定模块不会再次解析。

  1. emitFile

  2. resolve

插件的执行方式

PluginDriver 类中提供了多种插件执行方式,这些执行方式都是基于 hook 钩子来实现的。

runHook

这个方法是执行指定插件的指定钩子,并返回钩子的执行结果,是插件执行方式的基础方法。

ts
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 插件上下文实例就用在这里。

ts
const hookResult = (handler as Function).apply(context, parameters);

当然,从上述逻辑中可以看到,插件上下文实例还可以通过 replaceContext 方法来替换。

ts
if (replaceContext) {
  context = replaceContext(context, plugin);
}

那么具体在什么地方会用到 replaceContext 方法呢?

这是提供给 Rollup 内部使用的,可以发现在 resolveIdViaPlugins 方法中会用到。

ts
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 方法。

ts
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 方法有哪些变化,可以发现:

ts
// 插件上下文中的 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 方法类似,只是该方法执行的是同步钩子。

ts
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

ts
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)。

ts
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

适用的插件钩子: resolveDynamicImportloadshouldTransformCachedModule

ts
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

适用的插件钩子: renderDynamicImportresolveFileUrlresolveImportMeta

hookFirst 方法类似,只是该方法执行的是同步钩子,依旧是同步执行已排序好的插件,执行指定钩子,把第一个有返回结果的插件(即 result != null)的执行结果进行返回。

ts
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

适用的插件钩子: renderStartrenderErrorwatchChangecloseWatchermoduleParsedbuildStartbuildEndcloseBundle

并行执行已排序好的插件,执行指定钩子,忽略钩子的返回结果。这里还支持 sequential 属性,当 sequential 属性为 true 时,会由这个插件为止先并行执行,当并行完成插件集合后再并行后续的插件集合。

ts
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

适用的插件钩子: transformrenderChunk

链式执行已排序好的插件,执行指定钩子。每次执行的时候会将 第一个参数执行结果 传递给 reduce 方法来确认下一个插件的 第一个参数 值,直到最后一个插件执行完毕,返回最终的钩子执行结果。

ts
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 方法类似,只是该方法执行的是同步钩子。

ts
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

适用的插件钩子: bannerfooterintrooutro

hookParallel 执行方式类似,并行执行已排序好的插件。不同的点在于每一个插件的执行结果均会通过 reducer 方法处理,初始值为 await initialValue,最终返回一个累加后的结果。

ts
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 方法类似,只是该方法执行的是同步钩子。

ts
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

链式执行完所有已排序好的插件,执行指定钩子,忽略钩子的返回结果。

ts
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 方法按照用户提供的优先级(prenormalpost)进行排序。

  1. runHook: 异步执行单个插件的指定钩子。这是最基础的执行方法,其他方法都是基于它的封装。

  2. runHookSync: 与上述方法类似,同步执行单个插件的指定钩子。是基础的执行方法,其他同步性质的执行时机也是基于它的封装。

  3. hookFirstAndGetPlugin: 按顺序执行插件,返回第一个有结果的插件及其结果。应用于resolveId 钩子,需要确定性的结果。

  4. hookFirst: 与上述的 hookFirstAndGetPlugin 方法类似,但只返回结果不返回执行的插件。应用于 resolveDynamicImportloadshouldTransformCachedModule 钩子。

  5. hookFirstSync: 是 hookFirst 的同步版本。应用于 renderDynamicImportresolveFileUrlresolveImportMeta 同步钩子。

  6. hookParallel: 并行执行所有插件的指定钩子,忽略返回值。可以通过 sequential 选项控制并发执行。应用于 renderStartrenderErrorwatchChangecloseWatchermoduleParsedbuildStartbuildEndcloseBundle 不需要返回值的钩子。

  7. hookReduceArg0: 链式执行插件,每次通过 reduce 方法执行结果作为下一个插件的第一个参数。应用于 transformrenderChunk 需要累积处理的钩子。

  8. hookReduceArg0Sync: 是上述 hookReduceArg0 方法的同步版本。应用于 outputOptions 的同步钩子。

  9. hookReduceValue: 与 hookParallel 执行方式类似,并行执行已排序好的插件。不同的点在于每一个插件的执行结果均会通过 reducer 方法处理,初始值为 await initialValue,最终返回一个累加后的结果。应用于 bannerfooterintrooutro 钩子。

  10. hookReduceValueSync: 是上述 hookReduceValue 方法的同步版本。应用于 augmentChunkHash 钩子。

  11. hookSeq: 链式执行所有插件,忽略返回值。应用于 generateBundle 需要按顺序执行但不需要返回值的钩子。

插件钩子的执行时机

通过上述的描述,可以发现 Rollup 定义的插件钩子有以下几种:

ts
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;
}

钩子的分类

Contributors

Changelog

Discuss

Released under the CC BY-SA 4.0 License. (2619af4)