All files / packages/design-system/scripts/core write-core-dist.js

92.85% Statements 65/70
81.08% Branches 30/37
100% Functions 15/15
93.75% Lines 60/64

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 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291                      68x 68x     68x 68x     68x                             11x 11x 5x     6x 6x                   7x                             6x                   11x 11x 11x                   5x 5x                   4x 4x               68x                                 68x     4x 40x       4x                               8x 9x 5x   4x 5x 4x 4x 4x   4x                                                               8x   8x 8x                                       8x   8x                             8x 20x 9x 9x 11x 11x 11x 2x               8x     8x 8x                   9x 9x 4x                         5x 5x   5x     5x   5x 3x     5x 2x      
/**
 * Write Core Dist
 * Utility for writing CSS files to the dist-core directory
 */
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import fs from 'fs-extra';
import chalk from 'chalk';
import postcss from 'postcss';
import postcssNested from 'postcss-nested';
 
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
 
// Paths
const packageRoot = path.resolve(__dirname, '../..');
const defaultDistCorePath = path.join(packageRoot, 'dist-core/css');
 
// AgentForce CSS injection from design-system-2
const sdsSubsystemsRoot = path.resolve(packageRoot, '../design-system-2');
 
/**
 * Get AgentForce CSS content for injection.
 *
 * Reads directly from `src/sub-themes/agentforce/agentforce.css`. design-system-2's
 * production build does not emit a per-sub-theme dist artifact for AgentForce, so
 * the source file is the canonical (and only) location. The CSS is flattened and
 * formatted by the caller via `flattenNestedCss` before being appended to dist-core
 * output.
 *
 * @param {string} subsystemsRoot - Path to design-system-2 package root
 * @returns {string} AgentForce CSS content, or empty string when the source file is missing
 */
export function getAgentForceInjectionCSS(subsystemsRoot = sdsSubsystemsRoot) {
  const srcPath = path.join(subsystemsRoot, 'src/sub-themes/agentforce/agentforce.css');
  if (fs.existsSync(srcPath)) {
    return fs.readFileSync(srcPath, 'utf8');
  }
 
  console.log(chalk.yellow('Warning: AgentForce CSS not found, skipping injection'));
  return '';
}
 
/**
 * Build the component path for ui-force-components
 * @param {string} distCorePath - Base dist-core path
 * @param {string} componentName - Component directory name
 * @returns {string} Full component file path
 */
export function buildComponentPath(distCorePath, componentName) {
  return path.join(
    distCorePath,
    'ui-force-components/components/force',
    componentName,
    `${componentName}.css`,
  );
}
 
/**
 * Build the shared path for shared-slds-impl
 * @param {string} distCorePath - Base dist-core path
 * @param {string} filename - Filename (without extension)
 * @returns {string} Full shared file path
 */
export function buildSharedPath(distCorePath, filename) {
  return path.join(distCorePath, 'shared-slds-impl/resources/assets/css', `${filename}.css`);
}
 
/**
 * Write CSS to a file path
 * @param {string} css - CSS content
 * @param {string} filePath - Full file path
 * @param {string} label - Display label for logging
 */
export function writeCssFile(css, filePath, label) {
  fs.ensureDirSync(path.dirname(filePath));
  fs.writeFileSync(filePath, css, 'utf8');
  console.log(chalk.gray(`Writing file ${label} to ${filePath} - ${chalk.bgGreen('DONE')}`));
}
 
/**
 * Write CSS to ui-force-components directory
 * @param {string} css - CSS content
 * @param {string} componentName - Component directory name
 * @param {string} distCorePath - Base dist-core path (optional, for testing)
 */
export function writeDistComponent(css, componentName, distCorePath = defaultDistCorePath) {
  const componentPath = buildComponentPath(distCorePath, componentName);
  writeCssFile(css, componentPath, `${componentName}.css`);
}
 
/**
 * Write CSS to shared-slds-impl directory
 * @param {string} css - CSS content
 * @param {string} filename - Filename (without extension)
 * @param {string} distCorePath - Base dist-core path (optional, for testing)
 */
export function writeDistShared(css, filename, distCorePath = defaultDistCorePath) {
  const sharedPath = buildSharedPath(distCorePath, filename);
  writeCssFile(css, sharedPath, `${filename}.css`);
}
 
// Mis-expansions produced by the naive global `_ -> --` substitution that need
// to be rewritten back to the canonical SLDS class names. Mirrors the
// `exceptions` map in
// `packages/design-system-2/scripts/plugins/postcss-deprecated-selector.js` so
// the two pipelines stay in lockstep.
const DEPRECATED_SELECTOR_EXCEPTIONS = {
  // only the first underscore should be replaced
  '.slds-checkbox--faux--container': '.slds-checkbox--faux_container',
  '.slds-table--edit--container': '.slds-table--edit_container',
  '.slds-table--edit--container-message': '.slds-table--edit_container-message',
  '.slds-table--header-fixed--container': '.slds-table--header-fixed_container',
  // only the second underscore should be replaced
  '.slds-icon--container--circle': '.slds-icon_container--circle',
  '.slds-truncate--container--25': '.slds-truncate_container--25',
  '.slds-truncate--container--50': '.slds-truncate_container--50',
  '.slds-truncate--container--75': '.slds-truncate_container--75',
  '.slds-truncate--container--33': '.slds-truncate_container--33',
  '.slds-truncate--container--66': '.slds-truncate_container--66',
};
 
// Match a single `_` not adjacent to another `_` (i.e. SLDS modifier
// separator, never the BEM element separator `__`).
const DEPRECATED_SELECTOR_REGEX = /(?<!_)_(?!_)/g;
 
function applyExceptionFixups(selector) {
  for (const [wrong, right] of Object.entries(DEPRECATED_SELECTOR_EXCEPTIONS)) {
    Iif (selector.includes(wrong)) {
      return selector.replace(wrong, right);
    }
  }
  return selector;
}
 
/**
 * For each rule, append a `--`-modifier sibling next to every comma-separated
 * selector that contains a single-underscore SLDS modifier. Skips rules whose
 * selector contains a multi-arg `:not(.., ..)` (the comma confuses the
 * naive split), matching the legacy plugin's behavior.
 *
 * Implemented as a single `walkRules` pass: PostCSS does not revisit walked
 * rules when their `selector` is mutated, so we get correct one-shot
 * expansion without the duplicate-sibling bug that `Rule` visitors hit.
 *
 * @param {import('postcss').Root} root
 */
function expandDeprecatedSelectors(root) {
  root.walkRules((rule) => {
    if (!rule.selector.includes('_')) return;
    if (/:not\([^)]+,[^)]+\)/.test(rule.selector)) return;
 
    const expanded = rule.selector.split(',').map((selector) => {
      if (!DEPRECATED_SELECTOR_REGEX.test(selector)) return selector;
      DEPRECATED_SELECTOR_REGEX.lastIndex = 0;
      const dashed = applyExceptionFixups(selector.replaceAll(DEPRECATED_SELECTOR_REGEX, '--'));
      return `${selector},${dashed}`;
    });
    rule.selector = expanded.join(',');
  });
}
 
/**
 * Flatten CSS Nesting (e.g. `.parent { .child { ... } }`) into plain CSS
 * (`.parent .child { ... }`) and re-emit it in the same format the rest of
 * dist-core uses.
 *
 * Why this exists:
 *
 *  1. The AgentForce source (`design-system-2/src/sub-themes/agentforce/agentforce.css`)
 *     uses CSS Nesting. design-system-2's own pipeline runs it through
 *     `postcss-nested` before bundling. The Core CSS generator reads that
 *     source directly (no per-sub-theme dist artifact is emitted) so it
 *     must apply the same flattening here. Otherwise the appended CSS
 *     contains nested selectors that the Aura CSS parser rejects with
 *     "Expected to find closing brace '}'".
 *
 *  2. The rest of dist-core CSS is emitted by `scss-parser-aura` in a
 *     compact form: `selector{` (no space), 2-space indent, one declaration
 *     per line as `prop:value;` (no space after `:`), comma-separated
 *     value lists joined onto a single line, and one blank line between
 *     rules. PostCSS's default stringifier preserves source formatting,
 *     which would leave the appended block visibly different from the rest
 *     of the file (and produce a noisy diff vs prior releases). We
 *     normalize whitespace to match the dist-core convention so the
 *     appended section is indistinguishable from generator-emitted output.
 * @param {string} css - CSS with possible nesting
 * @returns {string} Flattened CSS in dist-core format
 */
export function flattenNestedCss(css) {
  Iif (!css) return css;
 
  const flattened = postcss([postcssNested()]).process(css, { from: undefined }).css;
  const root = postcss.parse(flattened);
 
  // Expand legacy single-underscore SLDS modifier classes (e.g. `.slds-button_brand`)
  // into a sibling `--`-modifier form (`.slds-button--brand`) so consumers using
  // the deprecated dual-dash naming continue to match. The previous Core CSS
  // generator (in the now-archived `design-system-dist` repo) consumed AgentForce
  // CSS from a per-sub-theme dist artifact emitted by `sds-subsystems`'s
  // `buildSubThemes.js`, which ran its own `postcss-deprecated-selector` plugin
  // to perform this expansion. The migrated Core generator reads the AgentForce
  // CSS source directly (design-system-2 does not emit a per-sub-theme dist
  // artifact), so the expansion has to be applied here to preserve output
  // parity.
  //
  // We replicate the algorithm inline (rather than importing the design-system-2
  // plugin) for two reasons: (1) the design-system-2 plugin uses PostCSS 8's
  // `Rule` visitor pattern which, in our minimal `[postcssNested(), …]` chain,
  // re-enters after `rule.selector` is mutated and produces duplicated `--`
  // siblings; (2) keeping the transform inline keeps the core-dist generator
  // self-contained and free of cross-package coupling. A single-pass
  // `walkRules` does not revisit on selector mutation, sidestepping the bug.
  expandDeprecatedSelectors(root);
 
  root.walkComments((c) => c.remove());
 
  // Match the legacy `design-system-dist` agentic-block formatting (which is
  // what the published 2.30.0 baseline uses). That format is:
  //
  //   - `selector{` with no space before `{`
  //   - `prop:value;` with no space after `:`
  //   - multi-line value lists collapsed onto a single line
  //   - declaration indent inherited from postcss-nested (4 spaces) and
  //     selector continuation indent inherited from postcss-nested (2 spaces)
  //
  // Importantly we leave `node.raws.before` alone for rules and decls so the
  // postcss-nested-emitted indentation survives. Re-normalizing those raws to
  // the rest-of-dist-core's 2-space style produces a noisy diff against the
  // baseline (which preserves the postcss-nested-native indent for this block).
  root.walk((node) => {
    if (node.type === 'rule') {
      node.raws.between = '';
      node.raws.semicolon = true;
    } else if (node.type === 'decl') {
      node.raws.between = ':';
      if (node.value && /\s/.test(node.value)) {
        node.value = node.value.replace(/\s+/g, ' ').trim();
      }
    } else Eif (node.type === 'atrule') {
      node.raws.between = '';
      node.raws.afterName = ' ';
    }
  });
 
  let out = root.toString();
  // Drop any leading whitespace before the first rule and reduce any
  // accidental triple+ blank lines to a single blank line.
  out = out.replace(/^\s+/, '').replace(/\n{3,}/g, '\n\n');
  return out;
}
 
/**
 * Append AgentForce CSS to content
 * @param {string} css - Original CSS
 * @param {string} subsystemsRoot - Path to design-system-2 package root
 * @returns {string} CSS with AgentForce appended
 */
export function appendAgentForceCss(css, subsystemsRoot = sdsSubsystemsRoot) {
  const agentForceCss = getAgentForceInjectionCSS(subsystemsRoot);
  if (!agentForceCss) return css;
  return css + '\n' + flattenNestedCss(agentForceCss);
}
 
/**
 * Write CSS to both component and shared directories
 * @param {Object} result - PostCSS result with css property
 * @param {string|null} componentName - Component name for ui-force-components (null to skip)
 * @param {string|null} sharedFilename - Filename for shared-slds-impl (null to skip)
 * @param {Object} options - Optional paths for testing
 * @param {string} options.distCorePath - Override dist-core path
 * @param {string} options.sdsSubsystemsRoot - Override design-system-2 root path
 */
export default function writeDist(result, componentName, sharedFilename, options = {}) {
  const distCorePath = options.distCorePath || defaultDistCorePath;
  const subsystemsRoot = options.sdsSubsystemsRoot || sdsSubsystemsRoot;
 
  const css = result.css;
 
  // Append AgentForce injection CSS
  const finalCss = appendAgentForceCss(css, subsystemsRoot);
 
  if (componentName) {
    writeDistComponent(finalCss, componentName, distCorePath);
  }
 
  if (sharedFilename) {
    writeDistShared(finalCss, sharedFilename, distCorePath);
  }
}