Skip to main content

turbopack_dev_server/
html.rs

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/// The HTML entry point of the dev server.
30///
31/// Generates an HTML page that includes the ES and CSS chunks.
32#[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    /// Create a new dev HTML asset.
71    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    /// Create a new dev HTML asset.
81    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
269/// Renders the HTML page that loads the given chunks.
270///
271/// All chunk tags go into `<head>`, where the runtime looks for the tags of
272/// already loaded chunks. Scripts are `async`: the runtime does not depend on
273/// the order in which chunks execute, but code in a chunk may run before
274/// `<body>` has been parsed.
275fn 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}