Skip to main content

turbopack_css/
module_asset.rs

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)]
41/// A CSS Module asset, as in `.module.css`. For a global CSS module, see [`CssModuleAsset`].
42pub 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        // The inner reference must come last so it is loaded as the last in the
80        // resulting css. @import or composes references must be loaded first so
81        // that the css style rules in them are overridable from the local css.
82
83        // This affects the order in which the resulting CSS chunks will be loaded:
84        // 1. @import or composes references are loaded first
85        // 2. The local CSS is loaded last
86
87        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        // modules can still effect global styles using `:root` selectors and other similar features
115        // We could do better with some static analysis if we want
116        ModuleSideEffects::SideEffectful.cell()
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(
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        // 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(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                                // TODO(PACK-4879): this should include detailed location
291                                // information
292                                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                                // TODO(PACK-4879): this should include detailed location
311                                // information
312                                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                        // TODO(alexkirsz) We should also warn if `original_name` can't be found in
326                        // the target module.
327
328                        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            // We generate a minimal map for runtime code so that the filename is
358            // displayed in dev tools.
359            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    // Start from 1 because 0 is reserved for dummy spans in SWC.
389    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}