Skip to content

Rollup's Plugin System

Initializing User Configuration

Rollup processes the user's configuration rawInputOptions at the entry point to obtain inputOptions and unsetInputOptions.

ts
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.

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

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

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

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.

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

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

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> }> {
  // 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.

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);
  // Omitted other logic
}

Graph class will create a unique instance of PluginDriver when it is initialized.

ts
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.

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

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.

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

  1. this.load method

    ts
    function 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 the preloadModule method, but it should be noted that:

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

  3. 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.

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

  1. 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).
  2. 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 the moduleParsed hook has been triggered through the moduleParsed 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, the moduleParsed hook will be triggered to publish the information of the current module (i.e., module.info).
  3. fetchModuleDependencies method will parse the paths of all dependency modules (including dynamic dependency modules and static dependency modules) of the current module, and through the fetchResolvedDependency 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.
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 method的作用

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

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.

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

The above PluginContext plugin context instance is used here.

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

Of course, the PluginContext instance can also be replaced through the replaceContext method.

ts
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.

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

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.

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

Compare the this.resolve method in the plugin context and the rewritten resolveId method, we can find:

ts
// 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.

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

Applicable plugin hooks: 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;
  }
}

We can see that hookFirstAndGetPlugin sorts the specified type (hookName) plugins according to the priority (pre, normal, post) provided by the user.

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

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.

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]);
  }
}

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.

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

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.

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

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.

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

Applicable plugin hooks: outputOptions.

This method executes logic similar to the hookReduceArg0 method, except that this method executes synchronous hooks.

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

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.

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

Applicable plugin hooks: augmentChunkHash.

Similar to the hookReduceValue method, except that this method executes synchronous hooks.

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

Applicable plugin hooks: generateBundle.

Chain execution of all sorted plugins, execute specified hooks, and ignore the return value of the hook.

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

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.

  1. 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.

  2. 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.

  3. hookFirstAndGetPlugin: Execute plugins in sequence and return the first plugin with a result. It is applied to resolveId hook, which requires deterministic results.

  4. hookFirst: Similar to the above hookFirstAndGetPlugin method, but only returns the result without returning the executed plugin. It is applied to resolveDynamicImport, load, shouldTransformCachedModule hooks.

  5. hookFirstSync: It is the synchronous version of hookFirst. It is applied to renderDynamicImport, resolveFileUrl, resolveImportMeta synchronous hooks.

  6. hookParallel: Parallel execution of all plugins' specified hooks, ignoring the return value. This method supports sequential attribute to control concurrent execution. It is applied to renderStart, renderError, watchChange, closeWatcher, moduleParsed, buildStart, buildEnd, closeBundle hooks that do not require return values.

  7. hookReduceArg0: Chain execution of 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. It is applied to transform, renderChunk hooks that need to accumulate processing.

  8. hookReduceArg0Sync: It is the synchronous version of the above hookReduceArg0 method. It is applied to outputOptions synchronous hooks.

  9. hookReduceValue: 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. It is applied to banner, footer, intro, outro hooks.

  10. hookReduceValueSync: It is the synchronous version of the above hookReduceValue method. It is applied to augmentChunkHash hook.

  11. hookSeq: Chain execution of all sorted plugins, execute specified hooks, and ignore the return value. It is applied to generateBundle 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:

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

Hook Classification

Contributors

Changelog

Discuss

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