1#![warn(missing_docs)]
2
3use nargo_bundler::targets::js::{writer::JsWriter, JsBackend};
4use nargo_ir::{IRModule, TemplateNodeIR};
5use nargo_types::{CompileMode, Result};
6use std::collections::HashSet;
7
8pub struct HydrateBackend {
9 pub runtime_path: String,
10 pub mode: CompileMode,
11}
12
13impl HydrateBackend {
14 pub fn new(mode: CompileMode) -> Self {
15 let runtime_path = match mode {
16 CompileMode::Vue2 => "nargo".to_string(),
17 CompileMode::Vue => "vue".to_string(),
18 };
19 Self { runtime_path, mode }
20 }
21}
22
23impl Default for HydrateBackend {
24 fn default() -> Self {
25 Self::new(CompileMode::Vue2)
26 }
27}
28
29impl HydrateBackend {
30 pub fn generate(&self, ir: &IRModule) -> Result<String> {
31 let mut writer = JsWriter::new();
32 let mut used_core = HashSet::new();
33
34 let mut body_writer = JsWriter::new();
36 Self::generate_hydrate_body(ir, &mut body_writer, &mut used_core)?;
37
38 if !used_core.is_empty() {
40 let mut imports: Vec<_> = used_core.into_iter().collect();
41 imports.sort();
42 let import_source = if self.mode == CompileMode::Vue { "vue".to_string() } else { format!("{}/core", self.runtime_path) };
43 writer.write_line(&format!("import {{ {} }} from '{}';", imports.join(", "), import_source));
44 writer.newline();
45 }
46
47 writer.append(body_writer);
49
50 Ok(writer.finish().0)
51 }
52
53 fn generate_hydrate_body(ir: &IRModule, writer: &mut JsWriter, used_core: &mut HashSet<String>) -> Result<()> {
54 writer.write_block("export function hydrate(root, ctx, options = {})", |writer| {
55 writer.write_line("if (window.__Nargo_STATE__) {");
57 writer.indent();
58 writer.write_line("Object.assign(ctx, window.__Nargo_STATE__);");
59 writer.dedent();
60 writer.write_line("}");
61 writer.newline();
62
63 let mut used_dom = HashSet::new();
65 if let Some(script) = &ir.script {
66 for stmt in &script.body {
67 JsWriter::generate_stmt(stmt, writer, ir, used_core, &mut used_dom, false);
68 }
69 }
70 if let Some(script_client) = &ir.script_client {
71 for stmt in &script_client.body {
72 JsWriter::generate_stmt(stmt, writer, ir, used_core, &mut used_dom, false);
73 }
74 }
75 writer.newline();
76
77 writer.write_line("const nodes = new Map();");
79 writer.write_line("const eventHandlers = new Map();");
80 writer.write_line("const selector = options.region ? `[data-nargo-region='${options.region}'] [data-nargo-id]` : '[data-nargo-id]';");
81 writer.write_line("const elements = root.querySelectorAll(selector);");
82 writer.write_line("for (let i = 0; i < elements.length; i++) {");
83 writer.indent();
84 writer.write_line("const el = elements[i];");
85 writer.write_line("const id = el.getAttribute('data-nargo-id');");
86 writer.write_line("if (id) nodes.set(id, el);");
87 writer.dedent();
88 writer.write_line("}");
89 writer.newline();
90
91 writer.write_line("root.addEventListener('click', (e) => {");
93 writer.indent();
94 writer.write_line("let target = e.target;");
95 writer.write_line("while (target && target !== root) {");
96 writer.indent();
97 writer.write_line("const id = target.getAttribute('data-nargo-id');");
98 writer.write_line("if (id) {");
99 writer.indent();
100 writer.write_line("const handlers = eventHandlers.get('click');");
101 writer.write_line("if (handlers && handlers[id]) {");
102 writer.indent();
103 writer.write_line("handlers[id](e);");
104 writer.dedent();
105 writer.write_line("}");
106 writer.dedent();
107 writer.write_line("}");
108 writer.write_line("target = target.parentElement;");
109 writer.dedent();
110 writer.write_line("}");
111 writer.dedent();
112 writer.write_line("});");
113 writer.newline();
114
115 if let Some(template) = &ir.template {
117 let mut node_index = 0;
118 for node in &template.nodes {
119 Self::generate_node_hydrate(node, writer, &mut node_index, used_core)?;
120 }
121 Ok(())
122 }
123 else {
124 Ok(())
125 }
126 });
127 writer.newline();
128
129 writer.write_block("export function partialHydrate(root, ctx, region)", |writer| {
131 writer.write_line("return hydrate(root, ctx, { region });");
132 Ok(())
133 });
134 Ok(())
135 }
136
137 fn generate_node_hydrate(node: &TemplateNodeIR, writer: &mut JsWriter, node_index: &mut usize, used_core: &mut HashSet<String>) -> Result<()> {
138 match node {
139 TemplateNodeIR::Element(el) => {
140 let current_index = *node_index;
141 *node_index += 1;
142
143 if !el.is_static {
145 let el_var = format!("el{}", current_index);
146 writer.write_line(&format!("const {} = nodes.get('{}');", el_var, current_index));
147
148 for attr in &el.attributes {
150 if (attr.is_directive && attr.name == "on") || attr.name.starts_with('@') {
151 let event_name = if attr.name == "on" {
152 attr.argument.as_deref().unwrap_or("")
153 }
154 else {
155 attr.name.trim_start_matches('@')
157 };
158 if !event_name.is_empty() {
159 if let Some(value) = &attr.value {
160 writer.write_line(&format!("if (!eventHandlers.has('{}')) eventHandlers.set('{}', {{}});", event_name, event_name));
161 writer.write_line(&format!("eventHandlers.get('{}')['{}'] = (e) => {};", event_name, current_index, value));
162 }
163 }
164 }
165 else if attr.is_dynamic || attr.name.starts_with(':') {
166 let attr_name = if attr.name == "bind" {
168 attr.argument.as_deref().unwrap_or("")
169 }
170 else if attr.name.starts_with(':') {
171 attr.name.trim_start_matches(':')
173 }
174 else {
175 &attr.name
176 };
177 if !attr_name.is_empty() {
178 let value = attr.value.as_deref().unwrap_or("null");
179 if value.contains('+') || value.contains('(') || value.contains('{') || value.contains('.') || value.contains('[') {
181 used_core.insert("createEffect".to_string());
182 let last_value_var = format!("last{}Val{}", attr_name, current_index);
183 writer.write_line(&format!("let {} = {};", last_value_var, value));
184 writer.write_block("createEffect(() =>", |writer| {
185 writer.write_line(&format!("const currentValue = {};", value));
186 writer.write_line(&format!("if (currentValue !== {}) {{ ", last_value_var));
187 writer.indent();
188 writer.write_line(&format!("{}.setAttribute('{}', currentValue);", el_var, attr_name));
189 writer.write_line(&format!("{} = currentValue;", last_value_var));
190 writer.dedent();
191 writer.write_line("}");
192 Ok(())
193 });
194 writer.write_line(");");
195 }
196 else {
197 writer.write_line(&format!("{}.setAttribute('{}', {});", el_var, attr_name, value));
199 }
200 }
201 }
202 }
203 }
204
205 for child in &el.children {
207 Self::generate_node_hydrate(child, writer, node_index, used_core)?;
208 }
209 Ok(())
210 }
211 TemplateNodeIR::Interpolation(expr) => {
212 let current_index = *node_index;
213 *node_index += 1;
214
215 writer.write_line(&format!("const text{} = nodes.get('{}');", current_index, current_index));
216 if expr.code.contains('+') || expr.code.contains('(') || expr.code.contains('{') || expr.code.contains('.') || expr.code.contains('[') {
218 used_core.insert("createEffect".to_string());
219 writer.write_line(&format!("let lastText{} = {};", current_index, expr.code));
220 writer.write_block("createEffect(() =>", |writer| {
221 writer.write_line(&format!("const currentText = {};", expr.code));
222 writer.write_line(&format!("if (currentText !== lastText{}) {{ ", current_index));
223 writer.indent();
224 writer.write_line(&format!("text{}.textContent = currentText;", current_index));
225 writer.write_line(&format!("lastText{} = currentText;", current_index));
226 writer.dedent();
227 writer.write_line("}");
228 Ok(())
229 });
230 writer.write_line(");");
231 }
232 else {
233 writer.write_line(&format!("text{}.textContent = {};", current_index, expr.code));
235 }
236 Ok(())
237 }
238 _ => {
239 *node_index += 1;
241 Ok(())
242 }
243 }
244 }
245}