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