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

95.5% Statements 85/89
92.5% Branches 37/40
100% Functions 14/14
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                              109x 109x 109x 109x       109x 109x   109x     109x             327x                 111x 111x         111x 111x 111x               109x             2x               2x               1x                         38x   114x 114x 13x 13x     101x         202x 101x         101x       38x   35x                 10x                           35x 35x 6x   29x 29x                                 34x 34x 2x     32x     34x   34x   27x 27x     27x       5x                     21x 21x   21x       29x     29x     29x     29x 29x         109x  
/**
 * 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;