All files / packages/fds-uif/generator-static/src component-resolver.ts

83.63% Statements 46/55
79.16% Branches 38/48
87.5% Functions 7/8
86.04% Lines 37/43

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                                                              20x 20x   10x 20x   8x 8x   7x 7x 7x   7x           20x       20x                 20x 20x                             13x   6x 6x   6x 6x 6x   5x 4x 1x 1x 1x         6x                         6x   1x 1x                         1x 1x 1x       1x             11x 7x 7x   11x      
/**
 * Component Resolver
 *
 * Resolves component references from a UIF registry into inline HTML.
 * Handles depth-limited recursive resolution and static prop application
 * to the child component's permutation state.
 */
 
import type { ComponentMetadata } from '@fds-uif/generator-base/browser';
import { analyzeUif } from '@fds-uif/generator-base/browser';
import type { StaticGenerationContext, PermutationState } from './types.js';
import { DEFAULT_OPTIONS } from './types.js';
import { buildDefaultState } from './variant-matrix.js';
import { buildHtmlFragment } from './html-builder.js';
 
/**
 * Resolve a component name to inline HTML via the registry.
 * Returns null when the registry is missing, the component is unknown,
 * or the depth limit has been reached.
 *
 * When a parent node carries modifier-resolved classes (e.g. Icon's svg child
 * has size/color modifiers targeting the PrimitiveIcon it composes),
 * `parentClasses` supplies those pre-resolved class tokens so they override
 * the child component's own root class.
 */
export function resolveComponentHtml(
  componentName: string,
  context: StaticGenerationContext,
  staticProps?: Record<string, unknown>,
  parentClasses?: string,
): string | null {
  const registry = context.options.componentRegistry;
  if (!registry) return null;
 
  const maxDepth = context.options.maxDepth ?? DEFAULT_OPTIONS.maxDepth;
  if (context.depth >= maxDepth) return null;
 
  const childUif = registry[componentName];
  if (!childUif) return null;
 
  try {
    const childMetadata = analyzeUif(childUif);
    const childState = applyStaticProps(buildDefaultState(childMetadata), childMetadata, staticProps);
 
    const extraClasses = parentClasses
      ? parentClasses.split(/\s+/).filter(Boolean)
      : extractBoundClassOverrides(childMetadata, staticProps);
 
    // Forward composition props into child slotOverrides so bound attributes
    // (e.g. <use href="{href}">) can resolve from parent-supplied values.
    const childOverrides = staticProps
      ? { ...context.options.slotOverrides, ...toStringRecord(staticProps) }
      : context.options.slotOverrides;
 
    const childContext: StaticGenerationContext = {
      metadata: childMetadata,
      options: { ...context.options, slotOverrides: childOverrides },
      indent: 0,
      state: childState,
      depth: context.depth + 1,
      extraClasses: extraClasses.length > 0 ? extraClasses : undefined,
    };
 
    const rawHtml = buildHtmlFragment(childMetadata.structure, childContext);
    return indentBlock(rawHtml, context.indent);
  } catch {
    return null;
  }
}
 
/**
 * Apply static prop overrides from a parent's componentProps.static
 * to the child component's permutation state (modifiers and variants).
 */
export function applyStaticProps(
  state: PermutationState,
  metadata: ComponentMetadata,
  staticProps?: Record<string, unknown>,
): PermutationState {
  if (!staticProps) return state;
 
  const modifiers = { ...state.modifiers };
  const variants = { ...state.variants };
 
  for (const [propName, value] of Object.entries(staticProps)) {
    const propMeta = metadata.props.find((p) => p.name === propName);
    if (!propMeta) continue;
 
    if (propMeta.source === 'modifier' || propMeta.source === 'state') {
      modifiers[propName] = value !== false && value !== 0 && value !== '' && value != null;
    } else if (EpropMeta.source === 'variant') {
      Eif (typeof value === 'string' || typeof value === 'number') {
        variants[propName] = String(value);
      }
    }
  }
 
  return { modifiers, variants };
}
 
/**
 * Inspect the child component's root bound attributes for a `class` binding.
 * If found, look up the corresponding prop name (e.g. "svgClass") in the
 * parent-supplied static props and return those classes for merging onto
 * the composed component's root element.
 */
function extractBoundClassOverrides(
  childMetadata: ComponentMetadata,
  staticProps?: Record<string, unknown>,
): string[] {
  if (!staticProps) return [];
 
  const boundClass = childMetadata.structure.boundAttributes.find((b) => b.attribute === 'class');
  Eif (!boundClass) return [];
 
  const val = staticProps[boundClass.prop];
  if (typeof val === 'string') return val.split(/\s+/).filter(Boolean);
  if (Array.isArray(val)) return val.map(String).filter(Boolean);
  return [];
}
 
/**
 * Convert a Record<string, unknown> to Record<string, string> for slot overrides.
 * Only includes entries whose values are non-empty strings.
 */
function toStringRecord(props: Record<string, unknown>): Record<string, string> {
  const result: Record<string, string> = {};
  for (const [key, val] of Object.entries(props)) {
    Iif (typeof val === 'string' && val !== '') {
      result[key] = val;
    }
  }
  return result;
}
 
/**
 * Indent every line of a multi-line string by a given depth.
 */
export function indentBlock(text: string, levels: number): string {
  if (levels === 0) return text;
  const pad = '  '.repeat(levels);
  return text
    .split('\n')
    .map((line) => (line ? `${pad}${line}` : line))
    .join('\n');
}