Skip to content

Instantly share code, notes, and snippets.

@DavidWells
Last active January 2, 2022 01:49
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save DavidWells/d0d0621a3f49c6a0a17aa6d6963de10b to your computer and use it in GitHub Desktop.
Save DavidWells/d0d0621a3f49c6a0a17aa6d6963de10b to your computer and use it in GitHub Desktop.
Netlify Plugin Spec

Simple plugin

Here is a simple plugin with no config and it runs on a single lifecycle event postBuild

// Plugin code
module.exports = function myPlugin(pluginConfig) {
  // custom code/init steps
  return {
    onPostBuild: (pluginAPI) => {
      // run thing on post build
    }
  }
}
/* alternative syntax, simple object */
module.exports = {
  onPostBuild: (pluginAPI) => {
    // run thing on post build
  }
}
# Netlify Config
build:
  command: npm run build
  publish: build
  functions: functions

plugins:
  - package: my-simple-plugin

What happens on build:

  1. npm run build from build.command runs
  2. onPostBuild functionality from plugin runs

PluginAPI

The pluginAPI passed into methods is as follows

module.exports = function myPluginXyz(pluginConfig) {
  return {
    /**
     * Plugin API
     * @param  {object} netlifyConfig   - Resolved value of Netlify configuration file
     * @param  {object} pluginConfig    - Initial plugin configuration
     * @param  {object} utils           - set of utility functions for working with Netlify
     * @param  {object} utils.cache     - Helper functions for dealing with build cache
     * @param  {object} utils.git       - Helper functions for dealing with git
     * @param  {object} utils.run       - Helper functions for dealing with executables
     * @param  {object} utils.functions - Helper functions for dealing with Netlify functions
     * @param  {object} utils.redirects - Helper functions for dealing with Netlify redirects
     * @param  {object} utils.headers   - Helper functions for dealing with Netlify headers
     * @param  {object} constants       - constant values referencing various env paths
     * @param  {object} constants.CONFIG_PATH     - path to netlify config file
     * @param  {object} constants.BUILD_DIR       - path to site build directory
     * @param  {object} constants.CACHE_DIR       - path to cache directory
     * @param  {object} constants.FUNCTIONS_SRC   - path to functions source code directory
     * @param  {object} constants.FUNCTIONS_DIST  - path to functions build directory
     * @param  {object} api             - scoped API instance of Netlify sdk
     * @return {object} outputs - output values from call
     */
    onPreBuild: ({ netlifyConfig, pluginConfig, utils, constants, api }) => {
      // do the thing
    },
  }
}

Basic plugin

Here is a basic plugin with no config and it runs on 3 lifecycle event preBuild, build, & postBuild

// Plugin code
module.exports = function myPlugin(pluginConfig) {
  return {
    onPreBuild: (pluginAPI) => {
      // run thing on pre-build
    },
    onBuild: (pluginAPI) => {
      // run thing on build
    },
    onPostBuild: (pluginAPI) => {
      // run thing on post build
    }
  }
}
# Netlify Config
build:
  command: npm run build
  publish: build
  functions: functions

plugins:
  - package: my-simple-plugin

What happens on build:

  1. onPreBuild functionality from plugin runs
  2. npm run build from build.command runs
  3. onBuild functionality from plugin runs
  4. onPostBuild functionality from plugin runs

Basic plugin with CLI commands & no lifecycle

Here is a basic plugin that exposes a CLI command. It runs on 0 lifecycle events.

Instead, it exposes a new CLI command netlify foo

// Plugin code
module.exports = function myPlugin(pluginConfig) {
  return {
    /* Commands exposes new CLI commands */
    commands: {
      foo: {
        usage: 'The foo command does xyz',
        options: {
          flagone: {
            usage: 'This flag sets the thing for the thing',
            required: true,
            shortcut: 'f'
          }
        },
        method: (pluginAPI) => {
          // Logic to run when "netlify foo -f" is executed
        }
      }
    }
  }
}
# Netlify Config
build:
  command: npm run build
  publish: build
  functions: functions

plugins:
  - package: my-simple-plugin

What happens on build:

Plugin contains no lifecycle events so nothing runs from plugin.

  1. npm run build from build.command runs

Basic plugin with config

Here is a basic plugin with config and it runs on 3 lifecycle event preBuild, build, & postBuild

// Plugin code
module.exports = function myPlugin(pluginConfig) {
  return {
    config: {
      fizz: 'string',
      pop: {
        type: 'string',
        required: true,
      }
    },
    onPreBuild: (pluginAPI) => {
      // run thing on pre-build
    },
    onBuild: (pluginAPI) => {
      // run thing on build
    },
    onPostBuild: (pluginAPI) => {
      // run thing on post build
    }
  }
}
# Netlify Config
build:
  command: npm run build
  publish: build
  functions: functions

plugins:
  - package: my-simple-plugin
    config:
      fizz: hello
      pop: there

What happens on build:

  1. Required inputs validated
  2. onPreBuild functionality from plugin runs
  3. npm run build from build.command runs
  4. onBuild functionality from plugin runs
  5. onPostBuild functionality from plugin runs

Basic Plugins using config & outputs

Here we use 2 plugins. Plugin one has required inputs and plugin two's output values are used in plugin one.

// Has required config property
module.exports = function pluginOne(pluginConfig) {
  return {
    config: {
      biz: {
        type: 'string',
        required: true,
      }
    },
    onBuild: ({ pluginConfig }) => {
      console.log(pluginConfig.bar)
    },
  }
}
// Exposes 1 output during onPreBuild phase
module.exports = function pluginTwo(pluginConfig) {
  return {
    /* Plugin outputs */
    outputs: {
      foo: {
        type: 'string',
        when: 'onPreBuild'
      }
    },
    onPreBuild: (pluginAPI) => {
      // Output value is returned from onPreBuild phase
      return {
        foo: 'hello'
      }
    },
  }
}
# Netlify Config
build:
  command: npm run build
  publish: build
  functions: functions

plugins:
  - package: plugin-one
    config:
      biz: ${pluginTwo.outputs.foo}
      # ^ output referenced from `id`
      # given to plugin-two
  - package: plugin-two
    id: pluginTwo

What happens on build:

Inputs & outputs dependancies are ordered up front via a DAG and cycles throw an error.

  1. Required inputs validated. Output recognized & lifecycle checked to verify ordering works. Re: DAG
  2. onPreBuild functionality from plugin-two runs
  3. Output foo from plugin-two returned. ${pluginTwo.outputs.foo} then fully resolved to hello.
  4. npm run build from build.command runs from inline lifecycle
  5. onBuild functionality from plugin-one runs with biz value set to hello

Plugins used programmatically

Here we use 3 plugins. The plugin-one & plugin-two are programmatically used in a third plugin called orchestratorPlugin. Because they are programmatically used, the order of lifecycle methods in plugin-one & plugin-two do not matter. Effectively they are used as normal NPM modules.

Programmatic usage from orchestrator plugin. Because these are used programmatically and are not defined in the Netlify config file, they can be called in any order the user wishes.

// This plugin is consumer of the 2 other plugins
// plugin-one & plugin-two are not defined in Netlify config file
const pluginOne = require('plugin-one')
const pluginTwo = require('plugin-two')

module.exports = function orchestratorPlugin(config) {
  return {
    name: 'orchestrator-plugin',
    config: {
      optOne: {
        type: 'string',
        required: true,
      },
      optTwo: {
        type: 'string',
        required: true,
      }
    },
    onInit: async ({ pluginConfig }) => {
      // initialize plugins with config
      const one = pluginOne({ biz: pluginConfig.optOne  })
      const two = pluginTwo({ zaz: pluginConfig.optTwo })

      const [outputFromOne, outputFromTwo] = await Promise.all([
        one.onPreBuild(pluginAPI),
        two.onBuild(pluginAPI),
      ])
      // Do stuff with outputFromOne / outputFromTwo
    },
  }
}
module.exports = function pluginOne(pluginConfig) {
  return {
    name: 'plugin-one',
    // Config needed for plugin
    config: {
      biz: {
        type: 'string',
        required: true,
      }
    },
    // outputs returned for DAG resolution
    outputs: {
      wow: {
        type: 'string',
        when: 'onBuild'
      }
    },
    onPreBuild: ({ pluginConfig }) => {
      console.log(pluginConfig.biz)
      return {
        wow: 'nice'
      }
    },
  }
}
// Has required config property
module.exports = function pluginTwo(pluginConfig) {
  return {
    name: 'plugin-two',
    config: {
      zaz: {
        type: 'string',
        required: true,
      }
    },
    outputs: {
      wow: {
        type: 'string',
        when: 'onBuild'
      }
    },
    onBuild: ({ pluginConfig }) => {
      console.log(pluginConfig.zaz)
    },
  }
}
build:
  publish: build

plugins:
  - package: orchestrator-plugin
    config:
      optOne: hello
      optTwo: goodbye

What happens on build:

Because they are programmatically used, the order of lifecycle methods in plugin-one & plugin-two do not matter.

  1. Required inputs validated from read config file
  2. orchestrator-plugin loads & onInit functionality runs with config set to optOne: hello & optTwo: goodbye
  3. plugin-one & plugin-two are initialized with config
  4. Then plugin-one.onPreBuild & plugin-two.onBuild methods are called
  5. Then output from plugin-one.onPreBuild & plugin-two.onBuild are referenced in the code of orchestrator-plugin.
  6. Then the build ends
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment