turbopack_css/
module_asset.rs

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)]
44/// A CSS Module asset, as in `.module.css`. For a global CSS module, see [`CssModuleAsset`].
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    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        // The inner reference must come last so it is loaded as the last in the
83        // resulting css. @import or composes references must be loaded first so
84        // that the css style rules in them are overridable from the local css.
85
86        // This affects the order in which the resulting CSS chunks will be loaded:
87        // 1. @import or composes references are loaded first
88        // 2. The local CSS is loaded last
89
90        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        // modules can still effect global styles using `:root` selectors and other similar features
118        // We could do better with some static analysis if we want
119        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/// A CSS class that is exported from a CSS module.
132///
133/// See [`ModuleCssClasses`] for more information.
134#[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/// A map of CSS classes exported from a CSS module.
150///
151/// ## Example
152///
153/// ```css
154/// :global(.class1) {
155///    color: red;
156/// }
157///
158/// .class2 {
159///   color: blue;
160/// }
161///
162/// .class3 {
163///   composes: class4 from "./other.module.css";
164/// }
165/// ```
166///
167/// The above CSS module would have the following exports:
168/// 1. class1: [Global("exported_class1")]
169/// 2. class2: [Local("exported_class2")]
170/// 3. class3: [Local("exported_class3), Import("class4", "./other.module.css")]
171#[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        // TODO(alexkirsz) Should we report an error on parse error here?
198        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                                // TODO(PACK-4879): this should include detailed location information
355                                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                                // TODO(PACK-4879): this should include detailed location information
372                                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                        // TODO(alexkirsz) We should also warn if `original_name` can't be found in
384                        // the target module.
385
386                        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            // We generate a minimal map for runtime code so that the filename is
417            // displayed in dev tools.
418            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    // Start from 1 because 0 is reserved for dummy spans in SWC.
435    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}