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