github: plugin-anything

preface

When the front-end team implements the engineered Cli suite, Node Server and other systems, there are usually three ways to meet the openness of functions: configuration, plug-in, and the combination of configuration and plug-in.

All three have their advantages and disadvantages:

  • Configuration change

    • advantage

      As the name implies, users can use the configuration file exposed by the tool to perform various quick configurations to affect the tool.

    • disadvantage

      The core functionality of the tool is built into the tool module, and its logic cannot be interfered with outside.

  • pluggable

    • advantage

      Within the tool, only a series of life cycles and task scheduling are maintained. All business functions are connected in the form of plug-ins, and users can customize their own functions with as much freedom as possible.

    • disadvantage

      Getting started costs are not as out-of-the-box as configuration files, requiring users to understand plug-in development specifications.

  • Configuration and plug-in combination

    This is now the mainstream way Webpack, Babel and other tools adopt this way, plug-in to greatly improve the ecosystem of such tools.

Webpack plug-in ideas

Webpack maintains a life cycle that does two things: 1. Core code runs; 2. Expose hooks in the lifecycle.

Document address.

How users import plug-ins:

webpackConfig: {
    plugins: [
        new PluginA(),
        new PluginB(),
        // ...]}Copy the code

How to develop plug-ins:

class PluginA {
    constructor(){}apply(compiler) {
        compiler.hooks.someHook.tap(...);
    }
}
Copy the code

At plug-in development time, the developer intercepts the someHook hook to intervene in the Webpack life cycle at that point.

In this code, Compiler.hooks. SomeHook is used because Webpack attaches exposed lifecycle hooks (such as someHook) to internal hooks, as in the following example:

hooks = {
    hookA: new WaterfallHook(),
    hookB: new BailHook(),
}
Copy the code

Webpack’s event mechanism relies on Tappable, which has many types of event levels that won’t be described here

To sum up: While implementing the core logic inside Webpack, it exposes a variety of lifecycle hooks for plug-in intervention to achieve flexible functionality.

Plug-in ideas for Babel

Babel’s plug-in mechanism is a little different from Webpack’s.

Babel is essentially a converter of StringA -> AST -> AST change -> StringB, and its plug-ins serve this core function.

When Babel runs, it first converts the String to an AST, iterates through the AST, modifies the AST node in the traversal diagram, and finally converts the AST to a new String.

Babel’s plug-in mechanism mainly affects the process of “walking through the AST and modifying the AST nodes in the traversal diagram” described above.

Develop the Babel plug-in

The Babel plug-in is developed by providing Vistor objects, which are callbacks to each AST node, intercepting the traversal behavior of the node, and modifying the node.

const Visitor = {
    ASTNode1() {
        // change node
    },
    ASTNode2() {
        // change node}}Copy the code

Use the Babel plug-in

It’s worth learning from this point that Babel accepts a wide variety of plug-in introductions, such as:

  • .babelrc.json / babel.config.json

    {
        "plugins": [
            "transform-runtime"."class-properties"]}Copy the code
  • babel.config.js

    module.exports = {
        plugins: [
            'transform-runtime'.'class-properties']};Copy the code
  • packge.json

    {
      "name": "my-package"."version": "1.0.0"."babel": {
        "presets": [...]. ."plugins": [...]. ,}}Copy the code
  • JS API

    const babel = require('babel-core');
    
    babel.transform(code, {
        plugins: [ ... ]
    });
    Copy the code

Most importantly, it provides plugins that can be read to interfere with Babel’s state and meet a number of new functional requirements.

In summary, Babel provides a variety of plug-in access methods, including configuration files, command lines, JS apis, and so on.

How to implement a plug-in tool

So, if we want to implement a plug-in tool, such as the command line, build suite, how do we provide a plug-in mechanism to make the tool as open as possible?

  • The tools themselves

    The tool exposes the necessary lifecycle hooks as it runs the core logic.

    A set of event handling logic needs to be maintained within the tool to intercept the callback function provided when the plug-in declares the cycle

  • Plugin developer

    • A plug-in developer can intercept a lifecycle hook and provide a callback function for that hook
    • Plug-in developers can customize new hooks for use by subsequent plug-ins
  • Plug-in consumer

    Plug-in consumers, that is, users, need to introduce plug-ins as easily as possible and be free to develop them.

    We can learn from Babel’s plugin-import approach, accept plugins options, and provide a variety of configurations (configuration files/JS apis).

Feedback in the code, there are three main steps:

  1. Initial chemical has lifecycle hooks
  2. Execute the user-provided plug-in
  3. Execute core logic and execute a lifecycle hook at some point

Here is the pseudo-code for the implementation logic of the tool:

class PluginedTool {
    constructor(options) {
        this.options = {
            plugins: options.plugins
        };
        
        // step1: init hooks
        
        this.hooks = {
            hookA: new WaterfallHook(),
            hookB: new WaterfallHook(),
        };
        
        // step2: run plugins
        
        for (let i = 0, len = this.options.plugins.length; i < len; i++) {
            // find plugin function
            const pluginFunction = this.findPluginFunction(this.options.plugins[i]);
            
            // run plugin and supply context object
            pluginFunction({ ... });
        }
        
        // step3: run core code and flush hooks
        
        this.hooks.hookA.fire();
        // do something
        this.hooks.hookB.fire();
    }
    
    private options: {};
}
Copy the code

As a result, a simple plug-in tool can trigger as many lifecycle hooks as possible while executing the tool’s core logic in step3 for extreme flexibility.

This mechanism can be applied to the command line, build suite, Node Server, etc.

How to implement multiple plug-in tools

Admittedly, as mentioned above, the implementation logic for most plugins is similar:

  1. Initial chemical has lifecycle hooks
  2. Execute the user-provided plug-in
  3. Execute core logic and execute a lifecycle hook at some point

The tool needs to be maintained: event registration, consumption mechanisms, its own life cycle, and so on.

Soon, when we plug-in multiple tools using the same logic, we found that this general logic could be abstracted without having to re-implement each of the facilities.

To this end, a plugin factory was written: plugin-anything.

Usage:

const { runPluginAnything } = require('plugin-anything');

runPluginAnything(
    {
        // Array< string >
        searchList: [
            // string: absolute folder path].// Array< string | FunctionContructor | Array<string | FunctionContructor, object> >
        plugins: [
            // string: plugin name
            // FunctionContructor: Plugin Constructor
            // Array: [ string | FunctionContructor, options object ]],}, {// init something like: hooks, customs config
        async init({ hooks, Events, customs }) {
            hooks.done = new Events();
            customs.myConfig = {};
        },

        // run lifecycle
        async lifecycle({ hooks, Events, customs }) {
            // flush hooks
            await hooks.done.flush('waterfall');

            // do something
            // ...
            // console.log(customs.myConfig);}});Copy the code

It’s nice to think that developers can quickly create a plug-in tool.

Thanks for reading.