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