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