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, 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 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#[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 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 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}