Maintain/Run/Environment.rs
1//=============================================================================//
2// File Path: Element/Maintain/Source/Run/Environment.rs
3//=============================================================================//
4// Module: Environment
5//
6// Brief Description: Environment variable management for run operations.
7//
8// RESPONSIBILITIES:
9// ================
10//
11// Primary:
12// - Resolve environment variables from profiles
13// - Merge environment variables from multiple sources
14// - Validate environment configuration
15//
16// Secondary:
17// - Provide environment variable templates
18// - Support environment inheritance
19//
20// ARCHITECTURAL ROLE:
21// ===================
22//
23// Position:
24// - Infrastructure/Environment management layer
25// - Environment resolution
26//
27// Dependencies (What this module requires):
28// - External crates: std
29// - Internal modules: Constant, Definition, Error
30// - Traits implemented: None
31//
32// Dependents (What depends on this module):
33// - Run CLI module
34// - Run Process module
35//
36//=============================================================================//
37// IMPLEMENTATION
38//=============================================================================//
39
40use crate::Run::Constant::*;
41use crate::Run::Definition::Profile;
42use crate::Run::Error::Result;
43
44use std::collections::HashMap;
45
46/// Resolves environment variables for a run profile.
47///
48/// This function combines environment variables from multiple sources:
49/// 1. Template defaults
50/// 2. Shell environment (if enabled)
51/// 3. Profile-specific variables
52/// 4. CLI overrides
53///
54/// # Arguments
55///
56/// * `profile` - The profile to resolve environment for
57/// * `merge_shell` - Whether to merge with shell environment
58/// * `overrides` - CLI-provided environment overrides
59///
60/// # Returns
61///
62/// A HashMap of resolved environment variables
63pub fn resolve(
64 profile: &Profile,
65 merge_shell: bool,
66 overrides: &[(String, String)],
67) -> Result<HashMap<String, String>> {
68 let mut env = HashMap::new();
69
70 // Layer 1: Template defaults
71 apply_template_defaults(&mut env);
72
73 // Layer 2: Shell environment (if enabled)
74 if merge_shell {
75 merge_shell_env(&mut env);
76 }
77
78 // Layer 3: Profile environment
79 if let Some(profile_env) = &profile.env {
80 for (key, value) in profile_env {
81 env.insert(key.clone(), value.clone());
82 }
83 }
84
85 // Layer 4: CLI overrides (highest priority)
86 for (key, value) in overrides {
87 env.insert(key.clone(), value.clone());
88 }
89
90 Ok(env)
91}
92
93/// Applies template default environment variables.
94///
95/// # Arguments
96///
97/// * `env` - The environment HashMap to populate
98fn apply_template_defaults(env: &mut HashMap<String, String>) {
99 // Default Node.js configuration
100 env.entry("NODE_VERSION".to_string()).or_insert("22".to_string());
101 env.entry("NODE_OPTIONS".to_string())
102 .or_insert("--max-old-space-size=16384".to_string());
103
104 // Default run configuration
105 env.entry("HOT_RELOAD".to_string()).or_insert("true".to_string());
106 env.entry("WATCH".to_string()).or_insert("true".to_string());
107 env.entry("LIVE_RELOAD_PORT".to_string())
108 .or_insert(DefaultLiveReloadPort.to_string());
109
110 // Default logging
111 env.entry("Level".to_string()).or_insert("silent".to_string());
112 env.entry("RUST_LOG".to_string()).or_insert("info".to_string());
113}
114
115/// Merges shell environment variables into the environment.
116///
117/// Only merges build system-related environment variables.
118///
119/// # Arguments
120///
121/// * `env` - The environment HashMap to merge into
122fn merge_shell_env(env: &mut HashMap<String, String>) {
123 let relevant_vars = [
124 "Browser", "Bundle", "Clean", "Compile", "Debug", "Dependency",
125 "Mountain", "Wind", "Electron", "BrowserProxy",
126 "NODE_ENV", "NODE_VERSION", "NODE_OPTIONS",
127 "RUST_LOG", "AIR_LOG_JSON", "AIR_LOG_FILE", "Level",
128 "HOT_RELOAD", "WATCH", "LIVE_RELOAD_PORT",
129 ];
130
131 for var in relevant_vars {
132 if let Ok(value) = std::env::var(var) {
133 env.insert(var.to_string(), value);
134 }
135 }
136}
137
138/// Validates environment variables for a run.
139///
140/// # Arguments
141///
142/// * `env` - The environment variables to validate
143///
144/// # Returns
145///
146/// A list of validation errors (empty if valid)
147pub fn validate(env: &HashMap<String, String>) -> Vec<String> {
148 let mut errors = Vec::new();
149
150 // Check NODE_VERSION is set
151 if !env.contains_key("NODE_VERSION") {
152 errors.push("NODE_VERSION environment variable is required".to_string());
153 }
154
155 // Check NODE_ENV is set
156 if !env.contains_key("NODE_ENV") {
157 errors.push("NODE_ENV environment variable is required".to_string());
158 }
159
160 // Check at least one workbench is enabled
161 let workbenches = ["Browser", "Wind", "Mountain", "Electron"];
162 let has_workbench = workbenches.iter().any(|w| {
163 env.get(*w).map(|v| v == "true").unwrap_or(false)
164 });
165
166 if !has_workbench {
167 errors.push("At least one workbench must be enabled (Browser, Wind, Mountain, or Electron)".to_string());
168 }
169
170 // Check LIVE_RELOAD_PORT is valid
171 if let Some(port_str) = env.get("LIVE_RELOAD_PORT") {
172 if let Ok(port) = port_str.parse::<u16>() {
173 if port == 0 {
174 errors.push("LIVE_RELOAD_PORT cannot be 0".to_string());
175 }
176 } else {
177 errors.push("LIVE_RELOAD_PORT must be a valid port number".to_string());
178 }
179 }
180
181 errors
182}
183
184/// Gets the workbench type from environment variables.
185///
186/// # Arguments
187///
188/// * `env` - The environment variables
189///
190/// # Returns
191///
192/// The name of the enabled workbench, or None if none enabled
193pub fn get_workbench(env: &HashMap<String, String>) -> Option<String> {
194 let workbenches = [
195 ("Browser", "Browser"),
196 ("Wind", "Wind"),
197 ("Mountain", "Mountain"),
198 ("Electron", "Electron"),
199 ];
200
201 for (var, name) in workbenches {
202 if env.get(var).map(|v| v == "true").unwrap_or(false) {
203 return Some(name.to_string());
204 }
205 }
206
207 None
208}
209
210/// Checks if debug mode is enabled.
211///
212/// # Arguments
213///
214/// * `env` - The environment variables
215///
216/// # Returns
217///
218/// True if debug mode is enabled
219pub fn is_debug(env: &HashMap<String, String>) -> bool {
220 env.get(DebugEnv).map(|v| v == "true").unwrap_or(false)
221}
222
223/// Checks if hot-reload is enabled.
224///
225/// # Arguments
226///
227/// * `env` - The environment variables
228///
229/// # Returns
230///
231/// True if hot-reload is enabled
232pub fn is_hot_reload_enabled(env: &HashMap<String, String>) -> bool {
233 env.get(HotReloadEnv).map(|v| v == "true").unwrap_or(true)
234}
235
236/// Checks if watch mode is enabled.
237///
238/// # Arguments
239///
240/// * `env` - The environment variables
241///
242/// # Returns
243///
244/// True if watch mode is enabled
245pub fn is_watch_enabled(env: &HashMap<String, String>) -> bool {
246 env.get(WatchEnv).map(|v| v == "true").unwrap_or(true)
247}
248
249/// Formats environment variables for display.
250///
251/// # Arguments
252///
253/// * `env` - The environment variables to format
254///
255/// # Returns
256///
257/// A formatted string representation
258pub fn format_for_display(env: &HashMap<String, String>) -> String {
259 let mut lines: Vec<String> = env.iter()
260 .map(|(k, v)| {
261 let display_value = if v.is_empty() {
262 "(empty)".to_string()
263 } else {
264 v.clone()
265 };
266 format!(" {} = {}", k, display_value)
267 })
268 .collect();
269
270 lines.sort();
271 lines.join("\n")
272}
273
274/// Filters environment variables to only include run-related ones.
275///
276/// # Arguments
277///
278/// * `env` - The full environment
279///
280/// # Returns
281///
282/// A filtered HashMap with only run-related variables
283pub fn filter_run_vars(env: &HashMap<String, String>) -> HashMap<String, String> {
284 let run_prefixes = [
285 "NODE_", "HOT_", "WATCH", "LIVE_", "AIR_", "RUST_",
286 "Browser", "Bundle", "Clean", "Compile", "Debug",
287 "Dependency", "Mountain", "Wind", "Electron", "Level",
288 ];
289
290 env.iter()
291 .filter(|(k, _)| {
292 run_prefixes.iter().any(|prefix| k.starts_with(prefix))
293 })
294 .map(|(k, v)| (k.clone(), v.clone()))
295 .collect()
296}