1mod resolve;
2pub mod render;
3
4use std::collections::HashMap;
5
6pub use render::PageAssets;
7pub use resolve::ResolvedComponent;
8pub use resolve::resolve_single;
9pub use resolve::resolve_with_files;
10pub use resolve::resolve_with_files_debug;
11
12pub fn compile_page(
17 entry_path: &str,
18 files: &HashMap<String, String>,
19 data_json: &str,
20) -> Result<String, String> {
21 compile_page_with_debug(entry_path, files, data_json, false, &HashMap::new())
22}
23
24pub fn compile_page_debug(
28 entry_path: &str,
29 files: &HashMap<String, String>,
30 data_json: &str,
31 file_origins: &HashMap<String, String>,
32) -> Result<String, String> {
33 compile_page_with_debug(entry_path, files, data_json, true, file_origins)
34}
35
36fn compile_page_with_debug(
37 entry_path: &str,
38 files: &HashMap<String, String>,
39 data_json: &str,
40 debug: bool,
41 file_origins: &HashMap<String, String>,
42) -> Result<String, String> {
43 let data: serde_json::Value = serde_json::from_str(data_json)
44 .map_err(|e| format!("Invalid JSON: {e}"))?;
45 let resolved = if debug {
46 resolve::resolve_with_files_debug(entry_path, files, &data, file_origins)?
47 } else {
48 resolve::resolve_with_files(entry_path, files, &data)?
49 };
50 render::render_page(&resolved, &data)
51}
52
53pub fn compile_page_assets(
58 entry_path: &str,
59 files: &HashMap<String, String>,
60 data_json: &str,
61 asset_prefix: &str,
62) -> Result<PageAssets, String> {
63 compile_page_assets_with_debug(entry_path, files, data_json, asset_prefix, false, &HashMap::new())
64}
65
66pub fn compile_page_assets_debug(
70 entry_path: &str,
71 files: &HashMap<String, String>,
72 data_json: &str,
73 asset_prefix: &str,
74 file_origins: &HashMap<String, String>,
75) -> Result<PageAssets, String> {
76 compile_page_assets_with_debug(entry_path, files, data_json, asset_prefix, true, file_origins)
77}
78
79fn compile_page_assets_with_debug(
80 entry_path: &str,
81 files: &HashMap<String, String>,
82 data_json: &str,
83 asset_prefix: &str,
84 debug: bool,
85 file_origins: &HashMap<String, String>,
86) -> Result<PageAssets, String> {
87 let data: serde_json::Value = serde_json::from_str(data_json)
88 .map_err(|e| format!("Invalid JSON: {e}"))?;
89 let resolved = if debug {
90 resolve::resolve_with_files_debug(entry_path, files, &data, file_origins)?
91 } else {
92 resolve::resolve_with_files(entry_path, files, &data)?
93 };
94
95 let page_name = entry_path.trim_end_matches(".van");
97
98 render::render_page_assets(&resolved, &data, page_name, asset_prefix)
99}
100
101pub fn compile_single(source: &str, data_json: &str) -> Result<String, String> {
105 let mut files = HashMap::new();
106 files.insert("main.van".to_string(), source.to_string());
107 compile_page("main.van", &files, data_json)
108}
109
110#[cfg(feature = "wasm")]
111use wasm_bindgen::prelude::*;
112
113#[cfg(feature = "wasm")]
114#[wasm_bindgen]
115pub fn compile_van(
116 entry_path: &str,
117 files_json: &str,
118 data_json: &str,
119) -> Result<String, JsValue> {
120 let files_value: serde_json::Value = serde_json::from_str(files_json)
122 .map_err(|e| JsValue::from_str(&format!("Invalid files JSON: {e}")))?;
123
124 let files_obj = files_value
125 .as_object()
126 .ok_or_else(|| JsValue::from_str("files_json must be a JSON object"))?;
127
128 let mut files = HashMap::new();
129 for (key, val) in files_obj {
130 let content = val
131 .as_str()
132 .ok_or_else(|| JsValue::from_str(&format!("File '{}' content must be a string", key)))?;
133 files.insert(key.clone(), content.to_string());
134 }
135
136 compile_page(entry_path, &files, data_json).map_err(|e| JsValue::from_str(&e))
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn test_compile_single_basic() {
145 let source = r#"
146<template>
147 <h1>{{ title }}</h1>
148</template>
149"#;
150 let data = r#"{"title": "Hello World"}"#;
151 let html = compile_single(source, data).unwrap();
152 assert!(html.contains("<h1>Hello World</h1>"));
153 assert!(html.contains("<!DOCTYPE html>"));
154 }
155
156 #[test]
157 fn test_compile_single_with_signals() {
158 let source = r#"
159<template>
160 <div>
161 <p>Count: {{ count }}</p>
162 <button @click="increment">+1</button>
163 </div>
164</template>
165
166<script setup>
167const count = ref(0)
168function increment() { count.value++ }
169</script>
170"#;
171 let data = r#"{}"#;
172 let html = compile_single(source, data).unwrap();
173 assert!(html.contains("Van"));
174 assert!(!html.contains("@click"));
175 assert!(html.contains("effect"));
176 }
177
178 #[test]
179 fn test_compile_page_invalid_json() {
180 let mut files = HashMap::new();
181 files.insert("main.van".to_string(), "<template><p>Hi</p></template>".to_string());
182 let result = compile_page("main.van", &files, "not json");
183 assert!(result.is_err());
184 assert!(result.unwrap_err().contains("Invalid JSON"));
185 }
186
187 #[test]
188 fn test_compile_page_with_style() {
189 let source = r#"
190<template>
191 <h1>Hello</h1>
192</template>
193
194<style>
195h1 { color: blue; }
196</style>
197"#;
198 let html = compile_single(source, "{}").unwrap();
199 assert!(html.contains("color: blue"));
200 }
201
202 #[test]
203 fn test_compile_page_multi_file() {
204 let mut files = HashMap::new();
205 files.insert(
206 "index.van".to_string(),
207 r#"
208<template>
209 <hello :name="title" />
210</template>
211
212<script setup>
213import Hello from './hello.van'
214</script>
215"#
216 .to_string(),
217 );
218 files.insert(
219 "hello.van".to_string(),
220 r#"
221<template>
222 <h1>Hello, {{ name }}!</h1>
223</template>
224
225<style>
226h1 { color: green; }
227</style>
228"#
229 .to_string(),
230 );
231
232 let data = r#"{"title": "Van"}"#;
233 let html = compile_page("index.van", &files, data).unwrap();
234 assert!(html.contains("<h1>Hello, Van!</h1>"));
235 assert!(html.contains("color: green"));
236 }
237
238 #[test]
239 fn test_compile_page_with_ts_import() {
240 let mut files = HashMap::new();
241 files.insert(
242 "index.van".to_string(),
243 r#"
244<template>
245 <div>
246 <p>Count: {{ count }}</p>
247 <button @click="increment">+1</button>
248 </div>
249</template>
250
251<script setup lang="ts">
252import { formatDate } from './utils/format.ts'
253import type { User } from './types.ts'
254const count = ref(0)
255function increment() { count.value++ }
256</script>
257"#
258 .to_string(),
259 );
260 files.insert(
261 "utils/format.ts".to_string(),
262 r#"function formatDate(d) { return d.toISOString(); }
263return { formatDate: formatDate };"#
264 .to_string(),
265 );
266 let data = r#"{}"#;
269 let html = compile_page("index.van", &files, data).unwrap();
270 assert!(html.contains("__mod_0"));
272 assert!(html.contains("formatDate"));
273 assert!(html.contains("Van"));
275 assert!(html.contains("effect"));
276 assert!(!html.contains("__mod_1"));
278 }
279
280 #[test]
281 fn test_compile_page_type_only_import_erased() {
282 let mut files = HashMap::new();
283 files.insert(
284 "index.van".to_string(),
285 r#"
286<template>
287 <div><p>{{ count }}</p></div>
288</template>
289
290<script setup lang="ts">
291import type { Config } from './config.ts'
292const count = ref(0)
293</script>
294"#
295 .to_string(),
296 );
297
298 let data = r#"{}"#;
299 let html = compile_page("index.van", &files, data).unwrap();
300 assert!(!html.contains("__mod_"));
302 assert!(html.contains("V.signal(0)"));
304 }
305}