All files / packages/sds-customization-compliance/src/audit themeData.ts

95.34% Statements 41/43
75% Branches 12/16
100% Functions 4/4
100% Lines 37/37

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                                                                            139x 139x 139x 1610x 1610x 1610x 1610x 3777x 3777x   1610x 1610x   139x           51x 51x 2188x 2188x 2188x 4400x 1820x 1578x 1578x     2188x 668x   51x                 51x 51x 51x 102x 88x 102x 88x 88x   51x       53x 51x 51x 51x                
/**
 * Project an {@link EffectiveNewModel} into the themeData shape compliance
 * checks consume.
 *
 * The mapping matches `design-system-2/scripts/buildCss/buildThemeData.js`
 * byte-for-byte (minus the wrapper `{component, source}` metadata the checks
 * don't read): same selector normalization, same first-appearance hook order
 * within a selector, same category taxonomy via {@link groupHooksByCategory}.
 *
 * Source of each field:
 *
 * - `selectors` — walked from `effective.base.declsBySelector`. For each
 *   selector we collect every hook referenced by any declaration's value
 *   (deduplicated, preserving first-appearance order), then bucket them by
 *   category. Selectors whose base rules read no component hooks are omitted.
 * - `baseAssignments` — walked from `effective.base.assignmentsBySelector`
 *   (already populated by the audit parse pass). Carried through verbatim.
 * - `themes` — wraps each `effective.themes[name].assignmentsBySelector`
 *   under `{ selectors }` to match the ds2 on-disk layout. Themes that were
 *   missing or empty at load time are omitted.
 *
 * This function is pure and synchronous — the CSS parse has already happened
 * during audit construction, so we're just reshaping in-memory data.
 *
 * @param effective The effective-new model returned by `buildEffectiveNewModel`.
 * @returns A {@link ThemeData} object, or `null` when `effective` is falsy.
 */
 
import { groupHooksByCategory } from './categorize.js';
import type { EffectiveNewModel, HookAssignment } from './types.js';
import type { ThemeData, ThemeDataAssignmentBlock, ThemeDataSelectorReads } from '../types.js';
 
interface ProjectedAssignments {
  values: Record<string, Record<string, string>>;
  rows: ThemeDataAssignmentBlock;
}
 
function projectAssignmentBlock(block: Record<string, Record<string, HookAssignment>>): ProjectedAssignments {
  const values: Record<string, Record<string, string>> = {};
  const rows: ThemeDataAssignmentBlock = {};
  for (const [selector, hooks] of Object.entries(block)) {
    Iif (Object.keys(hooks).length === 0) continue;
    const valueMap: Record<string, string> = {};
    const rowMap: ThemeDataAssignmentBlock[string] = {};
    for (const [hook, assignment] of Object.entries(hooks)) {
      valueMap[hook] = assignment.value;
      rowMap[hook] = { value: assignment.value, source: assignment.source, line: assignment.line };
    }
    values[selector] = valueMap;
    rows[selector] = rowMap;
  }
  return { values, rows };
}
 
function readSelectorsFromBase(
  base: NonNullable<EffectiveNewModel['base']>,
): Record<string, ThemeDataSelectorReads> {
  const selectors: Record<string, ThemeDataSelectorReads> = {};
  for (const [selector, decls] of base.declsBySelector) {
    const seen = new Set<string>();
    const hooks: string[] = [];
    for (const decl of decls) {
      for (const hook of decl.hooksRead) {
        if (seen.has(hook)) continue;
        seen.add(hook);
        hooks.push(hook);
      }
    }
    if (hooks.length === 0) continue;
    selectors[selector] = groupHooksByCategory(hooks);
  }
  return selectors;
}
 
interface ProjectedThemes {
  values: NonNullable<ThemeData['themes']>;
  rows: NonNullable<ThemeData['themeAssignmentRows']>;
}
 
function projectThemes(themes: EffectiveNewModel['themes'] | undefined): ProjectedThemes {
  const values: NonNullable<ThemeData['themes']> = {};
  const rows: NonNullable<ThemeData['themeAssignmentRows']> = {};
  for (const [name, block] of Object.entries(themes ?? {})) {
    if (!block) continue;
    const projected = projectAssignmentBlock(block.assignmentsBySelector ?? {});
    Iif (Object.keys(projected.values).length === 0) continue;
    values[name] = { selectors: projected.values };
    rows[name] = { selectors: projected.rows };
  }
  return { values, rows };
}
 
export function projectThemeData(effective: EffectiveNewModel | null): ThemeData | null {
  if (!effective?.base) return null;
  const baseAssignments = projectAssignmentBlock(effective.base.assignmentsBySelector);
  const themes = projectThemes(effective.themes);
  return {
    selectors: readSelectorsFromBase(effective.base),
    baseAssignments: baseAssignments.values,
    baseAssignmentRows: baseAssignments.rows,
    themes: themes.values,
    themeAssignmentRows: themes.rows,
  };
}