Update CSP, dependencies, and add choice label generator
Replaces nginx.conf CSP map with inline policy and updates the policy for development. Adds new dependencies including Mantine, Radix, Tabler, FontAwesome, and others. Removes the fetch-openapi.js script and introduces generate-choice-labels.cjs to auto-generate TypeScript choice label constants from Orval enums, updating the api:gen script to run this generator. Also updates orval and other dev dependencies, and makes minor formatting changes in orval.config.ts.
This commit is contained in:
212
frontend/scripts/generate-choice-labels.cjs
Normal file
212
frontend/scripts/generate-choice-labels.cjs
Normal file
@@ -0,0 +1,212 @@
|
||||
#!/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<T extends string>(
|
||||
labels: Record<string, string>,
|
||||
value: T | undefined | null
|
||||
): string {
|
||||
if (!value) return '';
|
||||
return labels[value] || value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get options array for select dropdowns
|
||||
*/
|
||||
export function getChoiceOptions<T extends string>(
|
||||
labels: Record<T, string>
|
||||
): 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);
|
||||
}
|
||||
Reference in New Issue
Block a user