1use anyhow::Result;
2use bincode::{Decode, Encode};
3use mime_guess::mime::TEXT_HTML_UTF_8;
4use turbo_rcstr::RcStr;
5use turbo_tasks::{ReadRef, ResolvedVc, TryJoinIterExt, Vc};
6use turbo_tasks_fs::{File, FileContent, FileSystemPath};
7use turbo_tasks_hash::{Xxh3Hash64Hasher, encode_base64};
8use turbopack_core::{
9 asset::{Asset, AssetContent},
10 chunk::{
11 ChunkableModule, ChunkingContext, ChunkingContextExt, EvaluatableAssets,
12 availability_info::AvailabilityInfo,
13 },
14 module::Module,
15 module_graph::{ModuleGraph, chunk_group_info::ChunkGroupKey},
16 output::{OutputAsset, OutputAssets, OutputAssetsReference, OutputAssetsWithReferenced},
17 version::{Version, VersionedContent},
18};
19
20#[turbo_tasks::task_input]
21#[derive(Clone, Debug, Eq, Hash, PartialEq, Encode, Decode)]
22pub struct DevHtmlEntry {
23 pub chunkable_module: ResolvedVc<Box<dyn ChunkableModule>>,
24 pub module_graph: ResolvedVc<ModuleGraph>,
25 pub chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
26 pub runtime_entries: Option<ResolvedVc<EvaluatableAssets>>,
27}
28
29#[turbo_tasks::value(shared)]
33#[derive(Clone)]
34pub struct DevHtmlAsset {
35 path: FileSystemPath,
36 entries: Vec<DevHtmlEntry>,
37 body: Option<RcStr>,
38}
39
40#[turbo_tasks::value_impl]
41impl OutputAssetsReference for DevHtmlAsset {
42 #[turbo_tasks::function]
43 fn references(self: Vc<Self>) -> Vc<OutputAssetsWithReferenced> {
44 self.chunk_group()
45 }
46}
47
48#[turbo_tasks::value_impl]
49impl OutputAsset for DevHtmlAsset {
50 #[turbo_tasks::function]
51 fn path(&self) -> Vc<FileSystemPath> {
52 self.path.clone().cell()
53 }
54}
55
56#[turbo_tasks::value_impl]
57impl Asset for DevHtmlAsset {
58 #[turbo_tasks::function]
59 fn content(self: Vc<Self>) -> Vc<AssetContent> {
60 self.html_content().content()
61 }
62
63 #[turbo_tasks::function]
64 fn versioned_content(self: Vc<Self>) -> Vc<Box<dyn VersionedContent>> {
65 Vc::upcast(self.html_content())
66 }
67}
68
69impl DevHtmlAsset {
70 pub fn new(path: FileSystemPath, entries: Vec<DevHtmlEntry>) -> Vc<Self> {
72 DevHtmlAsset {
73 path,
74 entries,
75 body: None,
76 }
77 .cell()
78 }
79
80 pub fn new_with_body(
82 path: FileSystemPath,
83 entries: Vec<DevHtmlEntry>,
84 body: RcStr,
85 ) -> Vc<Self> {
86 DevHtmlAsset {
87 path,
88 entries,
89 body: Some(body),
90 }
91 .cell()
92 }
93}
94
95#[turbo_tasks::value_impl]
96impl DevHtmlAsset {
97 #[turbo_tasks::function]
98 pub async fn with_path(self: Vc<Self>, path: FileSystemPath) -> Result<Vc<Self>> {
99 let mut html: DevHtmlAsset = self.owned().await?;
100 html.path = path;
101 Ok(html.cell())
102 }
103
104 #[turbo_tasks::function]
105 pub async fn with_body(self: Vc<Self>, body: RcStr) -> Result<Vc<Self>> {
106 let mut html: DevHtmlAsset = self.owned().await?;
107 html.body = Some(body);
108 Ok(html.cell())
109 }
110}
111
112#[turbo_tasks::value_impl]
113impl DevHtmlAsset {
114 #[turbo_tasks::function]
115 async fn html_content(self: Vc<Self>) -> Result<Vc<DevHtmlAssetContent>> {
116 let this = self.await?;
117 let context_path = this.path.parent();
118 let mut chunk_paths = vec![];
119 for chunk in &*self.chunk_group().await?.assets.await? {
120 let chunk_path = &*chunk.path().await?;
121 if let Some(relative_path) = context_path.get_path_to(chunk_path) {
122 chunk_paths.push(format!("/{relative_path}").into());
123 }
124 }
125
126 Ok(DevHtmlAssetContent::new(chunk_paths, this.body.clone()))
127 }
128
129 #[turbo_tasks::function]
130 async fn chunk_group(&self) -> Result<Vc<OutputAssetsWithReferenced>> {
131 let all_chunk_groups = self
132 .entries
133 .iter()
134 .map(async |entry| {
135 let &DevHtmlEntry {
136 chunkable_module,
137 chunking_context,
138 module_graph,
139 runtime_entries,
140 } = entry;
141
142 let asset_with_referenced = if let Some(runtime_entries) = runtime_entries {
143 let runtime_entries =
144 if let Some(evaluatable) = ResolvedVc::try_downcast(chunkable_module) {
145 runtime_entries
146 .with_entry(*evaluatable)
147 .to_resolved()
148 .await?
149 } else {
150 runtime_entries
151 };
152 chunking_context
153 .evaluated_chunk_group_assets(
154 chunkable_module.ident(),
155 ChunkGroupKey::Entry(
156 runtime_entries
157 .await?
158 .iter()
159 .map(|v| ResolvedVc::upcast(*v))
160 .collect(),
161 ),
162 *module_graph,
163 OutputAssets::empty(),
164 AvailabilityInfo::root(),
165 )
166 .await?
167 } else {
168 chunking_context
169 .root_chunk_group_assets(
170 chunkable_module.ident(),
171 ChunkGroupKey::Entry(vec![ResolvedVc::upcast(chunkable_module)]),
172 *module_graph,
173 )
174 .await?
175 };
176
177 Ok((
178 asset_with_referenced.assets.await?,
179 asset_with_referenced.referenced_assets.await?,
180 asset_with_referenced.references.await?,
181 ))
182 })
183 .try_join()
184 .await?;
185
186 let mut all_assets = Vec::new();
187 let mut all_referenced_assets = Vec::new();
188 let mut all_references = Vec::new();
189 for (asset, referenced_asset, reference) in all_chunk_groups {
190 all_assets.extend(asset);
191 all_referenced_assets.extend(referenced_asset);
192 all_references.extend(reference);
193 }
194
195 Ok(OutputAssetsWithReferenced {
196 assets: ResolvedVc::cell(all_assets),
197 referenced_assets: ResolvedVc::cell(all_referenced_assets),
198 references: ResolvedVc::cell(all_references),
199 }
200 .cell())
201 }
202}
203
204#[turbo_tasks::value(operation)]
205struct DevHtmlAssetContent {
206 chunk_paths: Vec<RcStr>,
207 body: Option<RcStr>,
208}
209
210impl DevHtmlAssetContent {
211 fn new(chunk_paths: Vec<RcStr>, body: Option<RcStr>) -> Vc<Self> {
212 DevHtmlAssetContent { chunk_paths, body }.cell()
213 }
214}
215
216#[turbo_tasks::value_impl]
217impl DevHtmlAssetContent {
218 #[turbo_tasks::function]
219 fn content(&self) -> Result<Vc<AssetContent>> {
220 let html: RcStr = render_html(&self.chunk_paths, self.body.as_deref())?.into();
221
222 Ok(AssetContent::file(
223 FileContent::Content(File::from(html).with_content_type(TEXT_HTML_UTF_8)).cell(),
224 ))
225 }
226
227 #[turbo_tasks::function]
228 async fn version(self: Vc<Self>) -> Result<Vc<DevHtmlAssetVersion>> {
229 let this = self.await?;
230 Ok(DevHtmlAssetVersion { content: this }.cell())
231 }
232}
233
234#[turbo_tasks::value_impl]
235impl VersionedContent for DevHtmlAssetContent {
236 #[turbo_tasks::function]
237 fn content(self: Vc<Self>) -> Vc<AssetContent> {
238 self.content()
239 }
240
241 #[turbo_tasks::function]
242 fn version(self: Vc<Self>) -> Vc<Box<dyn Version>> {
243 Vc::upcast(self.version())
244 }
245}
246
247#[turbo_tasks::value(operation)]
248struct DevHtmlAssetVersion {
249 content: ReadRef<DevHtmlAssetContent>,
250}
251
252#[turbo_tasks::value_impl]
253impl Version for DevHtmlAssetVersion {
254 #[turbo_tasks::function]
255 fn id(&self) -> Vc<RcStr> {
256 let mut hasher = Xxh3Hash64Hasher::new();
257 for relative_path in &*self.content.chunk_paths {
258 hasher.write_ref(relative_path);
259 }
260 if let Some(body) = &self.content.body {
261 hasher.write_ref(body);
262 }
263 let hash = hasher.finish();
264 let hash = encode_base64(hash);
265 Vc::cell(hash.into())
266 }
267}
268
269fn render_html(chunk_paths: &[RcStr], body: Option<&str>) -> Result<String> {
276 let mut scripts = Vec::new();
277 let mut stylesheets = Vec::new();
278
279 for relative_path in chunk_paths {
280 if relative_path.ends_with(".js") {
281 scripts.push(format!("<script async src=\"{relative_path}\"></script>"));
282 } else if relative_path.ends_with(".css") {
283 stylesheets.push(format!(
284 "<link data-turbopack rel=\"stylesheet\" href=\"{relative_path}\">"
285 ));
286 } else {
287 anyhow::bail!("chunk with unknown asset type: {}", relative_path)
288 }
289 }
290
291 Ok(format!(
292 "<!DOCTYPE html>\n<html>\n<head>\n{}\n</head>\n<body>\n{}\n</body>\n</html>",
293 stylesheets
294 .into_iter()
295 .chain(scripts)
296 .collect::<Vec<_>>()
297 .join("\n"),
298 body.unwrap_or_default(),
299 ))
300}
301
302#[cfg(test)]
303mod tests {
304 use turbo_rcstr::rcstr;
305
306 use super::render_html;
307
308 #[test]
309 fn chunk_tags_are_in_head() {
310 let html = render_html(
311 &[
312 rcstr!("runtime.js"),
313 rcstr!("a.css"),
314 rcstr!("main.js"),
315 rcstr!("b.css"),
316 ],
317 Some("<div id=\"root\"></div>"),
318 )
319 .unwrap();
320
321 assert_eq!(
322 html,
323 "<!DOCTYPE html>\n<html>\n<head>\n<link data-turbopack rel=\"stylesheet\" \
324 href=\"a.css\">\n<link data-turbopack rel=\"stylesheet\" href=\"b.css\">\n<script \
325 async src=\"runtime.js\"></script>\n<script async \
326 src=\"main.js\"></script>\n</head>\n<body>\n<div id=\"root\"></div>\n</body>\n</html>"
327 );
328 }
329
330 #[test]
331 fn empty_body() {
332 let html = render_html(&[rcstr!("main.js")], None).unwrap();
333 assert_eq!(
334 html,
335 "<!DOCTYPE html>\n<html>\n<head>\n<script async \
336 src=\"main.js\"></script>\n</head>\n<body>\n\n</body>\n</html>"
337 );
338 }
339
340 #[test]
341 fn unknown_chunk_type() {
342 assert!(render_html(&[rcstr!("main.wasm")], None).is_err());
343 }
344}