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