turbopack_dev_server/
html.rs

1use anyhow::Result;
2use mime_guess::mime::TEXT_HTML_UTF_8;
3use serde::{Deserialize, Serialize};
4use turbo_rcstr::RcStr;
5use turbo_tasks::{
6    NonLocalValue, ReadRef, ResolvedVc, TaskInput, TryJoinIterExt, Vc, trace::TraceRawVcs,
7};
8use turbo_tasks_fs::{File, FileSystemPath};
9use turbo_tasks_hash::{Xxh3Hash64Hasher, encode_hex};
10use turbopack_core::{
11    asset::{Asset, AssetContent},
12    chunk::{
13        ChunkableModule, ChunkingContext, ChunkingContextExt, EvaluatableAssets,
14        availability_info::AvailabilityInfo,
15    },
16    module::Module,
17    module_graph::{ModuleGraph, chunk_group_info::ChunkGroup},
18    output::{OutputAsset, OutputAssets},
19    version::{Version, VersionedContent},
20};
21
22#[derive(
23    Clone, Debug, Deserialize, Eq, Hash, NonLocalValue, PartialEq, Serialize, TaskInput, TraceRawVcs,
24)]
25pub struct DevHtmlEntry {
26    pub chunkable_module: ResolvedVc<Box<dyn ChunkableModule>>,
27    pub module_graph: ResolvedVc<ModuleGraph>,
28    pub chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
29    pub runtime_entries: Option<ResolvedVc<EvaluatableAssets>>,
30}
31
32/// The HTML entry point of the dev server.
33///
34/// Generates an HTML page that includes the ES and CSS chunks.
35#[turbo_tasks::value(shared)]
36#[derive(Clone)]
37pub struct DevHtmlAsset {
38    path: FileSystemPath,
39    entries: Vec<DevHtmlEntry>,
40    body: Option<RcStr>,
41}
42
43#[turbo_tasks::value_impl]
44impl OutputAsset for DevHtmlAsset {
45    #[turbo_tasks::function]
46    fn path(&self) -> Vc<FileSystemPath> {
47        self.path.clone().cell()
48    }
49
50    #[turbo_tasks::function]
51    fn references(self: Vc<Self>) -> Vc<OutputAssets> {
52        self.chunks()
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.chunks().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 chunks(&self) -> Result<Vc<OutputAssets>> {
131        let all_assets = self
132            .entries
133            .iter()
134            .map(|entry| async move {
135                let &DevHtmlEntry {
136                    chunkable_module,
137                    chunking_context,
138                    module_graph,
139                    runtime_entries,
140                } = entry;
141
142                let assets = 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.evaluated_chunk_group_assets(
153                        chunkable_module.ident(),
154                        ChunkGroup::Entry(
155                            runtime_entries
156                                .await?
157                                .iter()
158                                .map(|v| ResolvedVc::upcast(*v))
159                                .collect(),
160                        ),
161                        *module_graph,
162                        AvailabilityInfo::Root,
163                    )
164                } else {
165                    chunking_context.root_chunk_group_assets(
166                        chunkable_module.ident(),
167                        ChunkGroup::Entry(vec![ResolvedVc::upcast(chunkable_module)]),
168                        *module_graph,
169                    )
170                };
171
172                assets.await
173            })
174            .try_join()
175            .await?
176            .iter()
177            .flatten()
178            .copied()
179            .collect();
180
181        Ok(Vc::cell(all_assets))
182    }
183}
184
185#[turbo_tasks::value(operation)]
186struct DevHtmlAssetContent {
187    chunk_paths: Vec<RcStr>,
188    body: Option<RcStr>,
189}
190
191impl DevHtmlAssetContent {
192    fn new(chunk_paths: Vec<RcStr>, body: Option<RcStr>) -> Vc<Self> {
193        DevHtmlAssetContent { chunk_paths, body }.cell()
194    }
195}
196
197#[turbo_tasks::value_impl]
198impl DevHtmlAssetContent {
199    #[turbo_tasks::function]
200    fn content(&self) -> Result<Vc<AssetContent>> {
201        let mut scripts = Vec::new();
202        let mut stylesheets = Vec::new();
203
204        for relative_path in &*self.chunk_paths {
205            if relative_path.ends_with(".js") {
206                scripts.push(format!("<script src=\"{relative_path}\"></script>"));
207            } else if relative_path.ends_with(".css") {
208                stylesheets.push(format!(
209                    "<link data-turbopack rel=\"stylesheet\" href=\"{relative_path}\">"
210                ));
211            } else {
212                anyhow::bail!("chunk with unknown asset type: {}", relative_path)
213            }
214        }
215
216        let body = match &self.body {
217            Some(body) => body.as_str(),
218            None => "",
219        };
220
221        let html: RcStr = format!(
222            "<!DOCTYPE html>\n<html>\n<head>\n{}\n</head>\n<body>\n{}\n{}\n</body>\n</html>",
223            stylesheets.join("\n"),
224            body,
225            scripts.join("\n"),
226        )
227        .into();
228
229        Ok(AssetContent::file(
230            File::from(html).with_content_type(TEXT_HTML_UTF_8).into(),
231        ))
232    }
233
234    #[turbo_tasks::function]
235    async fn version(self: Vc<Self>) -> Result<Vc<DevHtmlAssetVersion>> {
236        let this = self.await?;
237        Ok(DevHtmlAssetVersion { content: this }.cell())
238    }
239}
240
241#[turbo_tasks::value_impl]
242impl VersionedContent for DevHtmlAssetContent {
243    #[turbo_tasks::function]
244    fn content(self: Vc<Self>) -> Vc<AssetContent> {
245        self.content()
246    }
247
248    #[turbo_tasks::function]
249    fn version(self: Vc<Self>) -> Vc<Box<dyn Version>> {
250        Vc::upcast(self.version())
251    }
252}
253
254#[turbo_tasks::value(operation)]
255struct DevHtmlAssetVersion {
256    content: ReadRef<DevHtmlAssetContent>,
257}
258
259#[turbo_tasks::value_impl]
260impl Version for DevHtmlAssetVersion {
261    #[turbo_tasks::function]
262    fn id(&self) -> Vc<RcStr> {
263        let mut hasher = Xxh3Hash64Hasher::new();
264        for relative_path in &*self.content.chunk_paths {
265            hasher.write_ref(relative_path);
266        }
267        if let Some(body) = &self.content.body {
268            hasher.write_ref(body);
269        }
270        let hash = hasher.finish();
271        let hex_hash = encode_hex(hash);
272        Vc::cell(hex_hash.into())
273    }
274}