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.
Irina Nazarova CEO at Evil Martians
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 resultingnode_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
- Replace
plugin = postcss.plugin(name, creator)
with justplugin = creator
. - Return an object with
postcssPlugin
property containing a plugin name and theOnce
method. - Move the plugin code to the
Once
method. - 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 RuleExit
—after 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.