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