envsyncer
Version:
Sync your root .env files into monorepo subfolders with ease
129 lines (104 loc) ⢠4.06 kB
text/typescript
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
const ENV_FILES = ['.env', '.env.local', '.env.development', '.env.production'];
function parseEnvVars(content: string): Set<string> {
const envVars = new Set<string>();
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
// Skip comments and empty lines
if (trimmed && !trimmed.startsWith('#')) {
const equalIndex = trimmed.indexOf('=');
if (equalIndex > 0) {
const varName = trimmed.substring(0, equalIndex);
envVars.add(varName);
}
}
}
return envVars;
}
function extractLocalEnvVars(localContent: string, rootEnvVars: Set<string>): string[] {
const lines = localContent.split('\n');
const localEnvLines: string[] = [];
for (const line of lines) {
const trimmed = line.trim();
// Keep comments and empty lines that are not at the start
if (!trimmed || trimmed.startsWith('#')) {
// Only keep local comments if they're not environment variable comments
if (localEnvLines.length > 0) {
localEnvLines.push(line);
}
continue;
}
const equalIndex = trimmed.indexOf('=');
if (equalIndex > 0) {
const varName = trimmed.substring(0, equalIndex);
// Only keep local env vars that are not in root
if (!rootEnvVars.has(varName)) {
localEnvLines.push(line);
}
}
}
// Remove trailing empty lines
while (localEnvLines.length > 0 && !localEnvLines[localEnvLines.length - 1].trim()) {
localEnvLines.pop();
}
return localEnvLines;
}
export async function syncEnvToTargets(targetDirs: string[]) {
const rootEnvFiles = ENV_FILES.filter((file) =>
fs.existsSync(path.join(process.cwd(), file))
);
if (rootEnvFiles.length === 0) {
console.log(chalk.red('ā No .env files found in root directory.'));
console.log(
chalk.gray('Looked for:', ENV_FILES.join(', '))
);
return;
}
console.log(chalk.blue('\nš Found env files:'), rootEnvFiles.join(', '));
for (const envFile of rootEnvFiles) {
const rootEnvPath = path.join(process.cwd(), envFile);
const rootEnvContent = fs.readFileSync(rootEnvPath, 'utf-8');
const rootEnvVars = parseEnvVars(rootEnvContent);
for (const dir of targetDirs) {
const targetPath = path.join(process.cwd(), dir, envFile);
try {
let localEnvLines: string[] = [];
// Backup local env vars if the file exists
if (fs.existsSync(targetPath)) {
const localEnvContent = fs.readFileSync(targetPath, 'utf-8');
localEnvLines = extractLocalEnvVars(localEnvContent, rootEnvVars);
}
// Start with root env content (preserves all formatting and comments)
let finalContent = rootEnvContent;
// Add local env vars if any exist
if (localEnvLines.length > 0) {
// Ensure root content ends with a newline
if (!finalContent.endsWith('\n')) {
finalContent += '\n';
}
// Add separator comment
finalContent += '\n# Local environment variables\n';
finalContent += localEnvLines.join('\n');
// Ensure final content ends with a newline
if (!finalContent.endsWith('\n')) {
finalContent += '\n';
}
}
fs.writeFileSync(targetPath, finalContent);
const localCount = localEnvLines.filter(line => line.trim() && !line.trim().startsWith('#')).length;
const message = localCount > 0
? `ā
Synced ${envFile} to ${chalk.gray(`${dir}/${envFile}`)} ${chalk.cyan(`(preserved ${localCount} local vars)`)}`
: `ā
Synced ${envFile} to ${chalk.gray(`${dir}/${envFile}`)}`;
console.log(chalk.green(message));
} catch (error) {
console.error(
chalk.red(`ā Failed to sync ${envFile} to ${dir}:`),
error
);
}
}
}
}