seam_server/build_loader/
loader.rs1use std::collections::HashMap;
4use std::path::Path;
5use std::sync::Arc;
6
7use crate::page::{LoaderDef, PageDef};
8
9use super::types::{pick_template, LoaderConfig, ParamConfig, RouteManifest, RpcHashMap};
10
11pub(super) fn build_input_fn(params: &HashMap<String, ParamConfig>) -> crate::page::LoaderInputFn {
14 let params: Vec<(String, String, String)> = params
15 .iter()
16 .map(|(key, cfg)| (key.clone(), cfg.from.clone(), cfg.param_type.clone()))
17 .collect();
18
19 Arc::new(move |route_params: &HashMap<String, String>| {
20 let mut obj = serde_json::Map::new();
21 for (key, from, param_type) in ¶ms {
22 let value = match from.as_str() {
23 "route" => {
24 let raw = route_params.get(key).cloned().unwrap_or_default();
25 match param_type.as_str() {
26 "uint32" | "int32" | "number" => {
27 if let Ok(n) = raw.parse::<i64>() {
28 serde_json::Value::Number(serde_json::Number::from(n))
29 } else {
30 serde_json::Value::String(raw)
31 }
32 }
33 _ => serde_json::Value::String(raw),
34 }
35 }
36 _ => serde_json::Value::Null,
37 };
38 obj.insert(key.clone(), value);
39 }
40 serde_json::Value::Object(obj)
41 })
42}
43
44pub(super) fn parse_loaders(loaders: &serde_json::Value) -> Vec<LoaderDef> {
46 let Some(obj) = loaders.as_object() else {
47 return Vec::new();
48 };
49
50 obj
51 .iter()
52 .filter_map(|(data_key, loader_val)| {
53 let config: LoaderConfig = serde_json::from_value(loader_val.clone()).ok()?;
54 Some(LoaderDef {
55 data_key: data_key.clone(),
56 procedure: config.procedure,
57 input_fn: build_input_fn(&config.params),
58 })
59 })
60 .collect()
61}
62
63pub(super) fn resolve_layout_chain(
66 layout_id: &str,
67 page_template: &str,
68 layouts: &HashMap<String, (String, Option<String>)>,
69) -> String {
70 let mut result = page_template.to_string();
71 let mut current = Some(layout_id.to_string());
72
73 while let Some(id) = current {
74 if let Some((tmpl, parent)) = layouts.get(&id) {
75 result = tmpl.replace("<!--seam:outlet-->", &result);
76 current = parent.clone();
77 } else {
78 break;
79 }
80 }
81
82 result
83}
84
85#[allow(clippy::too_many_lines)]
88pub fn load_build_output(dir: &str) -> Result<Vec<PageDef>, Box<dyn std::error::Error>> {
89 let base = Path::new(dir);
90 let manifest_path = base.join("route-manifest.json");
91 let content = std::fs::read_to_string(&manifest_path)?;
92 let manifest: RouteManifest = serde_json::from_str(&content)?;
93 let default_locale = manifest.i18n.as_ref().map(|c| c.default.as_str());
94
95 let mut layout_templates: HashMap<String, (String, Option<String>)> = HashMap::new();
97 for (id, entry) in &manifest.layouts {
98 if let Some(tmpl_path) = pick_template(&entry.template, &entry.templates, default_locale) {
99 let full_path = base.join(&tmpl_path);
100 let tmpl = std::fs::read_to_string(&full_path)?;
101 layout_templates.insert(id.clone(), (tmpl, entry.parent.clone()));
102 }
103 }
104
105 let mut layout_locale_templates: HashMap<String, HashMap<String, (String, Option<String>)>> =
107 HashMap::new();
108 if manifest.i18n.is_some() {
109 for (id, entry) in &manifest.layouts {
110 if let Some(ref templates) = entry.templates {
111 for (locale, tmpl_path) in templates {
112 let full_path = base.join(tmpl_path);
113 let tmpl = std::fs::read_to_string(&full_path)?;
114 layout_locale_templates
115 .entry(locale.clone())
116 .or_default()
117 .insert(id.clone(), (tmpl, entry.parent.clone()));
118 }
119 }
120 }
121 }
122
123 let mut pages = Vec::new();
124
125 for (route_path, entry) in &manifest.routes {
126 let page_template =
128 if let Some(tmpl_path) = pick_template(&entry.template, &entry.templates, default_locale) {
129 let full_path = base.join(&tmpl_path);
130 std::fs::read_to_string(&full_path)?
131 } else {
132 continue;
133 };
134
135 let template = if let Some(ref layout_id) = entry.layout {
137 let mut full = resolve_layout_chain(layout_id, &page_template, &layout_templates);
138 if let Some(ref meta) = entry.head_meta {
140 full = full.replace("</head>", &format!("{meta}</head>"));
141 }
142 full
143 } else {
144 page_template
145 };
146
147 let locale_templates = if manifest.i18n.is_some() {
149 if let Some(ref templates) = entry.templates {
150 let mut lt = HashMap::new();
151 for (locale, tmpl_path) in templates {
152 let full_path = base.join(tmpl_path);
153 let page_tmpl = std::fs::read_to_string(&full_path)?;
154 let resolved = if let Some(ref layout_id) = entry.layout {
155 let locale_layouts = layout_locale_templates.get(locale).unwrap_or(&layout_templates);
156 let mut full = resolve_layout_chain(layout_id, &page_tmpl, locale_layouts);
157 if let Some(ref meta) = entry.head_meta {
158 full = full.replace("</head>", &format!("{meta}</head>"));
159 }
160 full
161 } else {
162 page_tmpl
163 };
164 lt.insert(locale.clone(), resolved);
165 }
166 if lt.is_empty() {
167 None
168 } else {
169 Some(lt)
170 }
171 } else {
172 None
173 }
174 } else {
175 None
176 };
177
178 let axum_route = convert_route_path(route_path);
180
181 let mut all_loaders = Vec::new();
184 let mut layout_chain = Vec::new();
185 if let Some(ref layout_id) = entry.layout {
186 let mut chain = Some(layout_id.clone());
188 while let Some(id) = chain {
189 if let Some(layout_entry) = manifest.layouts.get(&id) {
190 let layout_loaders = parse_loaders(&layout_entry.loaders);
191 let loader_keys: Vec<String> =
192 layout_loaders.iter().map(|l| l.data_key.clone()).collect();
193 layout_chain.push(crate::page::LayoutChainEntry { id, loader_keys });
194 all_loaders.extend(layout_loaders);
195 chain = layout_entry.parent.clone();
196 } else {
197 break;
198 }
199 }
200 layout_chain.reverse();
202 }
203 let page_loaders = parse_loaders(&entry.loaders);
204 let page_loader_keys: Vec<String> = page_loaders.iter().map(|l| l.data_key.clone()).collect();
205 all_loaders.extend(page_loaders);
206
207 let mut i18n_keys = Vec::new();
209 if let Some(ref layout_id) = entry.layout {
210 let mut chain = Some(layout_id.clone());
211 while let Some(id) = chain {
212 if let Some(layout_entry) = manifest.layouts.get(&id) {
213 i18n_keys.extend(layout_entry.i18n_keys.iter().cloned());
214 chain = layout_entry.parent.clone();
215 } else {
216 break;
217 }
218 }
219 }
220 i18n_keys.extend(entry.i18n_keys.iter().cloned());
221
222 let data_id = manifest.data_id.clone().unwrap_or_else(|| "__SEAM_DATA__".to_string());
223 pages.push(PageDef {
224 route: axum_route,
225 template,
226 locale_templates,
227 loaders: all_loaders,
228 data_id: data_id.clone(),
229 layout_chain,
230 page_loader_keys,
231 i18n_keys,
232 });
233 }
234
235 Ok(pages)
236}
237
238pub fn load_i18n_config(dir: &str) -> Option<crate::page::I18nConfig> {
241 let base = Path::new(dir);
242 let manifest_path = base.join("route-manifest.json");
243 let content = std::fs::read_to_string(&manifest_path).ok()?;
244 let manifest: RouteManifest = serde_json::from_str(&content).ok()?;
245 let i18n = manifest.i18n?;
246
247 let mode = i18n.mode.unwrap_or_else(|| "memory".to_string());
248
249 let mut messages = HashMap::new();
252 if mode == "memory" {
253 let i18n_dir = base.join("i18n");
254 for locale in &i18n.locales {
255 let locale_path = i18n_dir.join(format!("{locale}.json"));
256 let parsed: HashMap<String, serde_json::Value> = std::fs::read_to_string(&locale_path)
257 .ok()
258 .and_then(|c| serde_json::from_str(&c).ok())
259 .unwrap_or_default();
260 messages.insert(locale.clone(), parsed);
261 }
262 }
263
264 let dist_dir = if mode == "paged" { Some(base.to_path_buf()) } else { None };
265
266 Some(crate::page::I18nConfig {
267 locales: i18n.locales,
268 default: i18n.default,
269 mode,
270 cache: i18n.cache,
271 route_hashes: i18n.route_hashes,
272 content_hashes: i18n.content_hashes,
273 messages,
274 dist_dir,
275 })
276}
277
278pub fn load_rpc_hash_map(dir: &str) -> Option<RpcHashMap> {
280 let path = Path::new(dir).join("rpc-hash-map.json");
281 let content = std::fs::read_to_string(&path).ok()?;
282 serde_json::from_str(&content).ok()
283}
284
285pub(super) fn convert_route_path(path: &str) -> String {
287 path
288 .split('/')
289 .map(|seg| {
290 if let Some(param) = seg.strip_prefix(':') {
291 format!("{{{param}}}")
292 } else {
293 seg.to_string()
294 }
295 })
296 .collect::<Vec<_>>()
297 .join("/")
298}