1pub(crate) mod single_item_chunk;
2pub mod source_map;
3
4use std::fmt::Write;
5
6use anyhow::{Result, bail};
7use swc_core::common::pass::Either;
8use turbo_rcstr::{RcStr, rcstr};
9use turbo_tasks::{
10 FxIndexSet, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, ValueDefault, ValueToString, Vc,
11};
12use turbo_tasks_fs::{
13 File, FileSystem, FileSystemPath,
14 rope::{Rope, RopeBuilder},
15};
16use turbopack_core::{
17 asset::{Asset, AssetContent},
18 chunk::{
19 AsyncModuleInfo, Chunk, ChunkItem, ChunkItemBatchGroup, ChunkItemExt,
20 ChunkItemOrBatchWithAsyncModuleInfo, ChunkItemWithAsyncModuleInfo, ChunkType,
21 ChunkableModule, ChunkingContext, MinifyType, OutputChunk, OutputChunkRuntimeInfo,
22 round_chunk_item_size,
23 },
24 code_builder::{Code, CodeBuilder},
25 ident::AssetIdent,
26 introspect::{
27 Introspectable, IntrospectableChildren,
28 module::IntrospectableModule,
29 utils::{children_from_output_assets, content_to_details},
30 },
31 module::Module,
32 output::{OutputAsset, OutputAssets},
33 reference_type::ImportContext,
34 server_fs::ServerFileSystem,
35 source_map::{GenerateSourceMap, OptionStringifiedSourceMap, utils::fileify_source_map},
36};
37
38use self::{single_item_chunk::chunk::SingleItemCssChunk, source_map::CssChunkSourceMapAsset};
39use crate::{ImportAssetReference, util::stringify_js};
40
41#[turbo_tasks::value]
42pub struct CssChunk {
43 pub chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
44 pub content: ResolvedVc<CssChunkContent>,
45}
46
47#[turbo_tasks::value_impl]
48impl CssChunk {
49 #[turbo_tasks::function]
50 pub fn new(
51 chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
52 content: ResolvedVc<CssChunkContent>,
53 ) -> Vc<Self> {
54 CssChunk {
55 chunking_context,
56 content,
57 }
58 .cell()
59 }
60
61 #[turbo_tasks::function]
62 fn chunk_content(&self) -> Vc<CssChunkContent> {
63 *self.content
64 }
65
66 #[turbo_tasks::function]
67 async fn code(self: Vc<Self>) -> Result<Vc<Code>> {
68 use std::io::Write;
69
70 let this = self.await?;
71
72 let source_maps = *this
73 .chunking_context
74 .reference_chunk_source_maps(Vc::upcast(self))
75 .await?;
76
77 let mut code = CodeBuilder::new(source_maps);
78 let mut body = CodeBuilder::new(source_maps);
79 let mut external_imports = FxIndexSet::default();
80 for css_item in &this.content.await?.chunk_items {
81 let content = &css_item.content().await?;
82 for import in &content.imports {
83 if let CssImport::External(external_import) = import {
84 external_imports.insert((*external_import.await?).to_string());
85 }
86 }
87
88 if matches!(
89 &*this.chunking_context.minify_type().await?,
90 MinifyType::NoMinify
91 ) {
92 let id = css_item.asset_ident().to_string().await?;
93 writeln!(body, "/* {id} */")?;
94 }
95
96 let close = write_import_context(&mut body, content.import_context).await?;
97
98 let source_map = if *self
99 .chunking_context()
100 .should_use_file_source_map_uris()
101 .await?
102 {
103 fileify_source_map(
104 content.source_map.as_ref(),
105 self.chunking_context().root_path().await?.clone_value(),
106 )
107 .await?
108 } else {
109 content.source_map.clone()
110 };
111
112 body.push_source(&content.inner_code, source_map);
113
114 writeln!(body, "{close}")?;
115 writeln!(body)?;
116 }
117
118 for external_import in external_imports {
119 writeln!(code, "@import {};", stringify_js(&external_import))?;
120 }
121
122 let built = &body.build();
123 code.push_code(built);
124
125 let c = code.build().cell();
126 Ok(c)
127 }
128
129 #[turbo_tasks::function]
130 async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
131 let code = self.code().await?;
132
133 let rope = if code.has_source_map() {
134 use std::io::Write;
135 let mut rope_builder = RopeBuilder::default();
136 rope_builder.concat(code.source_code());
137 let source_map_path = CssChunkSourceMapAsset::new(self).path().await?;
138 write!(
139 rope_builder,
140 "/*# sourceMappingURL={}*/",
141 urlencoding::encode(source_map_path.file_name())
142 )?;
143 rope_builder.build()
144 } else {
145 code.source_code().clone()
146 };
147
148 Ok(AssetContent::file(File::from(rope).into()))
149 }
150
151 #[turbo_tasks::function]
152 async fn ident_for_path(&self) -> Result<Vc<AssetIdent>> {
153 let CssChunkContent { chunk_items, .. } = &*self.content.await?;
154 let mut common_path = if let Some(chunk_item) = chunk_items.first() {
155 let path = chunk_item.asset_ident().path().await?.clone_value();
156 Some((path.clone(), path))
157 } else {
158 None
159 };
160
161 for &chunk_item in chunk_items.iter() {
164 if let Some((common_path_vc, common_path_ref)) = common_path.as_mut() {
165 let path = chunk_item.asset_ident().path().await?;
166 while !path.is_inside_or_equal_ref(common_path_ref) {
167 let parent = common_path_vc.parent();
168 if parent == *common_path_vc {
169 common_path = None;
170 break;
171 }
172 *common_path_vc = parent;
173 *common_path_ref = common_path_vc.clone();
174 }
175 }
176 }
177 let assets = chunk_items
178 .iter()
179 .map(|chunk_item| async move {
180 Ok((
181 rcstr!("chunk item"),
182 chunk_item.content_ident().to_resolved().await?,
183 ))
184 })
185 .try_join()
186 .await?;
187
188 let ident = AssetIdent {
189 path: if let Some((common_path, _)) = common_path {
190 common_path
191 } else {
192 ServerFileSystem::new().root().await?.clone_value()
193 },
194 query: RcStr::default(),
195 fragment: RcStr::default(),
196 assets,
197 modifiers: Vec::new(),
198 parts: Vec::new(),
199 layer: None,
200 content_type: None,
201 };
202
203 Ok(AssetIdent::new(ident))
204 }
205}
206
207pub async fn write_import_context(
208 body: &mut impl std::io::Write,
209 import_context: Option<ResolvedVc<ImportContext>>,
210) -> Result<String> {
211 let mut close = String::new();
212 if let Some(import_context) = import_context {
213 let import_context = &*import_context.await?;
214 if !&import_context.layers.is_empty() {
215 writeln!(body, "@layer {} {{", import_context.layers.join("."))?;
216 close.push_str("\n}");
217 }
218 if !&import_context.media.is_empty() {
219 writeln!(body, "@media {} {{", import_context.media.join(" and "))?;
220 close.push_str("\n}");
221 }
222 if !&import_context.supports.is_empty() {
223 writeln!(
224 body,
225 "@supports {} {{",
226 import_context.supports.join(" and ")
227 )?;
228 close.push_str("\n}");
229 }
230 }
231 Ok(close)
232}
233
234#[turbo_tasks::value]
235pub struct CssChunkContent {
236 pub chunk_items: Vec<ResolvedVc<Box<dyn CssChunkItem>>>,
237 pub referenced_output_assets: ResolvedVc<OutputAssets>,
238}
239
240#[turbo_tasks::value_impl]
241impl Chunk for CssChunk {
242 #[turbo_tasks::function]
243 async fn ident(self: Vc<Self>) -> Result<Vc<AssetIdent>> {
244 Ok(AssetIdent::from_path(self.path().await?.clone_value()))
245 }
246
247 #[turbo_tasks::function]
248 fn chunking_context(&self) -> Vc<Box<dyn ChunkingContext>> {
249 *self.chunking_context
250 }
251}
252
253#[turbo_tasks::value_impl]
254impl OutputChunk for CssChunk {
255 #[turbo_tasks::function]
256 async fn runtime_info(&self) -> Result<Vc<OutputChunkRuntimeInfo>> {
257 let content = self.content.await?;
258 let entries_chunk_items = &content.chunk_items;
259 let included_ids = entries_chunk_items
260 .iter()
261 .map(|chunk_item| chunk_item.id().to_resolved())
262 .try_join()
263 .await?;
264 let imports_chunk_items: Vec<_> = entries_chunk_items
265 .iter()
266 .map(|&chunk_item| async move {
267 let Some(css_item) = ResolvedVc::try_downcast::<Box<dyn CssChunkItem>>(chunk_item)
268 else {
269 return Ok(vec![]);
270 };
271 Ok(css_item
272 .content()
273 .await?
274 .imports
275 .iter()
276 .filter_map(|import| {
277 if let CssImport::Internal(_, item) = import {
278 Some(*item)
279 } else {
280 None
281 }
282 })
283 .collect())
284 })
285 .try_join()
286 .await?
287 .into_iter()
288 .flatten()
289 .collect();
290 let module_chunks = if entries_chunk_items.len() > 1 {
291 content
292 .chunk_items
293 .iter()
294 .chain(imports_chunk_items.iter())
295 .map(|item| {
296 Vc::upcast::<Box<dyn OutputAsset>>(SingleItemCssChunk::new(
297 *self.chunking_context,
298 **item,
299 ))
300 .to_resolved()
301 })
302 .try_join()
303 .await?
304 } else {
305 Vec::new()
306 };
307 Ok(OutputChunkRuntimeInfo {
308 included_ids: Some(ResolvedVc::cell(included_ids)),
309 module_chunks: Some(ResolvedVc::cell(module_chunks)),
310 ..Default::default()
311 }
312 .cell())
313 }
314}
315
316#[turbo_tasks::value_impl]
317impl OutputAsset for CssChunk {
318 #[turbo_tasks::function]
319 async fn path(self: Vc<Self>) -> Result<Vc<FileSystemPath>> {
320 let ident = self.ident_for_path();
321
322 Ok(self
323 .await?
324 .chunking_context
325 .chunk_path(Some(Vc::upcast(self)), ident, rcstr!(".css")))
326 }
327
328 #[turbo_tasks::function]
329 async fn references(self: Vc<Self>) -> Result<Vc<OutputAssets>> {
330 let this = self.await?;
331 let content = this.content.await?;
332 let mut references = content.referenced_output_assets.owned().await?;
333 let single_item_chunks = content.chunk_items.len() > 1;
334 references.extend(
335 content
336 .chunk_items
337 .iter()
338 .map(|item| async {
339 let references = item.references().await?.into_iter().copied();
340 Ok(if single_item_chunks {
341 Either::Left(
342 references.chain(std::iter::once(ResolvedVc::upcast(
343 SingleItemCssChunk::new(*this.chunking_context, **item)
344 .to_resolved()
345 .await?,
346 ))),
347 )
348 } else {
349 Either::Right(references)
350 })
351 })
352 .try_flat_join()
353 .await?,
354 );
355 if *this
356 .chunking_context
357 .reference_chunk_source_maps(Vc::upcast(self))
358 .await?
359 {
360 references.push(ResolvedVc::upcast(
361 CssChunkSourceMapAsset::new(self).to_resolved().await?,
362 ));
363 }
364 Ok(Vc::cell(references))
365 }
366}
367
368#[turbo_tasks::value_impl]
369impl Asset for CssChunk {
370 #[turbo_tasks::function]
371 fn content(self: Vc<Self>) -> Vc<AssetContent> {
372 self.content()
373 }
374}
375
376#[turbo_tasks::value_impl]
377impl GenerateSourceMap for CssChunk {
378 #[turbo_tasks::function]
379 fn generate_source_map(self: Vc<Self>) -> Vc<OptionStringifiedSourceMap> {
380 self.code().generate_source_map()
381 }
382}
383
384#[turbo_tasks::value_trait]
386pub trait CssChunkPlaceable: ChunkableModule + Module + Asset {}
387
388#[derive(Clone, Debug)]
389#[turbo_tasks::value(shared)]
390pub enum CssImport {
391 External(ResolvedVc<RcStr>),
392 Internal(
393 ResolvedVc<ImportAssetReference>,
394 ResolvedVc<Box<dyn CssChunkItem>>,
395 ),
396 Composes(ResolvedVc<Box<dyn CssChunkItem>>),
397}
398
399#[derive(Debug)]
400#[turbo_tasks::value(shared)]
401pub struct CssChunkItemContent {
402 pub import_context: Option<ResolvedVc<ImportContext>>,
403 pub imports: Vec<CssImport>,
404 pub inner_code: Rope,
405 pub source_map: Option<Rope>,
406}
407
408#[turbo_tasks::value_trait]
409pub trait CssChunkItem: ChunkItem {
410 #[turbo_tasks::function]
411 fn content(self: Vc<Self>) -> Vc<CssChunkItemContent>;
412}
413
414#[turbo_tasks::value_impl]
415impl Introspectable for CssChunk {
416 #[turbo_tasks::function]
417 fn ty(&self) -> Vc<RcStr> {
418 Vc::cell(rcstr!("css chunk"))
419 }
420
421 #[turbo_tasks::function]
422 fn title(self: Vc<Self>) -> Vc<RcStr> {
423 self.path().to_string()
424 }
425
426 #[turbo_tasks::function]
427 async fn details(self: Vc<Self>) -> Result<Vc<RcStr>> {
428 let content = content_to_details(self.content());
429 let mut details = String::new();
430 let this = self.await?;
431 let chunk_content = this.content.await?;
432 details += "Chunk items:\n\n";
433 for item in chunk_content.chunk_items.iter() {
434 writeln!(details, "- {}", item.asset_ident().to_string().await?)?;
435 }
436 details += "\nContent:\n\n";
437 write!(details, "{}", content.await?)?;
438 Ok(Vc::cell(details.into()))
439 }
440
441 #[turbo_tasks::function]
442 async fn children(self: Vc<Self>) -> Result<Vc<IntrospectableChildren>> {
443 let mut children = children_from_output_assets(OutputAsset::references(self))
444 .owned()
445 .await?;
446 children.extend(
447 self.await?
448 .content
449 .await?
450 .chunk_items
451 .iter()
452 .map(|chunk_item| async move {
453 Ok((
454 rcstr!("entry module"),
455 IntrospectableModule::new(chunk_item.module())
456 .to_resolved()
457 .await?,
458 ))
459 })
460 .try_join()
461 .await?,
462 );
463 Ok(Vc::cell(children))
464 }
465}
466
467#[derive(Default)]
468#[turbo_tasks::value]
469pub struct CssChunkType {}
470
471#[turbo_tasks::value_impl]
472impl ValueToString for CssChunkType {
473 #[turbo_tasks::function]
474 fn to_string(&self) -> Vc<RcStr> {
475 Vc::cell(rcstr!("css"))
476 }
477}
478
479#[turbo_tasks::value_impl]
480impl ChunkType for CssChunkType {
481 #[turbo_tasks::function]
482 fn is_style(self: Vc<Self>) -> Vc<bool> {
483 Vc::cell(true)
484 }
485
486 #[turbo_tasks::function]
487 async fn chunk(
488 &self,
489 chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
490 chunk_items_or_batches: Vec<ChunkItemOrBatchWithAsyncModuleInfo>,
491 _batch_groups: Vec<ResolvedVc<ChunkItemBatchGroup>>,
492 referenced_output_assets: ResolvedVc<OutputAssets>,
493 ) -> Result<Vc<Box<dyn Chunk>>> {
494 let mut chunk_items = Vec::new();
495 for item in chunk_items_or_batches {
497 match item {
498 ChunkItemOrBatchWithAsyncModuleInfo::ChunkItem(chunk_item) => {
499 chunk_items.push(chunk_item);
500 }
501 ChunkItemOrBatchWithAsyncModuleInfo::Batch(batch) => {
502 let batch = batch.await?;
503 chunk_items.extend(batch.chunk_items.iter().cloned());
504 }
505 }
506 }
507 let content = CssChunkContent {
508 chunk_items: chunk_items
509 .iter()
510 .map(async |ChunkItemWithAsyncModuleInfo { chunk_item, .. }| {
511 let Some(chunk_item) =
512 ResolvedVc::try_downcast::<Box<dyn CssChunkItem>>(*chunk_item)
513 else {
514 bail!("Chunk item is not an css chunk item but reporting chunk type css");
515 };
516 Ok(chunk_item)
518 })
519 .try_join()
520 .await?,
521 referenced_output_assets,
522 }
523 .cell();
524 Ok(Vc::upcast(CssChunk::new(*chunking_context, content)))
525 }
526
527 #[turbo_tasks::function]
528 async fn chunk_item_size(
529 &self,
530 _chunking_context: Vc<Box<dyn ChunkingContext>>,
531 chunk_item: Vc<Box<dyn ChunkItem>>,
532 _async_module_info: Option<Vc<AsyncModuleInfo>>,
533 ) -> Result<Vc<usize>> {
534 let Some(chunk_item) =
535 Vc::try_resolve_downcast::<Box<dyn CssChunkItem>>(chunk_item).await?
536 else {
537 bail!("Chunk item is not an css chunk item but reporting chunk type css");
538 };
539 Ok(Vc::cell(chunk_item.content().await.map_or(0, |content| {
540 round_chunk_item_size(content.inner_code.len())
541 })))
542 }
543}
544
545#[turbo_tasks::value_impl]
546impl ValueDefault for CssChunkType {
547 #[turbo_tasks::function]
548 fn value_default() -> Vc<Self> {
549 Self::default().cell()
550 }
551}