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