Search code examples
postcss

Nested postcss at-rules are not removed


Interactive Fiddle

Here is a short reproducible example for the behavior I want to achieve:

var postcss = require('postcss');
var plugin = postcss.plugin('keepme', () => (root) => {
  root.walkAtRules(/keepme|removeme/, (atRule) => {
    if (atRule.name === 'keepme') {
      atRule.replaceWith(atRule.nodes);
    } else {
      atRule.remove();
    }
  });
});
postcss([plugin]).process(`
  @keepme {
    @removeme {
      .selector { color: red; }
    }
  }
`).then(result => console.log(result.css));

Given the input

  @keepme {
    @removeme {
      .selector { color: red; }
    }
  }

I would like this to return an empty string.

Instead, I receive the output

@removeme {
  .selector { color: red; }
}

The @keepme rule seems to correctly replace itself with its nodes (which is then not executed?).

I'm not sure how to go about this. Any suggestions?


Solution

  • replaceWith is implemented like this:

    /**
     * Inserts node(s) before the current node and removes the current node.
     *
     * @param {...Node} nodes - node(s) to replace current one
     *
     * @example
     * if ( atrule.name == 'mixin' ) {
     *   atrule.replaceWith(mixinRules[atrule.params]);
     * }
     *
     * @return {Node} current node to methods chain
     */
    replaceWith(...nodes) {
        if (this.parent) {
            for (let node of nodes) {
                this.parent.insertBefore(this, node);
            }
            this.remove();
        }
        return this;
    }
    

    Given the list of at-rules to traverse:

    1. @keepme
    2. @removeme

    The rule-walker keeps an index of the currently-inspected rule. At index 1 it finds keepme. keepme.replaceWith(removeme) will insert removeme before keepme, then continue walking the ast...

    Since removeme was moved ahead, the walker has moved past it, and will not execute that rule.

    The fix is to modify replaceWith so it will move the child nodes after the inspected rule.

      root.walkAtRules(/keepme|removeme/, (atRule) => {
        if (atRule.name === 'keepme') {
          if (atRule.parent) {
            for (let node of atRule.nodes.reverse()) {
              atRule.parent.insertAfter(atRule, node);
            }
          }
        }
        atRule.remove();
      });
    

    This works as intended: Interactive fiddle