Maintain/Build/Rhai/
mod.rs

1//=============================================================================//
2// File Path: Element/Maintain/Source/Build/Rhai/mod.rs
3//=============================================================================//
4// Module: Rhai - Dynamic Script Configuration
5//
6// This module integrates Rhai scripting language for dynamic environment
7// variable configuration, allowing build processes to be customized without
8// recompiling the Rust maintain crate.
9//=============================================================================//
10
11pub mod ConfigLoader;
12pub mod ScriptRunner;
13pub mod EnvironmentResolver;
14
15// Re-export commonly used items
16pub use ConfigLoader::*;
17pub use ScriptRunner::*;
18pub use EnvironmentResolver::*;
19
20use rhai::Engine;
21
22//=============================================================================
23// Public API
24//=============================================================================
25
26/// Creates and configures a new Rhai engine with all necessary modules and functions.
27pub fn create_engine() -> Engine {
28	let mut engine = Engine::new();
29
30	// Optimize engine for script execution
31	engine.set_max_expr_depths(0, 0);
32	engine.set_max_operations(0);
33	engine.set_allow_shadowing(true);
34
35	// Register utility functions for scripts
36	register_utility_functions(&mut engine);
37
38	engine
39}
40
41/// Registers utility functions that can be called from Rhai scripts.
42fn register_utility_functions(engine: &mut Engine) {
43	// System information
44	engine.register_fn("get_os_type", || std::env::consts::OS.to_string());
45	engine.register_fn("get_arch", || std::env::consts::ARCH.to_string());
46	engine.register_fn("get_family", || std::env::consts::FAMILY.to_string());
47
48	// Environment access (read-only for safety)
49	engine.register_fn("get_env", |name: &str| -> String {
50		std::env::var(name).unwrap_or_default()
51	});
52
53	// File system utilities
54	engine.register_fn("path_exists", |path: &str| -> bool {
55		std::path::Path::new(path).exists()
56	});
57
58	// Time utilities
59	engine.register_fn("timestamp", || -> i64 {
60		std::time::SystemTime::now()
61			.duration_since(std::time::UNIX_EPOCH)
62			.unwrap_or_default()
63			.as_secs() as i64
64	});
65
66	// Logging functions
67	engine.register_fn("print", |s: &str| {
68		println!("[Rhai] {}", s);
69	});
70}
71
72//=============================================================================
73// Tests
74//=============================================================================
75
76#[cfg(test)]
77mod tests {
78	use super::*;
79
80	/// Expected environment variables for each profile
81	fn get_expected_env_vars(profile_name: &str) -> Vec<(&'static str, &'static str)> {
82		match profile_name {
83			"debug" => vec![
84				("Debug", "true"),
85				("Browser", "true"),
86				("Bundle", "true"),
87				("Clean", "true"),
88				("Compile", "false"),
89				("NODE_ENV", "development"),
90				("NODE_VERSION", "22"),
91				("NODE_OPTIONS", "--max-old-space-size=16384"),
92				("RUST_LOG", "debug"),
93				("AIR_LOG_JSON", "false"),
94				("AIR_LOG_FILE", ""),
95				("Dependency", "Microsoft/VSCode"),
96			],
97			"production" => vec![
98				("Debug", "false"),
99				("Browser", "false"),
100				("Bundle", "true"),
101				("Clean", "true"),
102				("Compile", "true"),
103				("NODE_ENV", "production"),
104				("NODE_VERSION", "22"),
105				("NODE_OPTIONS", "--max-old-space-size=8192"),
106				("RUST_LOG", "info"),
107				("AIR_LOG_JSON", "false"),
108				("Dependency", "Microsoft/VSCode"),
109			],
110			"release" => vec![
111				("Debug", "false"),
112				("Browser", "false"),
113				("Bundle", "true"),
114				("Clean", "true"),
115				("Compile", "true"),
116				("NODE_ENV", "production"),
117				("NODE_VERSION", "22"),
118				("NODE_OPTIONS", "--max-old-space-size=8192"),
119				("RUST_LOG", "warn"),
120				("AIR_LOG_JSON", "false"),
121				("Dependency", "Microsoft/VSCode"),
122			],
123			_ => vec![],
124		}
125	}
126
127	#[test]
128	fn test_config_loader_load() {
129		let result = ConfigLoader::load(".");
130
131		assert!(result.is_ok(), "ConfigLoader::load() should succeed but got error: {:?}", result.err());
132
133		let config = result.unwrap();
134
135		assert_eq!(config.version, "1.0.0", "Configuration version should be 1.0.0");
136		assert!(!config.profiles.is_empty(), "Configuration should have at least one profile");
137		assert!(config.profiles.contains_key("debug"), "Debug profile should exist");
138		assert!(config.profiles.contains_key("production"), "Production profile should exist");
139		assert!(config.profiles.contains_key("release"), "Release profile should exist");
140		assert!(config.templates.is_some(), "Configuration should have templates defined");
141	}
142
143	#[test]
144	fn test_config_loader_get_profile_debug() {
145		let config = ConfigLoader::load(".").expect("Failed to load configuration");
146		let profile = ConfigLoader::get_profile(&config, "debug");
147
148		assert!(profile.is_some(), "Debug profile should exist in configuration");
149
150		let debug_profile = profile.unwrap();
151		assert!(debug_profile.description.is_some(), "Debug profile should have a description");
152		assert!(debug_profile.env.is_some(), "Debug profile should have environment variables defined");
153		assert!(debug_profile.rhai_script.is_some(), "Debug profile should have a Rhai script defined");
154
155		let debug_env = debug_profile.env.as_ref().unwrap();
156		assert_eq!(debug_env.get("Debug"), Some(&"true".to_string()));
157		assert_eq!(debug_env.get("NODE_ENV"), Some(&"development".to_string()));
158		assert_eq!(debug_env.get("RUST_LOG"), Some(&"debug".to_string()));
159	}
160
161	#[test]
162	fn test_resolve_profile_env_debug() {
163		let config = ConfigLoader::load(".").expect("Failed to load configuration");
164		let env_vars = ConfigLoader::resolve_profile_env(&config, "debug");
165
166		assert_eq!(env_vars.get("Debug"), Some(&"true".to_string()));
167		assert_eq!(env_vars.get("NODE_ENV"), Some(&"development".to_string()));
168		assert!(env_vars.contains_key("MOUNTAIN_DIR"), "Template variable should be present");
169	}
170
171	#[test]
172	fn test_execute_profile_script_debug() {
173		let config = ConfigLoader::load(".").expect("Failed to load configuration");
174		let profile = ConfigLoader::get_profile(&config, "debug").expect("Profile 'debug' not found");
175
176		let script_path = profile.rhai_script.as_ref().expect("No Rhai script defined for debug profile");
177		let full_script_path = std::path::Path::new(".").join(".vscode").join(script_path);
178
179		if !full_script_path.exists() {
180			panic!("Script file not found: {}", full_script_path.display());
181		}
182
183		let engine = create_engine();
184		let context = ScriptRunner::ScriptContext {
185			profile_name: "debug".to_string(),
186			cwd: ".".to_string(),
187			manifest_dir: ".".to_string(),
188			target_triple: None,
189		};
190
191		let result = ScriptRunner::execute_profile_script(&engine, full_script_path.to_str().unwrap(), &context);
192
193		assert!(
194			result.is_ok(),
195			"Script execution should succeed but got error: {:?}",
196			result.err()
197		);
198
199		let script_result = result.unwrap();
200		assert!(script_result.success, "Script execution should report success");
201		assert!(script_result.error.is_none(), "Script execution should not have errors");
202		assert!(!script_result.env_vars.is_empty(), "Script should return environment variables");
203
204		let expected = get_expected_env_vars("debug");
205		for (key, expected_val) in expected {
206			let actual_val = script_result.env_vars.get(key).map(|s| s.as_str());
207			assert_eq!(
208				actual_val,
209				Some(expected_val),
210				"Env var '{}' should be '{}', got {:?}",
211				key,
212				expected_val,
213				actual_val
214			);
215		}
216	}
217
218	#[test]
219	fn test_execute_profile_script_production() {
220		let config = ConfigLoader::load(".").expect("Failed to load configuration");
221		let profile = ConfigLoader::get_profile(&config, "production").expect("Profile 'production' not found");
222
223		let script_path = profile.rhai_script.as_ref().expect("No Rhai script defined for production profile");
224		let full_script_path = std::path::Path::new(".").join(".vscode").join(script_path);
225
226		if !full_script_path.exists() {
227			panic!("Script file not found: {}", full_script_path.display());
228		}
229
230		let engine = create_engine();
231		let context = ScriptRunner::ScriptContext {
232			profile_name: "production".to_string(),
233			cwd: ".".to_string(),
234			manifest_dir: ".".to_string(),
235			target_triple: None,
236		};
237
238		let result = ScriptRunner::execute_profile_script(&engine, full_script_path.to_str().unwrap(), &context);
239
240		assert!(
241			result.is_ok(),
242			"Script execution should succeed but got error: {:?}",
243			result.err()
244		);
245
246		let script_result = result.unwrap();
247		assert!(script_result.success, "Script execution should report success");
248		assert!(script_result.error.is_none(), "Script execution should not have errors");
249
250		let expected = get_expected_env_vars("production");
251		for (key, expected_val) in expected {
252			let actual_val = script_result.env_vars.get(key).map(|s| s.as_str());
253			assert_eq!(
254				actual_val,
255				Some(expected_val),
256				"Env var '{}' should be '{}', got {:?}",
257				key,
258				expected_val,
259				actual_val
260			);
261		}
262	}
263
264	#[test]
265	fn test_execute_profile_script_release() {
266		let config = ConfigLoader::load(".").expect("Failed to load configuration");
267		let profile = ConfigLoader::get_profile(&config, "release").expect("Profile 'release' not found");
268
269		let script_path = profile.rhai_script.as_ref().expect("No Rhai script defined for release profile");
270		let full_script_path = std::path::Path::new(".").join(".vscode").join(script_path);
271
272		if !full_script_path.exists() {
273			panic!("Script file not found: {}", full_script_path.display());
274		}
275
276		let engine = create_engine();
277		let context = ScriptRunner::ScriptContext {
278			profile_name: "release".to_string(),
279			cwd: ".".to_string(),
280			manifest_dir: ".".to_string(),
281			target_triple: None,
282		};
283
284		let result = ScriptRunner::execute_profile_script(&engine, full_script_path.to_str().unwrap(), &context);
285
286		assert!(
287			result.is_ok(),
288			"Script execution should succeed but got error: {:?}",
289			result.err()
290		);
291
292		let script_result = result.unwrap();
293		assert!(script_result.success, "Script execution should report success");
294		assert!(script_result.error.is_none(), "Script execution should not have errors");
295
296		let expected = get_expected_env_vars("release");
297		for (key, expected_val) in expected {
298			let actual_val = script_result.env_vars.get(key).map(|s| s.as_str());
299			assert_eq!(
300				actual_val,
301				Some(expected_val),
302				"Env var '{}' should be '{}', got {:?}",
303				key,
304				expected_val,
305				actual_val
306			);
307		}
308	}
309
310	#[test]
311	fn test_execute_profile_script_bundler_preparation() {
312		let config = ConfigLoader::load(".").expect("Failed to load configuration");
313		let profile = ConfigLoader::get_profile(&config, "bundler-preparation")
314			.expect("Profile 'bundler-preparation' not found in configuration");
315
316		let script_path = profile.rhai_script.as_ref()
317			.expect("No Rhai script defined for bundler-preparation profile");
318		let full_script_path = std::path::Path::new(".").join(".vscode").join(script_path);
319
320		if !full_script_path.exists() {
321			eprintln!("Skipping test - script file not found: {}", full_script_path.display());
322			return;
323		}
324
325		let engine = create_engine();
326		let context = ScriptRunner::ScriptContext {
327			profile_name: "bundler-preparation".to_string(),
328			cwd: ".".to_string(),
329			manifest_dir: ".".to_string(),
330			target_triple: None,
331		};
332
333		let result = ScriptRunner::execute_profile_script(&engine, full_script_path.to_str().unwrap(), &context);
334
335		assert!(result.is_ok(), "Script execution should succeed but got error: {:?}", result.err());
336		let script_result = result.unwrap();
337		assert!(script_result.success, "Script execution should report success");
338		assert!(!script_result.env_vars.is_empty(), "Script should return environment variables");
339
340		// Check for bundler-specific variables
341		assert_eq!(script_result.env_vars.get("BUNDLER_TYPE"), Some(&"swc".to_string()));
342		assert_eq!(script_result.env_vars.get("SWC_TARGET"), Some(&"esnext".to_string()));
343	}
344
345	#[test]
346	fn test_execute_profile_script_swc_bundle() {
347		let config = ConfigLoader::load(".").expect("Failed to load configuration");
348		let profile = ConfigLoader::get_profile(&config, "swc-bundle")
349			.expect("Profile 'swc-bundle' not found in configuration");
350
351		let script_path = profile.rhai_script.as_ref()
352			.expect("No Rhai script defined for swc-bundle profile");
353		let full_script_path = std::path::Path::new(".").join(".vscode").join(script_path);
354
355		if !full_script_path.exists() {
356			eprintln!("Skipping test - script file not found: {}", full_script_path.display());
357			return;
358		}
359
360		let engine = create_engine();
361		let context = ScriptRunner::ScriptContext {
362			profile_name: "swc-bundle".to_string(),
363			cwd: ".".to_string(),
364			manifest_dir: ".".to_string(),
365			target_triple: None,
366		};
367
368		let result = ScriptRunner::execute_profile_script(&engine, full_script_path.to_str().unwrap(), &context);
369
370		assert!(result.is_ok(), "Script execution should succeed but got error: {:?}", result.err());
371		let script_result = result.unwrap();
372		assert!(script_result.success, "Script execution should report success");
373		assert!(!script_result.env_vars.is_empty(), "Script should return environment variables");
374
375		assert_eq!(script_result.env_vars.get("BUNDLER_TYPE"), Some(&"swc".to_string()));
376		assert_eq!(script_result.env_vars.get("NODE_ENV"), Some(&"production".to_string()));
377	}
378
379	#[test]
380	fn test_execute_profile_script_oxc_bundle() {
381		let config = ConfigLoader::load(".").expect("Failed to load configuration");
382		let profile = ConfigLoader::get_profile(&config, "oxc-bundle")
383			.expect("Profile 'oxc-bundle' not found in configuration");
384
385		let script_path = profile.rhai_script.as_ref()
386			.expect("No Rhai script defined for oxc-bundle profile");
387		let full_script_path = std::path::Path::new(".").join(".vscode").join(script_path);
388
389		if !full_script_path.exists() {
390			eprintln!("Skipping test - script file not found: {}", full_script_path.display());
391			return;
392		}
393
394		let engine = create_engine();
395		let context = ScriptRunner::ScriptContext {
396			profile_name: "oxc-bundle".to_string(),
397			cwd: ".".to_string(),
398			manifest_dir: ".".to_string(),
399			target_triple: None,
400		};
401
402		let result = ScriptRunner::execute_profile_script(&engine, full_script_path.to_str().unwrap(), &context);
403
404		assert!(result.is_ok(), "Script execution should succeed but got error: {:?}", result.err());
405		let script_result = result.unwrap();
406		assert!(script_result.success, "Script execution should report success");
407		assert!(!script_result.env_vars.is_empty(), "Script should return environment variables");
408
409		assert_eq!(script_result.env_vars.get("BUNDLER_TYPE"), Some(&"oxc".to_string()));
410		assert_eq!(script_result.env_vars.get("NODE_ENV"), Some(&"production".to_string()));
411	}
412
413	#[test]
414	fn test_env_vars_match_static_config() {
415		let config = ConfigLoader::load(".").expect("Failed to load configuration");
416
417		for profile_name in &["debug", "production", "release"] {
418			let profile = ConfigLoader::get_profile(&config, profile_name)
419				.expect(&format!("Profile '{}' not found", profile_name));
420
421			let script_path = profile.rhai_script.as_ref()
422				.expect(&format!("No Rhai script defined for {}", profile_name));
423
424			let full_script_path = std::path::Path::new(".").join(".vscode").join(script_path);
425
426			if !full_script_path.exists() {
427				continue;
428			}
429
430			let engine = create_engine();
431			let context = ScriptRunner::ScriptContext {
432				profile_name: profile_name.to_string(),
433				cwd: ".".to_string(),
434				manifest_dir: ".".to_string(),
435				target_triple: None,
436			};
437
438			let script_result = ScriptRunner::execute_profile_script(&engine, full_script_path.to_str().unwrap(), &context)
439				.expect(&format!("Failed to execute script for profile '{}'", profile_name));
440
441			let static_env = ConfigLoader::resolve_profile_env(&config, profile_name);
442
443			// Verify that Rhai script returns values that match static config where appropriate
444			if let Some(static_debug) = static_env.get("Debug") {
445				let dynamic_debug = script_result.env_vars.get("Debug");
446				assert_eq!(
447					dynamic_debug,
448					Some(static_debug),
449					"Debug value should match between static config and Rhai script for profile '{}'",
450					profile_name
451				);
452			}
453		}
454	}
455}