All files / packages/design-system-2/scripts/buildCss buildPearl.ts

100% Statements 30/30
100% Branches 6/6
100% Functions 6/6
100% Lines 29/29

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                                                                                1x           1x                     1x 19x   19x               19x 19x 19x 19x     19x 19x     19x         19x   19x 7x               12x                                 1x 46x     1x 8x 8x 8x                 1x 5x 5x   5x   8x 8x 8x        
/**
 * Pearl builder.
 *
 * Pearl is a bespoke theme: its production output differs in shape from the
 * standard themes, so it lives in its own builder and stays out of the shared
 * THEMES registry.
 *
 * getPearlPlan derives a config from createBuildConfig and overrides the output
 * shape into a build plan — an array of descriptors, each describing one file to
 * emit. buildPearl consumes the plan by running the CSS processor over each task.
 */
 
import path from 'node:path';
import * as glob from 'glob';
import fs from 'fs-extra';
import postcss from 'postcss';
import { createBuildConfig, root } from './config.ts';
import { modularThemePlugins } from './modular-theme-plugins.ts';
import { sortImports } from './sort-imports.ts';
import {
  PEARL_THEME,
  PEARL_HOOKS_DIR,
  PEARL_THEME_DIR,
  PEARL_HOOKS_FILE,
  PEARL_OVERRIDES_FILE,
  PEARL_CORE_OVERRIDES_FILE,
} from './constants.ts';
 
/** One file the pearl build emits: which sources feed it and where it lands. */
export type PearlBuildTask = {
  inputs: string[]; // Inputs sources to build pearl.
  outputPath: string; // Outpath path for the artifact to emit.
};
 
type PearlPlanOptions = {
  production?: boolean;
  system?: string;
};
 
// Extend watcher with pearl-specific paths.
const watchFiles = {
  paths: [path.resolve(root, PEARL_HOOKS_DIR), path.resolve(root, PEARL_THEME_DIR)],
  options: {},
};
 
/** Pearl source roots the dev watcher adds to its watch set. */
export const pearlWatchPaths = watchFiles.paths;
 
/**
 * Get the pearl build plan.
 *
 * Only the production flag changes the output. It uses the same development and production flow as the other builds.
 * Source paths come from the shared configuration. The theme is pearl.
 * This function sets the output paths. Pearl file names do not follow the standard names.
 * When production is false, the plan has one file next to the other theme files.
 * When production is true, the plan has two files in build/pearl-theme/modular.
 */
export const getPearlPlan = (options: PearlPlanOptions = {}): PearlBuildTask[] => {
  const { production = false, system = 'slds2' } = options;
 
  const config = createBuildConfig({
    system,
    theme: PEARL_THEME,
    production,
    watchFiles,
  });
 
  // Define sources.
  const tokens = config.sources.tokens;
  const hooks = path.resolve(root, PEARL_HOOKS_DIR, PEARL_HOOKS_FILE);
  const overrides = path.resolve(root, PEARL_THEME_DIR, PEARL_OVERRIDES_FILE);
  const coreOverrides = path.resolve(root, PEARL_THEME_DIR, PEARL_CORE_OVERRIDES_FILE);
 
  // Define how to bundle Pearl
  const withCore = [tokens, hooks, overrides, coreOverrides];
  const withoutCore = [tokens, hooks, overrides];
 
  // OutputPaths (dev)
  const devPath = path.resolve(root, `build/css/${system}.theme.${PEARL_THEME}.css`);
 
  // OutputPaths (production)
  //   - slds2.pearl.css      : without core overrides, deployable to internal NPM.
  //   - slds2.pearl.core.css : with core-monolith overrides, deployable to SCS.
  const outputDir = path.resolve(root, 'build/pearl-theme/modular');
 
  if (!production) {
    return [
      {
        inputs: withCore,
        outputPath: path.resolve(devPath),
      },
    ];
  }
 
  return [
    {
      inputs: withoutCore,
      outputPath: path.resolve(outputDir, `${system}.${PEARL_THEME}.css`),
    },
    {
      inputs: withCore,
      outputPath: path.resolve(outputDir, `${system}.${PEARL_THEME}.core.css`),
    },
  ];
};
 
/**
 * Resolve a task's inputs to files. Absolute file paths glob to themselves; the
 * tokens glob expands to the design-tokens sources. A missing pearl file simply
 * drops out (glob returns nothing for it).
 */
export const resolvePearlInputs = (task: PearlBuildTask): string[] =>
  task.inputs.flatMap((pattern) => glob.sync(pattern));
 
// Inline the sorted @import manifest through PostCSS and write the artifact.
const processCss = async (css: string, outputPath: string, plugins: postcss.AcceptedPlugin[]) => {
  console.info(`Compiling ${outputPath}...`);
  const result = await postcss(plugins).process(css, { from: '' });
  await fs.outputFile(outputPath, result.css);
};
 
/**
 * Emit the pearl artifacts. Consumes the plan: resolves each task's inputs,
 * sorts them into an @import manifest, and processes them into the output path.
 *
 * dev-standard → one modular file; production → the two deployable artifacts.
 */
export const buildPearl = async (options: PearlPlanOptions = {}) => {
  const plan = getPearlPlan(options);
  const plugins = modularThemePlugins(options.production);
 
  await Promise.all(
    plan.map(async (task) => {
      const files = resolvePearlInputs(task);
      const css = sortImports(files);
      await processCss(css, task.outputPath, plugins);
    }),
  );
};