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