or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

ast-nodes.mdcore-processing.mdindex.mdplugin-system.mdresults-errors.mdutilities.md
tile.json

plugin-system.mddocs/

Plugin System

PostCSS's plugin system allows developers to create transformations that process CSS AST nodes. This document covers the modern plugin API, legacy plugin support, and plugin development patterns.

Plugin Interface

Modern PostCSS plugins implement the Plugin interface with lifecycle hooks for different node types.

import { Plugin, Processors, Result, Root, Document, Rule, AtRule, Declaration, Comment } from 'postcss'

interface Plugin extends Processors {
  postcssPlugin: string
  prepare?(result: Result): Processors
}

interface PluginCreator<PluginOptions> {
  (opts?: PluginOptions): Plugin | Processor
  postcss: true
}

Basic Plugin Structure

// Modern plugin API
const myPlugin = (opts = {}) => {
  return {
    postcssPlugin: 'my-plugin',
    // Plugin lifecycle hooks go here
  }
}
myPlugin.postcss = true

// With TypeScript
interface MyPluginOptions {
  prefix?: string
  exclude?: string[]
}

const myTypedPlugin: PluginCreator<MyPluginOptions> = (opts = {}) => {
  const { prefix = 'auto-', exclude = [] } = opts
  
  return {
    postcssPlugin: 'my-typed-plugin',
    Rule(rule, { result }) {
      if (!exclude.includes(rule.selector)) {
        rule.selector = `${prefix}${rule.selector}`
      }
    }
  }
}
myTypedPlugin.postcss = true

Processors Interface

The Processors interface defines lifecycle hooks that plugins can implement:

interface Processors {
  Once?: RootProcessor
  OnceExit?: RootProcessor
  Root?: RootProcessor
  RootExit?: RootProcessor
  Document?: DocumentProcessor
  DocumentExit?: DocumentProcessor
  Rule?: RuleProcessor
  RuleExit?: RuleProcessor
  AtRule?: { [name: string]: AtRuleProcessor } | AtRuleProcessor
  AtRuleExit?: { [name: string]: AtRuleProcessor } | AtRuleProcessor
  Declaration?: { [prop: string]: DeclarationProcessor } | DeclarationProcessor
  DeclarationExit?: { [prop: string]: DeclarationProcessor } | DeclarationProcessor
  Comment?: CommentProcessor
  CommentExit?: CommentProcessor
}

Processor Function Types

import { postcss } from 'postcss'

type RootProcessor = (root: Root, helper: postcss.Helpers) => Promise<void> | void
type DocumentProcessor = (document: Document, helper: postcss.Helpers) => Promise<void> | void
type RuleProcessor = (rule: Rule, helper: postcss.Helpers) => Promise<void> | void
type AtRuleProcessor = (atRule: AtRule, helper: postcss.Helpers) => Promise<void> | void
type DeclarationProcessor = (decl: Declaration, helper: postcss.Helpers) => Promise<void> | void
type CommentProcessor = (comment: Comment, helper: postcss.Helpers) => Promise<void> | void

interface Helpers {
  result: Result
  postcss: typeof postcss
}

Lifecycle Hooks

Once and OnceExit

Execute once per processing run, regardless of AST changes:

const setupPlugin = () => ({
  postcssPlugin: 'setup-plugin',
  Once(root, { result }) {
    // Run once at the start
    result.messages.push({
      type: 'setup',
      plugin: 'setup-plugin',
      message: 'Processing started'
    })
  },
  OnceExit(root, { result }) {
    // Run once at the end
    result.messages.push({
      type: 'cleanup', 
      plugin: 'setup-plugin',
      message: 'Processing completed'
    })
  }
})

Root and RootExit

Execute on Root nodes (re-runs if AST changes):

const rootPlugin = () => ({
  postcssPlugin: 'root-plugin',
  Root(root, { result }) {
    // Process the entire stylesheet
    let ruleCount = 0
    root.walkRules(() => ruleCount++)
    
    if (ruleCount > 100) {
      result.warn('Large stylesheet detected', { node: root })
    }
  },
  RootExit(root, { result }) {
    // Final processing after all other plugins
    root.append(postcss.comment({ 
      text: `Generated by ${result.processor.version}` 
    }))
  }
})

Node-Specific Hooks

Process specific node types:

const nodePlugin = () => ({
  postcssPlugin: 'node-plugin',
  
  Rule(rule, { result }) {
    // Process every rule
    if (rule.selector.startsWith('.old-')) {
      rule.selector = rule.selector.replace('.old-', '.new-')
    }
  },
  
  Declaration(decl, { result }) {
    // Process every declaration
    if (decl.prop === 'color' && decl.value === 'red') {
      result.warn('Avoid hardcoded red color', { node: decl })
    }
  },
  
  Comment(comment, { result }) {
    // Process every comment
    if (comment.text.includes('TODO')) {
      result.warn('TODO found in CSS', { node: comment })
    }
  }
})

Filtered Hooks

Process specific at-rules or declarations by name:

const filteredPlugin = () => ({
  postcssPlugin: 'filtered-plugin',
  
  // Process only @media at-rules
  AtRule: {
    media(atRule, { result }) {
      // Add min-width if only max-width specified
      if (atRule.params.includes('max-width') && !atRule.params.includes('min-width')) {
        atRule.params = `${atRule.params} and (min-width: 320px)`
      }
    },
    
    import(atRule, { result }) {
      result.warn('Consider bundling imports', { node: atRule })
    }
  },
  
  // Process only specific properties
  Declaration: {
    'font-size'(decl, { result }) {
      if (decl.value.includes('px')) {
        decl.value = decl.value.replace(/(\d+)px/g, (match, num) => {
          return `${num / 16}rem`
        })
      }
    },
    
    'margin'(decl, { result }) {
      if (decl.value === '0px') {
        decl.value = '0'
      }
    }
  }
})

Plugin Helpers

The helper object provides utilities for plugin development:

const helperPlugin = () => ({
  postcssPlugin: 'helper-plugin',
  Declaration(decl, { result, postcss }) {
    // Access result for warnings and messages
    result.warn('Processing declaration', { node: decl })
    
    // Access postcss constructor functions
    const newRule = postcss.rule({ selector: '.generated' })
    newRule.append(postcss.decl({ prop: 'content', value: '""' }))
    
    // Insert generated nodes
    decl.root().append(newRule)
    
    // Add custom messages
    result.messages.push({
      type: 'custom',
      plugin: 'helper-plugin',
      declaration: decl.prop,
      value: decl.value
    })
  }
})

Asynchronous Plugins

Plugins can return promises for async operations:

const asyncPlugin = () => ({
  postcssPlugin: 'async-plugin',
  
  async Once(root, { result }) {
    // Async setup
    const config = await loadConfiguration()
    result.messages.push({ type: 'config', data: config })
  },
  
  async AtRule: {
    async import(atRule, { result }) {
      try {
        // Async file loading
        const content = await fs.readFile(atRule.params.slice(1, -1), 'utf8')
        const importedRoot = postcss.parse(content)
        
        // Replace @import with actual content
        atRule.replaceWith(importedRoot.nodes)
      } catch (error) {
        result.warn(`Failed to import: ${error.message}`, { node: atRule })
      }
    }
  }
})

Plugin with prepare()

Use prepare() for plugins that need per-result initialization:

const statefulPlugin = (opts = {}) => ({
  postcssPlugin: 'stateful-plugin',
  
  prepare(result) {
    // Initialize state per processing run
    const state = {
      ruleCount: 0,
      declarations: new Set(),
      startTime: Date.now()
    }
    
    return {
      Rule(rule) {
        state.ruleCount++
      },
      
      Declaration(decl) {
        state.declarations.add(decl.prop)
      },
      
      OnceExit(root) {
        const duration = Date.now() - state.startTime
        result.messages.push({
          type: 'stats',
          plugin: 'stateful-plugin',
          ruleCount: state.ruleCount,
          uniqueProps: state.declarations.size,
          duration
        })
      }
    }
  }
})

Legacy Plugin Support

PostCSS supports older plugin formats for backward compatibility:

interface Transformer extends TransformCallback {
  postcssPlugin: string
  postcssVersion: string
}

interface TransformCallback {
  (root: Root, result: Result): Promise<void> | void
}

type AcceptedPlugin =
  | { postcss: Processor | TransformCallback }
  | OldPlugin<any>
  | Plugin
  | PluginCreator<any>
  | Processor
  | TransformCallback

Legacy Plugin Examples

// Function-based legacy plugin
const legacyFunction = (root, result) => {
  root.walkRules(rule => {
    if (rule.selector === '.old') {
      rule.selector = '.new'
    }
  })
}

// Object-based legacy plugin  
const legacyObject = {
  postcss: (root, result) => {
    root.walkDecls('color', decl => {
      if (decl.value === 'red') {
        decl.value = 'blue'
      }
    })
  }
}

// Use legacy plugins
const processor = postcss([
  legacyFunction,
  legacyObject,
  modernPlugin()
])

Plugin Development Patterns

Property Value Transformation

const valueTransformPlugin = (transforms = {}) => ({
  postcssPlugin: 'value-transform',
  Declaration(decl) {
    const transform = transforms[decl.prop]
    if (transform) {
      decl.value = transform(decl.value)
    }
  }
})

// Usage
const processor = postcss([
  valueTransformPlugin({
    'font-size': (value) => value.replace(/px$/, 'rem'),
    'color': (value) => value === 'red' ? '#ff0000' : value
  })
])

Selector Transformation

const selectorPlugin = (options = {}) => {
  const { prefix = '', suffix = '' } = options
  
  return {
    postcssPlugin: 'selector-plugin',
    Rule(rule) {
      rule.selector = rule.selectors
        .map(selector => `${prefix}${selector}${suffix}`)
        .join(', ')
    }
  }
}

CSS Module Generation

const cssModulesPlugin = () => {
  const classMap = new Map()
  
  return {
    postcssPlugin: 'css-modules',
    prepare() {
      return {
        Rule(rule) {
          rule.selectors = rule.selectors.map(selector => {
            const match = selector.match(/\.(\w+)/)
            if (match) {
              const className = match[1]
              const hashedName = `_${className}_${hash(className)}`
              classMap.set(className, hashedName)
              return selector.replace(className, hashedName)
            }
            return selector
          })
        },
        
        OnceExit(root, { result }) {
          result.messages.push({
            type: 'css-modules',
            plugin: 'css-modules',
            classMap: Object.fromEntries(classMap)
          })
        }
      }
    }
  }
}

Validation Plugin

const validationPlugin = (rules = {}) => ({
  postcssPlugin: 'validation-plugin',
  
  Declaration(decl, { result }) {
    // Check for deprecated properties
    const deprecated = ['float', 'clear', 'vertical-align']
    if (deprecated.includes(decl.prop)) {
      result.warn(`Property ${decl.prop} is deprecated`, { 
        node: decl,
        word: decl.prop
      })
    }
    
    // Check for invalid values
    if (decl.prop === 'display' && !['block', 'inline', 'flex', 'grid'].includes(decl.value)) {
      result.warn(`Invalid display value: ${decl.value}`, {
        node: decl,
        word: decl.value
      })
    }
  },
  
  Rule(rule, { result }) {
    // Check for empty rules
    if (!rule.nodes || rule.nodes.length === 0) {
      result.warn('Empty rule detected', { node: rule })
    }
    
    // Check for overly specific selectors
    const specificity = calculateSpecificity(rule.selector)
    if (specificity > 1000) {
      result.warn('High specificity selector', {
        node: rule,
        word: rule.selector
      })
    }
  }
})

Plugin Testing

const postcss = require('postcss')

// Test plugin functionality
async function testPlugin() {
  const plugin = myPlugin({ option: 'value' })
  const processor = postcss([plugin])
  
  const input = '.foo { color: red; }'
  const result = await processor.process(input, { from: 'test.css' })
  
  console.log('Output:', result.css)
  console.log('Warnings:', result.warnings())
  console.log('Messages:', result.messages)
}

// Test with different inputs
const testCases = [
  { input: '.foo { color: red; }', expected: '.foo { color: blue; }' },
  { input: '.bar { font-size: 16px; }', expected: '.bar { font-size: 1rem; }' }
]

testCases.forEach(async ({ input, expected }) => {
  const result = await processor.process(input)
  console.assert(result.css === expected, `Expected ${expected}, got ${result.css}`)
})

Error Handling in Plugins

const robustPlugin = () => ({
  postcssPlugin: 'robust-plugin',
  
  Declaration(decl, { result }) {
    try {
      // Risky operation
      const parsed = parseComplexValue(decl.value)
      decl.value = transformValue(parsed)
    } catch (error) {
      // Report error with context
      const cssError = decl.error(`Failed to process ${decl.prop}: ${error.message}`)
      throw cssError
    }
  },
  
  Rule(rule, { result }) {
    // Validate selector
    try {
      validateSelector(rule.selector)
    } catch (error) {
      result.warn(`Invalid selector: ${error.message}`, {
        node: rule,
        word: rule.selector
      })
    }
  }
})