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