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