All files / packages/design-system/scripts/helpers styling-hooks.js

45.94% Statements 17/37
46.66% Branches 7/15
57.14% Functions 4/7
45.45% Lines 15/33

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                                      3x 3x   3x     3x             3x     3x                           101x 101x 101x   99x 98x                                                                                                 3x                                           3x   3x     3x                                        
// Copyright (c) 2015-present, salesforce.com, inc. All rights reserved
// Licensed under BSD 3-Clause - see LICENSE.txt or git.io/sfdc-license
 
/* This module provides utilities for injecting and managing styling hooks from the design-tokens package
 * into CSS content. It includes the following functions and constants:
 *
 * Constants:
 * - sdsStylingHooksSourceFiles: List of CSS files containing the styling hooks to be injected
 * - sdsStylingHooksBasePath: Path to the design-tokens package directory
 *
 * Functions:
 * - wrapInLayer: Extracts declarations from a design-tokens CSS file and wraps them in @layer { :where(html) { } }
 * - readStylingHooksContent: Reads design-tokens CSS files, wraps each in its @layer, and prepends the layer order
 * - minifyCssCustom: Minifies CSS content (custom solution to solve issues with old dependency)
 * - injectStylingHooks: Injects styling hooks into CSS content with original scope preserved
 *
 * This module exports all these functions and constants for use in other modules.
 */
 
const fs = require('fs');
const path = require('path');
 
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);
 
// Path to the design-tokens sibling package (built by turbo before design-system)
const sdsStylingHooksBasePath = path.resolve(
  __dirname,
  '../../../design-tokens/dist/themes/lightning-blue',
);
 
/**
 * Extracts declarations from a design-tokens CSS file and wraps them in @layer { :where(html) { } }.
 * design-tokens outputs `:root, .slds-theme_* { declarations }` - this rewrites the selector
 * to `:where(html)` for zero specificity and wraps in the appropriate cascade layer.
 * @param {string} css - Raw CSS content from design-tokens
 * @param {string} layerName - The @layer to wrap declarations in
 * @returns {string} - CSS wrapped in @layer and :where(html)
 */
function wrapInLayer(css, layerName) {
  const openBrace = css.indexOf('{');
  const closeBrace = css.lastIndexOf('}');
  Eif (openBrace === -1 || closeBrace === -1) return '';
 
  const declarations = css.slice(openBrace + 1, closeBrace).trim();
  return `@layer ${layerName} {\n  :where(html) {\n    ${declarations}\n  }\n}`;
}
 
/**
 * Reads design-tokens CSS files, wraps each in its @layer, and prepends the layer order.
 * @param {string} hooksDir - Directory containing the token files
 * @param {string[]} hookFiles - Array of token file names
 * @returns {string} - Combined CSS with @layer wrappers and order declaration
 */
function readStylingHooksContent(hooksDir, hookFiles) {
  const layerBlocks = hookFiles
    .map((file) => {
      const filePath = path.join(hooksDir, 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);
      if (!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')}`;
}
 
/**
 * Minifies CSS content
 *
 * This acts as a replacement for the existing minifyCss function
 * The old clean-css version doesn't support @layer, and :where()
 * See: https://github.com/clean-css/clean-css/issues/1272
 *
 * @param {string} css - Original CSS content
 * @returns {string} - Minified CSS content
 */
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(); // Remove leading and trailing whitespace
}
 
/**
 * Injects styling hooks into CSS content with original scope preserved
 * @param {string} css - Original CSS content
 * @param {string} hooksDir - Directory containing hook files
 * @param {string[]} hookFiles - Array of hook file names
 * @param {boolean} shouldMinify - Whether to minify the injected hooks
 * @returns {string} - CSS content with injected hooks
 */
function injectStylingHooks(
  css,
  hooksDir = sdsStylingHooksBasePath,
  hookFiles = sdsStylingHooksSourceFiles,
  shouldMinify = false,
) {
  let hooks = readStylingHooksContent(hooksDir, hookFiles);
 
  const hooksToInject = shouldMinify ? minifyCssCustom(hooks) : hooks;
 
  // Find the end of the :root (WCAG) block to inject hooks
  const rootEnd = css.indexOf('}', css.indexOf(':root')) + 1;
 
  // 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
  const result = `${rootCss}\n${hooksToInject}\n${remainingCss}`;
 
  return result;
}
 
module.exports = {
  sdsStylingHooksSourceFiles,
  sdsStylingHooksBasePath,
  readStylingHooksContent,
  wrapInLayer,
  minifyCssCustom,
  injectStylingHooks,
};