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