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#[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 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 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 ModuleSideEffects::SideEffectful.cell()
122 }
123}
124
125#[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#[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 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 continue;
296 };
297
298 let Some(css_module) =
299 ResolvedVc::try_downcast_type::<EcmascriptCssModule>(resolved_module)
300 else {
301 continue;
303 };
304
305 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 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 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}