Skip to main content

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