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