Skip to content

Introduction to Plugin Mechanisms and Their Comparison

Differences between Vite Plugin Mechanism and Rollup Plugin Mechanism

The conventions for Rollup plugins are as follows:

  1. Plugins should have a clear and understandable name with the rollup-plugin- prefix.
  2. Include the rollup-plugin keyword in package.json.
  3. Plugins should be tested. We recommend using mocha or ava which provide out-of-the-box promise capabilities.
  4. Use asynchronous methods whenever possible. For example, use fs.readFile instead of fs.readFileSync.
  5. Plugin documentation should be written in English.
  6. If possible, ensure the plugin outputs correct source mappings.
  7. If your plugin uses virtual modules (e.g., for helper functions), prefix module IDs with \0. This prevents other plugins from attempting to process virtual modules.

Rollup plugins are driven by the PluginDriver function, which provides the following hooks:

  1. hookFirst

    Description: Executes the corresponding plugin hooks in a chained Promise manner while keeping the call parameters unchanged. hookFirst returns the first non-null or non-undefined value from plugin calls.

    Use Cases:

    1. Chain calls return the first value processed by a plugin.
    2. Supports asynchronous plugins.
    3. Parameters remain unchanged, plugins are independent.

    Related plugin hooks: load, resolveDynamicImport, resolveId, shouldTransformCachedModule

    • resolveId

    Specifically used for path resolution. Converting relative paths to absolute paths usually only requires one plugin.

    ts
    // rollup/src/utils/resolveIdViaPlugins.ts
    export function resolveIdViaPlugins(
      source: string,
      importer: string | undefined,
      pluginDriver: PluginDriver,
      moduleLoaderResolveId: (
        source: string,
        importer: string | undefined,
        customOptions: CustomPluginOptions | undefined,
        isEntry: boolean | undefined,
        skip:
          | readonly {
              importer: string | undefined;
              plugin: Plugin;
              source: string;
            }[]
          | null
      ) => Promise<ResolvedId | null>,
      skip:
        | readonly {
            importer: string | undefined;
            plugin: Plugin;
            source: string;
          }[]
        | null,
      customOptions: CustomPluginOptions | undefined,
      isEntry: boolean
    ): Promise<ResolveIdResult> {
      let skipped: Set<Plugin> | null = null;
      let replaceContext: ReplaceContext | null = null;
      if (skip) {
        skipped = new Set();
        for (const skippedCall of skip) {
          if (
            source === skippedCall.source &&
            importer === skippedCall.importer
          ) {
            skipped.add(skippedCall.plugin);
          }
        }
        replaceContext = (pluginContext, plugin): PluginContext => ({
          ...pluginContext,
          resolve: (
            source,
            importer,
            { custom, isEntry, skipSelf } = BLANK
          ) => {
            return moduleLoaderResolveId(
              source,
              importer,
              custom,
              isEntry,
              skipSelf ? [...skip, { importer, plugin, source }] : skip
            );
          }
        });
      }
      return pluginDriver.hookFirst(
        'resolveId',
        [source, importer, { custom: customOptions, isEntry }],
        replaceContext,
        skipped
      );
    }
    • load

    load is strongly associated with virtual module loading. Usually, there is a one-to-one relationship between a virtual module and a plugin, so it only needs to be processed by one plugin.

    ts
    // rollup/src/ModuleLoader.ts
    source = await this.readQueue.run(
      async () =>
        (await this.pluginDriver.hookFirst('load', [id])) ??
        (await fs.readFile(id, 'utf8'))
    );
  2. hookFirstSync

    Description:

    Synchronously executes the corresponding plugin hooks while keeping the call parameters unchanged. hookFirstSync returns the first non-null or non-undefined value from plugin calls.

    Use Cases:

    1. Chain calls return the first value processed by a plugin.
    2. Does not support asynchronous plugins.
    3. Parameters remain unchanged, plugins are independent.

    Related plugin hooks: renderDynamicImport, resolveAssetUrl, resolveFileUrl, resolveImportMeta

  3. hookParallel

    Description: Executes with the same parameters, directly executes for those without return values and collects and executes in parallel for those with return values. Does not wait for the current plugin to complete execution, no return value.

    Use Cases:

    1. Tasks need to be completed as much as possible.
    2. Parameters remain unchanged, does not affect plugins.
    3. Supports both synchronous and asynchronous plugin hooks.

    Related plugin hooks: buildEnd, buildStart, moduleParsed, renderError, renderStart, writeBundle, closeBundle, closeWatcher, watchChange

  4. hookReduceArg0

    Description: Only modifies the first parameter, chains asynchronous calls to the corresponding plugin hook, uses a reduce function to decide modifications to the first parameter, with strong sequential dependencies between plugins.

    Use Cases:

    1. Need to chain-modify the first parameter of plugins.
    2. Supports asynchronous plugin calls.
    3. Chain calls.

    Related plugin hooks: options, generateBundle, renderChunk, transform

  5. hookReduceArg0Sync

    Description: Only modifies the first parameter, chains synchronous calls to the corresponding plugin hook, uses a reduce function to decide modifications to the first parameter, with strong sequential dependencies between plugins.

    Use Cases:

    1. Need to chain-modify the first parameter of plugins.
    2. Only supports synchronous plugin calls.
    3. Chain calls.

    Related plugin hooks: augmentChunkHash, outputOptions

  6. hookReduceValue

    Description: Plugin parameters remain unchanged, plugins are unaware of changes to initialValue. Determines the value of initialValue through plugin return values and the reduce function, with chained asynchronous calls.

    Use Cases:

    1. Specifically used to handle user-defined variables (initialValue), meaning it can be considered when variables are affected by plugin return values.
    2. Plugin parameters remain unchanged, does not affect plugin calls.
    3. Asynchronous plugins exist.

    Related plugin hooks: banner, footer, intro, outro

  7. hookReduceValueSync

    Description: Plugin parameters remain unchanged, plugins are unaware of changes to initialValue. Determines the value of initialValue through plugin return values and the reduce function, with chained synchronous calls.

    Use Cases:

    1. Specifically used to handle user-defined variables (initialValue), meaning it can be considered when variables are affected by plugin return values.
    2. Plugin parameters remain unchanged, does not affect plugin calls.
    3. No asynchronous plugins exist.

    Related plugin hooks: augmentChunkHash, outputOptions

  8. hookSeq

    Description: Plugin parameters remain unchanged, chains calls to various plugins.

    Use Cases:

    1. Strong plugin order requirements.
    2. Plugins are independent of each other.
    3. Asynchronous plugins exist.

    Related plugin hooks: options, generateBundle, renderChunk, transform

Rollup Plugin Execution Diagram: Note that Rollup injects context when executing plugins to provide additional capabilities.

ts
this.pluginContexts = new Map(
  this.plugins.map(plugin => [
    plugin,
    getPluginContext(
      plugin,
      pluginCache,
      graph,
      options,
      this.fileEmitter,
      existingPluginNames
    )
  ])
);

function runHook<H extends AsyncPluginHooks>(
  hookName: H,
  args: Parameters<PluginHooks[H]>,
  plugin: Plugin,
  permitValues: boolean,
  hookContext?: ReplaceContext | null
): EnsurePromise<ReturnType<PluginHooks[H]>> {
  const hook = plugin[hookName];
  if (!hook) return undefined as any;

  let context = this.pluginContexts.get(plugin)!;
  if (hookContext) {
    context = hookContext(context, plugin);
  }
  return Promise.resolve().then(() => {
    // ...
  });
}

The injected context capabilities include:

  1. addWatchFile: (id: string) => void

    Adds other files to be watched in watch mode, so that when these files change, the rebuild process will be triggered. id can be an absolute path or a relative path to the current working directory. This context method can only be used during the build phase, such as buildStart, load, resolveId, transform.

    Note: Usually used to improve rebuild speed in watch mode. The transform hook will only be triggered when the content of a given module actually changes. Using this.addWatchFile in transform, if a file change is detected, the transform hook will re-parse this module (whether rebuild is needed).

  2. cache

  3. emitAsset

  4. emitChunk

  5. emitFile

    Generates new modules that need to be included in the build output. The method returns a referenceId that users can use in various places to reference the newly generated module. emitFile supports two formats:

    ts
    interface EmittedChunk {
      type: 'chunk';
      id: string;
      name?: string;
      fileName?: string;
      implicitlyLoadedAfterOneOf?: string[];
      importer?: string;
      preserveSignature?:
        | 'strict'
        | 'allow-extension'
        | 'exports-only'
        | false;
    }
    
    interface EmittedAsset {
      type: 'asset';
      name?: string;
      fileName?: string;
      source?: string | Uint8Array;
    }

    In both formats above, either fileName or name can be provided. If fileName is provided,

  6. error

  7. getAssetFileName

  8. getChunkFileName

  9. getFileName

  10. getModuleIds

  11. getModuleInfo

  12. getWatchFiles

  13. isExternal

  14. load

  15. meta

  16. moduleIds

  17. parse

  18. resolve

  19. resolveId

  20. setAssetSource

  21. warnv

Rollup plugins call various hook functions during the build phase and output generation phase to trigger plugin hooks.

The execution flow diagram is as follows:

Rollup Plugin Mechanism Summary

Advantages: Rollup's plugins are similar to other large frameworks, providing unified interfaces and following the principle of convention over configuration. The 8 types of hook loading functions make Rollup's plugin development very flexible, though this also brings a learning curve.

Compared to Webpack, Rollup's plugin system is unique and does not distinguish between plugin and loader. The core of Rollup's plugin mechanism is the various hook functions during the build phase and output generation phase. Internally, it implements asynchronous hook scheduling based on Promise.

Disadvantages:

  1. All source code is mixed in one library, with seemingly arbitrary management of modules and utility functions.

  2. Cannot directly transplant any of its tools to our projects. In comparison, webpack's plugin system is encapsulated into a plugin tapable, which is more conducive to our learning and use.

Vite's Role

Vite leverages Rollup's capabilities during the build phase, therefore it needs to be compatible with Rollup's plugin ecosystem (compatibility of dev phase plugins to the build phase). It implements a similar plugin system by drawing inspiration from Rollup's plugin mechanism.

Therefore, for Vite, it implements the following capabilities:

  1. Implements scheduling of Rollup plugin hooks.
  2. Implements a plugin context mechanism similar to Rollup.
  3. Processes hook return values accordingly.
  4. Implements hook types.

Webpack Plugin Mechanism

Role of Tapable

Tapable is a library similar to EventEmitter in Node.js, but it focuses more on custom event triggering and handling. Through Tapable, we can register custom events and then execute them at appropriate times.

Contributors

Changelog

Discuss

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