1use anyhow::{Context, Result, bail};
2use bincode::{Decode, Encode};
3use indoc::formatdoc;
4use serde::Deserialize;
5use turbo_rcstr::{RcStr, rcstr};
6use turbo_tasks::{
7 Completion, Completions, NonLocalValue, ResolvedVc, TaskInput, TryFlatJoinIterExt, Vc,
8 fxindexmap, trace::TraceRawVcs,
9};
10use turbo_tasks_fs::{
11 File, FileContent, FileSystemEntryType, FileSystemPath, json::parse_json_with_source_context,
12};
13use turbopack_core::{
14 asset::{Asset, AssetContent},
15 changed::any_source_content_changed_of_module,
16 context::{AssetContext, ProcessResult},
17 file_source::FileSource,
18 ident::AssetIdent,
19 module_graph::{ModuleGraph, SingleModuleGraph},
20 reference_type::{EntryReferenceSubType, InnerAssets, ReferenceType},
21 resolve::{FindContextFileResult, find_context_file_or_package_key, options::ImportMapping},
22 source::Source,
23 source_map::GenerateSourceMap,
24 source_transform::SourceTransform,
25 virtual_source::VirtualSource,
26};
27use turbopack_ecmascript::runtime_functions::TURBOPACK_EXTERNAL_IMPORT;
28
29use crate::{
30 embed_js::embed_file_path,
31 evaluate::get_evaluate_entries,
32 execution_context::ExecutionContext,
33 transforms::{
34 util::{EmittedAsset, emitted_assets_to_virtual_sources},
35 webpack::{WebpackLoaderContext, evaluate_webpack_loader},
36 },
37};
38
39#[derive(Debug, Clone, Deserialize)]
40#[turbo_tasks::value]
41#[serde(rename_all = "camelCase")]
42struct PostCssProcessingResult {
43 css: String,
44 map: Option<String>,
45 assets: Option<Vec<EmittedAsset>>,
46}
47
48#[derive(
49 Default,
50 Copy,
51 Clone,
52 PartialEq,
53 Eq,
54 Hash,
55 Debug,
56 TraceRawVcs,
57 TaskInput,
58 NonLocalValue,
59 Encode,
60 Decode,
61)]
62pub enum PostCssConfigLocation {
63 #[default]
64 ProjectPath,
65 ProjectPathOrLocalPath,
66}
67
68#[turbo_tasks::value(shared)]
69#[derive(Clone, Default)]
70pub struct PostCssTransformOptions {
71 pub postcss_package: Option<ResolvedVc<ImportMapping>>,
72 pub config_location: PostCssConfigLocation,
73 pub placeholder_for_future_extensions: u8,
74}
75
76#[turbo_tasks::function]
77fn postcss_configs() -> Vc<Vec<RcStr>> {
78 Vc::cell(
79 [
80 ".postcssrc",
81 ".postcssrc.json",
82 ".postcssrc.yaml",
83 ".postcssrc.yml",
84 ".postcssrc.js",
85 ".postcssrc.mjs",
86 ".postcssrc.cjs",
87 ".postcssrc.ts",
88 ".postcssrc.mts",
89 ".postcssrc.cts",
90 ".config/postcssrc",
91 ".config/postcssrc.json",
92 ".config/postcssrc.yaml",
93 ".config/postcssrc.yml",
94 ".config/postcssrc.js",
95 ".config/postcssrc.mjs",
96 ".config/postcssrc.cjs",
97 ".config/postcssrc.ts",
98 ".config/postcssrc.mts",
99 ".config/postcssrc.cts",
100 "postcss.config.js",
101 "postcss.config.mjs",
102 "postcss.config.cjs",
103 "postcss.config.ts",
104 "postcss.config.mts",
105 "postcss.config.cts",
106 "postcss.config.json",
107 ]
108 .into_iter()
109 .map(RcStr::from)
110 .collect(),
111 )
112}
113
114#[turbo_tasks::value]
115pub struct PostCssTransform {
116 evaluate_context: ResolvedVc<Box<dyn AssetContext>>,
117 config_tracing_context: ResolvedVc<Box<dyn AssetContext>>,
118 execution_context: ResolvedVc<ExecutionContext>,
119 config_location: PostCssConfigLocation,
120 source_maps: bool,
121}
122
123#[turbo_tasks::value_impl]
124impl PostCssTransform {
125 #[turbo_tasks::function]
126 pub fn new(
127 evaluate_context: ResolvedVc<Box<dyn AssetContext>>,
128 config_tracing_context: ResolvedVc<Box<dyn AssetContext>>,
129 execution_context: ResolvedVc<ExecutionContext>,
130 config_location: PostCssConfigLocation,
131 source_maps: bool,
132 ) -> Vc<Self> {
133 PostCssTransform {
134 evaluate_context,
135 config_tracing_context,
136 execution_context,
137 config_location,
138 source_maps,
139 }
140 .cell()
141 }
142}
143
144#[turbo_tasks::value_impl]
145impl SourceTransform for PostCssTransform {
146 #[turbo_tasks::function]
147 fn transform(&self, source: ResolvedVc<Box<dyn Source>>) -> Vc<Box<dyn Source>> {
148 Vc::upcast(
149 PostCssTransformedAsset {
150 evaluate_context: self.evaluate_context,
151 config_tracing_context: self.config_tracing_context,
152 execution_context: self.execution_context,
153 config_location: self.config_location,
154 source,
155 source_map: self.source_maps,
156 }
157 .cell(),
158 )
159 }
160}
161
162#[turbo_tasks::value]
163struct PostCssTransformedAsset {
164 evaluate_context: ResolvedVc<Box<dyn AssetContext>>,
165 config_tracing_context: ResolvedVc<Box<dyn AssetContext>>,
166 execution_context: ResolvedVc<ExecutionContext>,
167 config_location: PostCssConfigLocation,
168 source: ResolvedVc<Box<dyn Source>>,
169 source_map: bool,
170}
171
172#[turbo_tasks::value_impl]
173impl Source for PostCssTransformedAsset {
174 #[turbo_tasks::function]
175 fn ident(&self) -> Vc<AssetIdent> {
176 self.source.ident()
177 }
178}
179
180#[turbo_tasks::value_impl]
181impl Asset for PostCssTransformedAsset {
182 #[turbo_tasks::function]
183 async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
184 Ok(*self.process().await?.content)
185 }
186}
187
188#[turbo_tasks::value]
189struct ProcessPostCssResult {
190 content: ResolvedVc<AssetContent>,
191 assets: Vec<ResolvedVc<VirtualSource>>,
192}
193
194#[turbo_tasks::function]
195async fn config_changed(
196 asset_context: Vc<Box<dyn AssetContext>>,
197 postcss_config_path: FileSystemPath,
198) -> Result<Vc<Completion>> {
199 let config_asset = asset_context
200 .process(
201 Vc::upcast(FileSource::new(postcss_config_path.clone())),
202 ReferenceType::Internal(InnerAssets::empty().to_resolved().await?),
203 )
204 .module();
205
206 Ok(Vc::<Completions>::cell(vec![
207 any_source_content_changed_of_module(config_asset)
208 .to_resolved()
209 .await?,
210 extra_configs_changed(asset_context, postcss_config_path)
211 .to_resolved()
212 .await?,
213 ])
214 .completed())
215}
216
217#[turbo_tasks::function]
218async fn extra_configs_changed(
219 asset_context: Vc<Box<dyn AssetContext>>,
220 postcss_config_path: FileSystemPath,
221) -> Result<Vc<Completion>> {
222 let parent_path = postcss_config_path.parent();
223
224 let config_paths = [
225 parent_path.join("tailwind.config.js")?,
226 parent_path.join("tailwind.config.mjs")?,
227 parent_path.join("tailwind.config.ts")?,
228 ];
229
230 let configs = config_paths
231 .into_iter()
232 .map(|path| async move {
233 Ok(
234 if matches!(&*path.get_type().await?, FileSystemEntryType::File) {
235 match *asset_context
236 .process(
237 Vc::upcast(FileSource::new(path)),
238 ReferenceType::Internal(InnerAssets::empty().to_resolved().await?),
239 )
240 .try_into_module()
241 .await?
242 {
243 Some(module) => Some(
244 any_source_content_changed_of_module(*module)
245 .to_resolved()
246 .await?,
247 ),
248 None => None,
249 }
250 } else {
251 None
252 },
253 )
254 })
255 .try_flat_join()
256 .await?;
257
258 Ok(Vc::<Completions>::cell(configs).completed())
259}
260
261#[turbo_tasks::value]
262pub struct JsonSource {
263 pub path: FileSystemPath,
264 pub key: ResolvedVc<Option<RcStr>>,
265 pub allow_json5: bool,
266}
267
268#[turbo_tasks::value_impl]
269impl JsonSource {
270 #[turbo_tasks::function]
271 pub fn new(
272 path: FileSystemPath,
273 key: ResolvedVc<Option<RcStr>>,
274 allow_json5: bool,
275 ) -> Vc<Self> {
276 JsonSource {
277 path,
278 key,
279 allow_json5,
280 }
281 .cell()
282 }
283}
284
285#[turbo_tasks::value_impl]
286impl Source for JsonSource {
287 #[turbo_tasks::function]
288 async fn ident(&self) -> Result<Vc<AssetIdent>> {
289 match &*self.key.await? {
290 Some(key) => Ok(AssetIdent::from_path(
291 self.path.append(".")?.append(key)?.append(".json")?,
292 )),
293 None => Ok(AssetIdent::from_path(self.path.append(".json")?)),
294 }
295 }
296}
297
298#[turbo_tasks::value_impl]
299impl Asset for JsonSource {
300 #[turbo_tasks::function]
301 async fn content(&self) -> Result<Vc<AssetContent>> {
302 let file_type = &*self.path.get_type().await?;
303 match file_type {
304 FileSystemEntryType::File => {
305 let json = if self.allow_json5 {
306 self.path.read_json5().content().await?
307 } else {
308 self.path.read_json().content().await?
309 };
310 let value = match &*self.key.await? {
311 Some(key) => {
312 let Some(value) = json.get(&**key) else {
313 anyhow::bail!("Invalid file type {:?}", file_type)
314 };
315 value
316 }
317 None => &*json,
318 };
319 Ok(AssetContent::file(
320 FileContent::Content(File::from(value.to_string())).cell(),
321 ))
322 }
323 FileSystemEntryType::NotFound => {
324 Ok(AssetContent::File(FileContent::NotFound.resolved_cell()).cell())
325 }
326 _ => Err(anyhow::anyhow!("Invalid file type {:?}", file_type)),
327 }
328 }
329}
330
331#[turbo_tasks::function]
332pub(crate) async fn config_loader_source(
333 project_path: FileSystemPath,
334 postcss_config_path: FileSystemPath,
335) -> Result<Vc<Box<dyn Source>>> {
336 let postcss_config_path_value = postcss_config_path.clone();
337 let postcss_config_path_filename = postcss_config_path_value.file_name();
338
339 if postcss_config_path_filename == "package.json" {
340 return Ok(Vc::upcast(JsonSource::new(
341 postcss_config_path,
342 Vc::cell(Some(rcstr!("postcss"))),
343 false,
344 )));
345 }
346
347 if postcss_config_path_value.path.ends_with(".json")
348 || postcss_config_path_filename == ".postcssrc"
349 {
350 return Ok(Vc::upcast(JsonSource::new(
351 postcss_config_path,
352 Vc::cell(None),
353 true,
354 )));
355 }
356
357 if !postcss_config_path_value.path.ends_with(".js") {
359 return Ok(Vc::upcast(FileSource::new(postcss_config_path)));
360 }
361
362 let Some(config_path) = project_path.get_relative_path_to(&postcss_config_path_value) else {
363 bail!("Unable to get relative path to postcss config");
364 };
365
366 let code = formatdoc! {
369 r#"
370 import {{ pathToFileURL }} from 'node:url';
371 import path from 'node:path';
372
373 const configPath = path.join(process.cwd(), {config_path});
374 // Absolute paths don't work with ESM imports on Windows:
375 // https://github.com/nodejs/node/issues/31710
376 // convert it to a file:// URL, which works on all platforms
377 const configUrl = pathToFileURL(configPath).toString();
378 const mod = await {TURBOPACK_EXTERNAL_IMPORT}(configUrl);
379
380 export default mod.default ?? mod;
381 "#,
382 config_path = serde_json::to_string(&config_path).expect("a string should be serializable"),
383 };
384
385 Ok(Vc::upcast(VirtualSource::new(
386 postcss_config_path.append("_.loader.mjs")?,
387 AssetContent::file(FileContent::Content(File::from(code)).cell()),
388 )))
389}
390
391#[turbo_tasks::function]
392async fn postcss_executor(
393 asset_context: Vc<Box<dyn AssetContext>>,
394 project_path: FileSystemPath,
395 postcss_config_path: FileSystemPath,
396) -> Result<Vc<ProcessResult>> {
397 let config_asset = asset_context
398 .process(
399 config_loader_source(project_path, postcss_config_path),
400 ReferenceType::Entry(EntryReferenceSubType::Undefined),
401 )
402 .module()
403 .to_resolved()
404 .await?;
405
406 Ok(asset_context.process(
407 Vc::upcast(FileSource::new(
408 embed_file_path(rcstr!("transforms/postcss.ts"))
409 .owned()
410 .await?,
411 )),
412 ReferenceType::Internal(ResolvedVc::cell(fxindexmap! {
413 rcstr!("CONFIG") => config_asset
414 })),
415 ))
416}
417
418async fn find_config_in_location(
419 project_path: FileSystemPath,
420 location: PostCssConfigLocation,
421 source: Vc<Box<dyn Source>>,
422) -> Result<Option<FileSystemPath>> {
423 if let FindContextFileResult::Found(config_path, _) =
424 &*find_context_file_or_package_key(project_path, postcss_configs(), rcstr!("postcss"))
425 .await?
426 {
427 return Ok(Some(config_path.clone()));
428 }
429
430 if matches!(location, PostCssConfigLocation::ProjectPathOrLocalPath)
431 && let FindContextFileResult::Found(config_path, _) = &*find_context_file_or_package_key(
432 source.ident().path().await?.parent(),
433 postcss_configs(),
434 rcstr!("postcss"),
435 )
436 .await?
437 {
438 return Ok(Some(config_path.clone()));
439 }
440
441 Ok(None)
442}
443
444#[turbo_tasks::value_impl]
445impl GenerateSourceMap for PostCssTransformedAsset {
446 #[turbo_tasks::function]
447 async fn generate_source_map(&self) -> Result<Vc<FileContent>> {
448 let source = Vc::try_resolve_sidecast::<Box<dyn GenerateSourceMap>>(*self.source).await?;
449 match source {
450 Some(source) => Ok(source.generate_source_map()),
451 None => Ok(FileContent::NotFound.cell()),
452 }
453 }
454}
455
456#[turbo_tasks::value_impl]
457impl PostCssTransformedAsset {
458 #[turbo_tasks::function]
459 async fn process(&self) -> Result<Vc<ProcessPostCssResult>> {
460 let ExecutionContext {
461 project_path,
462 chunking_context,
463 env,
464 } = &*self.execution_context.await?;
465
466 let Some(config_path) =
478 find_config_in_location(project_path.clone(), self.config_location, *self.source)
479 .await?
480 else {
481 return Ok(ProcessPostCssResult {
482 content: self.source.content().to_resolved().await?,
483 assets: Vec::new(),
484 }
485 .cell());
486 };
487
488 let source_content = self.source.content();
489 let AssetContent::File(file) = *source_content.await? else {
490 bail!("PostCSS transform only support transforming files");
491 };
492 let FileContent::Content(content) = &*file.await? else {
493 return Ok(ProcessPostCssResult {
494 content: AssetContent::File(FileContent::NotFound.resolved_cell()).resolved_cell(),
495 assets: Vec::new(),
496 }
497 .cell());
498 };
499 let content = content.content().to_str()?;
500 let evaluate_context = self.evaluate_context;
501 let source_map = self.source_map;
502
503 let config_changed = config_changed(*self.config_tracing_context, config_path.clone())
505 .to_resolved()
506 .await?;
507
508 let postcss_executor =
509 postcss_executor(*evaluate_context, project_path.clone(), config_path).module();
510
511 let entries = get_evaluate_entries(postcss_executor, *evaluate_context, None)
512 .to_resolved()
513 .await?;
514
515 let module_graph = ModuleGraph::from_single_graph(SingleModuleGraph::new_with_entries(
516 entries.graph_entries().to_resolved().await?,
517 false,
518 false,
519 ))
520 .connect()
521 .to_resolved()
522 .await?;
523
524 let css_fs_path = self.source.ident().path();
525
526 let css_path =
529 if let Some(css_path) = project_path.get_relative_path_to(&*css_fs_path.await?) {
530 css_path.into_owned()
531 } else {
532 "".into()
534 };
535
536 let config_value = evaluate_webpack_loader(WebpackLoaderContext {
537 entries,
538 cwd: project_path.clone(),
539 env: *env,
540 context_source_for_issue: self.source,
541 chunking_context: *chunking_context,
542 module_graph,
543 resolve_options_context: None,
544 args: vec![
545 ResolvedVc::cell(content.into()),
546 ResolvedVc::cell(css_path.into()),
547 ResolvedVc::cell(source_map.into()),
548 ],
549 additional_invalidation: config_changed,
550 })
551 .await?;
552
553 let Some(val) = &*config_value else {
554 return Ok(ProcessPostCssResult {
556 content: AssetContent::File(FileContent::NotFound.resolved_cell()).resolved_cell(),
557 assets: Vec::new(),
558 }
559 .cell());
560 };
561 let processed_css: PostCssProcessingResult = parse_json_with_source_context(val)
562 .context("Unable to deserializate response from PostCSS transform operation")?;
563
564 let file = File::from(processed_css.css);
566 let assets = emitted_assets_to_virtual_sources(processed_css.assets).await?;
567 let content =
568 AssetContent::File(FileContent::Content(file).resolved_cell()).resolved_cell();
569 Ok(ProcessPostCssResult { content, assets }.cell())
570 }
571}