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]
66 ProjectPath,
67 ProjectPathOrLocalPath,
71 LocalPathOrProjectPath,
75}
76
77#[turbo_tasks::value(shared)]
78#[derive(Clone, Default)]
79pub struct PostCssTransformOptions {
80 pub postcss_package: Option<ResolvedVc<ImportMapping>>,
81 pub config_location: PostCssConfigLocation,
82 pub placeholder_for_future_extensions: u8,
83}
84
85#[turbo_tasks::function]
86fn postcss_configs() -> Vc<Vec<RcStr>> {
87 Vc::cell(vec![
88 rcstr!(".postcssrc"),
89 rcstr!(".postcssrc.json"),
90 rcstr!(".postcssrc.yaml"),
91 rcstr!(".postcssrc.yml"),
92 rcstr!(".postcssrc.js"),
93 rcstr!(".postcssrc.mjs"),
94 rcstr!(".postcssrc.cjs"),
95 rcstr!(".postcssrc.ts"),
96 rcstr!(".postcssrc.mts"),
97 rcstr!(".postcssrc.cts"),
98 rcstr!(".config/postcssrc"),
99 rcstr!(".config/postcssrc.json"),
100 rcstr!(".config/postcssrc.yaml"),
101 rcstr!(".config/postcssrc.yml"),
102 rcstr!(".config/postcssrc.js"),
103 rcstr!(".config/postcssrc.mjs"),
104 rcstr!(".config/postcssrc.cjs"),
105 rcstr!(".config/postcssrc.ts"),
106 rcstr!(".config/postcssrc.mts"),
107 rcstr!(".config/postcssrc.cts"),
108 rcstr!("postcss.config.js"),
109 rcstr!("postcss.config.mjs"),
110 rcstr!("postcss.config.cjs"),
111 rcstr!("postcss.config.ts"),
112 rcstr!("postcss.config.mts"),
113 rcstr!("postcss.config.cts"),
114 rcstr!("postcss.config.json"),
115 ])
116}
117
118#[turbo_tasks::value]
119pub struct PostCssTransform {
120 evaluate_context: ResolvedVc<Box<dyn AssetContext>>,
121 config_tracing_context: ResolvedVc<Box<dyn AssetContext>>,
122 execution_context: ResolvedVc<ExecutionContext>,
123 config_location: PostCssConfigLocation,
124 source_maps: bool,
125}
126
127#[turbo_tasks::value_impl]
128impl PostCssTransform {
129 #[turbo_tasks::function]
130 pub fn new(
131 evaluate_context: ResolvedVc<Box<dyn AssetContext>>,
132 config_tracing_context: ResolvedVc<Box<dyn AssetContext>>,
133 execution_context: ResolvedVc<ExecutionContext>,
134 config_location: PostCssConfigLocation,
135 source_maps: bool,
136 ) -> Vc<Self> {
137 PostCssTransform {
138 evaluate_context,
139 config_tracing_context,
140 execution_context,
141 config_location,
142 source_maps,
143 }
144 .cell()
145 }
146}
147
148#[turbo_tasks::value_impl]
149impl SourceTransform for PostCssTransform {
150 #[turbo_tasks::function]
151 fn transform(
152 &self,
153 source: ResolvedVc<Box<dyn Source>>,
154 asset_context: ResolvedVc<Box<dyn AssetContext>>,
155 ) -> Vc<Box<dyn Source>> {
156 Vc::upcast(
157 PostCssTransformedAsset {
158 evaluate_context: self.evaluate_context,
159 config_tracing_context: self.config_tracing_context,
160 execution_context: self.execution_context,
161 config_location: self.config_location,
162 source,
163 asset_context,
164 source_map: self.source_maps,
165 }
166 .cell(),
167 )
168 }
169}
170
171#[turbo_tasks::value]
172struct PostCssTransformedAsset {
173 evaluate_context: ResolvedVc<Box<dyn AssetContext>>,
174 config_tracing_context: ResolvedVc<Box<dyn AssetContext>>,
175 execution_context: ResolvedVc<ExecutionContext>,
176 config_location: PostCssConfigLocation,
177 source: ResolvedVc<Box<dyn Source>>,
178 asset_context: ResolvedVc<Box<dyn AssetContext>>,
179 source_map: bool,
180}
181
182#[turbo_tasks::value_impl]
183impl Source for PostCssTransformedAsset {
184 #[turbo_tasks::function]
185 fn ident(&self) -> Vc<AssetIdent> {
186 self.source.ident()
187 }
188
189 #[turbo_tasks::function]
190 async fn description(&self) -> Result<Vc<RcStr>> {
191 let inner = self.source.description().await?;
192 Ok(Vc::cell(format!("PostCSS transform of {}", inner).into()))
193 }
194}
195
196#[turbo_tasks::value_impl]
197impl Asset for PostCssTransformedAsset {
198 #[turbo_tasks::function]
199 async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
200 Ok(*self.process().await?.content)
201 }
202}
203
204#[turbo_tasks::value]
205struct ProcessPostCssResult {
206 content: ResolvedVc<AssetContent>,
207 assets: Vec<ResolvedVc<VirtualSource>>,
208}
209
210#[turbo_tasks::function]
211async fn config_changed(
212 asset_context: Vc<Box<dyn AssetContext>>,
213 postcss_config_path: FileSystemPath,
214) -> Result<Vc<Completion>> {
215 let config_asset = asset_context
216 .process(
217 Vc::upcast(FileSource::new(postcss_config_path.clone())),
218 ReferenceType::Internal(InnerAssets::empty().to_resolved().await?),
219 )
220 .module();
221
222 Ok(Vc::<Completions>::cell(vec![
223 any_source_content_changed_of_module(config_asset)
224 .to_resolved()
225 .await?,
226 extra_configs_changed(asset_context, postcss_config_path)
227 .to_resolved()
228 .await?,
229 ])
230 .completed())
231}
232
233#[turbo_tasks::function]
234async fn extra_configs_changed(
235 asset_context: Vc<Box<dyn AssetContext>>,
236 postcss_config_path: FileSystemPath,
237) -> Result<Vc<Completion>> {
238 let parent_path = postcss_config_path.parent();
239
240 let config_paths = [
241 parent_path.join("tailwind.config.js")?,
242 parent_path.join("tailwind.config.mjs")?,
243 parent_path.join("tailwind.config.ts")?,
244 ];
245
246 let configs = config_paths
247 .into_iter()
248 .map(|path| async move {
249 Ok(
250 if matches!(&*path.get_type().await?, FileSystemEntryType::File) {
251 match *asset_context
252 .process(
253 Vc::upcast(FileSource::new(path)),
254 ReferenceType::Internal(InnerAssets::empty().to_resolved().await?),
255 )
256 .try_into_module()
257 .await?
258 {
259 Some(module) => Some(
260 any_source_content_changed_of_module(*module)
261 .to_resolved()
262 .await?,
263 ),
264 None => None,
265 }
266 } else {
267 None
268 },
269 )
270 })
271 .try_flat_join()
272 .await?;
273
274 Ok(Vc::<Completions>::cell(configs).completed())
275}
276
277#[turbo_tasks::value]
278pub struct JsonSource {
279 pub path: FileSystemPath,
280 pub key: ResolvedVc<Option<RcStr>>,
281 pub allow_json5: bool,
282}
283
284#[turbo_tasks::value_impl]
285impl JsonSource {
286 #[turbo_tasks::function]
287 pub fn new(
288 path: FileSystemPath,
289 key: ResolvedVc<Option<RcStr>>,
290 allow_json5: bool,
291 ) -> Vc<Self> {
292 JsonSource {
293 path,
294 key,
295 allow_json5,
296 }
297 .cell()
298 }
299}
300
301#[turbo_tasks::value_impl]
302impl Source for JsonSource {
303 #[turbo_tasks::function]
304 fn description(&self) -> Vc<RcStr> {
305 Vc::cell(format!("JSON content of {}", self.path).into())
306 }
307
308 #[turbo_tasks::function]
309 async fn ident(&self) -> Result<Vc<AssetIdent>> {
310 match &*self.key.await? {
311 Some(key) => Ok(AssetIdent::from_path(
312 self.path.append(".")?.append(key)?.append(".json")?,
313 )
314 .into_vc()),
315 None => Ok(AssetIdent::from_path(self.path.append(".json")?).into_vc()),
316 }
317 }
318}
319
320#[turbo_tasks::value_impl]
321impl Asset for JsonSource {
322 #[turbo_tasks::function]
323 async fn content(&self) -> Result<Vc<AssetContent>> {
324 let file_type = &*self.path.get_type().await?;
325 match file_type {
326 FileSystemEntryType::File => {
327 let json = if self.allow_json5 {
328 self.path.read_json5().content().await?
329 } else {
330 self.path.read_json().content().await?
331 };
332 let value = match &*self.key.await? {
333 Some(key) => {
334 let Some(value) = json.get(&**key) else {
335 anyhow::bail!("Invalid file type {:?}", file_type)
336 };
337 value
338 }
339 None => &*json,
340 };
341 Ok(AssetContent::file(
342 FileContent::Content(File::from(value.to_string())).cell(),
343 ))
344 }
345 FileSystemEntryType::NotFound => {
346 Ok(AssetContent::File(FileContent::NotFound.resolved_cell()).cell())
347 }
348 _ => bail!("Invalid file type {:?}", file_type),
349 }
350 }
351}
352
353#[turbo_tasks::function]
354pub(crate) async fn config_loader_source(
355 project_path: FileSystemPath,
356 postcss_config_path: FileSystemPath,
357) -> Result<Vc<Box<dyn Source>>> {
358 let postcss_config_path_value = postcss_config_path.clone();
359 let postcss_config_path_filename = postcss_config_path_value.file_name();
360
361 if postcss_config_path_filename == "package.json" {
362 return Ok(Vc::upcast(JsonSource::new(
363 postcss_config_path,
364 Vc::cell(Some(rcstr!("postcss"))),
365 false,
366 )));
367 }
368
369 if postcss_config_path_value.path.ends_with(".json")
370 || postcss_config_path_filename == ".postcssrc"
371 {
372 return Ok(Vc::upcast(JsonSource::new(
373 postcss_config_path,
374 Vc::cell(None),
375 true,
376 )));
377 }
378
379 if !postcss_config_path_value.path.ends_with(".js") {
381 return Ok(Vc::upcast(FileSource::new(postcss_config_path)));
382 }
383
384 let Some(config_path) = project_path.get_relative_path_to(&postcss_config_path_value) else {
385 bail!("Unable to get relative path to postcss config");
386 };
387
388 let code = formatdoc! {
391 r#"
392 import {{ pathToFileURL }} from 'node:url';
393 import path from 'node:path';
394
395 const configPath = path.join(process.cwd(), {config_path});
396 // Absolute paths don't work with ESM imports on Windows:
397 // https://github.com/nodejs/node/issues/31710
398 // convert it to a file:// URL, which works on all platforms
399 const configUrl = pathToFileURL(configPath).toString();
400 const mod = await {TURBOPACK_EXTERNAL_IMPORT}(configUrl);
401
402 export default mod.default ?? mod;
403 "#,
404 config_path = serde_json::to_string(&config_path).expect("a string should be serializable"),
405 };
406
407 Ok(Vc::upcast(VirtualSource::new(
408 postcss_config_path.append("_.loader.mjs")?,
409 AssetContent::file(FileContent::Content(File::from(code)).cell()),
410 )))
411}
412
413#[turbo_tasks::function]
414async fn postcss_executor(
415 asset_context: Vc<Box<dyn AssetContext>>,
416 project_path: FileSystemPath,
417 postcss_config_path: FileSystemPath,
418) -> Result<Vc<ProcessResult>> {
419 let config_asset = asset_context
420 .process(
421 config_loader_source(project_path, postcss_config_path),
422 ReferenceType::Entry(EntryReferenceSubType::Undefined),
423 )
424 .module()
425 .to_resolved()
426 .await?;
427
428 Ok(asset_context.process(
429 Vc::upcast(FileSource::new(
430 embed_file_path(rcstr!("transforms/postcss.ts"))
431 .owned()
432 .await?,
433 )),
434 ReferenceType::Internal(ResolvedVc::cell(fxindexmap! {
435 rcstr!("CONFIG") => config_asset
436 })),
437 ))
438}
439
440async fn find_config_in_location(
441 project_path: FileSystemPath,
442 location: PostCssConfigLocation,
443 source: Vc<Box<dyn Source>>,
444) -> Result<Option<FileSystemPath>> {
445 let search_paths = match location {
447 PostCssConfigLocation::ProjectPath => {
449 vec![project_path]
450 }
451 PostCssConfigLocation::ProjectPathOrLocalPath => {
453 vec![project_path, source.ident().await?.path.parent()]
454 }
455 PostCssConfigLocation::LocalPathOrProjectPath => {
457 vec![source.ident().await?.path.parent(), project_path]
458 }
459 };
460
461 for path in search_paths {
462 if let FindContextFileResult::Found(config_path, _) =
463 &*find_context_file_or_package_key(path, postcss_configs(), rcstr!("postcss")).await?
464 {
465 return Ok(Some(config_path.clone()));
466 }
467 }
468
469 Ok(None)
470}
471
472#[turbo_tasks::value_impl]
473impl GenerateSourceMap for PostCssTransformedAsset {
474 #[turbo_tasks::function]
475 async fn generate_source_map(&self) -> Result<Vc<FileContent>> {
476 let source = ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(self.source);
477 match source {
478 Some(source) => Ok(source.generate_source_map()),
479 None => Ok(FileContent::NotFound.cell()),
480 }
481 }
482}
483
484#[turbo_tasks::value_impl]
485impl PostCssTransformedAsset {
486 #[turbo_tasks::function]
487 async fn process(&self) -> Result<Vc<ProcessPostCssResult>> {
488 let ExecutionContext {
489 project_path,
490 chunking_context,
491 env,
492 node_backend,
493 } = &*self.execution_context.await?;
494
495 let Some(config_path) =
507 find_config_in_location(project_path.clone(), self.config_location, *self.source)
508 .await?
509 else {
510 return Ok(ProcessPostCssResult {
511 content: self.source.content().to_resolved().await?,
512 assets: Vec::new(),
513 }
514 .cell());
515 };
516
517 let source_content = self.source.content();
518 let AssetContent::File(file) = *source_content.await? else {
519 bail!("PostCSS transform only support transforming files");
520 };
521 let FileContent::Content(content) = &*file.await? else {
522 return Ok(ProcessPostCssResult {
523 content: AssetContent::File(FileContent::NotFound.resolved_cell()).resolved_cell(),
524 assets: Vec::new(),
525 }
526 .cell());
527 };
528 let content = content.content().to_str()?;
529 let evaluate_context = self.evaluate_context;
530 let source_map = self.source_map;
531
532 let config_changed = config_changed(*self.config_tracing_context, config_path.clone())
534 .to_resolved()
535 .await?;
536
537 let postcss_executor =
538 postcss_executor(*evaluate_context, project_path.clone(), config_path).module();
539
540 let entries =
541 get_evaluate_entries(postcss_executor, *evaluate_context, **node_backend, None)
542 .to_resolved()
543 .await?;
544
545 let module_graph = ModuleGraph::from_graphs(
546 vec![SingleModuleGraph::new_with_entries(
547 entries.graph_entries().to_resolved().await?,
548 false,
549 false,
550 )],
551 None,
552 )
553 .connect()
554 .to_resolved()
555 .await?;
556
557 let source_ident = self.source.ident().await?;
558
559 let css_path = if let Some(css_path) = project_path.get_relative_path_to(&source_ident.path)
562 {
563 css_path.into_owned()
564 } else {
565 "".into()
567 };
568
569 let config_value = evaluate_webpack_loader(WebpackLoaderContext {
570 entries,
571 cwd: project_path.clone(),
572 env: *env,
573 node_backend: *node_backend,
574 context_source_for_issue: self.source,
575 chunking_context: *chunking_context,
576 evaluate_context: self.evaluate_context,
577 module_graph,
578 resolve_options_context: None,
579 asset_context: self.asset_context,
580 args: vec![
581 ResolvedVc::cell(content.into()),
582 ResolvedVc::cell(css_path.into()),
583 ResolvedVc::cell(source_map.into()),
584 ],
585 additional_invalidation: config_changed,
586 loader_names: vec![turbo_rcstr::rcstr!("postcss")],
587 })
588 .await?;
589
590 let Some(val) = &*config_value else {
591 return Ok(ProcessPostCssResult {
593 content: AssetContent::File(FileContent::NotFound.resolved_cell()).resolved_cell(),
594 assets: Vec::new(),
595 }
596 .cell());
597 };
598 let processed_css: PostCssProcessingResult = parse_json_with_source_context(val)
599 .context("Unable to deserializate response from PostCSS transform operation")?;
600
601 let file = File::from(processed_css.css);
603 let assets = emitted_assets_to_virtual_sources(processed_css.assets).await?;
604 let content =
605 AssetContent::File(FileContent::Content(file).resolved_cell()).resolved_cell();
606 Ok(ProcessPostCssResult { content, assets }.cell())
607 }
608}