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