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