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