docs/writing-a-plugin.md
Table of Contents
Documentation:
Support:
There are many fields where writing new PostCSS plugin will help your work:
postcss-flexbugs-fixes and postcss-100vh-fix
are good examples.postcss-autoreset
are great example how PostCSS can increase code maintainability by isolation.postcss-preset-env. If you find a new draft, you can add a new plugin
and send it to this preset.postcss-easing-gradients with this proposal is a good example.
However, there are a lot of cases when you can’t send a proposal.
For instance, browser’s parser performance limited CSSWG nested syntax a lot
and you may want to have non-official Sass-like syntax from `postcss-nested.There are two ways to write a plugin:
For private plugin:
postcss/ folder with the name of your plugin.For public plugins:
module.exports = (opts = {}) => {
// Plugin creator to check options or prepare shared state
return {
postcssPlugin: 'PLUGIN NAME'
// Plugin listeners
}
}
module.exports.postcss = true
Most of the PostCSS plugins do two things:
will-change property).transform: translateZ(0) before
will-change as a polyfill for old browsers).PostCSS parses CSS to the tree of nodes (we call it AST). This tree may content:
Root: node of the top of the tree, which represent CSS file.AtRule: statements begin with @ like @charset "UTF-8"
or @media (screen) {}.Rule: selector with declaration inside. For instance input, button {}.Declaration: key-value pair like color: black;Comment: stand-alone comment. Comments inside selectors, at-rule
parameters and values are stored in node’s raws property.You can use AST Explorer to learn how PostCSS convert different CSS to AST.
You can find all nodes with specific types by adding method to plugin object:
module.exports = (opts = {}) => {
return {
postcssPlugin: 'PLUGIN NAME',
Once(root) {
// Calls once per file, since every file has single Root
},
Declaration(decl) {
// All declaration nodes
}
}
}
module.exports.postcss = true
Here is the full list of plugin’s events.
If you need declaration or at-rule with specific names, you can use quick search:
Declaration: {
color: decl => {
// All `color` declarations
}
'*': decl => {
// All declarations
}
},
AtRule: {
media: atRule => {
// All @media at-rules
}
}
For other cases, you can use regular expressions or specific parsers:
number, length and percentage.margin, padding and border properties.Other tools to analyze AST:
Don’t forget that regular expression and parsers are heavy tasks. You can use
String#includes() quick test before check node with heavy tool:
if (decl.value.includes('gradient(')) {
let value = valueParser(decl.value)
…
}
There two types or listeners: enter and exit. Once, Root, AtRule,
and Rule will be called before processing children. OnceExit, RootExit,
AtRuleExit, and RuleExit after processing all children inside node.
You may want to re-use some data between listeners. You can do with runtime-defined listeners:
module.exports = (opts = {}) => {
return {
postcssPlugin: 'vars-collector',
prepare(result) {
const variables = {}
return {
Declaration(node) {
if (node.variable) {
variables[node.prop] = node.value
}
},
OnceExit() {
console.log(variables)
}
}
}
}
}
You can use prepare() to generate listeners dynamically. For instance,
to use Browserslist to get declaration properties.
When you find the right nodes, you will need to change them or to insert/delete other nodes around.
PostCSS node has a DOM-like API to transform AST. Check out our API docs.
Nodes has methods to travel around (like Node#next or Node#parent),
look to children (like Container#some), remove a node
or add a new node inside.
Plugin’s methods will receive node creators in second argument:
Declaration (node, { Rule }) {
let newRule = new Rule({ selector: 'a', source: node.source })
node.root().append(newRule)
newRule.append(node)
}
If you added new nodes, it is important to copy Node#source to generate
correct source maps.
Plugins will re-visit all nodes, which you changed or added. If you will change
any children, plugin will re-visit parent as well. Only Once and
OnceExit will not be called again.
const plugin = () => {
return {
postcssPlugin: 'to-red',
Rule(rule) {
console.log(rule.toString())
},
Declaration(decl) {
console.log(decl.toString())
decl.value = 'red'
}
}
}
plugin.postcss = true
await postcss([plugin]).process('a { color: black }', { from })
// => a { color: black }
// => color: black
// => a { color: red }
// => color: red
Since visitors will re-visit node on any changes, just adding children will cause an infinite loop. To prevent it, you need to check that you already processed this node:
Declaration: {
'will-change': decl => {
if (decl.parent.some(decl => decl.prop === 'transform')) {
decl.cloneBefore({ prop: 'transform', value: 'translate3d(0, 0, 0)' })
}
}
}
You can also use Symbol to mark processed nodes:
const processed = Symbol('processed')
const plugin = () => {
return {
postcssPlugin: 'example',
Rule(rule) {
if (!rule[processed]) {
process(rule)
rule[processed] = true
}
}
}
}
plugin.postcss = true
Second argument also have result object to add warnings:
Declaration: {
bad: (decl, { result }) {
decl.warn(result, 'Deprecated property bad')
}
}
If your plugin depends on another file, you can attach a message to result
to signify to runners (webpack, Gulp etc.) that they should rebuild the CSS
when this file changes:
AtRule: {
import: (atRule, { result }) {
const importedFile = parseImport(atRule)
result.messages.push({
type: 'dependency',
plugin: 'postcss-import',
file: importedFile,
parent: result.opts.from
})
}
}
If the dependency is a directory you should use the dir-dependency
message type instead:
result.messages.push({
type: 'dir-dependency',
plugin: 'postcss-import',
dir: importedDir,
parent: result.opts.from
})
If you find an syntax error (for instance, undefined custom property), you can throw a special error:
if (!variables[name]) {
throw decl.error(`Unknown variable ${name}`, { word: name })
}
I hate programming
I hate programming
I hate programming
It works!
I love programming
You will have bugs and a minimum of 10 minutes in debugging even a simple plugin. You may found that simple origin idea will not work in real-world and you need to change everything.
Don’t worry. Every bug is findable, and finding another solution may make your plugin even better.
Start from writing tests. Plugin boilerplate has a test template
in index.test.js. Call npx jest to test your plugin.
Use Node.js debugger in your text editor or just console.log
to debug the code.
PostCSS community can help you since we are all experiencing the same problems. Don’t afraid to ask in special channel.
When your plugin is ready, call npx clean-publish in your repository.
clean-publish is a tool to remove development configs from the npm package.
We added this tool to our plugin boilerplate.
Write a tweet about your new plugin (even if it is a small one) with
@postcss mention. Or tell about your plugin in [our chat].
We will help you with marketing.
Add your new plugin to PostCSS plugin catalog.