react-spa-template/scripts/generate-app-config.js
2025-04-19 19:26:03 +02:00

180 lines
5.3 KiB
JavaScript

#!/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(
"<head>",
'<head>\n <script src="./app-config.js"></script>',
);
// 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