All files / packages/fds-uif/generator-react/src types-builder.ts

91.66% Statements 44/48
60% Branches 3/5
100% Functions 5/5
91.66% Lines 44/48

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 156 157 158 159 160 161 162 163 164 165 166 167                                  22x 22x   22x     10x 10x       10x 21x 21x       10x 22x 22x 22x     22x   22x             10x 10x 10x     10x       1x         3x     10x   10x                   2x 2x   8x                                                     18x     2x 2x     16x             18x 18x       6x     3x   3x         6x 18x   6x 18x     18x             20x       19x 21x       3x       20x    
/**
 * Types Builder - Props/Types generation
 *
 * Generates TypeScript prop interfaces and component signatures from UIF definitions.
 */
 
import {
  type PropMetadata,
  type ComponentMetadata,
  sanitizeIdentifier,
} from '@fds-uif/generator-base/browser';
import type { GenerationContext } from './types.js';
 
/**
 * Build the props interface for a component
 */
export function buildPropsInterface(metadata: ComponentMetadata, context: GenerationContext): string {
  const { name, props } = metadata;
  const lines: string[] = [];
 
  lines.push(`export interface ${name}Props {`);
 
  for (const prop of props) {
    const propDef = buildPropDefinition(prop, context);
    lines.push(propDef);
  }
 
  // Add className prop for composition
  if (!props.some((p) => p.name === 'className')) {
    lines.push('  /** Additional CSS classes */');
    lines.push('  className?: string;');
  }
 
  // Add style prop
  if (!props.some((p) => p.name === 'style')) {
    lines.push('  /** Inline styles */');
    lines.push('  style?: React.CSSProperties;');
    context.typeImports.add('React');
  }
 
  lines.push('}');
 
  return lines.join('\n');
}
 
/**
 * Build a single prop definition
 */
function buildPropDefinition(prop: PropMetadata, context: GenerationContext): string {
  const lines: string[] = [];
  const required = prop.required ? '' : '?';
  const type = mapPropType(prop, context);
 
  // Sanitize prop name to be a valid JS identifier
  const propName = sanitizeIdentifier(prop.name);
 
  // Add JSDoc comment if description exists
  if (prop.description) {
    lines.push(`  /** ${prop.description} */`);
  }
 
  // Add default value annotation if present
  if (prop.default !== undefined) {
    lines.push(`  /** @default ${JSON.stringify(prop.default)} */`);
  }
 
  lines.push(`  ${propName}${required}: ${type};`);
 
  return lines.join('\n');
}
 
/**
 * Map UIF prop type to TypeScript type
 */
function mapPropType(prop: PropMetadata, context: GenerationContext): string {
  // If explicit type is provided by analyzer, use it (with ReactNode handling)
  if (prop.type) {
    if (prop.type === 'ReactNode') {
      context.typeImports.add('ReactNode');
      return 'React.ReactNode';
    }
    return prop.type;
  }
 
  // Infer from source when no explicit type
  const sourceTypeMap: Record<string, string> = {
    state: 'boolean',
    modifier: 'boolean',
    variant: 'string',
    attribute: 'string',
  };
 
  if (prop.source === 'slot') {
    context.typeImports.add('ReactNode');
    return 'React.ReactNode';
  }
 
  return sourceTypeMap[prop.source] ?? 'unknown';
}
 
/**
 * Build component function signature
 */
export function buildFunctionSignature(
  metadata: ComponentMetadata,
  context: GenerationContext,
  options: { forwardRef?: boolean } = {},
): string {
  const { name } = metadata;
 
  if (options.forwardRef) {
    context.imports.add("import { forwardRef } from 'react';");
    return `export const ${name} = forwardRef<HTMLElement, ${name}Props>(function ${name}(props, ref)`;
  }
 
  return `export function ${name}(props: ${name}Props)`;
}
 
/**
 * Build props destructuring
 */
export function buildPropsDestructuring(metadata: ComponentMetadata, context: GenerationContext): string {
  const { props } = metadata;
  const destructured: string[] = [];
 
  for (const prop of props) {
    // Sanitize prop name to be a valid JS identifier
    const propName = sanitizeIdentifier(prop.name);
 
    if (prop.default !== undefined) {
      destructured.push(`${propName} = ${JSON.stringify(prop.default)}`);
    } else {
      destructured.push(propName);
    }
  }
 
  // Always include className and style
  if (!props.some((p) => p.name === 'className')) {
    destructured.push('className');
  }
  if (!props.some((p) => p.name === 'style')) {
    destructured.push('style');
  }
 
  return `const { ${destructured.join(', ')} } = props;`;
}
 
/**
 * Get all type imports needed
 */
export function getTypeImports(context: GenerationContext): string[] {
  const imports: string[] = [];
 
  if (context.typeImports.size > 0) {
    // Filter out 'React' since we import * as React
    const types = Array.from(context.typeImports)
      .filter((t) => t !== 'React')
      .sort();
 
    if (types.length > 0) {
      imports.push(`import type { ${types.join(', ')} } from 'react';`);
    }
  }
 
  return imports;
}