Skip to content

Commit da69da5

Browse files
committed
feat: implement bootstrap configuration analysis and environment file management for containerization
1 parent 347b312 commit da69da5

File tree

7 files changed

+574
-8
lines changed

7 files changed

+574
-8
lines changed

src/commands/project.commands.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,67 @@ async function runContainerDev(options: ContainerDevOptions): Promise<void> {
443443
}
444444
}
445445

446+
// Step 1.5: Check bootstrap config and create missing env files if needed
447+
try {
448+
const { analyzeBootstrapConfig, shouldCopyEnvFiles, getEnvFileForEnvironment } =
449+
await import("../containerize/analyzers/bootstrap-analyzer");
450+
const bootstrapConfig = await analyzeBootstrapConfig();
451+
452+
if (bootstrapConfig.hasEnvFileConfig && shouldCopyEnvFiles(bootstrapConfig)) {
453+
const devEnvFile = getEnvFileForEnvironment(bootstrapConfig, "development");
454+
455+
// Check if required env file is missing
456+
if (bootstrapConfig.missingEnvFiles.includes(devEnvFile)) {
457+
console.log(
458+
chalk.yellow(`⚠️ Required env file missing: ${devEnvFile}`),
459+
);
460+
461+
// Auto-create template if configured or prompt user
462+
if (bootstrapConfig.autoCreateTemplate) {
463+
console.log(chalk.gray(` Creating template ${devEnvFile}...`));
464+
await createEnvTemplate(cwd, devEnvFile, "development", bootstrapConfig.requiredVariables);
465+
console.log(chalk.green(` ✓ Created ${devEnvFile}`));
466+
} else {
467+
// Provide helpful instructions
468+
console.log(chalk.cyan("\n💡 To fix this, either:"));
469+
console.log(
470+
chalk.gray(` 1. Create ${devEnvFile} with your environment variables`),
471+
);
472+
console.log(
473+
chalk.gray(` 2. Add autoCreateTemplate: true to envFileConfig in bootstrap`),
474+
);
475+
console.log(
476+
chalk.gray(` 3. Use skipFileLoading: true for container deployments`),
477+
);
478+
console.log();
479+
480+
// Still continue - the container might work if env vars are set in docker-compose
481+
console.log(
482+
chalk.yellow(` ⚠️ Container may fail if ${devEnvFile} is required`),
483+
);
484+
console.log();
485+
}
486+
}
487+
488+
// Show required variables that need to be set
489+
if (bootstrapConfig.requiredVariables.length > 0) {
490+
console.log(chalk.cyan("📋 Required environment variables:"));
491+
bootstrapConfig.requiredVariables.forEach((varName) => {
492+
console.log(chalk.gray(` • ${varName}`));
493+
});
494+
console.log(
495+
chalk.gray(` Set these in ${devEnvFile} or docker-compose.development.yml`),
496+
);
497+
console.log();
498+
}
499+
}
500+
} catch (error) {
501+
// Non-fatal - continue with container startup
502+
console.log(
503+
chalk.gray(" (Bootstrap analysis skipped)"),
504+
);
505+
}
506+
446507
// Step 2: Auto-run docker:setup if local dependencies exist
447508
if (existsSync(packageDockerJson) && existsSync(dockerSetupFile)) {
448509
// Check if .docker-deps needs to be updated
@@ -550,6 +611,29 @@ function isDirEmpty(dir: string): boolean {
550611
}
551612
}
552613

614+
/**
615+
* Create an environment template file
616+
*/
617+
async function createEnvTemplate(
618+
cwd: string,
619+
fileName: string,
620+
environment: string,
621+
requiredVariables: string[],
622+
): Promise<void> {
623+
const filePath = join(cwd, fileName);
624+
625+
const commonVars = [
626+
"PORT=3000",
627+
`NODE_ENV=${environment}`,
628+
"# Add your environment variables below",
629+
];
630+
631+
const requiredVars = requiredVariables.map((key) => `${key}=`);
632+
const template = [...commonVars, ...requiredVars].join("\n");
633+
634+
await fs.writeFile(filePath, template, "utf-8");
635+
}
636+
553637
/**
554638
* Run docker-compose command
555639
*/
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import fs from "fs";
2+
import path from "path";
3+
4+
/**
5+
* Environment file mapping from bootstrap configuration
6+
*/
7+
export interface EnvFileMapping {
8+
[environment: string]: string;
9+
}
10+
11+
/**
12+
* Bootstrap configuration detected from main.ts
13+
*/
14+
export interface BootstrapConfig {
15+
/** Whether envFileConfig is used */
16+
hasEnvFileConfig: boolean;
17+
/** Whether skipFileLoading is set to true (container-friendly) */
18+
skipFileLoading: boolean;
19+
/** Whether ciMode is explicitly set */
20+
ciMode: boolean | undefined;
21+
/** Environment file mappings */
22+
envFiles: EnvFileMapping;
23+
/** Required environment variables */
24+
requiredVariables: string[];
25+
/** Whether autoCreateTemplate is enabled */
26+
autoCreateTemplate: boolean;
27+
/** Current environment if specified */
28+
currentEnvironment: string | undefined;
29+
/** Detected env files that exist on disk */
30+
existingEnvFiles: string[];
31+
/** Detected env files that are missing */
32+
missingEnvFiles: string[];
33+
/** Whether the configuration is container-ready */
34+
isContainerReady: boolean;
35+
/** Recommendations for container deployment */
36+
recommendations: string[];
37+
}
38+
39+
/**
40+
* Analyze the bootstrap configuration in main.ts
41+
* Detects environment file configurations that affect container deployment
42+
*/
43+
export async function analyzeBootstrapConfig(): Promise<BootstrapConfig> {
44+
const cwd = process.cwd();
45+
const result: BootstrapConfig = {
46+
hasEnvFileConfig: false,
47+
skipFileLoading: false,
48+
ciMode: undefined,
49+
envFiles: {},
50+
requiredVariables: [],
51+
autoCreateTemplate: false,
52+
currentEnvironment: undefined,
53+
existingEnvFiles: [],
54+
missingEnvFiles: [],
55+
isContainerReady: true,
56+
recommendations: [],
57+
};
58+
59+
// Find main.ts file
60+
const mainTsPath = findMainFile(cwd);
61+
if (!mainTsPath) {
62+
return result;
63+
}
64+
65+
const content = fs.readFileSync(mainTsPath, "utf-8");
66+
67+
// Check if bootstrap is imported and used
68+
if (!content.includes("bootstrap")) {
69+
return result;
70+
}
71+
72+
// Parse bootstrap configuration
73+
parseBootstrapConfig(content, result);
74+
75+
// Detect existing env files
76+
detectExistingEnvFiles(cwd, result);
77+
78+
// Determine if configuration is container-ready
79+
evaluateContainerReadiness(result);
80+
81+
return result;
82+
}
83+
84+
/**
85+
* Find the main.ts file in the project
86+
*/
87+
function findMainFile(cwd: string): string | null {
88+
const possiblePaths = [
89+
path.join(cwd, "src", "main.ts"),
90+
path.join(cwd, "src", "index.ts"),
91+
path.join(cwd, "main.ts"),
92+
path.join(cwd, "index.ts"),
93+
];
94+
95+
for (const filePath of possiblePaths) {
96+
if (fs.existsSync(filePath)) {
97+
return filePath;
98+
}
99+
}
100+
101+
return null;
102+
}
103+
104+
/**
105+
* Parse bootstrap configuration from file content
106+
* Uses regex patterns to extract configuration without full AST parsing
107+
*/
108+
function parseBootstrapConfig(content: string, result: BootstrapConfig): void {
109+
// Check for envFileConfig usage
110+
const envFileConfigMatch = content.match(/envFileConfig\s*[:{]/);
111+
if (envFileConfigMatch) {
112+
result.hasEnvFileConfig = true;
113+
}
114+
115+
// Check for skipFileLoading: true
116+
if (/skipFileLoading\s*:\s*true/.test(content)) {
117+
result.skipFileLoading = true;
118+
}
119+
120+
// Check for ciMode
121+
const ciModeMatch = content.match(/ciMode\s*:\s*(true|false)/);
122+
if (ciModeMatch) {
123+
result.ciMode = ciModeMatch[1] === "true";
124+
}
125+
126+
// Check for autoCreateTemplate
127+
if (/autoCreateTemplate\s*:\s*true/.test(content)) {
128+
result.autoCreateTemplate = true;
129+
}
130+
131+
// Extract files mapping
132+
const filesMatch = content.match(
133+
/files\s*:\s*\{([^}]+)\}/s,
134+
);
135+
if (filesMatch) {
136+
const filesContent = filesMatch[1];
137+
// Match key-value pairs like: development: ".env.dev"
138+
const envMappings = filesContent.matchAll(
139+
/(\w+)\s*:\s*["'`]([^"'`]+)["'`]/g,
140+
);
141+
for (const match of envMappings) {
142+
result.envFiles[match[1]] = match[2];
143+
}
144+
}
145+
146+
// Extract required variables
147+
const requiredMatch = content.match(
148+
/required\s*:\s*\[([^\]]+)\]/s,
149+
);
150+
if (requiredMatch) {
151+
const requiredContent = requiredMatch[1];
152+
const variables = requiredContent.matchAll(/["'`]([^"'`]+)["'`]/g);
153+
for (const match of variables) {
154+
result.requiredVariables.push(match[1]);
155+
}
156+
}
157+
158+
// Extract currentEnvironment
159+
const envMatch = content.match(
160+
/currentEnvironment\s*:\s*["'`]([^"'`]+)["'`]/,
161+
);
162+
if (envMatch) {
163+
result.currentEnvironment = envMatch[1];
164+
}
165+
}
166+
167+
/**
168+
* Detect which env files exist and which are missing
169+
*/
170+
function detectExistingEnvFiles(cwd: string, result: BootstrapConfig): void {
171+
// If no explicit files mapping, use convention
172+
if (Object.keys(result.envFiles).length === 0 && result.hasEnvFileConfig) {
173+
// Default convention: .env.{environment}
174+
const defaultEnvs = ["development", "production", "staging", "test"];
175+
for (const env of defaultEnvs) {
176+
result.envFiles[env] = `.env.${env}`;
177+
}
178+
}
179+
180+
// Check each mapped file
181+
for (const [env, fileName] of Object.entries(result.envFiles)) {
182+
const filePath = path.join(cwd, fileName);
183+
if (fs.existsSync(filePath)) {
184+
result.existingEnvFiles.push(fileName);
185+
} else {
186+
result.missingEnvFiles.push(fileName);
187+
}
188+
}
189+
190+
// Also check for common env files
191+
const commonEnvFiles = [".env", ".env.local", ".env.example"];
192+
for (const fileName of commonEnvFiles) {
193+
const filePath = path.join(cwd, fileName);
194+
if (
195+
fs.existsSync(filePath) &&
196+
!result.existingEnvFiles.includes(fileName)
197+
) {
198+
result.existingEnvFiles.push(fileName);
199+
}
200+
}
201+
}
202+
203+
/**
204+
* Evaluate if the bootstrap configuration is container-ready
205+
*/
206+
function evaluateContainerReadiness(result: BootstrapConfig): void {
207+
result.isContainerReady = true;
208+
result.recommendations = [];
209+
210+
// If envFileConfig is used but skipFileLoading is not true
211+
if (result.hasEnvFileConfig && !result.skipFileLoading && !result.ciMode) {
212+
// Check if there are missing env files for development
213+
const devEnvFile = result.envFiles["development"] || ".env.development";
214+
if (result.missingEnvFiles.includes(devEnvFile)) {
215+
result.isContainerReady = false;
216+
result.recommendations.push(
217+
`Create ${devEnvFile} or set skipFileLoading: true for containers`,
218+
);
219+
}
220+
221+
// Recommend container-friendly configuration
222+
if (!result.skipFileLoading) {
223+
result.recommendations.push(
224+
"Consider using skipFileLoading: true for Docker deployments",
225+
);
226+
result.recommendations.push(
227+
"Use docker-compose environment variables instead of .env files",
228+
);
229+
}
230+
}
231+
232+
// If there are required variables, they need to be provided
233+
if (result.requiredVariables.length > 0) {
234+
result.recommendations.push(
235+
`Ensure these variables are set in docker-compose: ${result.requiredVariables.join(", ")}`,
236+
);
237+
}
238+
}
239+
240+
/**
241+
* Get the env file for a specific environment
242+
*/
243+
export function getEnvFileForEnvironment(
244+
config: BootstrapConfig,
245+
environment: string,
246+
): string {
247+
return config.envFiles[environment] || `.env.${environment}`;
248+
}
249+
250+
/**
251+
* Check if env files should be copied to the container
252+
*/
253+
export function shouldCopyEnvFiles(config: BootstrapConfig): boolean {
254+
// Don't copy if skipFileLoading is true or ciMode is true
255+
if (config.skipFileLoading || config.ciMode) {
256+
return false;
257+
}
258+
259+
// Copy if envFileConfig is used and there are existing files
260+
return config.hasEnvFileConfig && config.existingEnvFiles.length > 0;
261+
}

src/containerize/analyzers/project-analyzer.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import fs from "fs";
22
import path from "path";
33
import Compiler from "../../utils/compiler";
4+
import {
5+
analyzeBootstrapConfig,
6+
type BootstrapConfig,
7+
} from "./bootstrap-analyzer";
48

59
export interface ProjectAnalysis {
610
nodeVersion: string;
@@ -17,6 +21,8 @@ export interface ProjectAnalysis {
1721
port: number;
1822
hasLocalDependencies: boolean;
1923
localDependencyPaths: string[];
24+
/** Bootstrap configuration analysis */
25+
bootstrapConfig: BootstrapConfig;
2026
}
2127

2228
export async function analyzeProject(): Promise<ProjectAnalysis> {
@@ -87,6 +93,9 @@ export async function analyzeProject(): Promise<ProjectAnalysis> {
8793
// Detect port
8894
const port = await detectPort(cwd);
8995

96+
// Analyze bootstrap configuration
97+
const bootstrapConfig = await analyzeBootstrapConfig();
98+
9099
return {
91100
nodeVersion,
92101
packageManager,
@@ -102,6 +111,7 @@ export async function analyzeProject(): Promise<ProjectAnalysis> {
102111
port,
103112
hasLocalDependencies,
104113
localDependencyPaths,
114+
bootstrapConfig,
105115
};
106116
}
107117

0 commit comments

Comments
 (0)