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