1use std::{fmt::Write, sync::Arc};
2
3use anyhow::{Context, Result};
4use indoc::formatdoc;
5use lightningcss::css_modules::CssModuleReference;
6use swc_core::common::{BytePos, FileName, LineCol, SourceMap};
7use turbo_rcstr::{RcStr, rcstr};
8use turbo_tasks::{FxIndexMap, IntoTraitRef, ResolvedVc, ValueToString, Vc};
9use turbo_tasks_fs::{FileSystemPath, rope::Rope};
10use turbopack_core::{
11 asset::{Asset, AssetContent},
12 chunk::{ChunkItem, ChunkType, ChunkableModule, ChunkingContext, ModuleChunkItemIdExt},
13 context::{AssetContext, ProcessResult},
14 ident::AssetIdent,
15 issue::{
16 Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, OptionIssueSource,
17 OptionStyledString, StyledString,
18 },
19 module::{Module, ModuleSideEffects},
20 module_graph::ModuleGraph,
21 output::OutputAssetsReference,
22 reference::{ModuleReference, ModuleReferences},
23 reference_type::{CssReferenceSubType, ReferenceType},
24 resolve::{origin::ResolveOrigin, parse::Request},
25 source::{OptionSource, Source},
26};
27use turbopack_ecmascript::{
28 chunk::{
29 EcmascriptChunkItem, EcmascriptChunkItemContent, EcmascriptChunkPlaceable,
30 EcmascriptChunkType, EcmascriptExports,
31 },
32 parse::generate_js_source_map,
33 runtime_functions::{TURBOPACK_EXPORT_VALUE, TURBOPACK_IMPORT},
34 utils::StringifyJs,
35};
36
37use crate::{
38 process::{CssWithPlaceholderResult, ProcessCss},
39 references::{compose::CssModuleComposeReference, internal::InternalCssAssetReference},
40};
41
42#[turbo_tasks::value]
43#[derive(Clone)]
44pub struct ModuleCssAsset {
46 pub source: ResolvedVc<Box<dyn Source>>,
47 pub asset_context: ResolvedVc<Box<dyn AssetContext>>,
48}
49
50#[turbo_tasks::value_impl]
51impl ModuleCssAsset {
52 #[turbo_tasks::function]
53 pub fn new(
54 source: ResolvedVc<Box<dyn Source>>,
55 asset_context: ResolvedVc<Box<dyn AssetContext>>,
56 ) -> Vc<Self> {
57 Self::cell(ModuleCssAsset {
58 source,
59 asset_context,
60 })
61 }
62}
63
64#[turbo_tasks::value_impl]
65impl Module for ModuleCssAsset {
66 #[turbo_tasks::function]
67 async fn ident(&self) -> Result<Vc<AssetIdent>> {
68 Ok(self
69 .source
70 .ident()
71 .with_modifier(rcstr!("css module"))
72 .with_layer(self.asset_context.into_trait_ref().await?.layer()))
73 }
74
75 #[turbo_tasks::function]
76 fn source(&self) -> Vc<OptionSource> {
77 Vc::cell(Some(self.source))
78 }
79
80 #[turbo_tasks::function]
81 async fn references(self: Vc<Self>) -> Result<Vc<ModuleReferences>> {
82 let references = self
91 .module_references()
92 .await?
93 .iter()
94 .copied()
95 .chain(
96 match *self
97 .inner(ReferenceType::Css(CssReferenceSubType::Inner))
98 .try_into_module()
99 .await?
100 {
101 Some(inner) => Some(
102 InternalCssAssetReference::new(*inner)
103 .to_resolved()
104 .await
105 .map(ResolvedVc::upcast)?,
106 ),
107 None => None,
108 },
109 )
110 .collect();
111
112 Ok(Vc::cell(references))
113 }
114
115 #[turbo_tasks::function]
116 fn side_effects(self: Vc<Self>) -> Vc<ModuleSideEffects> {
117 ModuleSideEffects::SideEffectful.cell()
120 }
121}
122
123#[turbo_tasks::value_impl]
124impl Asset for ModuleCssAsset {
125 #[turbo_tasks::function]
126 fn content(&self) -> Vc<AssetContent> {
127 self.source.content()
128 }
129}
130
131#[turbo_tasks::value]
135#[derive(Debug, Clone)]
136enum ModuleCssClass {
137 Local {
138 name: String,
139 },
140 Global {
141 name: String,
142 },
143 Import {
144 original: String,
145 from: ResolvedVc<CssModuleComposeReference>,
146 },
147}
148
149#[turbo_tasks::value(transparent)]
172#[derive(Debug, Clone)]
173struct ModuleCssClasses(
174 #[bincode(with = "turbo_bincode::indexmap")] FxIndexMap<String, Vec<ModuleCssClass>>,
175);
176
177#[turbo_tasks::value_impl]
178impl ModuleCssAsset {
179 #[turbo_tasks::function]
180 pub fn inner(&self, ty: ReferenceType) -> Vc<ProcessResult> {
181 self.asset_context.process(*self.source, ty)
182 }
183
184 #[turbo_tasks::function]
185 async fn classes(self: Vc<Self>) -> Result<Vc<ModuleCssClasses>> {
186 let inner = self
187 .inner(ReferenceType::Css(CssReferenceSubType::Analyze))
188 .module();
189
190 let inner = Vc::try_resolve_sidecast::<Box<dyn ProcessCss>>(inner)
191 .await?
192 .context("inner asset should be CSS processable")?;
193
194 let result = inner.get_css_with_placeholder().await?;
195 let mut classes = FxIndexMap::default();
196
197 if let CssWithPlaceholderResult::Ok {
199 exports: Some(exports),
200 ..
201 } = &*result
202 {
203 for (class_name, export_class_names) in exports {
204 let mut export = Vec::default();
205
206 export.push(ModuleCssClass::Local {
207 name: export_class_names.name.clone(),
208 });
209
210 for export_class_name in &export_class_names.composes {
211 export.push(match export_class_name {
212 CssModuleReference::Dependency { specifier, name } => {
213 ModuleCssClass::Import {
214 original: name.to_string(),
215 from: CssModuleComposeReference::new(
216 Vc::upcast(self),
217 Request::parse(RcStr::from(specifier.clone()).into()),
218 )
219 .to_resolved()
220 .await?,
221 }
222 }
223 CssModuleReference::Local { name } => ModuleCssClass::Local {
224 name: name.to_string(),
225 },
226 CssModuleReference::Global { name } => ModuleCssClass::Global {
227 name: name.to_string(),
228 },
229 })
230 }
231
232 classes.insert(class_name.to_string(), export);
233 }
234 }
235
236 Ok(Vc::cell(classes))
237 }
238
239 #[turbo_tasks::function]
240 async fn module_references(self: Vc<Self>) -> Result<Vc<ModuleReferences>> {
241 let mut references = vec![];
242
243 for (_, class_names) in &*self.classes().await? {
244 for class_name in class_names {
245 match class_name {
246 ModuleCssClass::Import { from, .. } => {
247 references.push(ResolvedVc::upcast(*from));
248 }
249 ModuleCssClass::Local { .. } | ModuleCssClass::Global { .. } => {}
250 }
251 }
252 }
253
254 Ok(Vc::cell(references))
255 }
256}
257
258#[turbo_tasks::value_impl]
259impl ChunkableModule for ModuleCssAsset {
260 #[turbo_tasks::function]
261 fn as_chunk_item(
262 self: ResolvedVc<Self>,
263 module_graph: ResolvedVc<ModuleGraph>,
264 chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
265 ) -> Vc<Box<dyn turbopack_core::chunk::ChunkItem>> {
266 Vc::upcast(
267 ModuleChunkItem {
268 chunking_context,
269 module_graph,
270 module: self,
271 }
272 .cell(),
273 )
274 }
275}
276
277#[turbo_tasks::value_impl]
278impl EcmascriptChunkPlaceable for ModuleCssAsset {
279 #[turbo_tasks::function]
280 fn get_exports(&self) -> Vc<EcmascriptExports> {
281 EcmascriptExports::Value.cell()
282 }
283}
284
285#[turbo_tasks::value_impl]
286impl ResolveOrigin for ModuleCssAsset {
287 #[turbo_tasks::function]
288 fn origin_path(&self) -> Vc<FileSystemPath> {
289 self.source.ident().path()
290 }
291
292 #[turbo_tasks::function]
293 fn asset_context(&self) -> Vc<Box<dyn AssetContext>> {
294 *self.asset_context
295 }
296}
297
298#[turbo_tasks::value]
299struct ModuleChunkItem {
300 module: ResolvedVc<ModuleCssAsset>,
301 module_graph: ResolvedVc<ModuleGraph>,
302 chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
303}
304
305#[turbo_tasks::value_impl]
306impl OutputAssetsReference for ModuleChunkItem {}
307
308#[turbo_tasks::value_impl]
309impl ChunkItem for ModuleChunkItem {
310 #[turbo_tasks::function]
311 fn asset_ident(&self) -> Vc<AssetIdent> {
312 self.module.ident()
313 }
314
315 #[turbo_tasks::function]
316 fn chunking_context(&self) -> Vc<Box<dyn ChunkingContext>> {
317 *self.chunking_context
318 }
319
320 #[turbo_tasks::function]
321 async fn ty(&self) -> Result<Vc<Box<dyn ChunkType>>> {
322 Ok(Vc::upcast(
323 Vc::<EcmascriptChunkType>::default().resolve().await?,
324 ))
325 }
326
327 #[turbo_tasks::function]
328 fn module(&self) -> Vc<Box<dyn Module>> {
329 Vc::upcast(*self.module)
330 }
331}
332
333#[turbo_tasks::value_impl]
334impl EcmascriptChunkItem for ModuleChunkItem {
335 #[turbo_tasks::function]
336 async fn content(&self) -> Result<Vc<EcmascriptChunkItemContent>> {
337 let classes = self.module.classes().await?;
338
339 let mut code = format!("{TURBOPACK_EXPORT_VALUE}({{\n");
340 for (export_name, class_names) in &*classes {
341 let mut exported_class_names = Vec::with_capacity(class_names.len());
342
343 for class_name in class_names {
344 match class_name {
345 ModuleCssClass::Import {
346 original: original_name,
347 from,
348 } => {
349 let resolved_module = from.resolve_reference().first_module().await?;
350
351 let Some(resolved_module) = &*resolved_module else {
352 CssModuleComposesIssue {
353 severity: IssueSeverity::Error,
354 source: IssueSource::from_source_only(self.module.await?.source),
356 message: formatdoc! {
357 r#"
358 Module {from} referenced in `composes: ... from {from};` can't be resolved.
359 "#,
360 from = &*from.await?.request.to_string().await?
361 }.into(),
362 }.resolved_cell().emit();
363 continue;
364 };
365
366 let Some(css_module) =
367 ResolvedVc::try_downcast_type::<ModuleCssAsset>(*resolved_module)
368 else {
369 CssModuleComposesIssue {
370 severity: IssueSeverity::Error,
371 source: IssueSource::from_source_only(self.module.await?.source),
373 message: formatdoc! {
374 r#"
375 Module {from} referenced in `composes: ... from {from};` is not a CSS module.
376 "#,
377 from = &*from.await?.request.to_string().await?
378 }.into(),
379 }.resolved_cell().emit();
380 continue;
381 };
382
383 let placeable: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>> =
387 ResolvedVc::upcast(css_module);
388
389 let module_id = placeable.chunk_item_id(*self.chunking_context).await?;
390 let module_id = StringifyJs(&*module_id);
391 let original_name = StringifyJs(&original_name);
392 exported_class_names
393 .push(format!("{TURBOPACK_IMPORT}({module_id})[{original_name}]"));
394 }
395 ModuleCssClass::Local { name: class_name }
396 | ModuleCssClass::Global { name: class_name } => {
397 exported_class_names.push(StringifyJs(&class_name).to_string());
398 }
399 }
400 }
401
402 writeln!(
403 code,
404 " {}: {},",
405 StringifyJs(export_name),
406 exported_class_names.join(" + \" \" + ")
407 )?;
408 }
409 code += "});\n";
410 let source_map = *self
411 .chunking_context
412 .reference_module_source_maps(*ResolvedVc::upcast(self.module))
413 .await?;
414 Ok(EcmascriptChunkItemContent {
415 inner_code: code.clone().into(),
416 source_map: if source_map {
419 Some(generate_minimal_source_map(
420 self.module.ident().to_string().await?.to_string(),
421 code,
422 )?)
423 } else {
424 None
425 },
426 ..Default::default()
427 }
428 .cell())
429 }
430}
431
432fn generate_minimal_source_map(filename: String, source: String) -> Result<Rope> {
433 let mut mappings = vec![];
434 let mut pos = 1;
436 for (index, line) in source.split_inclusive('\n').enumerate() {
437 mappings.push((
438 BytePos(pos),
439 LineCol {
440 line: index as u32,
441 col: 0,
442 },
443 ));
444 pos += line.len() as u32;
445 }
446 let sm: Arc<SourceMap> = Default::default();
447 sm.new_source_file(FileName::Custom(filename).into(), source);
448 let map = generate_js_source_map(&*sm, mappings, None, true, true)?;
449 Ok(map)
450}
451
452#[turbo_tasks::value(shared)]
453struct CssModuleComposesIssue {
454 severity: IssueSeverity,
455 source: IssueSource,
456 message: RcStr,
457}
458
459#[turbo_tasks::value_impl]
460impl Issue for CssModuleComposesIssue {
461 fn severity(&self) -> IssueSeverity {
462 self.severity
463 }
464
465 #[turbo_tasks::function]
466 fn title(&self) -> Vc<StyledString> {
467 StyledString::Text(rcstr!(
468 "An issue occurred while resolving a CSS module `composes:` rule"
469 ))
470 .cell()
471 }
472
473 #[turbo_tasks::function]
474 fn stage(&self) -> Vc<IssueStage> {
475 IssueStage::CodeGen.cell()
476 }
477
478 #[turbo_tasks::function]
479 fn file_path(&self) -> Vc<FileSystemPath> {
480 self.source.file_path()
481 }
482
483 #[turbo_tasks::function]
484 fn description(&self) -> Vc<OptionStyledString> {
485 Vc::cell(Some(
486 StyledString::Text(self.message.clone()).resolved_cell(),
487 ))
488 }
489
490 #[turbo_tasks::function]
491 fn source(&self) -> Vc<OptionIssueSource> {
492 Vc::cell(Some(self.source))
493 }
494}