PostCSS 8.0: Plugin migration guide

Cover for PostCSS 8.0: Plugin migration guide

PostCSS received a major update with the release of version 8.0, codenamed “President Ose”. Plugin creators can now opt in for a new API that сan increase build speeds and reduce the size of dependencies for the end-users of their tools. This guide describes the steps you need to take as a plugin developer to make the most out of the improved framework.

PostCSS—a framework for processing CSS with JavaScript—is one of the most popular frontend tools for the modern web, with over 25 million downloads a week.

So much code depends on PostCSS since it’s trusted by big projects like webpack or Rails, and because it’s a universe of plugins that improve the way frontend developers write CSS.

Simple JavaScript rules can either automate routine tasks, like linting and adding vendor prefixes, or enable novel ways of creating stylesheets that are not directly supported by current web standards.

Schedule call

Irina Nazarova CEO at Evil Martians

Schedule call

If you develop or maintain plugins for PostCSS—this post is for you. It lists the main things you should do to make your code conform to the latest version of the framework.

Take a look at the full description of the latest release on GitHub to learn what else is new in PostCSS 8.0, including better source maps support and more resilient CSS parsing.

Why do I need to update my plugin?

The previous versions of PostCSS had some limitations with regards to plugins:

  • Speed. Even if your plugin changes just a few style properties, it walks through the entire Abstract Syntax Tree of a CSS bundle. As different plugins are often used together, each of them needs to pass the whole tree on any property update—slowing down CSS builds for end-users.
  • Size of node_modules. Plugins can list different versions of PostCSS under their dependencies. This can cause the resulting node_modules folder to bloat if npm’s deduplication fails to do its job.
  • Compatibility. Plugins designed for older versions of PostCSS can use deprecated ways of building nodes (e.g., postcss.decl()). Mixing AST nodes created by different PostCSS versions can cause painful bugs.

Step 1: Move postcss to peerDependencies

The first step is very simple. Just remove PostCSS version 7 from your dependencies and add PostCSS version 8 to devDependencies.

npm uninstall postcss
npm install postcss --save-dev

Then, move PostCSS 8 to peerDependencies by editing your package.json:

  "dependencies": {
-   "postcss": "^7.0.10"
  },
  "devDependencies": {
+   "postcss": "^8.0.0"
  },
+ "peerDependencies": {
+   "postcss": "^8.0.0"
+ }
}

This will keep the size of the end-user’s node_modules directory under control: now, all plugins will use the same version of postcss as a dependency.

If your dependencies object is now empty, feel free to remove it:

- "dependencies": {
- }
  "devDependencies": {

Also, don’t forget to change the installation instructions in your plugin’s documentation:

- npm install --save-dev postcss-focus
+ npm install --save-dev postcss postcss-focus

Step 2: Use the updated API

  1. Replace plugin = postcss.plugin(name, creator) with just plugin = creator.
  2. Return an object with postcssPlugin property containing a plugin name and the Once method.
  3. Move the plugin code to the Once method.
  4. Add plugin.postcss = true to the end of the file.

Before:

- const plugin = postcss.plugin('postcss-dark-theme-class', (opts = {}) => {
-   checkOpts(opts)
-   return (root, result) => {
      root.walkAtRules(atrule => { … })
-   }
- })

  module.exports = plugin

After:

+ const plugin = (opts = {}) => {
+   checkOpts(opts)
+   return {
+     postcssPlugin: 'postcss-dark-theme-class',
+     Once (root, { result }) {
        root.walkAtRules(atrule => { … })
+     }
+   }
+ }
+ plugin.postcss = true

  module.exports = plugin

Don’t forget to set plugin.postcss = true. This allows PostCSS to distinguish between require('plugin') and require('plugin')(opts) end-user calls.

Step 3: Fully embrace the new API

PostCSS 8 does a single CSS tree scan. Multiple plugins can leverage the same scan for better performance.

To use single scans, you need to remove root.walk* calls and move the code to the Declaration(), Rule(), AtRule() or Comment() methods in the plugin’s object:

  const plugin = () => ({
    postcssPlugin: 'postcss-dark-theme-class',
-   Once (root) {
-     root.walkAtRules(atRule => {
-       // Slow
-     })
-   }
+   AtRule (atRule) {
+     // Faster
+   }
  })
  plugin.postcss = true

The full list of plugin events can be found in the API docs.

The AtRule listener will visit the node again if someone changes the at-rule parameters or any children inside the at-rule.

It’s important to avoid plugin freeze: the plugin adds children to the at-rule, PostCSS calls the plugin again on this at-rule, the plugin adds another child.

const plugin = () => ({
  postcssPlugin: 'postcss-',
  AtRule (atRule) {
    if (atRule.every(child => child.selector !== '.test')) {
      atRule.append({ selector: '.test' })
    }
  }
})
plugin.postcss = true

For declarations and at-rules you can make your code even faster by subscribing to a specific declaration property or an at-rule name:

  const plugin = () => ({
    postcssPlugin: 'postcss-example',
-   AtRule (atRule) {
-     if (atRule.name === 'media') {
-       // Faster
-     }
-   }
+   AtRule: {
+     media: atRule => {
+       // The fastest
+     }
+   }
  }
  plugin.postcss = true

Note that plugins will re-visit all changed or added nodes. You should check if your transformations were already applied, and if that is the case, ignore those nodes. Only the Once and OnceExit listeners will be called, exactly once.

const plugin = () => ({
  Declaration(decl) {
    console.log(decl.toString())
    decl.value = "red"
  }
})
plugin.postcss = true

await postcss([plugin]).process("a { color: black }", { from })
// => color: black
// => color: red

If you have a large plugin that is hard to rewrite, it’s OK to keep using walk method inside your Root listener.

There are two types of listeners: for “enter” and for “exit”. Once, Root, AtRule, or Rule will be called before processing children. OnceExit, RootExit, AtRuleExit, and RuleExitafter processing all the children of the node.

If you need a way to share data between listeners, you can use prepare():

const plugin = (opts = {}) => ({
  postcssPlugin: "PLUGIN NAME",
  prepare(result) {
    const variables = {}
    return {
      Declaration(node) {
        if (node.variable) {
          variables[node.prop] = node.value
        }
      },
      OnceExit() {
        console.log(variables)
      }
    }
  }
})

Step 4: Remove postcss imports

With the new PostCSS plugin API, you do not need to import postcss. You will get all classes and methods as a second argument to your Root function:

- const { list, Declaration } = require('postcss')

  const plugin = () => ({
    postcssPlugin: 'postcss-example',
-   Once (root) {
+   Once (root, { list, Declaration }) {
     }
  })
  plugin.postcss = true

This is also something that you can keep as-is for a while if you don’t feel like doing it immediately.

Step 5: Reduce the npm package size

That is not a mandatory step, but we want to promote an amazing tool for keeping your package.json clean of development configs before publishing your package to npm: clean-publish. If the PostCSS ecosystem starts to use it, we will collectively make node_modules even smaller.

Add it to your project:

npm install --save-dev clean-publish

Now use npx clean-publish instead of npm publish.

You can use the official Plugin Boilerplate and these plugins as an example for the adoption of a new API:

If you are looking for a commercial advice on a custom PostCSS integration, thinking about setting up better development practices for your technical team or need a revision of your product’s frontend infrastructure—feel free to contact Evil Martians.

Schedule call

Irina Nazarova CEO at Evil Martians

Through our open source PostCSS and Autoprefixer, used by millions of software engineers worldwide, we shape the landscape of frontend development. We can help you create products that developers love and rely on every day.