Skip to main content

forma_server/
ir_compat.rs

1use forma_ir::parser::IrModule;
2use forma_ir::IR_VERSION;
3use rust_embed::Embed;
4use std::collections::HashMap;
5
6use crate::{AssetManifest, RenderMode};
7
8/// Check that an IR module's version is compatible with the current runtime.
9pub fn check_ir_compatibility(module: &IrModule) -> Result<(), String> {
10    if module.header.version != IR_VERSION {
11        return Err(format!(
12            "IR version {} is not compatible with runtime version {}",
13            module.header.version, IR_VERSION
14        ));
15    }
16    Ok(())
17}
18
19/// Load and parse all IR modules from the asset manifest.
20///
21/// For each route with an `.ir` file, loads the binary from embedded assets,
22/// parses it, and checks compatibility. Returns render mode decisions and
23/// parsed modules.
24///
25/// Routes with valid IR get `Phase2SsrReconcile`. Invalid or missing IR
26/// falls back to `Phase1ClientMount`. Routes with no `.ir` field in the
27/// manifest are not included in the returned map — callers should treat
28/// absent routes as `Phase1ClientMount`.
29pub fn load_ir_modules<A: Embed>(
30    manifest: &AssetManifest,
31) -> (HashMap<String, RenderMode>, HashMap<String, IrModule>) {
32    let mut render_modes = HashMap::new();
33    let mut ir_modules = HashMap::new();
34
35    for (route_pattern, route_assets) in &manifest.routes {
36        if let Some(ref ir_filename) = route_assets.ir {
37            if let Some(ir_bytes) = crate::assets::asset_bytes::<A>(ir_filename) {
38                match IrModule::parse(&ir_bytes) {
39                    Ok(module) => match check_ir_compatibility(&module) {
40                        Ok(()) => {
41                            tracing::info!(route = %route_pattern, ir_file = %ir_filename, "Loaded IR module for SSR");
42                            render_modes
43                                .insert(route_pattern.clone(), RenderMode::Phase2SsrReconcile);
44                            ir_modules.insert(route_pattern.clone(), module);
45                        }
46                        Err(e) => {
47                            tracing::warn!(route = %route_pattern, error = %e, "IR compatibility check failed — Phase 1 fallback");
48                            render_modes
49                                .insert(route_pattern.clone(), RenderMode::Phase1ClientMount);
50                        }
51                    },
52                    Err(e) => {
53                        tracing::warn!(route = %route_pattern, error = %e, "Failed to parse IR module — Phase 1 fallback");
54                        render_modes.insert(route_pattern.clone(), RenderMode::Phase1ClientMount);
55                    }
56                }
57            }
58        }
59    }
60
61    if !ir_modules.is_empty() {
62        tracing::info!(
63            ssr_routes = ir_modules.len(),
64            "Phase 2 SSR enabled for {} route(s)",
65            ir_modules.len()
66        );
67    }
68
69    (render_modes, ir_modules)
70}