#!/usr/bin/env node /** * This script generates a app-config.js file with environment variables * starting with CLIENT_ for both development and production environments. * * It works in both local development and production environments, * eliminating the need for a separate Docker entrypoint script. */ import fs from "fs"; import path from "path"; import dotenv from "dotenv"; import { fileURLToPath } from "url"; // Get the directory name in ESM const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Determine if we're in production or development mode const isProduction = process.env.NODE_ENV === "production"; // Load environment variables from .env files // In production, we prioritize .env.production and .env // In development, we use the standard Vite hierarchy if (isProduction) { dotenv.config({ path: path.resolve(process.cwd(), ".env.production") }); dotenv.config({ path: path.resolve(process.cwd(), ".env") }); } else { // Development mode - follow Vite's loading order dotenv.config({ path: path.resolve(process.cwd(), ".env") }); dotenv.config({ path: path.resolve(process.cwd(), ".env.development") }); dotenv.config({ path: path.resolve(process.cwd(), ".env.local") }); dotenv.config({ path: path.resolve(process.cwd(), ".env.development.local"), }); } // Determine the target directory based on environment // In production, we use the dist directory (or a specified output directory) // In development, we use the public directory const targetDir = isProduction ? process.env.OUTPUT_DIR || path.resolve(process.cwd(), "dist") : path.resolve(process.cwd(), "public"); // Ensure the target directory exists if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir, { recursive: true }); console.log(`Created directory: ${targetDir}`); } // Function to convert SNAKE_CASE to camelCase function toCamelCase(str) { return str .toLowerCase() .replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); } // Create the app-config.js file const configFilePath = path.join(targetDir, "app-config.js"); let configContent = "// Runtime environment variables - Generated by generate-app-config.js\n"; configContent += "window.appConfig = {\n"; // Get all environment variables starting with CLIENT_ const clientEnvVars = Object.entries(process.env).filter(([key]) => key.startsWith("CLIENT_"), ); // Add each environment variable to the config clientEnvVars.forEach(([key, value]) => { // Remove CLIENT_ prefix const keyWithoutPrefix = key.replace(/^CLIENT_/, ""); // Convert to camelCase const camelKey = toCamelCase(keyWithoutPrefix); // Add the key-value pair to the config // If the value is 'true', 'false', 'null', or a number, don't add quotes if ( value === "true" || value === "false" || value === "null" || /^[0-9]+$/.test(value) ) { configContent += ` ${camelKey}: ${value},\n`; } else { configContent += ` ${camelKey}: "${value}",\n`; } }); // Add default values for essential variables if they don't exist if (!process.env.CLIENT_BASE_PATH) { configContent += ' basePath: "/",\n'; } if (!process.env.CLIENT_APP_NAME) { configContent += ' appName: "App",\n'; } if (!process.env.CLIENT_DEBUG) { configContent += " debug: " + (isProduction ? "false" : "true") + ",\n"; } // Close the object configContent += "};"; // Helper function for colorized logging like Vite function formatLog(label, message, color = "\x1b[36m ") { // Default to cyan color const reset = "\x1b[0m"; const dim = "\x1b[2m"; const arrow = `${dim}${color}➜${reset}`; const formattedLabel = `${color}${label}:${reset}`; return ` ${arrow} ${formattedLabel} ${message}`; } // Write the config file fs.writeFileSync(configFilePath, configContent); console.log(formatLog("Generated", `app-config.js at ${configFilePath}`)); // Check if index.html exists in the target directory const indexHtmlPath = path.join(targetDir, "index.html"); if (fs.existsSync(indexHtmlPath)) { let indexHtmlContent = fs.readFileSync(indexHtmlPath, "utf8"); // Check if the script tag already exists if (!indexHtmlContent.includes("app-config.js")) { // Insert the script tag after the opening head tag indexHtmlContent = indexHtmlContent.replace( "", '\n ', ); // Write the updated index.html fs.writeFileSync(indexHtmlPath, indexHtmlContent); console.log( formatLog( "Updated", `injected script tag into ${path.basename(indexHtmlPath)}`, ), ); } else { console.log( formatLog( "Note", `app-config.js script already exists in ${path.basename(indexHtmlPath)}`, "\x1b[33m ", ), ); // Yellow } } else { console.log( formatLog("Note", `index.html not found in ${targetDir}`, "\x1b[34m "), ); // Blue if (!isProduction) { console.log( formatLog( "Vite", "script will be injected during development", "\x1b[34m ", ), ); // Blue } else { console.log( formatLog( "Warning", "index.html not found in production build directory!", "\x1b[33m ", ), ); // Yellow } } console.log( formatLog( "Ready", `app-config generated for ${isProduction ? "production" : "development"} environment`, "\x1b[32m ", ), ); // Green