Skip to main content

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