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, 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    module::{Module, ModuleSideEffects},
14    module_graph::ModuleGraph,
15    reference::{ModuleReference, ModuleReferences},
16    reference_type::{CssReferenceSubType, ReferenceType},
17    resolve::{origin::ResolveOrigin, parse::Request},
18    source::{OptionSource, Source},
19};
20use turbopack_ecmascript::{
21    chunk::{
22        EcmascriptChunkItemContent, EcmascriptChunkItemOptions, EcmascriptChunkPlaceable,
23        EcmascriptExports, ecmascript_chunk_item,
24    },
25    parse::generate_js_source_map,
26    runtime_functions::{TURBOPACK_EXPORT_VALUE, TURBOPACK_IMPORT},
27    utils::StringifyJs,
28};
29
30use crate::{
31    process::{CssWithPlaceholderResult, ProcessCss},
32    references::{compose::CssModuleComposeReference, internal::InternalCssAssetReference},
33};
34
35/// A [CSS Module, as in `.module.css`][spec]. For a global CSS module, see [`CssModule`].
36///
37/// [spec]: https://github.com/css-modules/css-modules
38/// [`CssModule`]: crate::CssModule
39#[turbo_tasks::value]
40#[derive(Clone)]
41pub struct EcmascriptCssModule {
42    pub source: ResolvedVc<Box<dyn Source>>,
43    pub asset_context: ResolvedVc<Box<dyn AssetContext>>,
44    /// The path of `source`, precomputed so that `ResolveOrigin::origin_path` is synchronous.
45    origin_path: FileSystemPath,
46}
47
48#[turbo_tasks::value_impl]
49impl EcmascriptCssModule {
50    #[turbo_tasks::function]
51    pub async fn new(
52        source: ResolvedVc<Box<dyn Source>>,
53        asset_context: ResolvedVc<Box<dyn AssetContext>>,
54    ) -> Result<Vc<Self>> {
55        Ok(Self::cell(EcmascriptCssModule {
56            origin_path: source.ident().await?.path.clone(),
57            source,
58            asset_context,
59        }))
60    }
61}
62
63#[turbo_tasks::value_impl]
64impl Module for EcmascriptCssModule {
65    #[turbo_tasks::function]
66    async fn ident(&self) -> Result<Vc<AssetIdent>> {
67        Ok(self
68            .source
69            .ident()
70            .owned()
71            .await?
72            .with_modifier(rcstr!("css module"))
73            .with_layer(self.asset_context.into_trait_ref().await?.layer())
74            .into_vc())
75    }
76
77    #[turbo_tasks::function]
78    fn source(&self) -> Vc<OptionSource> {
79        Vc::cell(Some(self.source))
80    }
81
82    #[turbo_tasks::function]
83    async fn references(self: Vc<Self>) -> Result<Vc<ModuleReferences>> {
84        // The inner reference must come last so it is loaded as the last in the
85        // resulting css. @import or composes references must be loaded first so
86        // that the css style rules in them are overridable from the local css.
87
88        // This affects the order in which the resulting CSS chunks will be loaded:
89        // 1. @import or composes references are loaded first
90        // 2. The local CSS is loaded last
91
92        let references = self
93            .module_references()
94            .await?
95            .iter()
96            .copied()
97            .chain(
98                match *self
99                    .inner(ReferenceType::Css(CssReferenceSubType::Inner))
100                    .try_into_module()
101                    .await?
102                {
103                    Some(inner) => Some(
104                        InternalCssAssetReference::new(*inner)
105                            .to_resolved()
106                            .await
107                            .map(ResolvedVc::upcast)?,
108                    ),
109                    None => None,
110                },
111            )
112            .collect();
113
114        Ok(Vc::cell(references))
115    }
116
117    #[turbo_tasks::function]
118    fn side_effects(self: Vc<Self>) -> Vc<ModuleSideEffects> {
119        // modules can still effect global styles using `:root` selectors and other similar features
120        // We could do better with some static analysis if we want
121        ModuleSideEffects::SideEffectful.cell()
122    }
123}
124
125/// A CSS class that is exported from a CSS module.
126///
127/// See [`ModuleCssClasses`] for more information.
128#[turbo_tasks::value]
129#[derive(Debug, Clone)]
130enum ModuleCssClass {
131    Local {
132        name: String,
133    },
134    Global {
135        name: String,
136    },
137    Import {
138        original: String,
139        from: ResolvedVc<CssModuleComposeReference>,
140    },
141}
142
143/// A map of CSS classes exported from a CSS module.
144///
145/// ## Example
146///
147/// ```css
148/// :global(.class1) {
149///    color: red;
150/// }
151///
152/// .class2 {
153///   color: blue;
154/// }
155///
156/// .class3 {
157///   composes: class4 from "./other.module.css";
158/// }
159/// ```
160///
161/// The above CSS module would have the following exports:
162/// 1. class1: [Global("exported_class1")]
163/// 2. class2: [Local("exported_class2")]
164/// 3. class3: [Local("exported_class3), Import("class4", "./other.module.css")]
165#[turbo_tasks::value(transparent)]
166#[derive(Debug, Clone)]
167struct ModuleCssClasses(
168    #[bincode(with = "turbo_bincode::indexmap")] FxIndexMap<String, Vec<ModuleCssClass>>,
169);
170
171#[turbo_tasks::value_impl]
172impl EcmascriptCssModule {
173    #[turbo_tasks::function]
174    pub fn inner(&self, ty: ReferenceType) -> Vc<ProcessResult> {
175        self.asset_context.process(*self.source, ty)
176    }
177
178    #[turbo_tasks::function]
179    async fn classes(self: Vc<Self>) -> Result<Vc<ModuleCssClasses>> {
180        let inner = self
181            .inner(ReferenceType::Css(CssReferenceSubType::Analyze))
182            .module();
183
184        let inner = ResolvedVc::try_sidecast::<Box<dyn ProcessCss>>(inner.to_resolved().await?)
185            .context("inner asset should be CSS processable")?;
186
187        let result = inner.get_css_with_placeholder().await?;
188        let mut classes = FxIndexMap::default();
189
190        // TODO(alexkirsz) Should we report an error on parse error here?
191        if let CssWithPlaceholderResult::Ok {
192            exports: Some(exports),
193            ..
194        } = &*result
195        {
196            for (class_name, export_class_names) in exports {
197                let mut export = Vec::default();
198
199                export.push(ModuleCssClass::Local {
200                    name: export_class_names.name.clone(),
201                });
202
203                for export_class_name in &export_class_names.composes {
204                    export.push(match export_class_name {
205                        CssModuleReference::Dependency { specifier, name } => {
206                            ModuleCssClass::Import {
207                                original: name.to_string(),
208                                from: CssModuleComposeReference::new(
209                                    Vc::upcast(self),
210                                    Request::parse(RcStr::from(specifier.clone()).into()),
211                                )
212                                .to_resolved()
213                                .await?,
214                            }
215                        }
216                        CssModuleReference::Local { name } => ModuleCssClass::Local {
217                            name: name.to_string(),
218                        },
219                        CssModuleReference::Global { name } => ModuleCssClass::Global {
220                            name: name.to_string(),
221                        },
222                    })
223                }
224
225                classes.insert(class_name.to_string(), export);
226            }
227        }
228
229        Ok(Vc::cell(classes))
230    }
231
232    #[turbo_tasks::function]
233    async fn module_references(self: Vc<Self>) -> Result<Vc<ModuleReferences>> {
234        let mut references = vec![];
235
236        for (_, class_names) in &*self.classes().await? {
237            for class_name in class_names {
238                match class_name {
239                    ModuleCssClass::Import { from, .. } => {
240                        references.push(ResolvedVc::upcast(*from));
241                    }
242                    ModuleCssClass::Local { .. } | ModuleCssClass::Global { .. } => {}
243                }
244            }
245        }
246
247        Ok(Vc::cell(references))
248    }
249}
250
251#[turbo_tasks::value_impl]
252impl ChunkableModule for EcmascriptCssModule {
253    #[turbo_tasks::function]
254    fn as_chunk_item(
255        self: ResolvedVc<Self>,
256        module_graph: ResolvedVc<ModuleGraph>,
257        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
258    ) -> Vc<Box<dyn turbopack_core::chunk::ChunkItem>> {
259        ecmascript_chunk_item(ResolvedVc::upcast(self), module_graph, chunking_context)
260    }
261}
262
263#[turbo_tasks::value_impl]
264impl EcmascriptChunkPlaceable for EcmascriptCssModule {
265    #[turbo_tasks::function]
266    fn get_exports(&self) -> Vc<EcmascriptExports> {
267        EcmascriptExports::Value.cell()
268    }
269
270    #[turbo_tasks::function]
271    async fn chunk_item_content(
272        self: Vc<Self>,
273        chunking_context: Vc<Box<dyn ChunkingContext>>,
274        _module_graph: Vc<ModuleGraph>,
275        _async_module_info: Option<Vc<AsyncModuleInfo>>,
276        _estimated: bool,
277    ) -> Result<Vc<EcmascriptChunkItemContent>> {
278        let classes = self.classes().await?;
279
280        let mut code = format!("{TURBOPACK_EXPORT_VALUE}({{\n");
281        for (export_name, class_names) in &*classes {
282            let mut exported_class_names = Vec::with_capacity(class_names.len());
283
284            for class_name in class_names {
285                match class_name {
286                    ModuleCssClass::Import {
287                        original: original_name,
288                        from,
289                    } => {
290                        let resolved_module =
291                            from.resolve_reference().await?.first_module().await?;
292
293                        let Some(resolved_module) = resolved_module else {
294                            // Issue already emitted by CssModuleComposeReference::resolve_reference
295                            continue;
296                        };
297
298                        let Some(css_module) =
299                            ResolvedVc::try_downcast_type::<EcmascriptCssModule>(resolved_module)
300                        else {
301                            // Issue already emitted by CssModuleComposeReference::resolve_reference
302                            continue;
303                        };
304
305                        // TODO(alexkirsz) We should also warn if `original_name` can't be found in
306                        // the target module.
307
308                        let placeable: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>> =
309                            ResolvedVc::upcast(css_module);
310
311                        let module_id = placeable.chunk_item_id(chunking_context).await?;
312                        let module_id = StringifyJs(&module_id);
313                        let original_name = StringifyJs(&original_name);
314                        exported_class_names
315                            .push(format!("{TURBOPACK_IMPORT}({module_id})[{original_name}]"));
316                    }
317                    ModuleCssClass::Local { name: class_name }
318                    | ModuleCssClass::Global { name: class_name } => {
319                        exported_class_names.push(StringifyJs(&class_name).to_string());
320                    }
321                }
322            }
323
324            writeln!(
325                code,
326                "  {}: {},",
327                StringifyJs(export_name),
328                exported_class_names.join(" + \" \" + ")
329            )?;
330        }
331        code += "});\n";
332        let source_map = *chunking_context
333            .reference_module_source_maps(Vc::upcast(self))
334            .await?;
335        Ok(EcmascriptChunkItemContent {
336            inner_code: code.clone().into(),
337            // We generate a minimal map for runtime code so that the filename is
338            // displayed in dev tools.
339            source_map: if source_map {
340                Some(generate_minimal_source_map(
341                    turbofmt!("{}", self.ident()).await?.to_string(),
342                    code,
343                )?)
344            } else {
345                None
346            },
347            options: EcmascriptChunkItemOptions {
348                supports_arrow_functions: *chunking_context
349                    .environment()
350                    .runtime_versions()
351                    .supports_arrow_functions()
352                    .await?,
353                ..Default::default()
354            },
355            ..Default::default()
356        }
357        .cell())
358    }
359}
360
361#[turbo_tasks::value_impl]
362impl ResolveOrigin for EcmascriptCssModule {
363    fn origin_path(&self) -> FileSystemPath {
364        self.origin_path.clone()
365    }
366
367    fn asset_context(&self) -> ResolvedVc<Box<dyn AssetContext>> {
368        self.asset_context
369    }
370}
371
372fn generate_minimal_source_map(filename: String, source: String) -> Result<Rope> {
373    let mut mappings = vec![];
374    // Start from 1 because 0 is reserved for dummy spans in SWC.
375    let mut pos = 1;
376    for (index, line) in source.split_inclusive('\n').enumerate() {
377        mappings.push((
378            BytePos(pos),
379            LineCol {
380                line: index as u32,
381                col: 0,
382            },
383        ));
384        pos += line.len() as u32;
385    }
386    let sm: Arc<SourceMap> = Default::default();
387    sm.new_source_file(FileName::Custom(filename).into(), source);
388    let map = generate_js_source_map(&*sm, mappings, None, true, true, Default::default())?;
389    Ok(map)
390}