All files / packages/fds-uif/generator-lwc/src/utils renderWhenPlan.ts

100% Statements 56/56
96.87% Branches 31/32
100% Functions 9/9
100% Lines 46/46

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                                                            322x   9x 322x                         44x 44x 44x   44x 44x 44x 44x 44x 44x     44x 313x 313x   54x   53x 9x 9x 8x   9x       44x 44x 313x 313x 313x     44x 333x 331x 322x 313x 313x       44x   44x       9x 5x 5x   4x 4x 1x 1x   3x 4x     3x       42x     47x      
import type {
  ComponentMetadata,
  ComposedComponentMetadata,
  RenderWhen,
  RenderWhenPropMatch,
  SlotMetadata,
  StructureMetadata,
} from '@fds-uif/generator-base';
import { uppercaseFirstChar } from './uppercaseFirstChar.js';
 
/**
 * Maps each node that carries a `renderWhen` clause to the LWC `lwc:if`
 * binding name used for it, plus the list of prop-equality getters that need
 * to be emitted on the class.
 */
export interface RenderWhenPlan {
  bindings: Map<StructureMetadata | ComposedComponentMetadata, RenderWhenBinding>;
  getters: PropEqGetter[];
}
 
export interface RenderWhenBinding {
  /** The expression to use inside `lwc:if={ ... }`. */
  expression: string;
}
 
export interface PropEqGetter {
  name: string;
  match: RenderWhenPropMatch;
}
 
const isSlot = (c: unknown): c is SlotMetadata => !!c && (c as SlotMetadata).type === 'slot';
 
const isComposed = (c: unknown): c is ComposedComponentMetadata =>
  !!c && (c as ComposedComponentMetadata).type === 'component';
 
/**
 * Build a render-when plan by walking the structure tree.
 *
 * - `'slotFilled'` is intentionally *not* recorded here: the analyzer already
 *   surfaces those at the parent via `node.conditionals` (which the template
 *   builder consumes directly).
 * - `'propFilled'` binds to the prop named in the node's first
 *   `boundAttributes` entry (or the node's `name` as a fallback).
 * - `{ prop, eq }` produces a synthesized getter named `is{Prop}{Eq}`.
 */
export function buildRenderWhenPlan(metadata: ComponentMetadata): RenderWhenPlan {
  const bindings = new Map<StructureMetadata | ComposedComponentMetadata, RenderWhenBinding>();
  const getters: PropEqGetter[] = [];
  const usedGetterNames = new Set<string>();
 
  const reserve = (preferred: string): string => {
    let name = preferred;
    let i = 2;
    while (usedGetterNames.has(name)) name = `${preferred}${i++}`;
    usedGetterNames.add(name);
    return name;
  };
 
  const consider = (node: StructureMetadata | ComposedComponentMetadata): void => {
    const rw = node.renderWhen as RenderWhen | undefined;
    if (!rw) return;
 
    if (rw === 'slotFilled') return;
 
    if (rw === 'propFilled') {
      const propName = inferPropFilledTarget(node);
      if (propName) {
        bindings.set(node, { expression: propName });
      }
      return;
    }
 
    // { prop, eq } → `is{Prop}{Eq}`; { prop, filled } → `is{Prop}Filled`.
    const match = rw as RenderWhenPropMatch;
    const suffix = match.eq !== undefined ? pascalize(match.eq) : 'Filled';
    const getterName = reserve(`is${uppercaseFirstChar(match.prop)}${suffix}`);
    getters.push({ name: getterName, match });
    bindings.set(node, { expression: getterName });
  };
 
  const walk = (children: StructureMetadata['children'] | undefined): void => {
    if (!children) return;
    for (const child of children) {
      if (isSlot(child)) continue;
      consider(child);
      if (!isComposed(child)) walk(child.children);
    }
  };
 
  walk(metadata.structure.children);
 
  return { bindings, getters };
}
 
function inferPropFilledTarget(node: StructureMetadata | ComposedComponentMetadata): string | null {
  if (!isComposed(node)) {
    const ba = node.boundAttributes?.[0]?.prop;
    if (ba) return ba;
  } else {
    const bound = node.props?.bound;
    if (bound) {
      const first = Object.values(bound)[0];
      Eif (typeof first === 'string') return first;
    }
    const forwarded = node.props?.forwarded?.[0];
    if (forwarded) return forwarded;
  }
  // Last-ditch fallback: use the node's own name.
  return (node as StructureMetadata).name ?? null;
}
 
function pascalize(s: string): string {
  return s
    .split(/[-_\s]+/)
    .filter(Boolean)
    .map((seg) => uppercaseFirstChar(seg))
    .join('');
}