feat: Auto-generate .env.example files with empty values

This commit is contained in:
Aman Varshney
2025-05-24 13:04:30 +05:30
parent 0480bb7c82
commit 1cc9d81944
8 changed files with 114 additions and 96 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": minor
---
Auto-generate .env.example files with empty values

View File

@@ -3,13 +3,13 @@ import fs from "fs-extra";
import type { ProjectConfig } from "../types"; import type { ProjectConfig } from "../types";
import { generateAuthSecret } from "./auth-setup"; import { generateAuthSecret } from "./auth-setup";
interface EnvVariable { export interface EnvVariable {
key: string; key: string;
value: string | null | undefined; value: string | null | undefined;
condition: boolean; condition: boolean;
} }
async function addEnvVariablesToFile( export async function addEnvVariablesToFile(
filePath: string, filePath: string,
variables: EnvVariable[], variables: EnvVariable[],
): Promise<void> { ): Promise<void> {
@@ -22,11 +22,13 @@ async function addEnvVariablesToFile(
let modified = false; let modified = false;
let contentToAdd = ""; let contentToAdd = "";
const exampleVariables: string[] = [];
for (const { key, value, condition } of variables) { for (const { key, value, condition } of variables) {
if (condition) { if (condition) {
const regex = new RegExp(`^${key}=.*$`, "m"); const regex = new RegExp(`^${key}=.*$`, "m");
const valueToWrite = value ?? ""; const valueToWrite = value ?? "";
exampleVariables.push(`${key}=`);
if (regex.test(envContent)) { if (regex.test(envContent)) {
const existingMatch = envContent.match(regex); const existingMatch = envContent.match(regex);
@@ -51,6 +53,35 @@ async function addEnvVariablesToFile(
if (modified) { if (modified) {
await fs.writeFile(filePath, envContent.trimEnd()); await fs.writeFile(filePath, envContent.trimEnd());
} }
const exampleFilePath = filePath.replace(/\.env$/, ".env.example");
let exampleEnvContent = "";
if (await fs.pathExists(exampleFilePath)) {
exampleEnvContent = await fs.readFile(exampleFilePath, "utf8");
}
let exampleModified = false;
let exampleContentToAdd = "";
for (const exampleVar of exampleVariables) {
const key = exampleVar.split("=")[0];
const regex = new RegExp(`^${key}=.*$`, "m");
if (!regex.test(exampleEnvContent)) {
exampleContentToAdd += `${exampleVar}\n`;
exampleModified = true;
}
}
if (exampleContentToAdd) {
if (exampleEnvContent.length > 0 && !exampleEnvContent.endsWith("\n")) {
exampleEnvContent += "\n";
}
exampleEnvContent += exampleContentToAdd;
}
if (exampleModified || !(await fs.pathExists(exampleFilePath))) {
await fs.writeFile(exampleFilePath, exampleEnvContent.trimEnd());
}
} }
export async function setupEnvironmentVariables( export async function setupEnvironmentVariables(
@@ -160,8 +191,7 @@ export async function setupEnvironmentVariables(
if (database !== "none" && !specializedSetup) { if (database !== "none" && !specializedSetup) {
switch (database) { switch (database) {
case "postgres": case "postgres":
databaseUrl = databaseUrl = "postgresql://postgres:password@localhost:5432/postgres";
"postgresql://postgres:postgres@localhost:5432/mydb?schema=public";
break; break;
case "mysql": case "mysql":
databaseUrl = "mysql://root:password@localhost:3306/mydb"; databaseUrl = "mysql://root:password@localhost:3306/mydb";

View File

@@ -6,6 +6,7 @@ import fs from "fs-extra";
import pc from "picocolors"; import pc from "picocolors";
import type { ProjectConfig } from "../types"; import type { ProjectConfig } from "../types";
import { commandExists } from "../utils/command-exists"; import { commandExists } from "../utils/command-exists";
import { type EnvVariable, addEnvVariablesToFile } from "./env-setup";
type MongoDBConfig = { type MongoDBConfig = {
connectionString: string; connectionString: string;
@@ -85,27 +86,14 @@ async function initMongoDBAtlas(
async function writeEnvFile(projectDir: string, config?: MongoDBConfig) { async function writeEnvFile(projectDir: string, config?: MongoDBConfig) {
try { try {
const envPath = path.join(projectDir, "apps/server", ".env"); const envPath = path.join(projectDir, "apps/server", ".env");
await fs.ensureDir(path.dirname(envPath)); const variables: EnvVariable[] = [
{
let envContent = ""; key: "DATABASE_URL",
if (await fs.pathExists(envPath)) { value: config?.connectionString ?? "mongodb://localhost:27017/mydb",
envContent = await fs.readFile(envPath, "utf8"); condition: true,
} },
];
const mongoUrlLine = config await addEnvVariablesToFile(envPath, variables);
? `DATABASE_URL="${config.connectionString}"`
: `DATABASE_URL="mongodb://localhost:27017/mydb"`;
if (!envContent.includes("DATABASE_URL=")) {
envContent += `\n${mongoUrlLine}`;
} else {
envContent = envContent.replace(
/DATABASE_URL=.*(\r?\n|$)/,
`${mongoUrlLine}$1`,
);
}
await fs.writeFile(envPath, envContent.trim());
} catch (_error) { } catch (_error) {
consola.error("Failed to update environment configuration"); consola.error("Failed to update environment configuration");
} }
@@ -116,13 +104,17 @@ function displayManualSetupInstructions() {
${pc.green("MongoDB Atlas Manual Setup Instructions:")} ${pc.green("MongoDB Atlas Manual Setup Instructions:")}
1. Install Atlas CLI: 1. Install Atlas CLI:
${pc.blue("https://www.mongodb.com/docs/atlas/cli/stable/install-atlas-cli/")} ${pc.blue(
"https://www.mongodb.com/docs/atlas/cli/stable/install-atlas-cli/",
)}
2. Run the following command and follow the prompts: 2. Run the following command and follow the prompts:
${pc.blue("atlas deployments setup")} ${pc.blue("atlas deployments setup")}
3. Get your connection string from the Atlas dashboard: 3. Get your connection string from the Atlas dashboard:
Format: ${pc.dim("mongodb+srv://USERNAME:PASSWORD@CLUSTER.mongodb.net/DATABASE_NAME")} Format: ${pc.dim(
"mongodb+srv://USERNAME:PASSWORD@CLUSTER.mongodb.net/DATABASE_NAME",
)}
4. Add the connection string to your .env file: 4. Add the connection string to your .env file:
${pc.dim('DATABASE_URL="your_connection_string"')} ${pc.dim('DATABASE_URL="your_connection_string"')}
@@ -158,7 +150,9 @@ export async function setupMongoDBAtlas(config: ProjectConfig) {
mainSpinner.stop(pc.red("MongoDB Atlas setup failed")); mainSpinner.stop(pc.red("MongoDB Atlas setup failed"));
consola.error( consola.error(
pc.red( pc.red(
`Error during MongoDB Atlas setup: ${error instanceof Error ? error.message : String(error)}`, `Error during MongoDB Atlas setup: ${
error instanceof Error ? error.message : String(error)
}`,
), ),
); );

View File

@@ -6,6 +6,7 @@ import fs from "fs-extra";
import pc from "picocolors"; import pc from "picocolors";
import type { ProjectPackageManager } from "../types"; import type { ProjectPackageManager } from "../types";
import { getPackageExecutionCommand } from "../utils/get-package-execution-command"; import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
import { type EnvVariable, addEnvVariablesToFile } from "./env-setup";
type NeonConfig = { type NeonConfig = {
connectionString: string; connectionString: string;
@@ -120,12 +121,16 @@ async function createNeonProject(
async function writeEnvFile(projectDir: string, config?: NeonConfig) { async function writeEnvFile(projectDir: string, config?: NeonConfig) {
const envPath = path.join(projectDir, "apps/server", ".env"); const envPath = path.join(projectDir, "apps/server", ".env");
const envContent = config const variables: EnvVariable[] = [
? `DATABASE_URL="${config.connectionString}"` {
: `DATABASE_URL="postgresql://postgres:postgres@localhost:5432/mydb?schema=public"`; key: "DATABASE_URL",
value:
await fs.ensureDir(path.dirname(envPath)); config?.connectionString ??
await fs.writeFile(envPath, envContent); "postgresql://postgres:postgres@localhost:5432/mydb?schema=public",
condition: true,
},
];
await addEnvVariablesToFile(envPath, variables);
return true; return true;
} }

View File

@@ -7,6 +7,7 @@ import pc from "picocolors";
import type { ProjectPackageManager } from "../types"; import type { ProjectPackageManager } from "../types";
import { addPackageDependency } from "../utils/add-package-deps"; import { addPackageDependency } from "../utils/add-package-deps";
import { getPackageExecutionCommand } from "../utils/get-package-execution-command"; import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
import { type EnvVariable, addEnvVariablesToFile } from "./env-setup";
type PrismaConfig = { type PrismaConfig = {
databaseUrl: string; databaseUrl: string;
@@ -72,27 +73,16 @@ async function initPrismaDatabase(
async function writeEnvFile(projectDir: string, config?: PrismaConfig) { async function writeEnvFile(projectDir: string, config?: PrismaConfig) {
try { try {
const envPath = path.join(projectDir, "apps/server", ".env"); const envPath = path.join(projectDir, "apps/server", ".env");
await fs.ensureDir(path.dirname(envPath)); const variables: EnvVariable[] = [
{
let envContent = ""; key: "DATABASE_URL",
if (await fs.pathExists(envPath)) { value:
envContent = await fs.readFile(envPath, "utf8"); config?.databaseUrl ??
} "postgresql://postgres:postgres@localhost:5432/mydb?schema=public",
condition: true,
const databaseUrlLine = config },
? `DATABASE_URL="${config.databaseUrl}"` ];
: `DATABASE_URL="postgresql://postgres:postgres@localhost:5432/mydb?schema=public"`; await addEnvVariablesToFile(envPath, variables);
if (!envContent.includes("DATABASE_URL=")) {
envContent += `\n${databaseUrlLine}`;
} else {
envContent = envContent.replace(
/DATABASE_URL=.*(\r?\n|$)/,
`${databaseUrlLine}$1`,
);
}
await fs.writeFile(envPath, envContent.trim());
} catch (_error) { } catch (_error) {
consola.error("Failed to update environment configuration"); consola.error("Failed to update environment configuration");
} }

View File

@@ -6,6 +6,7 @@ import fs from "fs-extra";
import pc from "picocolors"; import pc from "picocolors";
import type { ProjectConfig, ProjectPackageManager } from "../types"; import type { ProjectConfig, ProjectPackageManager } from "../types";
import { getPackageExecutionCommand } from "../utils/get-package-execution-command"; import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
import { type EnvVariable, addEnvVariablesToFile } from "./env-setup";
async function writeSupabaseEnvFile( async function writeSupabaseEnvFile(
projectDir: string, projectDir: string,
@@ -13,38 +14,21 @@ async function writeSupabaseEnvFile(
): Promise<boolean> { ): Promise<boolean> {
try { try {
const envPath = path.join(projectDir, "apps/server", ".env"); const envPath = path.join(projectDir, "apps/server", ".env");
await fs.ensureDir(path.dirname(envPath));
let envContent = "";
if (await fs.pathExists(envPath)) {
envContent = await fs.readFile(envPath, "utf8");
}
const dbUrlToUse = const dbUrlToUse =
databaseUrl || "postgresql://postgres:postgres@127.0.0.1:54322/postgres"; databaseUrl || "postgresql://postgres:postgres@127.0.0.1:54322/postgres";
const variables: EnvVariable[] = [
const databaseUrlLine = `DATABASE_URL="${dbUrlToUse}"`; {
const directUrlLine = `DIRECT_URL="${dbUrlToUse}"`; key: "DATABASE_URL",
value: dbUrlToUse,
if (!envContent.includes("DATABASE_URL=")) { condition: true,
envContent += `\n${databaseUrlLine}`; },
} else { {
envContent = envContent.replace( key: "DIRECT_URL",
/DATABASE_URL=.*(\r?\n|$)/, value: dbUrlToUse,
`${databaseUrlLine}$1`, condition: true,
); },
} ];
await addEnvVariablesToFile(envPath, variables);
if (!envContent.includes("DIRECT_URL=")) {
envContent += `\n${directUrlLine}`;
} else {
envContent = envContent.replace(
/DIRECT_URL=.*(\r?\n|$)/,
`${directUrlLine}$1`,
);
}
await fs.writeFile(envPath, envContent.trim());
return true; return true;
} catch (error) { } catch (error) {
consola.error(pc.red("Failed to update .env file for Supabase.")); consola.error(pc.red("Failed to update .env file for Supabase."));

View File

@@ -11,9 +11,9 @@ import {
} from "@clack/prompts"; } from "@clack/prompts";
import consola from "consola"; import consola from "consola";
import { $ } from "execa"; import { $ } from "execa";
import fs from "fs-extra";
import pc from "picocolors"; import pc from "picocolors";
import { commandExists } from "../utils/command-exists"; import { commandExists } from "../utils/command-exists";
import { type EnvVariable, addEnvVariablesToFile } from "./env-setup";
type TursoConfig = { type TursoConfig = {
dbUrl: string; dbUrl: string;
@@ -138,7 +138,9 @@ async function createTursoDatabase(dbName: string, groupName: string | null) {
try { try {
s.start( s.start(
`Creating Turso database "${dbName}"${groupName ? ` in group "${groupName}"` : ""}...`, `Creating Turso database "${dbName}"${
groupName ? ` in group "${groupName}"` : ""
}...`,
); );
if (groupName) { if (groupName) {
@@ -173,14 +175,19 @@ async function createTursoDatabase(dbName: string, groupName: string | null) {
async function writeEnvFile(projectDir: string, config?: TursoConfig) { async function writeEnvFile(projectDir: string, config?: TursoConfig) {
const envPath = path.join(projectDir, "apps/server", ".env"); const envPath = path.join(projectDir, "apps/server", ".env");
const envContent = config const variables: EnvVariable[] = [
? `DATABASE_URL="${config.dbUrl}" {
DATABASE_AUTH_TOKEN="${config.authToken}"` key: "DATABASE_URL",
: `DATABASE_URL= value: config?.dbUrl ?? "",
DATABASE_AUTH_TOKEN=`; condition: true,
},
await fs.ensureDir(path.dirname(envPath)); {
await fs.writeFile(envPath, envContent); key: "DATABASE_AUTH_TOKEN",
value: config?.authToken ?? "",
condition: true,
},
];
await addEnvVariablesToFile(envPath, variables);
} }
function displayManualSetupInstructions() { function displayManualSetupInstructions() {
@@ -288,7 +295,9 @@ export async function setupTurso(config: ProjectConfig): Promise<void> {
setupSpinner.stop(pc.red("Failed to set up Turso database")); setupSpinner.stop(pc.red("Failed to set up Turso database"));
consola.error( consola.error(
pc.red( pc.red(
`Error during Turso setup: ${error instanceof Error ? error.message : String(error)}`, `Error during Turso setup: ${
error instanceof Error ? error.message : String(error)
}`,
), ),
); );
await writeEnvFile(projectDir); await writeEnvFile(projectDir);

View File

@@ -28,6 +28,7 @@ node_modules/
# env # env
.env* .env*
.env.production .env.production
!.env.example
.dev.vars .dev.vars
# logs # logs