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