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, OutputAssetsWithReferenced},
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.chunk_group().all_assets()
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(|entry| async move {
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 ChunkGroup::Entry(
156 runtime_entries
157 .await?
158 .iter()
159 .map(|v| ResolvedVc::upcast(*v))
160 .collect(),
161 ),
162 *module_graph,
163 AvailabilityInfo::Root,
164 )
165 .await?
166 } else {
167 chunking_context
168 .root_chunk_group_assets(
169 chunkable_module.ident(),
170 ChunkGroup::Entry(vec![ResolvedVc::upcast(chunkable_module)]),
171 *module_graph,
172 )
173 .await?
174 };
175
176 Ok((
177 asset_with_referenced.assets.await?,
178 asset_with_referenced.referenced_assets.await?,
179 ))
180 })
181 .try_join()
182 .await?;
183
184 let mut all_assets = Vec::new();
185 let mut all_referenced_assets = Vec::new();
186 for (asset, referenced_asset) in all_chunk_groups {
187 all_assets.extend(asset);
188 all_referenced_assets.extend(referenced_asset);
189 }
190
191 Ok(OutputAssetsWithReferenced {
192 assets: ResolvedVc::cell(all_assets),
193 referenced_assets: ResolvedVc::cell(all_referenced_assets),
194 }
195 .cell())
196 }
197}
198
199#[turbo_tasks::value(operation)]
200struct DevHtmlAssetContent {
201 chunk_paths: Vec<RcStr>,
202 body: Option<RcStr>,
203}
204
205impl DevHtmlAssetContent {
206 fn new(chunk_paths: Vec<RcStr>, body: Option<RcStr>) -> Vc<Self> {
207 DevHtmlAssetContent { chunk_paths, body }.cell()
208 }
209}
210
211#[turbo_tasks::value_impl]
212impl DevHtmlAssetContent {
213 #[turbo_tasks::function]
214 fn content(&self) -> Result<Vc<AssetContent>> {
215 let mut scripts = Vec::new();
216 let mut stylesheets = Vec::new();
217
218 for relative_path in &*self.chunk_paths {
219 if relative_path.ends_with(".js") {
220 scripts.push(format!("<script src=\"{relative_path}\"></script>"));
221 } else if relative_path.ends_with(".css") {
222 stylesheets.push(format!(
223 "<link data-turbopack rel=\"stylesheet\" href=\"{relative_path}\">"
224 ));
225 } else {
226 anyhow::bail!("chunk with unknown asset type: {}", relative_path)
227 }
228 }
229
230 let body = match &self.body {
231 Some(body) => body.as_str(),
232 None => "",
233 };
234
235 let html: RcStr = format!(
236 "<!DOCTYPE html>\n<html>\n<head>\n{}\n</head>\n<body>\n{}\n{}\n</body>\n</html>",
237 stylesheets.join("\n"),
238 body,
239 scripts.join("\n"),
240 )
241 .into();
242
243 Ok(AssetContent::file(
244 File::from(html).with_content_type(TEXT_HTML_UTF_8).into(),
245 ))
246 }
247
248 #[turbo_tasks::function]
249 async fn version(self: Vc<Self>) -> Result<Vc<DevHtmlAssetVersion>> {
250 let this = self.await?;
251 Ok(DevHtmlAssetVersion { content: this }.cell())
252 }
253}
254
255#[turbo_tasks::value_impl]
256impl VersionedContent for DevHtmlAssetContent {
257 #[turbo_tasks::function]
258 fn content(self: Vc<Self>) -> Vc<AssetContent> {
259 self.content()
260 }
261
262 #[turbo_tasks::function]
263 fn version(self: Vc<Self>) -> Vc<Box<dyn Version>> {
264 Vc::upcast(self.version())
265 }
266}
267
268#[turbo_tasks::value(operation)]
269struct DevHtmlAssetVersion {
270 content: ReadRef<DevHtmlAssetContent>,
271}
272
273#[turbo_tasks::value_impl]
274impl Version for DevHtmlAssetVersion {
275 #[turbo_tasks::function]
276 fn id(&self) -> Vc<RcStr> {
277 let mut hasher = Xxh3Hash64Hasher::new();
278 for relative_path in &*self.content.chunk_paths {
279 hasher.write_ref(relative_path);
280 }
281 if let Some(body) = &self.content.body {
282 hasher.write_ref(body);
283 }
284 let hash = hasher.finish();
285 let hex_hash = encode_hex(hash);
286 Vc::cell(hex_hash.into())
287 }
288}