turbopack_css/
module_asset.rs

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        // The inner reference must come last so it is loaded as the last in the
77        // resulting css. @import or composes references must be loaded first so
78        // that the css style rules in them are overridable from the local css.
79
80        // This affects the order in which the resulting CSS chunks will be loaded:
81        // 1. @import or composes references are loaded first
82        // 2. The local CSS is loaded last
83
84        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/// A CSS class that is exported from a CSS module.
121///
122/// See [`ModuleCssClasses`] for more information.
123#[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/// A map of CSS classes exported from a CSS module.
139///
140/// ## Example
141///
142/// ```css
143/// :global(.class1) {
144///    color: red;
145/// }
146///
147/// .class2 {
148///   color: blue;
149/// }
150///
151/// .class3 {
152///   composes: class4 from "./other.module.css";
153/// }
154/// ```
155///
156/// The above CSS module would have the following exports:
157/// 1. class1: [Global("exported_class1")]
158/// 2. class2: [Local("exported_class2")]
159/// 3. class3: [Local("exported_class3), Import("class4", "./other.module.css")]
160#[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        // TODO(alexkirsz) Should we report an error on parse error here?
186        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                        // TODO(alexkirsz) We should also warn if `original_name` can't be found in
369                        // the target module.
370
371                        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            // We generate a minimal map for runtime code so that the filename is
404            // displayed in dev tools.
405            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    // Start from 1 because 0 is reserved for dummy spans in SWC.
422    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}