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