All files / packages/design-system-2/scripts/buildCss file-lists-theme-layer.ts

100% Statements 32/32
100% Branches 6/6
100% Functions 8/8
100% Lines 30/30

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                                                                                                4x                                   2855x                                               1x 19x 19x 19x 19x 1843x 1843x 1843x 798x 1045x 798x     19x                                           1x 1518x 1518x 1518x 1518x                 1x 15x           1x 17x 17x                                                       1x 12x 12x   1512x 12x 12x                  
/**
 * Theme-layer file list builders.
 *
 * These lists produce the theme-layer bundles (slds2.theme-layer.cosmos.css, etc.)
 * served when the SldsV2ThemeLayer gate (W-21432872) is enabled. Core switches
 * between the SCS resource names:
 *   - Gate OFF: "slds-plus"             -> slds2.cosmos.css          (built by file-lists.js)
 *   - Gate ON:  "slds-plus-theme-layer" -> slds2.theme-layer.cosmos.css  (built here)
 *
 * How build outputs reach core:
 *   1. This repo builds dist/css/bundled/slds2.cosmos.css and build/css/theme-layer/bundled/slds2.theme-layer.cosmos.css
 *   2. Those files are copied to aura/slds-scs, renamed to slds-plus-{v}.css and
 *      slds-plus-theme-layer-{v}.css, and registered in scs/files.json
 *   3. slds-scs is deployed to SCS (Static Content Service) via TNRP
 *   4. ServletUtilAdapterImpl resolves the SCS resource name at runtime, switching between
 *      "slds-plus" (gate off) and "slds-plus-theme-layer" (gate on)
 *
 * Related:
 *   - Core gate & serving logic (W-21432872, getSldsPlusCssUrl in ServletUtilAdapterImpl.java):
 *     https://gitcore.soma.salesforce.com/core-2206/core-262-public/pull/45140
 *   - Theme-layer build: https://github.com/salesforce-experience-platform-emu/salesforce-design-system/pull/1407
 *   - SCS repo: https://git.soma.salesforce.com/aura/slds-scs
 *
 * Exports:
 *   getThemeBaseFileList()      - all themes/base.css (no foundation)
 *   getThemeOnlyFileList()      - all themes/{theme}.css (no foundation)
 *   getUtilitiesFiles()         - reset.css + utilities/*.css
 *   getThemeLayerDistFileList() - dist monolith: foundation + utilities + components + base + theme
 */
 
import * as glob from 'glob';
import path from 'node:path';
import { args } from './args.ts';
import config, { root, designTokensDist } from './config.ts';
import { isNonEmpty, ignoreThemes, getFoundationFilesForTheme } from './file-lists.ts';
 
/**
 * Returns foundation token + legacy hook files for a specific theme (no config tokens).
 *
 * Used by buildModularThemes (legacy slds2.theme.{theme}.css) and as a building
 * block in buildThemeLayerBase (which adds theme-layer component CSS separately
 * via getThemeOnlyFileList).
 *
 * Does NOT include theme-layer component CSS (component themes/ directories);
 * those belong exclusively in the theme-layer outputs.
 *
 * @param {string} themeName Theme name (e.g. "cosmos", "lightning-blue").
 */
export const getModularThemeFileList = (themeName) => [
  ...glob.sync(path.resolve(designTokensDist, `themes/${themeName}/**/*.css`)),
  ...glob.sync(config.sources.legacyHooksShared),
  ...glob.sync(path.resolve(root, `src/legacy-hooks/${themeName}/**/*.css`)),
];
 
// ─── File list builders ──────────────────────────────────────────────────────
 
/**
 * The single source of truth for "has this component migrated to the theme
 * layer?": true when `<componentDir>/themes/base.css` exists.
 *
 * Both the canonical paint selection (`getThemeBaseFileList`) and the dist
 * de-duplication (`isSupersededLegacyMain`) key off this one fact, so the
 * legacy-vs-base.css decision is expressed in exactly one place.
 *
 * @param {string} componentDir Absolute path to a `src/<system>/<component>/` dir.
 */
const hasThemeBase = (componentDir) => glob.sync(path.resolve(componentDir, 'themes/base.css')).length > 0;
 
/**
 * Theme-layer base: every component's base paint, in priority order:
 *
 *   1. If `<component>/themes/base.css` exists, use it (the theme-layer scaffold
 *      has landed for that component).
 *   2. Otherwise fall back to `<component>/<component>.css` (the legacy paint),
 *      so the theme-layer bundle still ships visually-equivalent CSS for
 *      components that haven't migrated yet.
 *
 * That fallback is what lets us delete duplicated `themes/base.css` files
 * (which were just copies of the legacy CSS) without losing component paint
 * from the theme-layer bundle. When a component opens its hook API for real,
 * its `themes/base.css` lands and takes precedence over the fallback.
 *
 * `getThemeBaseFileList` is the authoritative owner of each component's *main*
 * paint file: exactly one entry per component directory. Any list that also
 * pulls component CSS from the raw glob (e.g. `getThemeLayerDistFileList`) must
 * exclude the superseded legacy mains (`isSupersededLegacyMain`) so the main
 * paint is not contributed twice.
 *
 * Shared across themes; called once, not per-theme.
 */
export const getThemeBaseFileList = () => {
  const system = args['--system'];
  const componentDirs = glob.sync(path.resolve(root, `src/${system}/*/`));
  const files = [];
  for (const dir of componentDirs) {
    const componentName = path.basename(dir);
    const legacyCss = path.resolve(dir, `${componentName}.css`);
    if (hasThemeBase(dir)) {
      files.push(path.resolve(dir, 'themes/base.css'));
    } else if (glob.sync(legacyCss).length > 0) {
      files.push(legacyCss);
    }
  }
  return files.filter(isNonEmpty);
};
 
/**
 * True when `file` is a component's legacy *main* paint
 * (`<component>/<component>.css`) that has been superseded by a landed
 * `themes/base.css` scaffold.
 *
 * A migrated component keeps its legacy `<component>.css` on disk (it is not
 * deleted when the scaffold lands), so the raw component glob still matches it.
 * `getThemeBaseFileList` already contributes the `themes/base.css` in its place;
 * a list that unions the raw glob with `getThemeBaseFileList()` would therefore
 * ship both the legacy paint and the base.css paint for the same component.
 * Filtering the glob with `!isSupersededLegacyMain` keeps exactly one paint
 * source per component and reuses the same `hasThemeBase` decision.
 *
 * Only the exact `<component>/<component>.css` main is superseded; sibling
 * legacy files (`card.deprecated.css`, `icon.type.css`, …) return false and are
 * preserved, as are the mains of components that have not migrated.
 *
 * @param {string} file Absolute path to a CSS file from the component glob.
 */
export const isSupersededLegacyMain = (file) => {
  const componentDir = path.dirname(file);
  const componentName = path.basename(componentDir);
  const isMain = path.basename(file) === `${componentName}.css`;
  return isMain && hasThemeBase(componentDir);
};
 
/**
 * Theme-layer theme-only: all themes/{themeName}.css files concatenated (no foundation).
 * Called once per theme in the THEMES loop.
 *
 * @param {string} themeName Theme name (e.g. "cosmos", "lightning-blue").
 */
export const getThemeOnlyFileList = (themeName) =>
  glob.sync(path.resolve(root, `src/${args['--system']}/**/themes/${themeName}.css`)).filter(isNonEmpty);
 
/**
 * Returns reset.css + all other utilities CSS files.
 * Reset is listed first to match importOrder.
 */
export const getUtilitiesFiles = () => {
  const system = args['--system'];
  return [
    ...glob.sync(path.resolve(root, `src/${system}/utilities/reset.css`)),
    ...glob.sync(path.resolve(root, `src/${system}/utilities/*.css`), {
      ignore: [path.resolve(root, `src/${system}/utilities/reset.css`)],
    }),
  ];
};
 
/**
 * Theme-layer dist: foundation + component CSS + reset/utilities + theme base + theme overrides.
 * Produces a single monolith file per theme for production distribution.
 *
 * Order: tokens, reset/utilities, base component CSS, themes/base.css, themes/{theme}.css
 *
 * `getThemeBaseFileList()` owns each component's main paint (one entry per
 * component: `themes/base.css` if migrated, else the legacy `<component>.css`).
 * The raw component glob is used only for the *supplemental* files it uniquely
 * provides (utilities, `*.deprecated.css`, `icon.type.css`, and the mains of
 * un-migrated components); its copies of migrated components' legacy mains are
 * dropped via `isSupersededLegacyMain`. Each component therefore ships exactly
 * one main paint source, matching the either/or behavior of the modular base
 * build. Without this the bundle carries both, and a component whose base.css
 * lowered its selector specificity (e.g. dropping a `[role=...]` qualifier)
 * would be overridden by its own leftover legacy rule, silently defeating its
 * hooks.
 *
 * @param {string} themeName Theme name (e.g. "cosmos", "lightning-blue").
 */
export const getThemeLayerDistFileList = (themeName) => {
  const foundation = getFoundationFilesForTheme(themeName);
  const componentCss = glob
    .sync(config.sources.input, { ignore: ignoreThemes() })
    .filter((file) => !isSupersededLegacyMain(file));
  const subThemes = glob.sync(config.sources.subThemes, { ignore: ignoreThemes() });
  return [
    ...foundation,
    ...getUtilitiesFiles(),
    ...componentCss,
    ...subThemes,
    ...getThemeBaseFileList(),
    ...getThemeOnlyFileList(themeName),
  ];
};