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