1use crate::ViteDevInfo;
4
5const LIVE_RELOAD_SCRIPT: &str = r#"<script>new EventSource("/_seam/dev/reload").onmessage=function(){location.reload()}</script>"#;
6
7pub fn wrap_document(
12 skeleton: &str,
13 css_files: &[String],
14 js_files: &[String],
15 dev_mode: bool,
16 vite: Option<&ViteDevInfo>,
17 root_id: &str,
18) -> String {
19 let mut doc = String::from("<!DOCTYPE html><html><head><meta charset=\"utf-8\">");
20 if let Some(v) = vite {
21 doc.push_str(&format!(
23 concat!(
24 "<script type=\"module\">",
25 "import RefreshRuntime from '{origin}/@react-refresh';",
26 "RefreshRuntime.injectIntoGlobalHook(window);",
27 "window.$RefreshReg$ = () => {{}};",
28 "window.$RefreshSig$ = () => (type) => type;",
29 "window.__vite_plugin_react_preamble_installed__ = true",
30 "</script>"
31 ),
32 origin = v.origin,
33 ));
34 doc.push_str(&format!(
36 r#"<script type="module" src="{origin}/@vite/client"></script>"#,
37 origin = v.origin,
38 ));
39 doc.push_str(&format!(
41 r#"<script type="module" src="{origin}/{entry}"></script>"#,
42 origin = v.origin,
43 entry = v.entry,
44 ));
45 } else {
46 for f in css_files {
47 doc.push_str(&format!(r#"<link rel="stylesheet" href="/_seam/static/{f}">"#));
48 }
49 doc.push_str("<!--seam:page-styles-->");
51 doc.push_str("<!--seam:prefetch-->");
52 }
53 doc.push_str(&format!("</head><body><div id=\"{root_id}\">"));
54 doc.push_str(skeleton);
55 doc.push_str("</div>");
56 if vite.is_none() {
57 for f in js_files {
58 doc.push_str(&format!(r#"<script type="module" src="/_seam/static/{f}"></script>"#));
59 }
60 doc.push_str("<!--seam:page-scripts-->");
61 }
62 if dev_mode && vite.is_none() {
63 doc.push_str(LIVE_RELOAD_SCRIPT);
64 }
65 doc.push_str("</body></html>");
66 doc
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn wraps_with_assets() {
75 let result = wrap_document(
76 "<p>Hello</p>",
77 &["style-abc.css".into()],
78 &["main-xyz.js".into()],
79 false,
80 None,
81 "__seam",
82 );
83 assert_eq!(
84 result,
85 concat!(
86 "<!DOCTYPE html><html><head><meta charset=\"utf-8\">",
87 "<link rel=\"stylesheet\" href=\"/_seam/static/style-abc.css\">",
88 "<!--seam:page-styles--><!--seam:prefetch-->",
89 "</head><body>",
90 "<div id=\"__seam\"><p>Hello</p></div>",
91 "<script type=\"module\" src=\"/_seam/static/main-xyz.js\"></script>",
92 "<!--seam:page-scripts-->",
93 "</body></html>"
94 )
95 );
96 }
97
98 #[test]
99 fn wraps_without_assets() {
100 let result = wrap_document("<p>Hi</p>", &[], &[], false, None, "__seam");
101 assert_eq!(
102 result,
103 concat!(
104 "<!DOCTYPE html><html><head><meta charset=\"utf-8\">",
105 "<!--seam:page-styles--><!--seam:prefetch-->",
106 "</head><body>",
107 "<div id=\"__seam\"><p>Hi</p></div>",
108 "<!--seam:page-scripts-->",
109 "</body></html>"
110 )
111 );
112 }
113
114 #[test]
115 fn skeleton_with_metadata_stays_in_body() {
116 let skeleton = "<title>My Page</title><meta name=\"desc\"><p>content</p>";
118 let result = wrap_document(skeleton, &["style.css".into()], &[], false, None, "__seam");
119
120 let root_start = result.find("__seam").unwrap();
121 let root_section = &result[root_start..];
122 assert!(root_section.contains("<title>My Page</title>"), "title stays in body");
123 assert!(root_section.contains("<p>content</p>"), "body content in root div");
124 }
125
126 #[test]
127 fn dev_mode_injects_live_reload_script() {
128 let result = wrap_document("<p>dev</p>", &[], &["app.js".into()], true, None, "__seam");
129 assert!(result.contains("EventSource"), "dev_mode should inject EventSource live reload");
130 assert!(result.contains("/_seam/dev/reload"));
131 let script_pos = result.find("EventSource").unwrap();
132 let module_pos = result.find("app.js").unwrap();
133 let page_scripts_pos = result.find("<!--seam:page-scripts-->").unwrap();
134 let body_end = result.find("</body>").unwrap();
135 assert!(module_pos < page_scripts_pos);
136 assert!(script_pos > page_scripts_pos);
137 assert!(script_pos < body_end);
138 }
139
140 #[test]
141 fn production_mode_no_reload_script() {
142 let result = wrap_document("<p>prod</p>", &[], &["app.js".into()], false, None, "__seam");
143 assert!(!result.contains("EventSource"), "production mode must not inject live reload");
144 }
145
146 #[test]
147 fn vite_mode_injects_three_scripts() {
148 let vite = ViteDevInfo {
149 origin: "http://localhost:5173".to_string(),
150 entry: "src/client/main.tsx".to_string(),
151 };
152 let result = wrap_document(
153 "<p>vite</p>",
154 &["ignored.css".into()],
155 &["ignored.js".into()],
156 false,
157 Some(&vite),
158 "__seam",
159 );
160
161 assert!(result.contains("@react-refresh"), "must inject React Refresh preamble");
163 assert!(result.contains("/@vite/client"), "must inject Vite HMR client");
164 assert!(result.contains("src/client/main.tsx"), "must inject app entry");
165
166 assert!(!result.contains("/_seam/static/"), "vite mode must not reference static assets");
168 assert!(!result.contains("ignored.css"));
169 assert!(!result.contains("ignored.js"));
170 }
171
172 #[test]
173 fn vite_mode_skips_sse_reload() {
174 let vite = ViteDevInfo {
175 origin: "http://localhost:5173".to_string(),
176 entry: "src/client/main.tsx".to_string(),
177 };
178 let result = wrap_document("<p>vite-dev</p>", &[], &[], true, Some(&vite), "__seam");
179
180 assert!(result.contains("/@vite/client"));
182 assert!(!result.contains("/_seam/dev/reload"), "vite mode must not inject SSE reload");
184 }
185
186 #[test]
187 fn vite_proxy_mode_uses_relative_dev_scripts() {
188 let vite = ViteDevInfo { origin: String::new(), entry: "src/client/main.tsx".to_string() };
189 let result = wrap_document("<p>vite-proxy</p>", &[], &[], true, Some(&vite), "__seam");
190
191 assert!(result.contains("import RefreshRuntime from '/@react-refresh'"));
192 assert!(result.contains(r#"<script type="module" src="/@vite/client"></script>"#));
193 assert!(result.contains(r#"<script type="module" src="/src/client/main.tsx"></script>"#));
194 }
195
196 #[test]
197 fn no_metadata_passes_through() {
198 let result = wrap_document("<div><p>Hello</p></div>", &[], &[], false, None, "__seam");
199 assert!(result.contains("<div id=\"__seam\"><div><p>Hello</p></div></div>"));
200 }
201
202 #[test]
203 fn slot_markers_present_in_production() {
204 let result =
205 wrap_document("<p>test</p>", &["a.css".into()], &["a.js".into()], false, None, "__seam");
206 assert!(result.contains("<!--seam:page-styles-->"));
207 assert!(result.contains("<!--seam:prefetch-->"));
208 assert!(result.contains("<!--seam:page-scripts-->"));
209 let head_end = result.find("</head>").unwrap();
211 let page_styles = result.find("<!--seam:page-styles-->").unwrap();
212 let page_scripts = result.find("<!--seam:page-scripts-->").unwrap();
213 assert!(page_styles < head_end);
214 assert!(page_scripts > head_end);
215 }
216
217 #[test]
218 fn slot_markers_absent_in_vite_mode() {
219 let vite = ViteDevInfo {
220 origin: "http://localhost:5173".to_string(),
221 entry: "src/main.tsx".to_string(),
222 };
223 let result = wrap_document("<p>test</p>", &[], &[], false, Some(&vite), "__seam");
224 assert!(!result.contains("<!--seam:page-styles-->"));
225 assert!(!result.contains("<!--seam:prefetch-->"));
226 assert!(!result.contains("<!--seam:page-scripts-->"));
227 }
228
229 #[test]
230 fn skeleton_with_conditional_stays_in_body() {
231 let skeleton =
233 "<!--seam:if:x--><!--seam:d:attr:content--><meta name=\"og\"><!--seam:endif:x--><p>body</p>";
234 let result = wrap_document(skeleton, &[], &[], false, None, "__seam");
235
236 let root_start = result.find("__seam").unwrap();
237 let root_section = &result[root_start..];
238 assert!(root_section.contains("<!--seam:if:x-->"), "conditional stays in body");
239 assert!(root_section.contains("<meta name=\"og\">"), "meta stays in body");
240 assert!(root_section.contains("<p>body</p>"), "body content in root div");
241 }
242}