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 .into_vc()),
320 None => Ok(AssetIdent::from_path(self.path.append(".json")?).into_vc()),
321 }
322 }
323}
324
325#[turbo_tasks::value_impl]
326impl Asset for JsonSource {
327 #[turbo_tasks::function]
328 async fn content(&self) -> Result<Vc<AssetContent>> {
329 let file_type = &*self.path.get_type().await?;
330 match file_type {
331 FileSystemEntryType::File => {
332 let json = if self.allow_json5 {
333 self.path.read_json5().content().await?
334 } else {
335 self.path.read_json().content().await?
336 };
337 let value = match &*self.key.await? {
338 Some(key) => {
339 let Some(value) = json.get(&**key) else {
340 anyhow::bail!("Invalid file type {:?}", file_type)
341 };
342 value
343 }
344 None => &*json,
345 };
346 Ok(AssetContent::file(
347 FileContent::Content(File::from(value.to_string())).cell(),
348 ))
349 }
350 FileSystemEntryType::NotFound => {
351 Ok(AssetContent::File(FileContent::NotFound.resolved_cell()).cell())
352 }
353 _ => bail!("Invalid file type {:?}", file_type),
354 }
355 }
356}
357
358#[turbo_tasks::function]
359pub(crate) async fn config_loader_source(
360 project_path: FileSystemPath,
361 postcss_config_path: FileSystemPath,
362) -> Result<Vc<Box<dyn Source>>> {
363 let postcss_config_path_value = postcss_config_path.clone();
364 let postcss_config_path_filename = postcss_config_path_value.file_name();
365
366 if postcss_config_path_filename == "package.json" {
367 return Ok(Vc::upcast(JsonSource::new(
368 postcss_config_path,
369 Vc::cell(Some(rcstr!("postcss"))),
370 false,
371 )));
372 }
373
374 if postcss_config_path_value.path.ends_with(".json")
375 || postcss_config_path_filename == ".postcssrc"
376 {
377 return Ok(Vc::upcast(JsonSource::new(
378 postcss_config_path,
379 Vc::cell(None),
380 true,
381 )));
382 }
383
384 if !postcss_config_path_value.path.ends_with(".js") {
386 return Ok(Vc::upcast(FileSource::new(postcss_config_path)));
387 }
388
389 let Some(config_path) = project_path.get_relative_path_to(&postcss_config_path_value) else {
390 bail!("Unable to get relative path to postcss config");
391 };
392
393 let code = formatdoc! {
396 r#"
397 import {{ pathToFileURL }} from 'node:url';
398 import path from 'node:path';
399
400 const configPath = path.join(process.cwd(), {config_path});
401 // Absolute paths don't work with ESM imports on Windows:
402 // https://github.com/nodejs/node/issues/31710
403 // convert it to a file:// URL, which works on all platforms
404 const configUrl = pathToFileURL(configPath).toString();
405 const mod = await {TURBOPACK_EXTERNAL_IMPORT}(configUrl);
406
407 export default mod.default ?? mod;
408 "#,
409 config_path = serde_json::to_string(&config_path).expect("a string should be serializable"),
410 };
411
412 Ok(Vc::upcast(VirtualSource::new(
413 postcss_config_path.append("_.loader.mjs")?,
414 AssetContent::file(FileContent::Content(File::from(code)).cell()),
415 )))
416}
417
418#[turbo_tasks::function]
419async fn postcss_executor(
420 asset_context: Vc<Box<dyn AssetContext>>,
421 project_path: FileSystemPath,
422 postcss_config_path: FileSystemPath,
423) -> Result<Vc<ProcessResult>> {
424 let config_asset = asset_context
425 .process(
426 config_loader_source(project_path, postcss_config_path),
427 ReferenceType::Entry(EntryReferenceSubType::Undefined),
428 )
429 .module()
430 .to_resolved()
431 .await?;
432
433 Ok(asset_context.process(
434 Vc::upcast(FileSource::new(
435 embed_file_path(rcstr!("transforms/postcss.ts"))
436 .owned()
437 .await?,
438 )),
439 ReferenceType::Internal(ResolvedVc::cell(fxindexmap! {
440 rcstr!("CONFIG") => config_asset
441 })),
442 ))
443}
444
445async fn find_config_in_location(
446 project_path: FileSystemPath,
447 location: PostCssConfigLocation,
448 source: Vc<Box<dyn Source>>,
449) -> Result<Option<FileSystemPath>> {
450 let search_paths = match location {
452 PostCssConfigLocation::ProjectPath => {
454 vec![project_path]
455 }
456 PostCssConfigLocation::ProjectPathOrLocalPath => {
458 vec![project_path, source.ident().await?.path.parent()]
459 }
460 PostCssConfigLocation::LocalPathOrProjectPath => {
462 vec![source.ident().await?.path.parent(), project_path]
463 }
464 };
465
466 for path in search_paths {
467 if let FindContextFileResult::Found(config_path, _) =
468 &*find_context_file_or_package_key(path, postcss_configs(), rcstr!("postcss")).await?
469 {
470 return Ok(Some(config_path.clone()));
471 }
472 }
473
474 Ok(None)
475}
476
477#[turbo_tasks::value_impl]
478impl GenerateSourceMap for PostCssTransformedAsset {
479 #[turbo_tasks::function]
480 async fn generate_source_map(&self) -> Result<Vc<FileContent>> {
481 let source = ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(self.source);
482 match source {
483 Some(source) => Ok(source.generate_source_map()),
484 None => Ok(FileContent::NotFound.cell()),
485 }
486 }
487}
488
489#[turbo_tasks::value_impl]
490impl PostCssTransformedAsset {
491 #[turbo_tasks::function]
492 async fn process(&self) -> Result<Vc<ProcessPostCssResult>> {
493 let ExecutionContext {
494 project_path,
495 chunking_context,
496 env,
497 node_backend,
498 } = &*self.execution_context.await?;
499
500 let Some(config_path) =
512 find_config_in_location(project_path.clone(), self.config_location, *self.source)
513 .await?
514 else {
515 return Ok(ProcessPostCssResult {
516 content: self.source.content().to_resolved().await?,
517 assets: Vec::new(),
518 }
519 .cell());
520 };
521
522 let source_content = self.source.content();
523 let AssetContent::File(file) = *source_content.await? else {
524 bail!("PostCSS transform only support transforming files");
525 };
526 let FileContent::Content(content) = &*file.await? else {
527 return Ok(ProcessPostCssResult {
528 content: AssetContent::File(FileContent::NotFound.resolved_cell()).resolved_cell(),
529 assets: Vec::new(),
530 }
531 .cell());
532 };
533 let content = content.content().to_str()?;
534 let evaluate_context = self.evaluate_context;
535 let source_map = self.source_map;
536
537 let config_changed = config_changed(*self.config_tracing_context, config_path.clone())
539 .to_resolved()
540 .await?;
541
542 let postcss_executor =
543 postcss_executor(*evaluate_context, project_path.clone(), config_path).module();
544
545 let entries =
546 get_evaluate_entries(postcss_executor, *evaluate_context, **node_backend, None)
547 .to_resolved()
548 .await?;
549
550 let module_graph = ModuleGraph::from_graphs(
551 vec![SingleModuleGraph::new_with_entries(
552 entries.graph_entries().to_resolved().await?,
553 false,
554 false,
555 )],
556 None,
557 )
558 .connect()
559 .to_resolved()
560 .await?;
561
562 let source_ident = self.source.ident().await?;
563
564 let css_path = if let Some(css_path) = project_path.get_relative_path_to(&source_ident.path)
567 {
568 css_path.into_owned()
569 } else {
570 "".into()
572 };
573
574 let config_value = evaluate_webpack_loader(WebpackLoaderContext {
575 entries,
576 cwd: project_path.clone(),
577 env: *env,
578 node_backend: *node_backend,
579 context_source_for_issue: self.source,
580 chunking_context: *chunking_context,
581 evaluate_context: self.evaluate_context,
582 module_graph,
583 resolve_options_context: None,
584 asset_context: self.asset_context,
585 args: vec![
586 ResolvedVc::cell(content.into()),
587 ResolvedVc::cell(css_path.into()),
588 ResolvedVc::cell(source_map.into()),
589 ],
590 additional_invalidation: config_changed,
591 })
592 .await?;
593
594 let Some(val) = &*config_value else {
595 return Ok(ProcessPostCssResult {
597 content: AssetContent::File(FileContent::NotFound.resolved_cell()).resolved_cell(),
598 assets: Vec::new(),
599 }
600 .cell());
601 };
602 let processed_css: PostCssProcessingResult = parse_json_with_source_context(val)
603 .context("Unable to deserializate response from PostCSS transform operation")?;
604
605 let file = File::from(processed_css.css);
607 let assets = emitted_assets_to_virtual_sources(processed_css.assets).await?;
608 let content =
609 AssetContent::File(FileContent::Content(file).resolved_cell()).resolved_cell();
610 Ok(ProcessPostCssResult { content, assets }.cell())
611 }
612}