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, 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)]
43/// A CSS Module asset, as in `.module.css`. For a global CSS module, see [`CssModuleAsset`].
44pub 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        // 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(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/// A CSS class that is exported from a CSS module.
119///
120/// See [`ModuleCssClasses`] for more information.
121#[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/// A map of CSS classes exported from a CSS module.
137///
138/// ## Example
139///
140/// ```css
141/// :global(.class1) {
142///    color: red;
143/// }
144///
145/// .class2 {
146///   color: blue;
147/// }
148///
149/// .class3 {
150///   composes: class4 from "./other.module.css";
151/// }
152/// ```
153///
154/// The above CSS module would have the following exports:
155/// 1. class1: [Global("exported_class1")]
156/// 2. class2: [Local("exported_class2")]
157/// 3. class3: [Local("exported_class3), Import("class4", "./other.module.css")]
158#[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        // TODO(alexkirsz) Should we report an error on parse error here?
183        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                                // TODO(PACK-4879): this should include detailed location information
337                                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                                // TODO(PACK-4879): this should include detailed location information
354                                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                        // TODO(alexkirsz) We should also warn if `original_name` can't be found in
366                        // the target module.
367
368                        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            // We generate a minimal map for runtime code so that the filename is
401            // displayed in dev tools.
402            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    // Start from 1 because 0 is reserved for dummy spans in SWC.
419    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}