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, turbofmt,
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, 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(async |path| {
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_filename = postcss_config_path.file_name();
346
347 if postcss_config_path_filename == "package.json" {
348 return Ok(Vc::upcast(JsonSource::new(
349 postcss_config_path,
350 Vc::cell(Some(rcstr!("postcss"))),
351 false,
352 )));
353 }
354
355 if postcss_config_path.path.ends_with(".json") || postcss_config_path_filename == ".postcssrc" {
356 return Ok(Vc::upcast(JsonSource::new(
357 postcss_config_path,
358 Vc::cell(None),
359 true,
360 )));
361 }
362
363 if !postcss_config_path.path.ends_with(".js") {
365 return Ok(Vc::upcast(FileSource::new(postcss_config_path)));
366 }
367
368 let Some(config_path) = project_path.get_relative_path_to(&postcss_config_path) else {
369 bail!("Unable to get relative path to postcss config");
370 };
371
372 let code = formatdoc! {
375 r#"
376 import {{ pathToFileURL }} from 'node:url';
377 import path from 'node:path';
378
379 const configPath = path.join(process.cwd(), {config_path});
380 // Absolute paths don't work with ESM imports on Windows:
381 // https://github.com/nodejs/node/issues/31710
382 // convert it to a file:// URL, which works on all platforms
383 const configUrl = pathToFileURL(configPath).toString();
384 const mod = await {TURBOPACK_EXTERNAL_IMPORT}(configUrl);
385
386 export default mod.default ?? mod;
387 "#,
388 config_path = serde_json::to_string(&config_path).expect("a string should be serializable"),
389 };
390
391 Ok(Vc::upcast(VirtualSource::new(
392 postcss_config_path.append("_.loader.mjs")?,
393 AssetContent::file(FileContent::Content(File::from(code)).cell()),
394 )))
395}
396
397#[turbo_tasks::function]
398async fn postcss_executor(
399 asset_context: Vc<Box<dyn AssetContext>>,
400 project_path: FileSystemPath,
401 postcss_config_path: FileSystemPath,
402) -> Result<Vc<ProcessResult>> {
403 let config_asset = asset_context
404 .process(
405 config_loader_source(project_path, postcss_config_path.clone()),
406 ReferenceType::Entry(EntryReferenceSubType::Undefined),
407 )
408 .module()
409 .to_resolved()
410 .await?;
411
412 Ok(asset_context.process(
413 Vc::upcast(FileSource::new_with_query(
414 embed_file_path(rcstr!("transforms/postcss.ts"))
415 .owned()
416 .await?,
417 turbofmt!("?config={postcss_config_path}").await?,
418 )),
419 ReferenceType::Internal(ResolvedVc::cell(fxindexmap! {
420 rcstr!("CONFIG") => config_asset
421 })),
422 ))
423}
424
425async fn find_config_in_location(
426 project_path: FileSystemPath,
427 location: PostCssConfigLocation,
428 source: Vc<Box<dyn Source>>,
429) -> Result<Option<FileSystemPath>> {
430 let search_paths = match location {
432 PostCssConfigLocation::ProjectPath => {
434 vec![project_path]
435 }
436 PostCssConfigLocation::ProjectPathOrLocalPath => {
438 vec![project_path, source.ident().await?.path.parent()]
439 }
440 PostCssConfigLocation::LocalPathOrProjectPath => {
442 vec![source.ident().await?.path.parent(), project_path]
443 }
444 };
445
446 for path in search_paths {
447 if let FindContextFileResult::Found(config_path, _) =
448 &*find_context_file_or_package_key(path, postcss_configs(), rcstr!("postcss")).await?
449 {
450 return Ok(Some(config_path.clone()));
451 }
452 }
453
454 Ok(None)
455}
456
457#[turbo_tasks::value_impl]
458impl GenerateSourceMap for PostCssTransformedAsset {
459 #[turbo_tasks::function]
460 async fn generate_source_map(&self) -> Result<Vc<FileContent>> {
461 let source = ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(self.source);
462 match source {
463 Some(source) => Ok(source.generate_source_map()),
464 None => Ok(FileContent::NotFound.cell()),
465 }
466 }
467}
468
469#[turbo_tasks::value_impl]
470impl PostCssTransformedAsset {
471 #[turbo_tasks::function]
472 async fn process(&self) -> Result<Vc<ProcessPostCssResult>> {
473 let ExecutionContext {
474 project_path,
475 chunking_context,
476 env,
477 node_backend,
478 } = &*self.execution_context.await?;
479
480 let Some(config_path) =
492 find_config_in_location(project_path.clone(), self.config_location, *self.source)
493 .await?
494 else {
495 return Ok(ProcessPostCssResult {
496 content: self.source.content().to_resolved().await?,
497 assets: Vec::new(),
498 }
499 .cell());
500 };
501
502 let source_content = self.source.content();
503 let AssetContent::File(file) = *source_content.await? else {
504 bail!("PostCSS transform only support transforming files");
505 };
506 let FileContent::Content(content) = &*file.await? else {
507 return Ok(ProcessPostCssResult {
508 content: AssetContent::File(FileContent::NotFound.resolved_cell()).resolved_cell(),
509 assets: Vec::new(),
510 }
511 .cell());
512 };
513 let content = content.content().to_str()?;
514 let evaluate_context = self.evaluate_context;
515 let source_map = self.source_map;
516
517 let config_changed = config_changed(*self.config_tracing_context, config_path.clone())
519 .to_resolved()
520 .await?;
521
522 let postcss_executor =
523 postcss_executor(*evaluate_context, project_path.clone(), config_path).module();
524
525 let entries =
526 get_evaluate_entries(postcss_executor, *evaluate_context, **node_backend, None)
527 .to_resolved()
528 .await?;
529
530 let module_graph = ModuleGraph::from_graphs(
531 vec![SingleModuleGraph::new_with_entries(
532 entries.graph_entries().to_resolved().await?,
533 false,
534 false,
535 )],
536 None,
537 )
538 .connect()
539 .to_resolved()
540 .await?;
541
542 let source_ident = self.source.ident().await?;
543
544 let css_path = if let Some(css_path) = project_path.get_relative_path_to(&source_ident.path)
547 {
548 css_path.into_owned()
549 } else {
550 "".into()
552 };
553
554 let config_value = evaluate_webpack_loader(WebpackLoaderContext {
555 entries,
556 cwd: project_path.clone(),
557 env: *env,
558 node_backend: *node_backend,
559 context_source_for_issue: self.source,
560 chunking_context: *chunking_context,
561 evaluate_context: self.evaluate_context,
562 module_graph,
563 resolve_options_context: None,
564 asset_context: self.asset_context,
565 args: vec![
566 ResolvedVc::cell(content.into()),
567 ResolvedVc::cell(css_path.into()),
568 ResolvedVc::cell(source_map.into()),
569 ],
570 additional_invalidation: config_changed,
571 loader_names: vec![turbo_rcstr::rcstr!("postcss")],
572 })
573 .await?;
574
575 let Some(val) = &*config_value else {
576 return Ok(ProcessPostCssResult {
578 content: AssetContent::File(FileContent::NotFound.resolved_cell()).resolved_cell(),
579 assets: Vec::new(),
580 }
581 .cell());
582 };
583 let processed_css: PostCssProcessingResult = parse_json_with_source_context(val)
584 .context("Unable to deserializate response from PostCSS transform operation")?;
585
586 let file = File::from(processed_css.css);
588 let assets = emitted_assets_to_virtual_sources(processed_css.assets).await?;
589 let content =
590 AssetContent::File(FileContent::Content(file).resolved_cell()).resolved_cell();
591 Ok(ProcessPostCssResult { content, assets }.cell())
592 }
593}