Search code examples
postcss

Postcss 8 plugin: How to avoid loop into Declaration function?


Hello postcss experts!

I’m updating an old plugin to postCSS 8 API but I meet some issues.

This simple postCSS plugin fall into an infinite loop:

module.exports = (options = {}) => {
  return {
    postcssPlugin: 'postcss-failing-plugin',
    Declaration(decl) {
      if (decl.prop.startsWith('--')) {
        decl.prop = decl.prop.replace(/^--/, `--prefix-`);
      }
    },
  };
};

module.exports.postcss = true;

The documentation mention this behaviour:

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. writing a plugin

But nothing to avoid it.

How to edit a value in Declaration without making an infinite loop?


Solution

  • You may be repeatedly adding a prefix to custom property declarations that are already prefixed, causing the declaration visitor to run infinitely.

    You can use a negative lookahead assertion (?!) to match custom properties that do not begin with a specific custom property prefix, i.e. ^--(?!prefix-).

    const matcher = /^--(?!prefix-)/
    const replacement = '--prefix-'
    
    const ensure = value => value.replace(matcher, replacement)
    
    // these _should not_ receive a new prefix
    ensure('foo')          // "foo"
    ensure('prefix-foo')   // "prefix-foo"
    ensure('--prefix-foo') // "--prefix-foo"
    
    // these _should_ receive a new prefixed
    ensure('--foo')            // "--prefix-foo"
    ensure('--prefixable-foo') // "--prefix-prefixable-foo"
    

    As applied to your example

    module.exports = (options = {}) => {
      return {
        postcssPlugin: 'postcss-failing-plugin',
        Declaration(decl) {
          /** Matches a `--` property not beginning with `--prefix-`. */
          const match = /^--(?!prefix-)/
    
          if (match.test(decl.prop)) {
            decl.prop = decl.prop.replace(match, `--prefix-`);
          }
        },
      };
    };
    
    module.exports.postcss = true;