Files
rxminder/scripts/generate-unified-config.ts
William Valentin f3936f23fe feat: add unified configuration generation and migration scripts
- Add generate-unified-config.ts for generating environment configs
- Support multiple output formats: env files, Kubernetes configs, Docker
- Include migration script for transitioning from legacy configuration
- Support dry-run and verbose modes for safe configuration changes
- Generate type-safe environment exports for each environment
- Provide comprehensive error handling and validation
2025-09-08 09:44:50 -07:00

613 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bun
/**
* Generate Configuration Files from Unified Config
*
* This script reads the unified configuration and generates all necessary
* config files for different environments and deployment targets.
*
* Usage:
* bun scripts/generate-unified-config.ts [environment]
* bun scripts/generate-unified-config.ts production
* bun scripts/generate-unified-config.ts --all
*/
import { writeFileSync, mkdirSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import {
createUnifiedConfigForEnvironment,
exportAsEnvVars,
type Environment,
type UnifiedConfig,
} from '../config/unified.config';
interface GeneratorOptions {
environment?: Environment;
outputDir?: string;
generateAll?: boolean;
dryRun?: boolean;
verbose?: boolean;
}
class ConfigGenerator {
private options: GeneratorOptions;
private projectRoot: string;
constructor(options: GeneratorOptions = {}) {
this.options = options;
this.projectRoot = join(__dirname, '..');
}
/**
* Generate all configuration files
*/
async generate(): Promise<void> {
const environments: Environment[] = this.options.generateAll
? ['development', 'staging', 'production']
: [this.options.environment || this.getCurrentEnvironment()];
console.warn('🔧 Generating unified configuration files...');
console.warn(`📁 Project root: ${this.projectRoot}`);
console.warn(`🌍 Environments: ${environments.join(', ')}`);
for (const env of environments) {
await this.generateForEnvironment(env);
}
console.warn('✅ Configuration generation completed!');
}
/**
* Generate configuration files for a specific environment
*/
private async generateForEnvironment(
environment: Environment
): Promise<void> {
console.warn(`\n🔄 Generating configuration for: ${environment}`);
// Create config instance for specific environment
const config = createUnifiedConfigForEnvironment(environment);
const envVars = exportAsEnvVars(config);
// Generate different config file formats
await Promise.all([
this.generateDotEnv(environment, envVars),
this.generateKubernetesConfig(environment, config, envVars),
this.generateDockerEnv(environment, envVars),
this.generateViteEnv(environment, envVars),
this.generateTypeScriptConfig(environment, config),
]);
console.warn(`✅ Generated configuration for ${environment}`);
}
/**
* Generate .env file
*/
private async generateDotEnv(
environment: Environment,
envVars: Record<string, string>
): Promise<void> {
const content = this.createEnvFileContent(environment, envVars);
const filePath = join(this.projectRoot, `.env.${environment}`);
this.writeFile(filePath, content, `📄 .env.${environment}`);
}
/**
* Generate Kubernetes config.env file
*/
private async generateKubernetesConfig(
environment: Environment,
config: UnifiedConfig,
envVars: Record<string, string>
): Promise<void> {
const overlayDir = join(
this.projectRoot,
'k8s-kustomize',
'overlays',
environment
);
this.ensureDirectoryExists(overlayDir);
// Generate config.env for kustomize
const configContent = this.createKubernetesConfigContent(
environment,
envVars
);
const configPath = join(overlayDir, 'config.env');
this.writeFile(
configPath,
configContent,
`🚢 Kubernetes config.env (${environment})`
);
// Generate kustomization.yaml updates
await this.updateKustomizationFile(environment, config);
}
/**
* Generate Docker environment file
*/
private async generateDockerEnv(
environment: Environment,
envVars: Record<string, string>
): Promise<void> {
const dockerDir = join(this.projectRoot, 'docker');
this.ensureDirectoryExists(dockerDir);
const content = this.createDockerEnvContent(environment, envVars);
const filePath = join(dockerDir, `.env.${environment}`);
this.writeFile(filePath, content, `🐳 Docker .env.${environment}`);
}
/**
* Generate Vite-specific environment file
*/
private async generateViteEnv(
environment: Environment,
envVars: Record<string, string>
): Promise<void> {
const viteVars = Object.entries(envVars)
.filter(([key]) => key.startsWith('VITE_') || this.isViteRelevant(key))
.reduce(
(acc, [key, value]) => {
acc[key.startsWith('VITE_') ? key : `VITE_${key}`] = value;
return acc;
},
{} as Record<string, string>
);
const content = this.createViteEnvContent(environment, viteVars);
const filePath = join(this.projectRoot, `.env.vite.${environment}`);
this.writeFile(filePath, content, `⚡ Vite .env.vite.${environment}`);
}
/**
* Generate TypeScript config export
*/
private async generateTypeScriptConfig(
environment: Environment,
config: UnifiedConfig
): Promise<void> {
const configDir = join(this.projectRoot, 'config', 'generated');
this.ensureDirectoryExists(configDir);
const content = this.createTypeScriptConfigContent(environment, config);
const filePath = join(configDir, `${environment}.config.ts`);
this.writeFile(filePath, content, `📝 TypeScript ${environment}.config.ts`);
}
/**
* Create .env file content
*/
private createEnvFileContent(
environment: Environment,
envVars: Record<string, string>
): string {
const timestamp = new Date().toISOString();
return `# ${environment.toUpperCase()} Environment Configuration
# Generated automatically from unified configuration
# Generated on: ${timestamp}
#
# This file was auto-generated. Do not edit manually.
# To make changes, update config/unified.config.ts and regenerate.
${Object.entries(envVars)
.map(([key, value]) => `${key}=${value}`)
.join('\n')}
# Additional environment-specific variables can be added below
# (These will not be overwritten during regeneration)
`;
}
/**
* Create Kubernetes config.env content
*/
private createKubernetesConfigContent(
environment: Environment,
envVars: Record<string, string>
): string {
const timestamp = new Date().toISOString();
// Filter to Kubernetes-relevant variables
const k8sVars = Object.entries(envVars)
.filter(
([key]) => !key.startsWith('VITE_') && this.isKubernetesRelevant(key)
)
.reduce(
(acc, [key, value]) => {
acc[key] = value;
return acc;
},
{} as Record<string, string>
);
return `# Kubernetes configuration for ${environment}
# Generated automatically from unified configuration
# Generated on: ${timestamp}
${Object.entries(k8sVars)
.map(([key, value]) => `${key}=${value}`)
.join('\n')}
`;
}
/**
* Create Docker environment content
*/
private createDockerEnvContent(
environment: Environment,
envVars: Record<string, string>
): string {
const timestamp = new Date().toISOString();
return `# Docker environment for ${environment}
# Generated on: ${timestamp}
# Container Configuration
DOCKER_IMAGE=${envVars.DOCKER_IMAGE}
CONTAINER_REGISTRY=${envVars.CONTAINER_REGISTRY}
CONTAINER_REPOSITORY=${envVars.CONTAINER_REPOSITORY}
CONTAINER_TAG=${envVars.CONTAINER_TAG}
# Application Configuration
APP_NAME=${envVars.APP_NAME}
NODE_ENV=${envVars.NODE_ENV}
PORT=${envVars.PORT}
# Database Configuration
COUCHDB_URL=${envVars.COUCHDB_URL}
COUCHDB_USER=${envVars.COUCHDB_USER}
COUCHDB_PASSWORD=${envVars.COUCHDB_PASSWORD}
`;
}
/**
* Create Vite environment content
*/
private createViteEnvContent(
environment: Environment,
viteVars: Record<string, string>
): string {
const timestamp = new Date().toISOString();
return `# Vite environment variables for ${environment}
# Generated on: ${timestamp}
#
# These variables are available in the frontend build process
${Object.entries(viteVars)
.map(([key, value]) => `${key}=${value}`)
.join('\n')}
`;
}
/**
* Create TypeScript config content
*/
private createTypeScriptConfigContent(
environment: Environment,
config: UnifiedConfig
): string {
const timestamp = new Date().toISOString();
return `/**
* Generated configuration for ${environment}
* Generated on: ${timestamp}
*
* This file exports the resolved configuration for the ${environment} environment.
* It can be imported by other TypeScript files for type-safe configuration access.
*/
import type { UnifiedConfig } from '../unified.config';
export const ${environment}Config: UnifiedConfig = ${JSON.stringify(config, null, 2)} as const;
export default ${environment}Config;
`;
}
/**
* Update kustomization.yaml file with current configuration
*/
private async updateKustomizationFile(
environment: Environment,
config: UnifiedConfig
): Promise<void> {
const overlayDir = join(
this.projectRoot,
'k8s-kustomize',
'overlays',
environment
);
const kustomizationPath = join(overlayDir, 'kustomization.yaml');
if (!existsSync(kustomizationPath)) {
// Create new kustomization.yaml
const content = this.createKustomizationContent(environment, config);
this.writeFile(
kustomizationPath,
content,
`🎛️ kustomization.yaml (${environment})`
);
} else {
console.warn(
` Kustomization file exists: ${kustomizationPath} (not overwriting)`
);
}
}
/**
* Create kustomization.yaml content
*/
private createKustomizationContent(
environment: Environment,
config: UnifiedConfig
): string {
return `apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
metadata:
name: ${config.app.name}-${environment}
# Reference the base configuration
resources:
- ../../base
- namespace.yaml
# Override namespace for ${environment}
namespace: ${config.kubernetes.namespace}
# ${environment.charAt(0).toUpperCase() + environment.slice(1)}-specific labels
labels:
- pairs:
environment: ${environment}
tier: ${environment === 'production' ? 'prod' : environment}
# ${environment.charAt(0).toUpperCase() + environment.slice(1)} image tags and configurations
images:
- name: frontend-image
newName: ${config.container.registry}/${config.container.repository}
newTag: ${config.container.tag}
- name: couchdb-image
newName: couchdb
newTag: 3.3.2
# ${environment.charAt(0).toUpperCase() + environment.slice(1)} replicas
replicas:
- name: ${config.app.name}-frontend
count: ${config.kubernetes.replicas.frontend}
- name: ${config.app.name}-couchdb
count: ${config.kubernetes.replicas.database}
# Environment-specific patches
patches:
# Resource limits
- target:
kind: Deployment
name: ${config.app.name}-frontend
patch: |-
- op: replace
path: /spec/template/spec/containers/0/resources
value:
requests:
memory: "${config.kubernetes.resources.frontend.requests.memory}"
cpu: "${config.kubernetes.resources.frontend.requests.cpu}"
limits:
memory: "${config.kubernetes.resources.frontend.limits.memory}"
cpu: "${config.kubernetes.resources.frontend.limits.cpu}"
- target:
kind: StatefulSet
name: ${config.app.name}-couchdb
patch: |-
- op: replace
path: /spec/template/spec/containers/0/resources
value:
requests:
memory: "${config.kubernetes.resources.database.requests.memory}"
cpu: "${config.kubernetes.resources.database.requests.cpu}"
limits:
memory: "${config.kubernetes.resources.database.limits.memory}"
cpu: "${config.kubernetes.resources.database.limits.cpu}"
# ConfigMap generation
configMapGenerator:
- name: ${config.app.name}-config
envs:
- config.env
behavior: create
`;
}
/**
* Check if a variable is relevant for Vite
*/
private isViteRelevant(key: string): boolean {
const viteRelevantKeys = [
'APP_NAME',
'APP_VERSION',
'APP_BASE_URL',
'COUCHDB_URL',
'COUCHDB_USER',
'COUCHDB_PASSWORD',
'MAILGUN_API_KEY',
'MAILGUN_DOMAIN',
'MAILGUN_FROM_NAME',
'MAILGUN_FROM_EMAIL',
'GOOGLE_CLIENT_ID',
'GITHUB_CLIENT_ID',
'ENABLE_EMAIL_VERIFICATION',
'ENABLE_OAUTH',
'ENABLE_ADMIN_INTERFACE',
'DEBUG_MODE',
];
return viteRelevantKeys.includes(key);
}
/**
* Check if a variable is relevant for Kubernetes
*/
private isKubernetesRelevant(key: string): boolean {
const k8sIrrelevantKeys = ['VITE_', 'CONTAINER_', 'DOCKER_'];
return !k8sIrrelevantKeys.some(prefix => key.startsWith(prefix));
}
/**
* Get current environment from NODE_ENV
*/
private getCurrentEnvironment(): Environment {
const env = process.env.NODE_ENV || 'development';
return ['development', 'staging', 'production', 'test'].includes(env)
? (env as Environment)
: 'development';
}
/**
* Ensure directory exists
*/
private ensureDirectoryExists(dirPath: string): void {
if (!existsSync(dirPath)) {
mkdirSync(dirPath, { recursive: true });
if (this.options.verbose) {
console.warn(` 📁 Created directory: ${dirPath}`);
}
}
}
/**
* Write file with logging
*/
private writeFile(
filePath: string,
content: string,
description: string
): void {
if (this.options.dryRun) {
console.warn(` 🔍 [DRY RUN] Would write: ${description}`);
console.warn(` Path: ${filePath}`);
return;
}
// Ensure directory exists
this.ensureDirectoryExists(dirname(filePath));
writeFileSync(filePath, content, 'utf8');
console.warn(` ✅ Generated: ${description}`);
if (this.options.verbose) {
console.warn(` Path: ${filePath}`);
console.warn(` Size: ${content.length} bytes`);
}
}
}
/**
* CLI Interface
*/
async function main() {
const args = process.argv.slice(2);
const options: GeneratorOptions = {};
// Parse command line arguments
for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case '--all':
options.generateAll = true;
break;
case '--dry-run':
options.dryRun = true;
break;
case '--verbose':
case '-v':
options.verbose = true;
break;
case '--help':
case '-h':
showHelp();
process.exit(0);
break;
default:
if (!arg.startsWith('--') && !options.environment) {
options.environment = arg as Environment;
}
break;
}
}
// Validate environment
if (
options.environment &&
!['development', 'staging', 'production', 'test'].includes(
options.environment
)
) {
console.error(`❌ Invalid environment: ${options.environment}`);
console.error(
' Valid environments: development, staging, production, test'
);
process.exit(1);
}
try {
const generator = new ConfigGenerator(options);
await generator.generate();
} catch (error) {
console.error('❌ Configuration generation failed:', error);
process.exit(1);
}
}
/**
* Show help message
*/
function showHelp() {
console.warn(`
🔧 Unified Configuration Generator
USAGE:
bun scripts/generate-unified-config.ts [environment] [options]
ARGUMENTS:
environment Target environment (development, staging, production, test)
OPTIONS:
--all Generate configuration for all environments
--dry-run Show what would be generated without writing files
--verbose, -v Show detailed output
--help, -h Show this help message
EXAMPLES:
bun scripts/generate-unified-config.ts development
bun scripts/generate-unified-config.ts production --verbose
bun scripts/generate-unified-config.ts --all
bun scripts/generate-unified-config.ts --dry-run
GENERATED FILES:
📄 .env.[environment] - Environment variables
🚢 k8s-kustomize/overlays/[env]/config.env - Kubernetes configuration
🐳 docker/.env.[environment] - Docker environment
⚡ .env.vite.[environment] - Vite-specific variables
📝 config/generated/[env].config.ts - TypeScript config export
The generator reads from config/unified.config.ts and creates all necessary
configuration files for the specified environment(s).
`);
}
// Run CLI if this file is executed directly
if (import.meta.main) {
main().catch(console.error);
}
export { ConfigGenerator, type GeneratorOptions };