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#[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 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 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}