1#![warn(missing_docs)]
2
3mod prefetch;
4mod router;
5
6use nargo_bundler::targets::js::{JsBackend, JsWriter};
7use nargo_ir::{IRModule, TemplateNodeIR};
8use nargo_types::{CompileMode, Result};
9use std::{
10 collections::{HashMap, HashSet},
11 hash::{Hash, Hasher},
12};
13
14pub use prefetch::PrefetchManager;
15pub use router::{RouteHandler, RouteMatch, Router};
16
17type CacheKey = String;
19
20type CacheValue = String;
22
23pub struct SsrBackend {
24 pub runtime_path: String,
25 pub resumable: bool,
26 pub mode: CompileMode,
27 pub cache: HashMap<CacheKey, CacheValue>,
28 pub cache_enabled: bool,
29}
30
31impl SsrBackend {
32 pub fn new(mode: CompileMode) -> Self {
33 let runtime_path = match mode {
34 CompileMode::Vue2 => "nargo".to_string(),
35 CompileMode::Vue => "vue".to_string(),
36 };
37 Self { runtime_path, resumable: false, mode, cache: HashMap::new(), cache_enabled: true }
38 }
39
40 pub fn with_resumable(mut self, resumable: bool) -> Self {
41 self.resumable = resumable;
42 self
43 }
44
45 pub fn with_cache(mut self, enabled: bool) -> Self {
46 self.cache_enabled = enabled;
47 self
48 }
49
50 pub fn clear_cache(&mut self) {
51 self.cache.clear();
52 }
53}
54
55impl Default for SsrBackend {
56 fn default() -> Self {
57 Self::new(CompileMode::Vue2)
58 }
59}
60
61impl SsrBackend {
62 pub fn generate(&mut self, ir: &IRModule) -> Result<String> {
63 let cache_key = self.generate_cache_key(ir);
65
66 if self.cache_enabled {
68 if let Some(cached) = self.cache.get(&cache_key) {
69 return Ok(cached.clone());
70 }
71 }
72
73 let mut writer = JsWriter::new();
74 let mut used_core = HashSet::new();
75
76 let mut body_writer = JsWriter::new();
78 self.generate_ssr_body(ir, &mut body_writer, &mut used_core)?;
79
80 if !used_core.is_empty() {
82 let mut imports: Vec<_> = used_core.into_iter().collect();
83 imports.sort();
84 let import_source = if self.mode == CompileMode::Vue { "vue".to_string() } else { format!("{}/core", self.runtime_path) };
85 writer.write_line(&format!("import {{ {} }} from '{}';", imports.join(", "), import_source));
86 writer.newline();
87 }
88
89 writer.append(body_writer);
91
92 let result = writer.finish().0;
93
94 if self.cache_enabled {
96 self.cache.insert(cache_key, result.clone());
97 }
98
99 Ok(result)
100 }
101
102 fn generate_cache_key(&self, ir: &IRModule) -> CacheKey {
103 let mut key = format!("{}-{:?}", ir.name, self.mode);
105 if let Some(template) = &ir.template {
106 key.push_str(&format!("-{}", template.nodes.len()));
107 }
108 key
109 }
110
111 fn generate_ssr_body(&self, ir: &IRModule, writer: &mut JsWriter, used_core: &mut HashSet<String>) -> Result<()> {
112 let _ = writer.write_block("export function render(ctx)", |writer| {
113 let js_backend = JsBackend::new(false, false, None, self.mode);
115 let mut used_dom = HashSet::new();
116
117 if let Some(script) = &ir.script_server {
119 for stmt in &script.body {
120 JsWriter::generate_stmt(stmt, writer, ir, used_core, &mut used_dom, false);
121 }
122 }
123 if let Some(script) = &ir.script {
124 for stmt in &script.body {
125 JsWriter::generate_stmt(stmt, writer, ir, used_core, &mut used_dom, false);
126 }
127 }
128
129 writer.write("let html = [];");
131 writer.newline();
132
133 if self.resumable {
134 writer.write_line("html.push(`<script type=\"nargo/state\">${JSON.stringify(ctx)}</script>`);");
136 }
137
138 if let Some(template) = &ir.template {
139 let mut node_index = 0;
140 for node in &template.nodes {
141 self.generate_node_ssr_optimized(node, writer, &mut node_index);
142 }
143 }
144
145 writer.write_line("return html.join('');");
147 Ok(())
148 });
149 Ok(())
150 }
151
152 fn generate_node_ssr_optimized(&self, node: &TemplateNodeIR, writer: &mut JsWriter, node_index: &mut usize) {
153 match node {
154 TemplateNodeIR::Element(el) => {
155 let current_index = *node_index;
156 *node_index += 1;
157
158 let mut start_tag = String::with_capacity(64);
160 start_tag.push_str("<");
161 start_tag.push_str(&el.tag);
162
163 if !el.is_static {
165 start_tag.push_str(&format!(" data-nargo-id=\"{}\"", current_index));
166 }
167
168 for attr in &el.attributes {
169 if !attr.is_directive {
170 if attr.is_dynamic {
171 writer.write_line(&format!("html.push('{}');", start_tag));
172 writer.write_line(&format!("html.push('{}=\"' + ({}) + '\"');", attr.name, attr.value.as_deref().unwrap_or("")));
173 start_tag.clear();
174 }
175 else {
176 match &attr.value {
177 Some(v) => start_tag.push_str(&format!(" {}=\"{}\"", attr.name, v)),
178 None => start_tag.push_str(&format!(" {}", attr.name)),
179 }
180 }
181 }
182 else if self.resumable {
183 if attr.is_directive && (attr.name == "on" || attr.name.starts_with("@")) {
185 let event_name = if attr.name == "on" {
186 attr.argument.as_deref().unwrap_or("")
187 }
188 else {
189 attr.name.trim_start_matches('@')
191 };
192
193 if !event_name.is_empty() {
194 let handler = attr.value.as_deref().unwrap_or("");
197 if !handler.is_empty() {
198 start_tag.push_str(&format!(" on:{}=\"/assets/{}.js#{}\"", event_name, "test", handler));
199 }
200 }
201 }
202 }
203 }
204
205 if !start_tag.is_empty() {
206 start_tag.push_str(">\n");
207 writer.write_line(&format!("html.push('{}');", start_tag));
208 }
209
210 for child in &el.children {
211 self.generate_node_ssr_optimized(child, writer, node_index);
212 }
213
214 writer.write_line(&format!("html.push('</{}>');", el.tag));
215 }
216 TemplateNodeIR::Text(text, _, _) => {
217 *node_index += 1;
218 let escaped_text = text.replace("'", "\\'");
220 writer.write_line(&format!("html.push('{}');", escaped_text));
221 }
222 TemplateNodeIR::Interpolation(expr) => {
223 let current_index = *node_index;
224 *node_index += 1;
225 writer.write_line(&format!("html.push('<span data-nargo-id=\"{}\">');", current_index));
227 writer.write_line(&format!("html.push({});", expr.code));
228 writer.write_line(&format!("html.push('</span>');"));
229 }
230 TemplateNodeIR::Comment(comment, _, _) => {
231 *node_index += 1;
232 let escaped_comment = comment.replace("'", "\\'");
233 writer.write_line(&format!("html.push('<!-- {} -->');", escaped_comment));
234 }
235 TemplateNodeIR::Hoisted(id) => {
236 *node_index += 1;
237 writer.write_line(&format!("html.push({});", id));
238 }
239 TemplateNodeIR::If(if_node) => {
240 let current_index = *node_index;
241 *node_index += 1;
242
243 writer.write_line(&format!("if ({}) {{", if_node.condition.code));
245 writer.indent();
246 for child in &if_node.consequent {
247 self.generate_node_ssr_optimized(child, writer, node_index);
248 }
249 writer.dedent();
250
251 for (condition, nodes) in &if_node.else_ifs {
252 writer.write_line(&format!("}} else if ({}) {{", condition.code));
253 writer.indent();
254 for child in nodes {
255 self.generate_node_ssr_optimized(child, writer, node_index);
256 }
257 writer.dedent();
258 }
259
260 if let Some(alt) = &if_node.alternate {
261 writer.write_line("} else {");
262 writer.indent();
263 for child in alt {
264 self.generate_node_ssr_optimized(child, writer, node_index);
265 }
266 writer.dedent();
267 }
268 writer.write_line("}");
269 }
270 TemplateNodeIR::For(for_node) => {
271 let current_index = *node_index;
272 *node_index += 1;
273
274 writer.write_line(&format!("({}).forEach(({}, {}) => {{", for_node.iterator.collection.code, for_node.iterator.item, for_node.iterator.index.as_deref().unwrap_or("_i")));
276 writer.indent();
277 for child in &for_node.body {
278 self.generate_node_ssr_optimized(child, writer, node_index);
279 }
280 writer.dedent();
281 writer.write_line("});");
282 }
283 }
284 }
285
286 fn generate_node_ssr(&self, node: &TemplateNodeIR, writer: &mut JsWriter, node_index: &mut usize) {
287 match node {
288 TemplateNodeIR::Element(el) => {
289 let current_index = *node_index;
290 *node_index += 1;
291
292 let mut start_tag = format!("html += '<{}", el.tag);
293
294 if !el.is_static {
296 start_tag.push_str(&format!(" data-nargo-id=\"{}\"", current_index));
297 }
298
299 for attr in &el.attributes {
300 if !attr.is_directive {
301 if attr.is_dynamic {
302 start_tag.push_str(&format!(" {}=\"' + ({}) + '\"", attr.name, attr.value.as_deref().unwrap_or("")));
303 }
304 else {
305 match &attr.value {
306 Some(v) => start_tag.push_str(&format!(" {}=\"{}\"", attr.name, v)),
307 None => start_tag.push_str(&format!(" {}", attr.name)),
308 }
309 }
310 }
311 else if self.resumable {
312 if attr.is_directive && (attr.name == "on" || attr.name.starts_with("@")) {
314 let event_name = if attr.name == "on" {
315 attr.argument.as_deref().unwrap_or("")
316 }
317 else {
318 attr.name.trim_start_matches('@')
320 };
321
322 if !event_name.is_empty() {
323 let handler = attr.value.as_deref().unwrap_or("");
326 if !handler.is_empty() {
327 start_tag.push_str(&format!(" on:{}=\"/assets/{}.js#{}\"", event_name, "test", handler));
328 }
329 }
330 }
331 }
332 }
333 start_tag.push_str(">';");
334 writer.write_line(&start_tag);
335
336 for child in &el.children {
337 self.generate_node_ssr(child, writer, node_index);
338 }
339
340 writer.write_line(&format!("html += '</{}>';", el.tag));
341 }
342 TemplateNodeIR::Text(text, _, _) => {
343 *node_index += 1;
344 writer.write_line(&format!("html += '{}';", text.replace("'", "\\'")));
345 }
346 TemplateNodeIR::Interpolation(expr) => {
347 let current_index = *node_index;
348 *node_index += 1;
349 writer.write_line(&format!("html += '<span data-nargo-id=\"{}\">' + ({}) + '</span>';", current_index, expr.code));
351 }
352 TemplateNodeIR::Comment(comment, _, _) => {
353 *node_index += 1;
354 writer.write_line(&format!("html += '<!-- {} -->';", comment.replace("'", "\\'")));
355 }
356 TemplateNodeIR::Hoisted(id) => {
357 *node_index += 1;
358 writer.write_line(&format!("html += {};", id));
359 }
360 TemplateNodeIR::If(if_node) => {
361 let current_index = *node_index;
362 *node_index += 1;
363
364 writer.write_line(&format!("if ({}) {{", if_node.condition.code));
366 writer.indent();
367 for child in &if_node.consequent {
368 self.generate_node_ssr(child, writer, node_index);
369 }
370 writer.dedent();
371
372 for (condition, nodes) in &if_node.else_ifs {
373 writer.write_line(&format!("}} else if ({}) {{", condition.code));
374 writer.indent();
375 for child in nodes {
376 self.generate_node_ssr(child, writer, node_index);
377 }
378 writer.dedent();
379 }
380
381 if let Some(alt) = &if_node.alternate {
382 writer.write_line("} else {");
383 writer.indent();
384 for child in alt {
385 self.generate_node_ssr(child, writer, node_index);
386 }
387 writer.dedent();
388 }
389 writer.write_line("}");
390 }
391 TemplateNodeIR::For(for_node) => {
392 let current_index = *node_index;
393 *node_index += 1;
394
395 writer.write_line(&format!("({}).forEach(({}, {}) => {{", for_node.iterator.collection.code, for_node.iterator.item, for_node.iterator.index.as_deref().unwrap_or("_i")));
397 writer.indent();
398 for child in &for_node.body {
399 self.generate_node_ssr(child, writer, node_index);
400 }
401 writer.dedent();
402 writer.write_line("});");
403 }
404 }
405 }
406}