Skip to main content

seam_skeleton/
document.rs

1/* src/cli/skeleton/src/document.rs */
2
3use crate::ViteDevInfo;
4
5const LIVE_RELOAD_SCRIPT: &str = r#"<script>new EventSource("/_seam/dev/reload").onmessage=function(){location.reload()}</script>"#;
6
7/// Wrap a skeleton HTML fragment in a compact HTML5 document with asset references.
8/// Produces minimal single-line output for production templates.
9/// When `dev_mode` is true, injects a live reload SSE script before `</body>`.
10/// When `vite` is Some, replaces static CSS/JS refs with Vite dev server scripts.
11pub 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		// React Fast Refresh preamble
22		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		// Vite HMR client
35		doc.push_str(&format!(
36			r#"<script type="module" src="{origin}/@vite/client"></script>"#,
37			origin = v.origin,
38		));
39		// App entry
40		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		// Per-page asset slots (replaced at runtime by engine when page_assets is present)
50		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		// With structured head, metadata in skeleton JSX stays in body (not extracted)
117		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		// All three Vite scripts present
162		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		// No static asset references
167		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		// Vite scripts present
181		assert!(result.contains("/@vite/client"));
182		// SSE live reload must NOT be injected — Vite HMR handles reload
183		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		// Verify ordering: page-styles before </head>, page-scripts before </body>
210		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		// Conditional directives in skeleton are no longer extracted to head
232		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}