preface

This article documents how rollup develops a plug-in. Documentation Reference: Official documentation.

Rollup plugin overview

First, the Rollup plug-in is an object that needs to follow some of the rollup rules. Rollup also provides properties and hook functions for plug-ins. Before developing rollup plug-ins, check the list of plug-ins to see if they are available.

Convention of the rollup plug-in

Let’s start with the rollup convention:

  • The plug-in should have an explicit name prefixed with rollup-plugin–.
  • Contains the rollup-plugin keyword package.json.
  • Plug-ins should be tested. We recommend Mocha or AVA, which support promises out of the box.
  • Use asynchronous methods whenever possible.
  • Document your plug-in in English.
  • If appropriate, make sure your plug-in outputs the correct source mapping.
  • If your plug-in uses a “virtual module” (for example, for auxiliary functions), prefix \0. This prevents other plug-ins from trying to handle it.

The rollup plug-in property value

The rollup plug-in has a single property value: name, a string value, the plug-in name, which is used for error messages and warnings.

Hooks for rollup plugins

Rollup plugin hooks are divided into build hooks and Output generation hooks. Hooks are functions that are called at various stages of build and production.

build hooks

Rollup.rollup (inputOptions). These hooks locate, supply, and transform input files before processing. Hooks affect how builds run. There are different kinds of hooks:

  • Async: Asynchronous hook. Hooks of this type need to return a promise resolve value. Otherwise it will be marked as sync

  • Sync: synchronization hook

  • First: When the plugin executes this type of hook, the hook is executed sequentially until a bull or undefind is returned

  • Sequential: This type of hook runs in the specified order. If one of the hooks is asynchronous, subsequent hooks wait until the current hook is parsed

  • Parallel: Parallel: This type of hook, like the sequential type above, is asynchronous when a hook is executed sequentially, and subsequent hooks do not have to wait and can be executed in parallel.

These types of hooks are clearly described in the official documentation and have flow charts, as shown below:

The first hook in the build phase is Options, and the last one is always buildEnd. CloseBundle is called after this if a build error occurs. The meaning and usage of hooks are no longer posted here, see documentation.

Output Generation Hooks

You can provide information about the generated packages and modify the build when you are done. These work in the same way and of the same type as Build Hooks, but bundle.write(outputOptions) is called each time bundle.generate(outputOptions)or is called. Plug-ins that use only output generation hooks can also be passed in with output options and therefore run only against certain outputs.

The first hook in the output generation phase is outputOptions, the output bundle.generate(…) generated by generateBundle. WriteBundle output successfully generates bundle.write(…) , or at any time during the renderError output generation, an error occurred.

The flow chart is as follows:

Communication between plug-ins

The main plug-in communication modes are:

  • Add custom content to this.resolve for communication
function requestingPlugin() {
  return {
    name: 'requesting',
    async buildStart() {
      const resolution = await this.resolve('foo', undefined, {
        custom: { resolving: { specialResolution: true } }
      });
      console.log(resolution.id); // "special"
    }
  };
}

function resolvingPlugin() {
  return {
    name: 'resolving',
    resolveId(id, importer, { custom }) {
      if (custom.resolving?.specialResolution) {
        return 'special';
      }
      return null;
    }
  };
}
Copy the code
  • Custom meta communication through modules
function annotatingPlugin() { return { name: 'annotating', transform(code, id) { if (thisModuleIsSpecial(code, id)) { return { meta: { annotating: { special: true } } }; }}}; } function readingPlugin() { let parentApi; return { name: 'reading', buildEnd() { const specialModules = Array.from(this.getModuleIds()).filter( id => this.getModuleInfo(id).meta.annotating?.special ); // do something with this list } }; }Copy the code
  • Direct plug-in communication

Note that API attributes never conflict with any upcoming plug-in hooks.

function parentPlugin() { return { name: 'parent', api: { //... methods and properties exposed for other plugins doSomething(... args) { // do something interesting } } // ... plugin hooks } } function dependentPlugin() { let parentApi; return { name: 'dependent', buildStart({ plugins }) { const parentName = 'parent'; const parentPlugin = options.plugins .find(plugin => plugin.name === parentName); if (! parentPlugin) { // or handle this silently if it is optional throw new Error(`This plugin depends on the "${parentName}"  plugin.`); } // now you can access the API methods in subsequent hooks parentApi = parentPlugin.api; } transform(code, id) { if (thereIsAReasonToDoSomething(id)) { parentApi.doSomething(id); }}}}Copy the code

Develop plug-ins by analyzing their source code

Rollup-plugin-uglify rollup-plugin-uglify rollup-plugin-uglify rollup-plugin-uglify rollup-plugin-uglify

const { codeFrameColumns } = require("@babel/code-frame");
const Worker = require("jest-worker").default;
const serialize = require("serialize-javascript");

function uglify(userOptions = {}) {
  if (userOptions.sourceMap != null) {
    throw Error("sourceMap option is removed, use sourcemap instead");
  }

  const normalizedOptions = Object.assign({}, userOptions, {
    sourceMap: userOptions.sourcemap !== false
  });

  ["sourcemap"].forEach(key => {
    if (normalizedOptions.hasOwnProperty(key)) {
      delete normalizedOptions[key];
    }
  });

  const minifierOptions = serialize(normalizedOptions);

  return {
    name: "uglify",

    renderStart() {
      this.worker = new Worker(require.resolve("./transform.js"), {
        numWorkers: userOptions.numWorkers
      });
    },

    renderChunk(code) {
      return this.worker.transform(code, minifierOptions).catch(error => {
        const { message, line, col: column } = error;
        console.error(
          codeFrameColumns(code, { start: { line, column } }, { message })
        );
        throw error;
      });
    },

    generateBundle() {
      this.worker.end();
    },

    renderError() {
      this.worker.end();
    }
  };
}

exports.uglify = uglify;
Copy the code

RenderStart and renderChunk are used to process the code, and renderError and generateBundle are closed.

Simply wrap a plug-in yourself

This plugin simply removes all console from the project, as shown below

export default function removeConsole() {
    return {
        name: 'remove-console',
        transform(code, id) {
            const Reg = /console\.log\(.*\)/ig;
            return code.replace(Reg, "")
        },
    }
}

Copy the code

Yes, these are just a few lines of code, and of course this is a demo plugin, but a real production plugin requires a lot of boundary judgment.

Afterword.

These are my study notes for the rollup plugin. I hope it will be helpful.