turbopack_css/chunk/
mod.rs

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().owned().await?,
106                )
107                .await?
108            } else {
109                content.source_map.clone()
110            };
111
112            body.push_source(&content.inner_code, source_map);
113
114            if !close.is_empty() {
115                writeln!(body, "{close}")?;
116            }
117            writeln!(body)?;
118        }
119
120        for external_import in external_imports {
121            writeln!(code, "@import {};", stringify_js(&external_import))?;
122        }
123
124        let built = &body.build();
125        code.push_code(built);
126
127        let c = code.build().cell();
128        Ok(c)
129    }
130
131    #[turbo_tasks::function]
132    async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
133        let code = self.code().await?;
134
135        let rope = if code.has_source_map() {
136            use std::io::Write;
137            let mut rope_builder = RopeBuilder::default();
138            rope_builder.concat(code.source_code());
139            let source_map_path = CssChunkSourceMapAsset::new(self).path().await?;
140            write!(
141                rope_builder,
142                "/*# sourceMappingURL={}*/",
143                urlencoding::encode(source_map_path.file_name())
144            )?;
145            rope_builder.build()
146        } else {
147            code.source_code().clone()
148        };
149
150        Ok(AssetContent::file(File::from(rope).into()))
151    }
152
153    #[turbo_tasks::function]
154    async fn ident_for_path(&self) -> Result<Vc<AssetIdent>> {
155        let CssChunkContent { chunk_items, .. } = &*self.content.await?;
156        let mut common_path = if let Some(chunk_item) = chunk_items.first() {
157            let path = chunk_item.asset_ident().path().owned().await?;
158            Some((path.clone(), path))
159        } else {
160            None
161        };
162
163        // The included chunk items and the availability info describe the chunk
164        // uniquely
165        for &chunk_item in chunk_items.iter() {
166            if let Some((common_path_vc, common_path_ref)) = common_path.as_mut() {
167                let path = chunk_item.asset_ident().path().await?;
168                while !path.is_inside_or_equal_ref(common_path_ref) {
169                    let parent = common_path_vc.parent();
170                    if parent == *common_path_vc {
171                        common_path = None;
172                        break;
173                    }
174                    *common_path_vc = parent;
175                    *common_path_ref = common_path_vc.clone();
176                }
177            }
178        }
179        let assets = chunk_items
180            .iter()
181            .map(|chunk_item| async move {
182                Ok((
183                    rcstr!("chunk item"),
184                    chunk_item.content_ident().to_resolved().await?,
185                ))
186            })
187            .try_join()
188            .await?;
189
190        let ident = AssetIdent {
191            path: if let Some((common_path, _)) = common_path {
192                common_path
193            } else {
194                ServerFileSystem::new().root().owned().await?
195            },
196            query: RcStr::default(),
197            fragment: RcStr::default(),
198            assets,
199            modifiers: Vec::new(),
200            parts: Vec::new(),
201            layer: None,
202            content_type: None,
203        };
204
205        Ok(AssetIdent::new(ident))
206    }
207}
208
209pub async fn write_import_context(
210    body: &mut impl std::io::Write,
211    import_context: Option<ResolvedVc<ImportContext>>,
212) -> Result<String> {
213    let mut close = String::new();
214    if let Some(import_context) = import_context {
215        let import_context = &*import_context.await?;
216        if !&import_context.layers.is_empty() {
217            writeln!(body, "@layer {} {{", import_context.layers.join("."))?;
218            close.push_str("\n}");
219        }
220        if !&import_context.media.is_empty() {
221            writeln!(body, "@media {} {{", import_context.media.join(" and "))?;
222            close.push_str("\n}");
223        }
224        if !&import_context.supports.is_empty() {
225            writeln!(
226                body,
227                "@supports {} {{",
228                import_context.supports.join(" and ")
229            )?;
230            close.push_str("\n}");
231        }
232    }
233    Ok(close)
234}
235
236#[turbo_tasks::value]
237pub struct CssChunkContent {
238    pub chunk_items: Vec<ResolvedVc<Box<dyn CssChunkItem>>>,
239    pub referenced_output_assets: ResolvedVc<OutputAssets>,
240}
241
242#[turbo_tasks::value_impl]
243impl Chunk for CssChunk {
244    #[turbo_tasks::function]
245    async fn ident(self: Vc<Self>) -> Result<Vc<AssetIdent>> {
246        Ok(AssetIdent::from_path(self.path().owned().await?))
247    }
248
249    #[turbo_tasks::function]
250    fn chunking_context(&self) -> Vc<Box<dyn ChunkingContext>> {
251        *self.chunking_context
252    }
253}
254
255#[turbo_tasks::value_impl]
256impl OutputChunk for CssChunk {
257    #[turbo_tasks::function]
258    async fn runtime_info(&self) -> Result<Vc<OutputChunkRuntimeInfo>> {
259        if !*self
260            .chunking_context
261            .is_dynamic_chunk_content_loading_enabled()
262            .await?
263        {
264            return Ok(OutputChunkRuntimeInfo::empty());
265        }
266
267        let content = self.content.await?;
268        let entries_chunk_items = &content.chunk_items;
269        let included_ids = entries_chunk_items
270            .iter()
271            .map(|chunk_item| chunk_item.id().to_resolved())
272            .try_join()
273            .await?;
274        let imports_chunk_items: Vec<_> = entries_chunk_items
275            .iter()
276            .map(|&chunk_item| async move {
277                let Some(css_item) = ResolvedVc::try_downcast::<Box<dyn CssChunkItem>>(chunk_item)
278                else {
279                    return Ok(vec![]);
280                };
281                Ok(css_item
282                    .content()
283                    .await?
284                    .imports
285                    .iter()
286                    .filter_map(|import| {
287                        if let CssImport::Internal(_, item) = import {
288                            Some(*item)
289                        } else {
290                            None
291                        }
292                    })
293                    .collect())
294            })
295            .try_join()
296            .await?
297            .into_iter()
298            .flatten()
299            .collect();
300        let module_chunks = if content.chunk_items.len() > 1 {
301            content
302                .chunk_items
303                .iter()
304                .chain(imports_chunk_items.iter())
305                .map(|item| {
306                    Vc::upcast::<Box<dyn OutputAsset>>(SingleItemCssChunk::new(
307                        *self.chunking_context,
308                        **item,
309                    ))
310                    .to_resolved()
311                })
312                .try_join()
313                .await?
314        } else {
315            Vec::new()
316        };
317        Ok(OutputChunkRuntimeInfo {
318            included_ids: Some(ResolvedVc::cell(included_ids)),
319            module_chunks: Some(ResolvedVc::cell(module_chunks)),
320            ..Default::default()
321        }
322        .cell())
323    }
324}
325
326#[turbo_tasks::value_impl]
327impl OutputAsset for CssChunk {
328    #[turbo_tasks::function]
329    async fn path(self: Vc<Self>) -> Result<Vc<FileSystemPath>> {
330        let ident = self.ident_for_path();
331
332        Ok(self
333            .await?
334            .chunking_context
335            .chunk_path(Some(Vc::upcast(self)), ident, rcstr!(".css")))
336    }
337
338    #[turbo_tasks::function]
339    async fn references(self: Vc<Self>) -> Result<Vc<OutputAssets>> {
340        let this = self.await?;
341        let content = this.content.await?;
342        let mut references = content.referenced_output_assets.owned().await?;
343        let should_generate_single_item_chunks = content.chunk_items.len() > 1
344            && *this
345                .chunking_context
346                .is_dynamic_chunk_content_loading_enabled()
347                .await?;
348        references.extend(
349            content
350                .chunk_items
351                .iter()
352                .map(|item| async {
353                    let references = item.references().await?.into_iter().copied();
354                    Ok(if should_generate_single_item_chunks {
355                        Either::Left(
356                            references.chain(std::iter::once(ResolvedVc::upcast(
357                                SingleItemCssChunk::new(*this.chunking_context, **item)
358                                    .to_resolved()
359                                    .await?,
360                            ))),
361                        )
362                    } else {
363                        Either::Right(references)
364                    })
365                })
366                .try_flat_join()
367                .await?,
368        );
369        if *this
370            .chunking_context
371            .reference_chunk_source_maps(Vc::upcast(self))
372            .await?
373        {
374            references.push(ResolvedVc::upcast(
375                CssChunkSourceMapAsset::new(self).to_resolved().await?,
376            ));
377        }
378        Ok(Vc::cell(references))
379    }
380}
381
382#[turbo_tasks::value_impl]
383impl Asset for CssChunk {
384    #[turbo_tasks::function]
385    fn content(self: Vc<Self>) -> Vc<AssetContent> {
386        self.content()
387    }
388}
389
390#[turbo_tasks::value_impl]
391impl GenerateSourceMap for CssChunk {
392    #[turbo_tasks::function]
393    fn generate_source_map(self: Vc<Self>) -> Vc<OptionStringifiedSourceMap> {
394        self.code().generate_source_map()
395    }
396}
397
398// TODO: remove
399#[turbo_tasks::value_trait]
400pub trait CssChunkPlaceable: ChunkableModule + Module + Asset {}
401
402#[derive(Clone, Debug)]
403#[turbo_tasks::value(shared)]
404pub enum CssImport {
405    External(ResolvedVc<RcStr>),
406    Internal(
407        ResolvedVc<ImportAssetReference>,
408        ResolvedVc<Box<dyn CssChunkItem>>,
409    ),
410    Composes(ResolvedVc<Box<dyn CssChunkItem>>),
411}
412
413#[derive(Debug)]
414#[turbo_tasks::value(shared)]
415pub struct CssChunkItemContent {
416    pub import_context: Option<ResolvedVc<ImportContext>>,
417    pub imports: Vec<CssImport>,
418    pub inner_code: Rope,
419    pub source_map: Option<Rope>,
420}
421
422#[turbo_tasks::value_trait]
423pub trait CssChunkItem: ChunkItem {
424    #[turbo_tasks::function]
425    fn content(self: Vc<Self>) -> Vc<CssChunkItemContent>;
426}
427
428#[turbo_tasks::value_impl]
429impl Introspectable for CssChunk {
430    #[turbo_tasks::function]
431    fn ty(&self) -> Vc<RcStr> {
432        Vc::cell(rcstr!("css chunk"))
433    }
434
435    #[turbo_tasks::function]
436    fn title(self: Vc<Self>) -> Vc<RcStr> {
437        self.path().to_string()
438    }
439
440    #[turbo_tasks::function]
441    async fn details(self: Vc<Self>) -> Result<Vc<RcStr>> {
442        let content = content_to_details(self.content());
443        let mut details = String::new();
444        let this = self.await?;
445        let chunk_content = this.content.await?;
446        details += "Chunk items:\n\n";
447        for item in chunk_content.chunk_items.iter() {
448            writeln!(details, "- {}", item.asset_ident().to_string().await?)?;
449        }
450        details += "\nContent:\n\n";
451        write!(details, "{}", content.await?)?;
452        Ok(Vc::cell(details.into()))
453    }
454
455    #[turbo_tasks::function]
456    async fn children(self: Vc<Self>) -> Result<Vc<IntrospectableChildren>> {
457        let mut children = children_from_output_assets(OutputAsset::references(self))
458            .owned()
459            .await?;
460        children.extend(
461            self.await?
462                .content
463                .await?
464                .chunk_items
465                .iter()
466                .map(|chunk_item| async move {
467                    Ok((
468                        rcstr!("entry module"),
469                        IntrospectableModule::new(chunk_item.module())
470                            .to_resolved()
471                            .await?,
472                    ))
473                })
474                .try_join()
475                .await?,
476        );
477        Ok(Vc::cell(children))
478    }
479}
480
481#[derive(Default)]
482#[turbo_tasks::value]
483pub struct CssChunkType {}
484
485#[turbo_tasks::value_impl]
486impl ValueToString for CssChunkType {
487    #[turbo_tasks::function]
488    fn to_string(&self) -> Vc<RcStr> {
489        Vc::cell(rcstr!("css"))
490    }
491}
492
493#[turbo_tasks::value_impl]
494impl ChunkType for CssChunkType {
495    #[turbo_tasks::function]
496    fn is_style(self: Vc<Self>) -> Vc<bool> {
497        Vc::cell(true)
498    }
499
500    #[turbo_tasks::function]
501    async fn chunk(
502        &self,
503        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
504        chunk_items_or_batches: Vec<ChunkItemOrBatchWithAsyncModuleInfo>,
505        _batch_groups: Vec<ResolvedVc<ChunkItemBatchGroup>>,
506        referenced_output_assets: ResolvedVc<OutputAssets>,
507    ) -> Result<Vc<Box<dyn Chunk>>> {
508        let mut chunk_items = Vec::new();
509        // TODO operate with batches
510        for item in chunk_items_or_batches {
511            match item {
512                ChunkItemOrBatchWithAsyncModuleInfo::ChunkItem(chunk_item) => {
513                    chunk_items.push(chunk_item);
514                }
515                ChunkItemOrBatchWithAsyncModuleInfo::Batch(batch) => {
516                    let batch = batch.await?;
517                    chunk_items.extend(batch.chunk_items.iter().cloned());
518                }
519            }
520        }
521        let content = CssChunkContent {
522            chunk_items: chunk_items
523                .iter()
524                .map(async |ChunkItemWithAsyncModuleInfo { chunk_item, .. }| {
525                    let Some(chunk_item) =
526                        ResolvedVc::try_downcast::<Box<dyn CssChunkItem>>(*chunk_item)
527                    else {
528                        bail!("Chunk item is not an css chunk item but reporting chunk type css");
529                    };
530                    // CSS doesn't need to care about async_info, so we can discard it
531                    Ok(chunk_item)
532                })
533                .try_join()
534                .await?,
535            referenced_output_assets,
536        }
537        .cell();
538        Ok(Vc::upcast(CssChunk::new(*chunking_context, content)))
539    }
540
541    #[turbo_tasks::function]
542    async fn chunk_item_size(
543        &self,
544        _chunking_context: Vc<Box<dyn ChunkingContext>>,
545        chunk_item: Vc<Box<dyn ChunkItem>>,
546        _async_module_info: Option<Vc<AsyncModuleInfo>>,
547    ) -> Result<Vc<usize>> {
548        let Some(chunk_item) =
549            Vc::try_resolve_downcast::<Box<dyn CssChunkItem>>(chunk_item).await?
550        else {
551            bail!("Chunk item is not an css chunk item but reporting chunk type css");
552        };
553        Ok(Vc::cell(chunk_item.content().await.map_or(0, |content| {
554            round_chunk_item_size(content.inner_code.len())
555        })))
556    }
557}
558
559#[turbo_tasks::value_impl]
560impl ValueDefault for CssChunkType {
561    #[turbo_tasks::function]
562    fn value_default() -> Vc<Self> {
563        Self::default().cell()
564    }
565}