All files / packages/design-system-2/scripts/plugins postcss-scope-tokens.js

98% Statements 49/50
93.75% Branches 30/32
100% Functions 6/6
100% Lines 41/41

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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141                                                      1x 1x                 1x                                   1x 37x 34x   33x 33x 36x 36x 100x 33x 33x     36x 33x 2x   30x                 1x       25x         25x   25x   65x     64x   34x 34x     30x     30x 30x 30x       25x 24x     24x 24x 25x 1x   24x       24x   24x 1x   23x             1x      
/**
 * postcss-scope-tokens
 *
 * Transforms all SLDS design token declaration blocks into a layered cascade
 * architecture using CSS `@layer`.
 *
 * Two presets are exported for convenience:
 *
 *   LEGACY (default):
 *     @layer deprecated, defaults, shared, theme, component;
 *     --slds-g-* / --slds-r-* → defaults, --lwc-* → deprecated,
 *     --slds-s-* → shared, --slds-c-* → component
 *
 *   THEME_LAYER_PRESET:
 *     @layer deprecated, theme, customization;
 *     --slds-g-* / --slds-r-* / --slds-s-* → theme, --lwc-* → deprecated
 *
 * For every matched token block the plugin:
 *   1. Strips all selector aliases (`.slds-theme_*`, `.slds-theme--*`); only `:where(html)` is kept.
 *   2. Wraps the rule in the appropriate `@layer` at-rule.
 *
 * The `@layer` order declaration is injected as the very first node in the output,
 * immediately after any existing header comment, so the cascade order is established
 * before any token definitions.
 */
 
/** Legacy layer order and token mapping (used by dist/css/ outputs). */
const LEGACY_LAYER_ORDER = 'deprecated, defaults, shared, theme, component';
const LEGACY_TOKEN_LAYER = new Map([
  ['--slds-r-', 'defaults'],
  ['--slds-g-', 'defaults'],
  ['--slds-s-', 'shared'],
  ['--slds-c-', 'component'],
  ['--lwc-', 'deprecated'],
]);
 
/** Theme-layer preset (used by all theme-layer outputs: build/ and dist/). */
export const THEME_LAYER_PRESET = {
  layerOrder: 'deprecated, theme, customization',
  tokenLayer: new Map([
    ['--slds-r-', 'theme'],
    ['--slds-g-', 'theme'],
    ['--slds-s-', 'theme'],
    ['--lwc-', 'deprecated'],
  ]),
};
 
/**
 * Determine which layer all declarations in a rule belong to.
 * Returns the layer name if every declaration maps to the same layer, otherwise null.
 *
 * @param {import('postcss').Rule} rule
 * @param {Map<string, string>} tokenLayerMap
 * @returns {string | null}
 */
const resolveLayer = (rule, tokenLayerMap) => {
  const declarations = rule.nodes?.filter((n) => n.type === 'decl') ?? [];
  if (declarations.length === 0) return null;
 
  let layer = null;
  for (const decl of declarations) {
    let match = null;
    for (const [prefix, name] of tokenLayerMap) {
      if (decl.prop.startsWith(prefix)) {
        match = name;
        break;
      }
    }
    if (match === null) return null;
    if (layer === null) layer = match;
    else Iif (layer !== match) return null;
  }
  return layer;
};
 
/**
 * @param {{ injectLayerOrder?: boolean, layerOrder?: string, tokenLayer?: Map<string, string> }} [opts]
 * @param {boolean}  [opts.injectLayerOrder=true]  When false, skip the `@layer` order declaration.
 * @param {string}   [opts.layerOrder]             Custom layer order string (defaults to legacy).
 * @param {Map<string, string>} [opts.tokenLayer]  Custom token-prefix-to-layer mapping (defaults to legacy).
 */
const plugin = ({
  injectLayerOrder = true,
  layerOrder = LEGACY_LAYER_ORDER,
  tokenLayer = LEGACY_TOKEN_LAYER,
} = {}) => ({
  postcssPlugin: 'postcss-scope-tokens',
 
  // Track whether the @layer order declaration has been injected yet.
  prepare() {
    let layerOrderInjected = !injectLayerOrder;
 
    return {
      Rule(rule, { AtRule }) {
        if (!rule.selector.includes(':root') && !rule.selector.includes(':where(html)')) return;
 
        // Skip rules already inside an @layer — they've already been processed.
        if (rule.parent?.type === 'atrule' && rule.parent.name === 'layer') return;
 
        const layer = resolveLayer(rule, tokenLayer);
        if (layer === null) return;
 
        // Rewrite selector to bare :where(html), dropping all theme class aliases.
        rule.selector = ':where(html)';
 
        // Wrap in @layer <name> { … }
        const atLayer = new AtRule({ name: 'layer', params: layer });
        atLayer.append(rule.clone());
        rule.replaceWith(atLayer);
      },
 
      OnceExit(root, { AtRule, Declaration }) {
        if (layerOrderInjected) return;
        layerOrderInjected = true;
 
        // Find the insertion point: after any leading comment, before first real rule/at-rule.
        let insertAfter = null;
        for (const node of root.nodes) {
          if (node.type === 'comment') {
            insertAfter = node;
          } else {
            break;
          }
        }
 
        const layerDecl = new AtRule({ name: 'layer', params: layerOrder });
 
        if (insertAfter) {
          insertAfter.after(layerDecl);
        } else {
          root.prepend(layerDecl);
        }
      },
    };
  },
});
 
plugin.postcss = true;
 
export default plugin;