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

92.06% Statements 58/63
74.19% Branches 23/31
100% Functions 11/11
91.52% Lines 54/59

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   22x 10x 10x       22x 21x 21x       22x 22x 22x 22x     22x   22x             10x 10x 10x     10x     10x 1x       10x 3x     10x   10x               10x 10x 2x 2x   8x                                                     18x   18x 2x 2x     16x             18x 18x   18x   6x   6x 3x   3x         18x 18x   18x 18x     18x             20x   20x   19x 21x     19x 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
  Eif (!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)
  Eif (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
  Eif (!props.some((p) => p.name === 'className')) {
    destructured.push('className');
  }
  Eif (!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;
}