1use std::{fmt::Write, sync::Arc};
2
3use anyhow::{Context, Result};
4use lightningcss::css_modules::CssModuleReference;
5use swc_core::common::{BytePos, FileName, LineCol, SourceMap};
6use turbo_rcstr::{RcStr, rcstr};
7use turbo_tasks::{FxIndexMap, IntoTraitRef, ResolvedVc, Vc, turbofmt};
8use turbo_tasks_fs::{FileSystemPath, rope::Rope};
9use turbopack_core::{
10 chunk::{AsyncModuleInfo, ChunkableModule, ChunkingContext, ModuleChunkItemIdExt},
11 context::{AssetContext, ProcessResult},
12 ident::AssetIdent,
13 issue::{
14 Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, OptionIssueSource,
15 OptionStyledString, StyledString,
16 },
17 module::{Module, ModuleSideEffects},
18 module_graph::ModuleGraph,
19 reference::{ModuleReference, ModuleReferences},
20 reference_type::{CssReferenceSubType, ReferenceType},
21 resolve::{origin::ResolveOrigin, parse::Request},
22 source::{OptionSource, Source},
23};
24use turbopack_ecmascript::{
25 chunk::{
26 EcmascriptChunkItemContent, EcmascriptChunkPlaceable, EcmascriptExports,
27 ecmascript_chunk_item,
28 },
29 parse::generate_js_source_map,
30 runtime_functions::{TURBOPACK_EXPORT_VALUE, TURBOPACK_IMPORT},
31 utils::StringifyJs,
32};
33
34use crate::{
35 process::{CssWithPlaceholderResult, ProcessCss},
36 references::{compose::CssModuleComposeReference, internal::InternalCssAssetReference},
37};
38
39#[turbo_tasks::value]
40#[derive(Clone)]
41pub struct ModuleCssAsset {
43 pub source: ResolvedVc<Box<dyn Source>>,
44 pub asset_context: ResolvedVc<Box<dyn AssetContext>>,
45}
46
47#[turbo_tasks::value_impl]
48impl ModuleCssAsset {
49 #[turbo_tasks::function]
50 pub fn new(
51 source: ResolvedVc<Box<dyn Source>>,
52 asset_context: ResolvedVc<Box<dyn AssetContext>>,
53 ) -> Vc<Self> {
54 Self::cell(ModuleCssAsset {
55 source,
56 asset_context,
57 })
58 }
59}
60
61#[turbo_tasks::value_impl]
62impl Module for ModuleCssAsset {
63 #[turbo_tasks::function]
64 async fn ident(&self) -> Result<Vc<AssetIdent>> {
65 Ok(self
66 .source
67 .ident()
68 .with_modifier(rcstr!("css module"))
69 .with_layer(self.asset_context.into_trait_ref().await?.layer()))
70 }
71
72 #[turbo_tasks::function]
73 fn source(&self) -> Vc<OptionSource> {
74 Vc::cell(Some(self.source))
75 }
76
77 #[turbo_tasks::function]
78 async fn references(self: Vc<Self>) -> Result<Vc<ModuleReferences>> {
79 let references = self
88 .module_references()
89 .await?
90 .iter()
91 .copied()
92 .chain(
93 match *self
94 .inner(ReferenceType::Css(CssReferenceSubType::Inner))
95 .try_into_module()
96 .await?
97 {
98 Some(inner) => Some(
99 InternalCssAssetReference::new(*inner)
100 .to_resolved()
101 .await
102 .map(ResolvedVc::upcast)?,
103 ),
104 None => None,
105 },
106 )
107 .collect();
108
109 Ok(Vc::cell(references))
110 }
111
112 #[turbo_tasks::function]
113 fn side_effects(self: Vc<Self>) -> Vc<ModuleSideEffects> {
114 ModuleSideEffects::SideEffectful.cell()
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(
163 #[bincode(with = "turbo_bincode::indexmap")] FxIndexMap<String, Vec<ModuleCssClass>>,
164);
165
166#[turbo_tasks::value_impl]
167impl ModuleCssAsset {
168 #[turbo_tasks::function]
169 pub fn inner(&self, ty: ReferenceType) -> Vc<ProcessResult> {
170 self.asset_context.process(*self.source, ty)
171 }
172
173 #[turbo_tasks::function]
174 async fn classes(self: Vc<Self>) -> Result<Vc<ModuleCssClasses>> {
175 let inner = self
176 .inner(ReferenceType::Css(CssReferenceSubType::Analyze))
177 .module();
178
179 let inner = ResolvedVc::try_sidecast::<Box<dyn ProcessCss>>(inner.to_resolved().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(RcStr::from(specifier.clone()).into()),
206 )
207 .to_resolved()
208 .await?,
209 }
210 }
211 CssModuleReference::Local { name } => ModuleCssClass::Local {
212 name: name.to_string(),
213 },
214 CssModuleReference::Global { name } => ModuleCssClass::Global {
215 name: name.to_string(),
216 },
217 })
218 }
219
220 classes.insert(class_name.to_string(), export);
221 }
222 }
223
224 Ok(Vc::cell(classes))
225 }
226
227 #[turbo_tasks::function]
228 async fn module_references(self: Vc<Self>) -> Result<Vc<ModuleReferences>> {
229 let mut references = vec![];
230
231 for (_, class_names) in &*self.classes().await? {
232 for class_name in class_names {
233 match class_name {
234 ModuleCssClass::Import { from, .. } => {
235 references.push(ResolvedVc::upcast(*from));
236 }
237 ModuleCssClass::Local { .. } | ModuleCssClass::Global { .. } => {}
238 }
239 }
240 }
241
242 Ok(Vc::cell(references))
243 }
244}
245
246#[turbo_tasks::value_impl]
247impl ChunkableModule for ModuleCssAsset {
248 #[turbo_tasks::function]
249 fn as_chunk_item(
250 self: ResolvedVc<Self>,
251 module_graph: ResolvedVc<ModuleGraph>,
252 chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
253 ) -> Vc<Box<dyn turbopack_core::chunk::ChunkItem>> {
254 ecmascript_chunk_item(ResolvedVc::upcast(self), module_graph, chunking_context)
255 }
256}
257
258#[turbo_tasks::value_impl]
259impl EcmascriptChunkPlaceable for ModuleCssAsset {
260 #[turbo_tasks::function]
261 fn get_exports(&self) -> Vc<EcmascriptExports> {
262 EcmascriptExports::Value.cell()
263 }
264
265 #[turbo_tasks::function]
266 async fn chunk_item_content(
267 self: Vc<Self>,
268 chunking_context: Vc<Box<dyn ChunkingContext>>,
269 _module_graph: Vc<ModuleGraph>,
270 _async_module_info: Option<Vc<AsyncModuleInfo>>,
271 _estimated: bool,
272 ) -> Result<Vc<EcmascriptChunkItemContent>> {
273 let classes = self.classes().await?;
274
275 let mut code = format!("{TURBOPACK_EXPORT_VALUE}({{\n");
276 for (export_name, class_names) in &*classes {
277 let mut exported_class_names = Vec::with_capacity(class_names.len());
278
279 for class_name in class_names {
280 match class_name {
281 ModuleCssClass::Import {
282 original: original_name,
283 from,
284 } => {
285 let resolved_module = from.resolve_reference().first_module().await?;
286
287 let Some(resolved_module) = &*resolved_module else {
288 CssModuleComposesIssue {
289 severity: IssueSeverity::Error,
290 source: IssueSource::from_source_only(self.await?.source),
293 message: turbofmt!(
294 "Module {} referenced in `composes: ... from ...;` can't be \
295 resolved.\n",
296 from.await?.request
297 )
298 .await?,
299 }
300 .resolved_cell()
301 .emit();
302 continue;
303 };
304
305 let Some(css_module) =
306 ResolvedVc::try_downcast_type::<ModuleCssAsset>(*resolved_module)
307 else {
308 CssModuleComposesIssue {
309 severity: IssueSeverity::Error,
310 source: IssueSource::from_source_only(self.await?.source),
313 message: turbofmt!(
314 "Module {} referenced in `composes: ... from ...;` is not a \
315 CSS module.\n",
316 from.await?.request
317 )
318 .await?,
319 }
320 .resolved_cell()
321 .emit();
322 continue;
323 };
324
325 let placeable: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>> =
329 ResolvedVc::upcast(css_module);
330
331 let module_id = placeable.chunk_item_id(chunking_context).await?;
332 let module_id = StringifyJs(&module_id);
333 let original_name = StringifyJs(&original_name);
334 exported_class_names
335 .push(format!("{TURBOPACK_IMPORT}({module_id})[{original_name}]"));
336 }
337 ModuleCssClass::Local { name: class_name }
338 | ModuleCssClass::Global { name: class_name } => {
339 exported_class_names.push(StringifyJs(&class_name).to_string());
340 }
341 }
342 }
343
344 writeln!(
345 code,
346 " {}: {},",
347 StringifyJs(export_name),
348 exported_class_names.join(" + \" \" + ")
349 )?;
350 }
351 code += "});\n";
352 let source_map = *chunking_context
353 .reference_module_source_maps(Vc::upcast(self))
354 .await?;
355 Ok(EcmascriptChunkItemContent {
356 inner_code: code.clone().into(),
357 source_map: if source_map {
360 Some(generate_minimal_source_map(
361 turbofmt!("{}", self.ident()).await?.to_string(),
362 code,
363 )?)
364 } else {
365 None
366 },
367 ..Default::default()
368 }
369 .cell())
370 }
371}
372
373#[turbo_tasks::value_impl]
374impl ResolveOrigin for ModuleCssAsset {
375 #[turbo_tasks::function]
376 fn origin_path(&self) -> Vc<FileSystemPath> {
377 self.source.ident().path()
378 }
379
380 #[turbo_tasks::function]
381 fn asset_context(&self) -> Vc<Box<dyn AssetContext>> {
382 *self.asset_context
383 }
384}
385
386fn generate_minimal_source_map(filename: String, source: String) -> Result<Rope> {
387 let mut mappings = vec![];
388 let mut pos = 1;
390 for (index, line) in source.split_inclusive('\n').enumerate() {
391 mappings.push((
392 BytePos(pos),
393 LineCol {
394 line: index as u32,
395 col: 0,
396 },
397 ));
398 pos += line.len() as u32;
399 }
400 let sm: Arc<SourceMap> = Default::default();
401 sm.new_source_file(FileName::Custom(filename).into(), source);
402 let map = generate_js_source_map(&*sm, mappings, None, true, true, Default::default())?;
403 Ok(map)
404}
405
406#[turbo_tasks::value(shared)]
407struct CssModuleComposesIssue {
408 severity: IssueSeverity,
409 source: IssueSource,
410 message: RcStr,
411}
412
413#[turbo_tasks::value_impl]
414impl Issue for CssModuleComposesIssue {
415 fn severity(&self) -> IssueSeverity {
416 self.severity
417 }
418
419 #[turbo_tasks::function]
420 fn title(&self) -> Vc<StyledString> {
421 StyledString::Text(rcstr!(
422 "An issue occurred while resolving a CSS module `composes:` rule"
423 ))
424 .cell()
425 }
426
427 #[turbo_tasks::function]
428 fn stage(&self) -> Vc<IssueStage> {
429 IssueStage::CodeGen.cell()
430 }
431
432 #[turbo_tasks::function]
433 fn file_path(&self) -> Vc<FileSystemPath> {
434 self.source.file_path()
435 }
436
437 #[turbo_tasks::function]
438 fn description(&self) -> Vc<OptionStyledString> {
439 Vc::cell(Some(
440 StyledString::Text(self.message.clone()).resolved_cell(),
441 ))
442 }
443
444 #[turbo_tasks::function]
445 fn source(&self) -> Vc<OptionIssueSource> {
446 Vc::cell(Some(self.source))
447 }
448}