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::{
6    NonLocalValue, ReadRef, ResolvedVc, TaskInput, TryJoinIterExt, Vc, trace::TraceRawVcs,
7};
8use turbo_tasks_fs::{File, FileContent, 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, OutputAssetsReference, OutputAssetsWithReferenced},
19    version::{Version, VersionedContent},
20};
21
22#[derive(
23    Clone, Debug, Eq, Hash, NonLocalValue, PartialEq, TaskInput, TraceRawVcs, Encode, Decode,
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 OutputAssetsReference for DevHtmlAsset {
45    #[turbo_tasks::function]
46    fn references(self: Vc<Self>) -> Vc<OutputAssetsWithReferenced> {
47        self.chunk_group()
48    }
49}
50
51#[turbo_tasks::value_impl]
52impl OutputAsset for DevHtmlAsset {
53    #[turbo_tasks::function]
54    fn path(&self) -> Vc<FileSystemPath> {
55        self.path.clone().cell()
56    }
57}
58
59#[turbo_tasks::value_impl]
60impl Asset for DevHtmlAsset {
61    #[turbo_tasks::function]
62    fn content(self: Vc<Self>) -> Vc<AssetContent> {
63        self.html_content().content()
64    }
65
66    #[turbo_tasks::function]
67    fn versioned_content(self: Vc<Self>) -> Vc<Box<dyn VersionedContent>> {
68        Vc::upcast(self.html_content())
69    }
70}
71
72impl DevHtmlAsset {
73    /// Create a new dev HTML asset.
74    pub fn new(path: FileSystemPath, entries: Vec<DevHtmlEntry>) -> Vc<Self> {
75        DevHtmlAsset {
76            path,
77            entries,
78            body: None,
79        }
80        .cell()
81    }
82
83    /// Create a new dev HTML asset.
84    pub fn new_with_body(
85        path: FileSystemPath,
86        entries: Vec<DevHtmlEntry>,
87        body: RcStr,
88    ) -> Vc<Self> {
89        DevHtmlAsset {
90            path,
91            entries,
92            body: Some(body),
93        }
94        .cell()
95    }
96}
97
98#[turbo_tasks::value_impl]
99impl DevHtmlAsset {
100    #[turbo_tasks::function]
101    pub async fn with_path(self: Vc<Self>, path: FileSystemPath) -> Result<Vc<Self>> {
102        let mut html: DevHtmlAsset = self.owned().await?;
103        html.path = path;
104        Ok(html.cell())
105    }
106
107    #[turbo_tasks::function]
108    pub async fn with_body(self: Vc<Self>, body: RcStr) -> Result<Vc<Self>> {
109        let mut html: DevHtmlAsset = self.owned().await?;
110        html.body = Some(body);
111        Ok(html.cell())
112    }
113}
114
115#[turbo_tasks::value_impl]
116impl DevHtmlAsset {
117    #[turbo_tasks::function]
118    async fn html_content(self: Vc<Self>) -> Result<Vc<DevHtmlAssetContent>> {
119        let this = self.await?;
120        let context_path = this.path.parent();
121        let mut chunk_paths = vec![];
122        for chunk in &*self.chunk_group().await?.assets.await? {
123            let chunk_path = &*chunk.path().await?;
124            if let Some(relative_path) = context_path.get_path_to(chunk_path) {
125                chunk_paths.push(format!("/{relative_path}").into());
126            }
127        }
128
129        Ok(DevHtmlAssetContent::new(chunk_paths, this.body.clone()))
130    }
131
132    #[turbo_tasks::function]
133    async fn chunk_group(&self) -> Result<Vc<OutputAssetsWithReferenced>> {
134        let all_chunk_groups = self
135            .entries
136            .iter()
137            .map(|entry| async move {
138                let &DevHtmlEntry {
139                    chunkable_module,
140                    chunking_context,
141                    module_graph,
142                    runtime_entries,
143                } = entry;
144
145                let asset_with_referenced = if let Some(runtime_entries) = runtime_entries {
146                    let runtime_entries =
147                        if let Some(evaluatable) = ResolvedVc::try_downcast(chunkable_module) {
148                            runtime_entries
149                                .with_entry(*evaluatable)
150                                .to_resolved()
151                                .await?
152                        } else {
153                            runtime_entries
154                        };
155                    chunking_context
156                        .evaluated_chunk_group_assets(
157                            chunkable_module.ident(),
158                            ChunkGroup::Entry(
159                                runtime_entries
160                                    .await?
161                                    .iter()
162                                    .map(|v| ResolvedVc::upcast(*v))
163                                    .collect(),
164                            ),
165                            *module_graph,
166                            AvailabilityInfo::root(),
167                        )
168                        .await?
169                } else {
170                    chunking_context
171                        .root_chunk_group_assets(
172                            chunkable_module.ident(),
173                            ChunkGroup::Entry(vec![ResolvedVc::upcast(chunkable_module)]),
174                            *module_graph,
175                        )
176                        .await?
177                };
178
179                Ok((
180                    asset_with_referenced.assets.await?,
181                    asset_with_referenced.referenced_assets.await?,
182                    asset_with_referenced.references.await?,
183                ))
184            })
185            .try_join()
186            .await?;
187
188        let mut all_assets = Vec::new();
189        let mut all_referenced_assets = Vec::new();
190        let mut all_references = Vec::new();
191        for (asset, referenced_asset, reference) in all_chunk_groups {
192            all_assets.extend(asset);
193            all_referenced_assets.extend(referenced_asset);
194            all_references.extend(reference);
195        }
196
197        Ok(OutputAssetsWithReferenced {
198            assets: ResolvedVc::cell(all_assets),
199            referenced_assets: ResolvedVc::cell(all_referenced_assets),
200            references: ResolvedVc::cell(all_references),
201        }
202        .cell())
203    }
204}
205
206#[turbo_tasks::value(operation)]
207struct DevHtmlAssetContent {
208    chunk_paths: Vec<RcStr>,
209    body: Option<RcStr>,
210}
211
212impl DevHtmlAssetContent {
213    fn new(chunk_paths: Vec<RcStr>, body: Option<RcStr>) -> Vc<Self> {
214        DevHtmlAssetContent { chunk_paths, body }.cell()
215    }
216}
217
218#[turbo_tasks::value_impl]
219impl DevHtmlAssetContent {
220    #[turbo_tasks::function]
221    fn content(&self) -> Result<Vc<AssetContent>> {
222        let mut scripts = Vec::new();
223        let mut stylesheets = Vec::new();
224
225        for relative_path in &*self.chunk_paths {
226            if relative_path.ends_with(".js") {
227                scripts.push(format!("<script src=\"{relative_path}\"></script>"));
228            } else if relative_path.ends_with(".css") {
229                stylesheets.push(format!(
230                    "<link data-turbopack rel=\"stylesheet\" href=\"{relative_path}\">"
231                ));
232            } else {
233                anyhow::bail!("chunk with unknown asset type: {}", relative_path)
234            }
235        }
236
237        let body = match &self.body {
238            Some(body) => body.as_str(),
239            None => "",
240        };
241
242        let html: RcStr = format!(
243            "<!DOCTYPE html>\n<html>\n<head>\n{}\n</head>\n<body>\n{}\n{}\n</body>\n</html>",
244            stylesheets.join("\n"),
245            body,
246            scripts.join("\n"),
247        )
248        .into();
249
250        Ok(AssetContent::file(
251            FileContent::Content(File::from(html).with_content_type(TEXT_HTML_UTF_8)).cell(),
252        ))
253    }
254
255    #[turbo_tasks::function]
256    async fn version(self: Vc<Self>) -> Result<Vc<DevHtmlAssetVersion>> {
257        let this = self.await?;
258        Ok(DevHtmlAssetVersion { content: this }.cell())
259    }
260}
261
262#[turbo_tasks::value_impl]
263impl VersionedContent for DevHtmlAssetContent {
264    #[turbo_tasks::function]
265    fn content(self: Vc<Self>) -> Vc<AssetContent> {
266        self.content()
267    }
268
269    #[turbo_tasks::function]
270    fn version(self: Vc<Self>) -> Vc<Box<dyn Version>> {
271        Vc::upcast(self.version())
272    }
273}
274
275#[turbo_tasks::value(operation)]
276struct DevHtmlAssetVersion {
277    content: ReadRef<DevHtmlAssetContent>,
278}
279
280#[turbo_tasks::value_impl]
281impl Version for DevHtmlAssetVersion {
282    #[turbo_tasks::function]
283    fn id(&self) -> Vc<RcStr> {
284        let mut hasher = Xxh3Hash64Hasher::new();
285        for relative_path in &*self.content.chunk_paths {
286            hasher.write_ref(relative_path);
287        }
288        if let Some(body) = &self.content.body {
289            hasher.write_ref(body);
290        }
291        let hash = hasher.finish();
292        let hex_hash = encode_hex(hash);
293        Vc::cell(hex_hash.into())
294    }
295}