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