logicaffeine_cli/browser/
bundle.rs1use std::collections::BTreeMap;
4
5use logicaffeine_compile::page_host::PAGE_STEM as STEM;
6use logicaffeine_compile::vm::reach::HostEntries;
7
8#[derive(Clone, Debug)]
10pub struct PageBundle {
11 files: BTreeMap<String, (String, Vec<u8>)>,
12 entry: String,
13}
14
15impl PageBundle {
16 pub fn new(entry: &str, document: impl Into<Vec<u8>>) -> PageBundle {
18 let mut files = BTreeMap::new();
19 files.insert(
20 entry.to_string(),
21 ("text/html; charset=utf-8".to_string(), document.into()),
22 );
23 PageBundle {
24 files,
25 entry: entry.to_string(),
26 }
27 }
28
29 pub fn with_file(mut self, path: &str, bytes: impl Into<Vec<u8>>) -> PageBundle {
31 self.files.insert(
32 path.to_string(),
33 (crate::content_type_for(path).to_string(), bytes.into()),
34 );
35 self
36 }
37
38 pub fn with_head_markup(mut self, markup: &str) -> PageBundle {
41 if let Some((_, doc)) = self.files.get_mut(&self.entry) {
42 let text =
43 String::from_utf8_lossy(doc).replacen("</head>", &format!("{markup}</head>"), 1);
44 *doc = text.into_bytes();
45 }
46 self
47 }
48
49 pub fn at(mut self, path: &str) -> PageBundle {
53 let dir = &path[..=path.rfind('/').unwrap_or(0)];
54 for file in [format!("{STEM}.wasm"), format!("{STEM}.js")] {
55 if let Some(bytes) = self.files.remove(&format!("/{file}")) {
56 self.files.insert(format!("{dir}{file}"), bytes);
57 }
58 }
59 if let Some(doc) = self.files.remove(&self.entry) {
60 self.files.insert(path.to_string(), doc);
61 }
62 self.entry = path.to_string();
63 self
64 }
65
66 pub fn page(source: &str) -> Result<PageBundle, String> {
69 Self::page_with(source, &HostEntries::default())
70 }
71
72 pub fn page_with(source: &str, entries: &HostEntries) -> Result<PageBundle, String> {
76 let shell = logicaffeine_compile::web_host::browser_host_html(STEM);
77 Self::with_module(source, shell, &entries.clone().drawing_fresh())
78 }
79
80 pub fn prerendered(source: &str) -> Result<PageBundle, String> {
85 if let Some(why) = crate::commands::web_bundle::prerender_refusal(source) {
86 return Err(format!("the page cannot be pre-rendered: {why}"));
87 }
88 let doc = crate::prerender_page_html(source, STEM);
89 let bundle = Self::with_module(source, doc, &HostEntries::default())?;
90 Ok(match declared_page_path(source) {
91 Some(path) if path != "/" && !path.contains(':') => bundle.at(&path),
92 _ => bundle,
93 })
94 }
95
96 fn with_module(
97 source: &str,
98 document: String,
99 entries: &HostEntries,
100 ) -> Result<PageBundle, String> {
101 let wasm =
102 logicaffeine_compile::compile::compile_to_wasm_with(source, entries).map_err(|e| {
103 let d = e.diagnostic_parts(source);
104 format!(
105 "the page does not compile to WebAssembly (line {}): {}",
106 d.line_num, d.message
107 )
108 })?;
109 Ok(PageBundle::new("/", document)
110 .with_file(&format!("/{STEM}.wasm"), wasm)
111 .with_file(
112 &format!("/{STEM}.js"),
113 logicaffeine_compile::web_host::browser_host_js(STEM),
114 ))
115 }
116
117 pub fn entry(&self) -> &str {
119 &self.entry
120 }
121
122 pub fn file(&self, path: &str) -> Option<&[u8]> {
124 self.files.get(path).map(|(_, b)| b.as_slice())
125 }
126
127 pub(crate) fn paths(&self) -> std::collections::HashSet<String> {
128 self.files.keys().cloned().collect()
129 }
130
131 pub(crate) fn served(&self) -> BTreeMap<String, (String, Vec<u8>)> {
132 self.files.clone()
133 }
134}
135
136fn declared_page_path(source: &str) -> Option<String> {
138 logicaffeine_compile::ui_bridge::with_parsed_program(source, |parsed, interner| {
139 parsed.ok().and_then(|(stmts, ..)| {
140 logicaffeine_compile::page_host::auto_mounted_page_path(stmts, interner)
141 })
142 })
143}