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, ResolvedVc, TryFlatJoinIterExt, Vc, fxindexmap, trace::TraceRawVcs,
8};
9use turbo_tasks_fs::{
10 File, FileContent, FileSystemEntryType, FileSystemPath, json::parse_json_with_source_context,
11};
12use turbopack_core::{
13 asset::{Asset, AssetContent},
14 changed::any_source_content_changed_of_module,
15 context::{AssetContext, ProcessResult},
16 file_source::FileSource,
17 ident::AssetIdent,
18 module_graph::{ModuleGraph, SingleModuleGraph},
19 reference_type::{EntryReferenceSubType, InnerAssets, ReferenceType},
20 resolve::{FindContextFileResult, find_context_file_or_package_key, options::ImportMapping},
21 source::Source,
22 source_map::GenerateSourceMap,
23 source_transform::SourceTransform,
24 virtual_source::VirtualSource,
25};
26use turbopack_ecmascript::runtime_functions::TURBOPACK_EXTERNAL_IMPORT;
27
28use crate::{
29 embed_js::embed_file_path,
30 evaluate::get_evaluate_entries,
31 execution_context::ExecutionContext,
32 transforms::{
33 util::{EmittedAsset, emitted_assets_to_virtual_sources},
34 webpack::{WebpackLoaderContext, evaluate_webpack_loader},
35 },
36};
37
38#[derive(Debug, Clone, Deserialize)]
39#[turbo_tasks::value]
40#[serde(rename_all = "camelCase")]
41struct PostCssProcessingResult {
42 css: String,
43 map: Option<String>,
44 assets: Option<Vec<EmittedAsset>>,
45}
46
47#[turbo_tasks::task_input]
48#[derive(Default, Copy, Clone, PartialEq, Eq, Hash, Debug, TraceRawVcs, Encode, Decode)]
49pub enum PostCssConfigLocation {
50 #[default]
53 ProjectPath,
54 ProjectPathOrLocalPath,
58 LocalPathOrProjectPath,
62}
63
64#[turbo_tasks::value(shared)]
65#[derive(Clone, Default)]
66pub struct PostCssTransformOptions {
67 pub postcss_package: Option<ResolvedVc<ImportMapping>>,
68 pub config_location: PostCssConfigLocation,
69 pub placeholder_for_future_extensions: u8,
70}
71
72#[turbo_tasks::function]
73fn postcss_configs() -> Vc<Vec<RcStr>> {
74 Vc::cell(vec![
75 rcstr!(".postcssrc"),
76 rcstr!(".postcssrc.json"),
77 rcstr!(".postcssrc.yaml"),
78 rcstr!(".postcssrc.yml"),
79 rcstr!(".postcssrc.js"),
80 rcstr!(".postcssrc.mjs"),
81 rcstr!(".postcssrc.cjs"),
82 rcstr!(".postcssrc.ts"),
83 rcstr!(".postcssrc.mts"),
84 rcstr!(".postcssrc.cts"),
85 rcstr!(".config/postcssrc"),
86 rcstr!(".config/postcssrc.json"),
87 rcstr!(".config/postcssrc.yaml"),
88 rcstr!(".config/postcssrc.yml"),
89 rcstr!(".config/postcssrc.js"),
90 rcstr!(".config/postcssrc.mjs"),
91 rcstr!(".config/postcssrc.cjs"),
92 rcstr!(".config/postcssrc.ts"),
93 rcstr!(".config/postcssrc.mts"),
94 rcstr!(".config/postcssrc.cts"),
95 rcstr!("postcss.config.js"),
96 rcstr!("postcss.config.mjs"),
97 rcstr!("postcss.config.cjs"),
98 rcstr!("postcss.config.ts"),
99 rcstr!("postcss.config.mts"),
100 rcstr!("postcss.config.cts"),
101 rcstr!("postcss.config.json"),
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(
139 &self,
140 source: ResolvedVc<Box<dyn Source>>,
141 asset_context: ResolvedVc<Box<dyn AssetContext>>,
142 ) -> Vc<Box<dyn Source>> {
143 Vc::upcast(
144 PostCssTransformedAsset {
145 evaluate_context: self.evaluate_context,
146 config_tracing_context: self.config_tracing_context,
147 execution_context: self.execution_context,
148 config_location: self.config_location,
149 source,
150 asset_context,
151 source_map: self.source_maps,
152 }
153 .cell(),
154 )
155 }
156}
157
158#[turbo_tasks::value]
159struct PostCssTransformedAsset {
160 evaluate_context: ResolvedVc<Box<dyn AssetContext>>,
161 config_tracing_context: ResolvedVc<Box<dyn AssetContext>>,
162 execution_context: ResolvedVc<ExecutionContext>,
163 config_location: PostCssConfigLocation,
164 source: ResolvedVc<Box<dyn Source>>,
165 asset_context: ResolvedVc<Box<dyn AssetContext>>,
166 source_map: bool,
167}
168
169#[turbo_tasks::value_impl]
170impl Source for PostCssTransformedAsset {
171 #[turbo_tasks::function]
172 fn ident(&self) -> Vc<AssetIdent> {
173 self.source.ident()
174 }
175
176 #[turbo_tasks::function]
177 async fn description(&self) -> Result<Vc<RcStr>> {
178 let inner = self.source.description().await?;
179 Ok(Vc::cell(format!("PostCSS transform of {}", inner).into()))
180 }
181}
182
183#[turbo_tasks::value_impl]
184impl Asset for PostCssTransformedAsset {
185 #[turbo_tasks::function]
186 async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
187 Ok(*self.process().await?.content)
188 }
189}
190
191#[turbo_tasks::value]
192struct ProcessPostCssResult {
193 content: ResolvedVc<AssetContent>,
194 assets: Vec<ResolvedVc<VirtualSource>>,
195}
196
197#[turbo_tasks::function]
198async fn config_changed(
199 asset_context: Vc<Box<dyn AssetContext>>,
200 postcss_config_path: FileSystemPath,
201) -> Result<Vc<Completion>> {
202 let config_asset = asset_context
203 .process(
204 Vc::upcast(FileSource::new(postcss_config_path.clone())),
205 ReferenceType::Internal(InnerAssets::empty().to_resolved().await?),
206 )
207 .module();
208
209 Ok(Vc::<Completions>::cell(vec![
210 any_source_content_changed_of_module(config_asset)
211 .to_resolved()
212 .await?,
213 extra_configs_changed(asset_context, postcss_config_path)
214 .to_resolved()
215 .await?,
216 ])
217 .completed())
218}
219
220#[turbo_tasks::function]
221async fn extra_configs_changed(
222 asset_context: Vc<Box<dyn AssetContext>>,
223 postcss_config_path: FileSystemPath,
224) -> Result<Vc<Completion>> {
225 let parent_path = postcss_config_path.parent();
226
227 let config_paths = [
228 parent_path.join("tailwind.config.js")?,
229 parent_path.join("tailwind.config.mjs")?,
230 parent_path.join("tailwind.config.ts")?,
231 ];
232
233 let configs = config_paths
234 .into_iter()
235 .map(|path| async move {
236 Ok(
237 if matches!(&*path.get_type().await?, FileSystemEntryType::File) {
238 match *asset_context
239 .process(
240 Vc::upcast(FileSource::new(path)),
241 ReferenceType::Internal(InnerAssets::empty().to_resolved().await?),
242 )
243 .try_into_module()
244 .await?
245 {
246 Some(module) => Some(
247 any_source_content_changed_of_module(*module)
248 .to_resolved()
249 .await?,
250 ),
251 None => None,
252 }
253 } else {
254 None
255 },
256 )
257 })
258 .try_flat_join()
259 .await?;
260
261 Ok(Vc::<Completions>::cell(configs).completed())
262}
263
264#[turbo_tasks::value]
265pub struct JsonSource {
266 pub path: FileSystemPath,
267 pub key: ResolvedVc<Option<RcStr>>,
268 pub allow_json5: bool,
269}
270
271#[turbo_tasks::value_impl]
272impl JsonSource {
273 #[turbo_tasks::function]
274 pub fn new(
275 path: FileSystemPath,
276 key: ResolvedVc<Option<RcStr>>,
277 allow_json5: bool,
278 ) -> Vc<Self> {
279 JsonSource {
280 path,
281 key,
282 allow_json5,
283 }
284 .cell()
285 }
286}
287
288#[turbo_tasks::value_impl]
289impl Source for JsonSource {
290 #[turbo_tasks::function]
291 fn description(&self) -> Vc<RcStr> {
292 Vc::cell(format!("JSON content of {}", self.path).into())
293 }
294
295 #[turbo_tasks::function]
296 async fn ident(&self) -> Result<Vc<AssetIdent>> {
297 match &*self.key.await? {
298 Some(key) => Ok(AssetIdent::from_path(
299 self.path.append(".")?.append(key)?.append(".json")?,
300 )
301 .into_vc()),
302 None => Ok(AssetIdent::from_path(self.path.append(".json")?).into_vc()),
303 }
304 }
305}
306
307#[turbo_tasks::value_impl]
308impl Asset for JsonSource {
309 #[turbo_tasks::function]
310 async fn content(&self) -> Result<Vc<AssetContent>> {
311 let file_type = &*self.path.get_type().await?;
312 match file_type {
313 FileSystemEntryType::File => {
314 let json = if self.allow_json5 {
315 self.path.read_json5().content().await?
316 } else {
317 self.path.read_json().content().await?
318 };
319 let value = match &*self.key.await? {
320 Some(key) => {
321 let Some(value) = json.get(&**key) else {
322 anyhow::bail!("Invalid file type {:?}", file_type)
323 };
324 value
325 }
326 None => &*json,
327 };
328 Ok(AssetContent::file(
329 FileContent::Content(File::from(value.to_string())).cell(),
330 ))
331 }
332 FileSystemEntryType::NotFound => {
333 Ok(AssetContent::File(FileContent::NotFound.resolved_cell()).cell())
334 }
335 _ => bail!("Invalid file type {:?}", file_type),
336 }
337 }
338}
339
340#[turbo_tasks::function]
341pub(crate) async fn config_loader_source(
342 project_path: FileSystemPath,
343 postcss_config_path: FileSystemPath,
344) -> Result<Vc<Box<dyn Source>>> {
345 let postcss_config_path_value = postcss_config_path.clone();
346 let postcss_config_path_filename = postcss_config_path_value.file_name();
347
348 if postcss_config_path_filename == "package.json" {
349 return Ok(Vc::upcast(JsonSource::new(
350 postcss_config_path,
351 Vc::cell(Some(rcstr!("postcss"))),
352 false,
353 )));
354 }
355
356 if postcss_config_path_value.path.ends_with(".json")
357 || postcss_config_path_filename == ".postcssrc"
358 {
359 return Ok(Vc::upcast(JsonSource::new(
360 postcss_config_path,
361 Vc::cell(None),
362 true,
363 )));
364 }
365
366 if !postcss_config_path_value.path.ends_with(".js") {
368 return Ok(Vc::upcast(FileSource::new(postcss_config_path)));
369 }
370
371 let Some(config_path) = project_path.get_relative_path_to(&postcss_config_path_value) else {
372 bail!("Unable to get relative path to postcss config");
373 };
374
375 let code = formatdoc! {
378 r#"
379 import {{ pathToFileURL }} from 'node:url';
380 import path from 'node:path';
381
382 const configPath = path.join(process.cwd(), {config_path});
383 // Absolute paths don't work with ESM imports on Windows:
384 // https://github.com/nodejs/node/issues/31710
385 // convert it to a file:// URL, which works on all platforms
386 const configUrl = pathToFileURL(configPath).toString();
387 const mod = await {TURBOPACK_EXTERNAL_IMPORT}(configUrl);
388
389 export default mod.default ?? mod;
390 "#,
391 config_path = serde_json::to_string(&config_path).expect("a string should be serializable"),
392 };
393
394 Ok(Vc::upcast(VirtualSource::new(
395 postcss_config_path.append("_.loader.mjs")?,
396 AssetContent::file(FileContent::Content(File::from(code)).cell()),
397 )))
398}
399
400#[turbo_tasks::function]
401async fn postcss_executor(
402 asset_context: Vc<Box<dyn AssetContext>>,
403 project_path: FileSystemPath,
404 postcss_config_path: FileSystemPath,
405) -> Result<Vc<ProcessResult>> {
406 let config_asset = asset_context
407 .process(
408 config_loader_source(project_path, postcss_config_path),
409 ReferenceType::Entry(EntryReferenceSubType::Undefined),
410 )
411 .module()
412 .to_resolved()
413 .await?;
414
415 Ok(asset_context.process(
416 Vc::upcast(FileSource::new(
417 embed_file_path(rcstr!("transforms/postcss.ts"))
418 .owned()
419 .await?,
420 )),
421 ReferenceType::Internal(ResolvedVc::cell(fxindexmap! {
422 rcstr!("CONFIG") => config_asset
423 })),
424 ))
425}
426
427async fn find_config_in_location(
428 project_path: FileSystemPath,
429 location: PostCssConfigLocation,
430 source: Vc<Box<dyn Source>>,
431) -> Result<Option<FileSystemPath>> {
432 let search_paths = match location {
434 PostCssConfigLocation::ProjectPath => {
436 vec![project_path]
437 }
438 PostCssConfigLocation::ProjectPathOrLocalPath => {
440 vec![project_path, source.ident().await?.path.parent()]
441 }
442 PostCssConfigLocation::LocalPathOrProjectPath => {
444 vec![source.ident().await?.path.parent(), project_path]
445 }
446 };
447
448 for path in search_paths {
449 if let FindContextFileResult::Found(config_path, _) =
450 &*find_context_file_or_package_key(path, postcss_configs(), rcstr!("postcss")).await?
451 {
452 return Ok(Some(config_path.clone()));
453 }
454 }
455
456 Ok(None)
457}
458
459#[turbo_tasks::value_impl]
460impl GenerateSourceMap for PostCssTransformedAsset {
461 #[turbo_tasks::function]
462 async fn generate_source_map(&self) -> Result<Vc<FileContent>> {
463 let source = ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(self.source);
464 match source {
465 Some(source) => Ok(source.generate_source_map()),
466 None => Ok(FileContent::NotFound.cell()),
467 }
468 }
469}
470
471#[turbo_tasks::value_impl]
472impl PostCssTransformedAsset {
473 #[turbo_tasks::function]
474 async fn process(&self) -> Result<Vc<ProcessPostCssResult>> {
475 let ExecutionContext {
476 project_path,
477 chunking_context,
478 env,
479 node_backend,
480 } = &*self.execution_context.await?;
481
482 let Some(config_path) =
494 find_config_in_location(project_path.clone(), self.config_location, *self.source)
495 .await?
496 else {
497 return Ok(ProcessPostCssResult {
498 content: self.source.content().to_resolved().await?,
499 assets: Vec::new(),
500 }
501 .cell());
502 };
503
504 let source_content = self.source.content();
505 let AssetContent::File(file) = *source_content.await? else {
506 bail!("PostCSS transform only support transforming files");
507 };
508 let FileContent::Content(content) = &*file.await? else {
509 return Ok(ProcessPostCssResult {
510 content: AssetContent::File(FileContent::NotFound.resolved_cell()).resolved_cell(),
511 assets: Vec::new(),
512 }
513 .cell());
514 };
515 let content = content.content().to_str()?;
516 let evaluate_context = self.evaluate_context;
517 let source_map = self.source_map;
518
519 let config_changed = config_changed(*self.config_tracing_context, config_path.clone())
521 .to_resolved()
522 .await?;
523
524 let postcss_executor =
525 postcss_executor(*evaluate_context, project_path.clone(), config_path).module();
526
527 let entries =
528 get_evaluate_entries(postcss_executor, *evaluate_context, **node_backend, None)
529 .to_resolved()
530 .await?;
531
532 let module_graph = ModuleGraph::from_graphs(
533 vec![SingleModuleGraph::new_with_entries(
534 entries.graph_entries().to_resolved().await?,
535 false,
536 false,
537 )],
538 None,
539 )
540 .connect()
541 .to_resolved()
542 .await?;
543
544 let source_ident = self.source.ident().await?;
545
546 let css_path = if let Some(css_path) = project_path.get_relative_path_to(&source_ident.path)
549 {
550 css_path.into_owned()
551 } else {
552 "".into()
554 };
555
556 let config_value = evaluate_webpack_loader(WebpackLoaderContext {
557 entries,
558 cwd: project_path.clone(),
559 env: *env,
560 node_backend: *node_backend,
561 context_source_for_issue: self.source,
562 chunking_context: *chunking_context,
563 evaluate_context: self.evaluate_context,
564 module_graph,
565 resolve_options_context: None,
566 asset_context: self.asset_context,
567 args: vec![
568 ResolvedVc::cell(content.into()),
569 ResolvedVc::cell(css_path.into()),
570 ResolvedVc::cell(source_map.into()),
571 ],
572 additional_invalidation: config_changed,
573 loader_names: vec![turbo_rcstr::rcstr!("postcss")],
574 })
575 .await?;
576
577 let Some(val) = &*config_value else {
578 return Ok(ProcessPostCssResult {
580 content: AssetContent::File(FileContent::NotFound.resolved_cell()).resolved_cell(),
581 assets: Vec::new(),
582 }
583 .cell());
584 };
585 let processed_css: PostCssProcessingResult = parse_json_with_source_context(val)
586 .context("Unable to deserializate response from PostCSS transform operation")?;
587
588 let file = File::from(processed_css.css);
590 let assets = emitted_assets_to_virtual_sources(processed_css.assets).await?;
591 let content =
592 AssetContent::File(FileContent::Content(file).resolved_cell()).resolved_cell();
593 Ok(ProcessPostCssResult { content, assets }.cell())
594 }
595}