turbopack_dev_server/
html.rs1use 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#[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 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.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}