All files / packages/design-system-2/scripts/plugins postcss-merge-layers.js

100% Statements 27/27
87.5% Branches 14/16
100% Functions 7/7
100% Lines 23/23

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75                                    8x       8x         8x 8x 18x 9x       8x   18x   15x   15x   4x 4x 4x   4x     11x           8x 2x 5x 5x 5x         2x 3x           1x      
/**
 * postcss-merge-layers
 *
 * Merges multiple `@layer <name> { ... }` blocks that share the same layer name
 * into a single `@layer <name> { ... }` block per unique name.
 *
 * The merged block is placed at the position of the first occurrence.
 * The `@layer` order declaration (e.g. `@layer deprecated, theme, customization;`)
 * is left untouched.
 *
 * This is useful for theme-layer outputs where postcss-scope-tokens and source
 * files each produce their own `@layer theme` blocks, resulting in many repeated
 * wrappers in the final output.
 */
 
/**
 * @type {import('postcss').PluginCreator}
 */
const postcssMergeLayers = () => ({
  postcssPlugin: 'postcss-merge-layers',
  OnceExit(root) {
    /** @type {Map<string, import('postcss').AtRule>} */
    const targets = new Map();
 
    // Find the layer order declaration (e.g. `@layer deprecated, theme, customization;`)
    // to use as the canonical sort order for merged blocks.
    /** @type {string[]} */
    let declaredOrder = [];
    root.walkAtRules('layer', (atRule) => {
      if (!atRule.nodes && declaredOrder.length === 0) {
        declaredOrder = atRule.params.split(',').map((s) => s.trim());
      }
    });
 
    root.walkAtRules('layer', (atRule) => {
      // Skip order declarations
      if (!atRule.nodes) return;
 
      const name = atRule.params.trim();
 
      if (targets.has(name)) {
        // Move all children into the first occurrence, then remove the empty shell
        const target = targets.get(name);
        atRule.each((node) => {
          target.append(node.clone());
        });
        atRule.remove();
      } else {
        // First occurrence becomes the merge target
        targets.set(name, atRule);
      }
    });
 
    // Reorder merged blocks to match the declared layer order so output is
    // deterministic regardless of source file glob ordering.
    if (declaredOrder.length > 0 && targets.size > 1) {
      const sorted = [...targets.entries()].sort((a, b) => {
        const aIdx = declaredOrder.indexOf(a[0]);
        const bIdx = declaredOrder.indexOf(b[0]);
        return (aIdx === -1 ? declaredOrder.length : aIdx) - (bIdx === -1 ? declaredOrder.length : bIdx);
      });
 
      // Move each block after the previous one to enforce order.
      // The first block anchors the sequence; subsequent blocks are placed after it.
      for (let i = 1; i < sorted.length; i++) {
        sorted[i - 1][1].after(sorted[i][1]);
      }
    }
  },
});
 
postcssMergeLayers.postcss = true;
 
export default postcssMergeLayers;