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