#!/usr/bin/env node /** * Generates TypeScript file with choice labels from Orval-generated enum files * * Usage: node scripts/generate-choice-labels.js * * This script reads the TypeScript enum files generated by Orval and extracts * choice labels from JSDoc comments (format: * `value` - Label) and generates * a TypeScript file with runtime-accessible label mappings. */ const fs = require('fs'); const path = require('path'); // Configuration const MODELS_DIRS = [ path.join(__dirname, '../src/api/generated/private/models'), path.join(__dirname, '../src/api/generated/public/models') ]; const OUTPUT_PATH = path.join(__dirname, '../src/constants/choiceLabels.ts'); // Convert camelCase to UPPER_SNAKE_CASE function camelToSnakeCase(str) { return str .replace(/([A-Z])/g, '_$1') .replace(/^_/, '') // Remove leading underscore .toUpperCase(); } // Extract enum labels from TypeScript file comments function parseEnumFile(filePath) { const content = fs.readFileSync(filePath, 'utf8'); const filename = path.basename(filePath, '.ts'); // Extract all JSDoc comment blocks // Match all: /** ... */ const commentRegex = /\/\*\*\s*\n([\s\S]*?)\*\//g; const comments = []; let match; while ((match = commentRegex.exec(content)) !== null) { comments.push(match[1]); } if (comments.length === 0) { return null; } // Try to find labels in all comment blocks // Parse labels from comment lines like: * `value` - Label // Note: The asterisk might have 0 or more spaces after it const labelPattern = /\*\s*`([^`]+)`\s*-\s*(.+)/g; const labels = {}; for (const comment of comments) { let labelMatch; while ((labelMatch = labelPattern.exec(comment)) !== null) { const [, value, label] = labelMatch; labels[value] = label.trim(); } } // Only return if we found labels if (Object.keys(labels).length === 0) { return null; } // Extract enum name from the type definition // Match: export type EnumName = ... const typeMatch = content.match(/export\s+type\s+(\w+Enum)\s*=/); const enumName = typeMatch ? typeMatch[1] : null; if (!enumName) { return null; } return { enumName, labels }; } // Main function function generateLabels() { console.log('🔍 Scanning Orval-generated enum files...'); const enums = {}; let totalEnumFiles = 0; // Check each models directory for (const MODELS_DIR of MODELS_DIRS) { const dirName = path.basename(path.dirname(MODELS_DIR)); if (!fs.existsSync(MODELS_DIR)) { console.warn(`⚠️ Models directory not found: ${MODELS_DIR}`); continue; } console.log(`📁 Scanning ${dirName}/models...`); // Read all enum files const files = fs.readdirSync(MODELS_DIR); const enumFiles = files.filter(f => f.endsWith('Enum.ts')); console.log(` Found ${enumFiles.length} enum files`); totalEnumFiles += enumFiles.length; // Parse each enum file for (const file of enumFiles) { const filePath = path.join(MODELS_DIR, file); const result = parseEnumFile(filePath); if (result) { // Check if enum already exists from another directory if (enums[result.enumName]) { // Compare labels to see if they're the same const existing = JSON.stringify(enums[result.enumName]); const current = JSON.stringify(result.labels); if (existing !== current) { console.warn(` ⚠️ ${result.enumName}: Different labels found in ${dirName}, keeping first version`); } } else { enums[result.enumName] = result.labels; console.log(` ✓ ${result.enumName}: ${Object.keys(result.labels).length} labels`); } } } } if (totalEnumFiles === 0) { console.error('❌ No enum files found in any models directory'); console.error(' Run "npm run api:gen" first to generate API client'); process.exit(1); } if (Object.keys(enums).length === 0) { console.error('❌ No enum labels found in generated files'); process.exit(1); } // Generate TypeScript file console.log('📝 Generating TypeScript file...'); const tsContent = `/** * Auto-generated choice labels from Orval-generated enum files * Generated by: scripts/generate-choice-labels.cjs * * DO NOT EDIT MANUALLY - Run \`npm run api:gen\` to regenerate */ ${Object.entries(enums).map(([enumName, labels]) => { const enumBaseName = enumName.replace(/Enum$/, ''); const constantName = camelToSnakeCase(enumBaseName) + '_LABELS'; return `/** * Labels for ${enumName} * ${Object.entries(labels).map(([value, label]) => `* ${value}: ${label}`).join('\n * ')} */ export const ${constantName} = ${JSON.stringify(labels, null, 2)} as const; `; }).join('\n')} /** * Type-safe helper to get choice label */ export function getChoiceLabel( labels: Record, value: T | undefined | null ): string { if (!value) return ''; return labels[value] || value; } /** * Get options array for select dropdowns */ export function getChoiceOptions( labels: Record ): Array<{ value: T; label: string }> { return Object.entries(labels).map(([value, label]) => ({ value: value as T, label: label as string, })); } // Auto-generate all OPTIONS exports ${Object.entries(enums).map(([enumName]) => { const enumBaseName = enumName.replace(/Enum$/, ''); const labelsConstName = camelToSnakeCase(enumBaseName) + '_LABELS'; const optionsConstName = camelToSnakeCase(enumBaseName) + '_OPTIONS'; return `export const ${optionsConstName} = getChoiceOptions(${labelsConstName});`; }).join('\n')} `; // Ensure output directory exists const outputDir = path.dirname(OUTPUT_PATH); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // Write file fs.writeFileSync(OUTPUT_PATH, tsContent, 'utf8'); console.log(`✅ Generated ${OUTPUT_PATH}`); console.log(` Found ${Object.keys(enums).length} enum types with labels`); } // Run the script try { generateLabels(); } catch (error) { console.error('❌ Error generating choice labels:', error.message); process.exit(1); }