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

100% Statements 22/22
100% Branches 4/4
100% Functions 6/6
100% Lines 20/20

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                                          9x 9x 9x     9x       9x     9x                                                   9x 30x   30x 1x       173x     29x   29x                 57x                                                                 9x                                 9x 123x                           123x     9x 123x        
/**
 * Config snapshot factory for the CSS build.
 *
 * createBuildConfig is pure. It takes an input object and returns one config
 * snapshot: resolved source globs, output paths, watch settings, the theme list,
 * and the selected theme. The rest of the pipeline reads the snapshot it receives.
 *
 * The default export is a temporary adapter. It calls the factory with the
 * parsed argv so modules that still import a shared `config` keep compiling.
 * It will be removed once every consumer reads a snapshot passed to it.
 *
 * Token sources resolve straight from @salesforce-ux/design-tokens in
 * node_modules. No intermediate copy step runs first.
 */
 
import path from 'node:path';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { args, THEME } from './args.ts';
import { THEMES, deriveFilename, fillName } from './utils/names.ts';
 
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const require = createRequire(import.meta.url);
 
/** Resolves to packages/design-system-2/. */
export const root = path.resolve(__dirname, '../../');
 
/** Resolves to the @salesforce-ux/design-tokens dist/ folder in node_modules. */
export const designTokensDist =
  path.dirname(require.resolve('@salesforce-ux/design-tokens/package.json')) + '/dist';
 
/** Returns the theme-layer dev output directory, build/css/theme-layer/. */
export const themeLayerOutputDir = () => path.resolve(root, 'build/css/theme-layer');
 
// The shape the caller passes for watch settings. Paths carry {system} and
// {theme} placeholders. The factory resolves them to absolute paths before the
// watcher starts. chokidar never sees a placeholder.
type WatchFilesInput = {
  paths: string[];
  options: Record<string, unknown>;
};
 
// The input createBuildConfig accepts for one variant.
type BuildConfigInput = {
  system: string;
  theme: string;
  production?: boolean;
  scoped?: boolean;
  watchFiles: WatchFilesInput;
};
 
/**
 * Build one config snapshot from an input object.
 *
 * Source globs and watch paths fill {system} and {theme} through the shared
 * helper. Output paths derive from the variant filename. The snapshot stores
 * resolved absolute paths so builders read them directly.
 */
export const createBuildConfig = (input: BuildConfigInput) => {
  const { system, theme, production = false, scoped = false, watchFiles } = input;
 
  if (!system) {
    throw new Error('Error: --system is required.');
  }
 
  // Fill {system} and {theme} for this variant.
  const fill = (template: string) => fillName(template, { system, theme });
 
  // Base output filename without the .css extension.
  const filename = deriveFilename({ system, theme, scoped, production });
 
  return {
    system,
    // Selected theme for this variant.
    theme,
    // Every theme this build produces. The set lives in utils/names.ts.
    themes: THEMES,
    filename,
    // The factory resolves each watch path to absolute. The options pass through unchanged.
    watchFiles: {
      paths: watchFiles.paths.map((watchPath) => path.resolve(root, fill(watchPath))),
      options: watchFiles.options,
    },
    // Source directories for this system.
    directories: {
      input: path.resolve(root, fill('src/{system}')),
      subThemes: path.resolve(root, 'src/sub-themes'),
    },
    // Glob patterns that select the input CSS.
    sources: {
      input: path.resolve(root, fill('src/{system}/**/*.css')),
      // Config tokens from design-tokens dist. These do not vary by theme.
      config: path.resolve(designTokensDist, 'config/*.css'),
      // Token CSS for the selected theme from design-tokens dist.
      tokens: path.resolve(designTokensDist, fill('themes/{theme}/**/*.css')),
      // Legacy component hooks scoped to the selected theme.
      legacyHooks: path.resolve(root, fill('src/legacy-hooks/{theme}/**/*.css')),
      // Legacy hooks at the root of legacy-hooks/. These do not vary by theme.
      legacyHooksShared: path.resolve(root, 'src/legacy-hooks/*.css'),
      subThemes: path.resolve(root, 'src/sub-themes/**/*.css'),
    },
    // Dev build output paths. index.ts derives the production paths.
    outputs: {
      css: path.resolve(root, `build/css/${filename}.css`),
      imports: path.resolve(root, `build/storybook/${filename}.imports.css`),
    },
  };
};
 
/**
 * Base output filename for the current argv. Kept for modules that still read a
 * shared filename. We will remove it with the adapter.
 */
export const FILENAME = deriveFilename({
  system: args['--system'],
  theme: THEME,
  scoped: Boolean(args['--scoped']),
  production: Boolean(args['--production']),
});
 
// Temporary adapter. Builds one snapshot from the CLI flags so the builders
// that still import a shared `config` keep working.
//
// The snapshot builds the first time a field is read, not when this file is
// imported. So a test can import createBuildConfig and pass its own input,
// without setting CLI flags. A real build reads a field and gets the snapshot.
type BuildConfig = ReturnType<typeof createBuildConfig>;
 
let adapter: BuildConfig | undefined;
 
const resolveAdapter = (): BuildConfig => {
  adapter ??= createBuildConfig({
    system: args['--system'],
    theme: THEME,
    production: Boolean(args['--production']),
    scoped: Boolean(args['--scoped']),
    watchFiles: {
      paths: ['src/{system}', 'src/sub-themes'],
      options: {
        persistent: true,
        ignoreInitial: true,
        awaitWriteFinish: true,
      },
    },
  });
  return adapter;
};
 
const config = new Proxy({} as BuildConfig, {
  get: (_target, prop) => resolveAdapter()[prop as keyof BuildConfig],
});
 
export default config;