All files / packages/design-system/scripts/core postcss-inject-styling-hooks.js

93.44% Statements 57/61
89.28% Branches 25/28
100% Functions 13/13
93.1% Lines 54/58

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 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221                              105x 105x 105x 105x       105x 105x   105x     105x             315x                 107x 107x         107x 107x 107x               105x             2x               2x               1x                         36x   108x 108x 13x 13x     95x         190x 95x         95x       36x   33x                 10x                           33x 33x 6x   27x 27x                                 32x 32x 2x     30x     32x   32x   25x 25x     25x       5x                     20x 20x   20x       27x     27x     27x     27x 27x         105x  
/**
 * PostCSS Plugin: Inject Styling Hooks
 * Injects SLDS styling hooks CSS custom properties into the stylesheet
 *
 * This plugin works by:
 * 1. Converting the entire CSS to a string
 * 2. Injecting styling hooks after the first :root block
 * 3. Re-parsing the processed CSS
 */
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
import path from 'node:path';
import fs from 'fs-extra';
import postcss from 'postcss';
 
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const require = createRequire(import.meta.url);
const { wrapInLayer } = require('../helpers/styling-hooks.js');
export { wrapInLayer };
 
// Path to design-tokens package in monorepo root node_modules
const packageRoot = path.resolve(__dirname, '../..');
const monorepoRoot = path.resolve(packageRoot, '../..');
 
const LAYER_ORDER = '@layer deprecated, defaults, shared, theme, component;';
 
// Map of CSS files to their @layer name
const designTokensLayerMap = [
  { file: 'lightning-blue.reference.tokens.css', layer: 'defaults' },
  { file: 'lightning-blue.global.tokens.css', layer: 'defaults' },
  { file: 'lightning-blue.shared.tokens.css', layer: 'shared' },
];
 
// List of CSS file names (derived from the layer map)
const sdsStylingHooksSourceFiles = designTokensLayerMap.map((entry) => entry.file);
 
/**
 * Resolve the styling hooks base path
 * Checks monorepo root node_modules first, then local package dist
 * @returns {string} Resolved base path
 */
export function resolveStylingHooksPath() {
  // Check monorepo root node_modules (published package uses themes/slds directly)
  const rootNodeModulesPath = path.resolve(monorepoRoot, 'node_modules/@salesforce-ux/design-tokens/themes/lightning-blue');
  Iif (fs.existsSync(rootNodeModulesPath)) {
    return rootNodeModulesPath;
  }
 
  // Fallback to sibling package dist (for local development)
  const siblingPkgPath = path.resolve(packageRoot, '../design-tokens/dist/themes/lightning-blue');
  Eif (fs.existsSync(siblingPkgPath)) {
    return siblingPkgPath;
  }
 
  // Final fallback to local node_modules
  return path.resolve(packageRoot, 'node_modules/@salesforce-ux/design-tokens/themes/lightning-blue');
}
 
// Default styling hooks base path
let sdsStylingHooksBasePath = resolveStylingHooksPath();
 
/**
 * Set the styling hooks base path (for testing)
 * @param {string} basePath - Base path to styling hooks
 */
export function setStylingHooksBasePath(basePath) {
  sdsStylingHooksBasePath = basePath;
}
 
/**
 * Get the current styling hooks base path
 * @returns {string} Current base path
 */
export function getStylingHooksBasePath() {
  return sdsStylingHooksBasePath;
}
 
/**
 * Get the list of styling hook files to read
 * @returns {string[]} Array of file names
 */
export function getStylingHookFiles() {
  return sdsStylingHooksSourceFiles;
}
 
/**
 * Reads design-tokens CSS files, wraps each in its @layer, and prepends the layer order.
 * @param {string} basePath - Base path to design-tokens (optional)
 * @param {string[]} hookFiles - Array of token file names (optional)
 * @returns {string} Combined CSS with @layer wrappers and order declaration
 */
export function readStylingHooksContent(
  basePath = sdsStylingHooksBasePath,
  hookFiles = sdsStylingHooksSourceFiles,
) {
  const layerBlocks = hookFiles
    .map((file) => {
      const filePath = path.join(basePath, file);
      if (!fs.existsSync(filePath)) {
        console.warn(`Design tokens file not found: ${filePath}`);
        return '';
      }
 
      const raw = fs
        .readFileSync(filePath, 'utf8')
        .replace(/\/\*[\s\S]*?\*\//g, '') // Remove all comments (generation + inline)
        .trim();
 
      const entry = designTokensLayerMap.find((e) => e.file === file);
      Iif (!entry) {
        console.warn(`No @layer mapping for token file: ${file}`);
        return raw;
      }
 
      return wrapInLayer(raw, entry.layer);
    })
    .filter(Boolean);
 
  if (layerBlocks.length === 0) return '';
 
  return `${LAYER_ORDER}\n${layerBlocks.join('\n')}`;
}
 
/**
 * Custom CSS minification that preserves @layer and :where()
 * @param {string} css - CSS content
 * @returns {string} Minified CSS
 */
export function minifyCssCustom(css) {
  return css
    .replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '') // Remove comments
    .replace(/\s+/g, ' ') // Replace multiple spaces with single space
    .replace(/\s*([{}:;,])\s*/g, '$1') // Remove spaces around braces, colons, semicolons, and commas
    .replace(/;\}/g, '}') // Remove semicolons before closing braces
    .trim();
}
 
/**
 * Find the position after the first :root block
 * @param {string} css - CSS content
 * @returns {{ found: boolean, position: number }}
 */
export function findRootBlockEnd(css) {
  const rootIndex = css.indexOf(':root');
  if (rootIndex === -1) {
    return { found: false, position: 0 };
  }
  const rootEnd = css.indexOf('}', rootIndex) + 1;
  return { found: true, position: rootEnd };
}
 
/**
 * Inject styling hooks into CSS content with original scope preserved
 * @param {string} css - Original CSS content
 * @param {string} basePath - Base path to styling hooks
 * @param {string[]} hookFiles - Array of hook file names
 * @param {boolean} shouldMinify - Whether to minify the injected hooks
 * @returns {string} CSS with styling hooks injected
 */
export function injectStylingHooks(
  css,
  basePath = sdsStylingHooksBasePath,
  hookFiles = sdsStylingHooksSourceFiles,
  shouldMinify = false,
) {
  const hooks = readStylingHooksContent(basePath, hookFiles);
  if (!hooks) {
    return css;
  }
 
  const hooksToInject = shouldMinify ? minifyCssCustom(hooks) : hooks;
 
  // Find the end of the :root (WCAG) block to inject hooks
  const { found, position: rootEnd } = findRootBlockEnd(css);
 
  if (found) {
    // Trim the existing CSS and the remaining CSS
    const rootCss = css.slice(0, rootEnd).trim();
    const remainingCss = css.slice(rootEnd).trim();
 
    // Ensure exactly one newline between sections
    return `${rootCss}\n${hooksToInject}\n${remainingCss}`;
  }
 
  // If no :root found, prepend hooks
  return `${hooksToInject}\n${css}`;
}
 
/**
 * PostCSS plugin factory
 * @param {Object} options - Plugin options
 * @param {string} options.basePath - Override styling hooks base path
 * @param {boolean} options.minify - Whether to minify the hooks (default: false)
 * @returns {Object} PostCSS plugin
 */
export default function postcssInjectStylingHooks(options = {}) {
  const basePath = options.basePath || sdsStylingHooksBasePath;
  const shouldMinify = options.minify || false;
 
  return {
    postcssPlugin: 'postcss-inject-styling-hooks',
    Once(root) {
      // Convert the AST to a string
      const css = root.toString();
 
      // Inject styling hooks
      const processedCss = injectStylingHooks(css, basePath, sdsStylingHooksSourceFiles, shouldMinify);
 
      // Remove all existing nodes from the AST
      root.removeAll();
 
      // Parse the processed CSS and append it to the root
      const parsedRoot = postcss.parse(processedCss);
      root.append(parsedRoot.nodes);
    },
  };
}
 
postcssInjectStylingHooks.postcss = true;