turbopack_node/transforms/
postcss.rs

1use anyhow::{Context, Result, bail};
2use indoc::formatdoc;
3use serde::{Deserialize, Serialize};
4use turbo_rcstr::RcStr;
5use turbo_tasks::{
6    Completion, Completions, NonLocalValue, ResolvedVc, TaskInput, TryFlatJoinIterExt, Value, Vc,
7    fxindexmap, trace::TraceRawVcs,
8};
9use turbo_tasks_bytes::stream::SingleValue;
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    issue::{
20        Issue, IssueDescriptionExt, IssueSeverity, IssueStage, OptionStyledString, StyledString,
21    },
22    reference_type::{EntryReferenceSubType, InnerAssets, ReferenceType},
23    resolve::{FindContextFileResult, find_context_file_or_package_key, options::ImportMapping},
24    source::Source,
25    source_map::{GenerateSourceMap, OptionStringifiedSourceMap},
26    source_transform::SourceTransform,
27    virtual_source::VirtualSource,
28};
29use turbopack_ecmascript::runtime_functions::TURBOPACK_EXTERNAL_IMPORT;
30
31use super::{
32    util::{EmittedAsset, emitted_assets_to_virtual_sources},
33    webpack::WebpackLoaderContext,
34};
35use crate::{
36    embed_js::embed_file_path, execution_context::ExecutionContext,
37    transforms::webpack::evaluate_webpack_loader,
38};
39
40#[derive(Debug, Serialize, Deserialize, Clone)]
41#[serde(rename_all = "camelCase")]
42#[turbo_tasks::value(serialization = "custom")]
43struct PostCssProcessingResult {
44    css: String,
45    map: Option<String>,
46    assets: Option<Vec<EmittedAsset>>,
47}
48
49#[derive(
50    Default,
51    Copy,
52    Clone,
53    PartialEq,
54    Eq,
55    Hash,
56    Debug,
57    TraceRawVcs,
58    Serialize,
59    Deserialize,
60    TaskInput,
61    NonLocalValue,
62)]
63pub enum PostCssConfigLocation {
64    #[default]
65    ProjectPath,
66    ProjectPathOrLocalPath,
67}
68
69#[turbo_tasks::value(shared)]
70#[derive(Clone, Default)]
71pub struct PostCssTransformOptions {
72    pub postcss_package: Option<ResolvedVc<ImportMapping>>,
73    pub config_location: PostCssConfigLocation,
74    pub placeholder_for_future_extensions: u8,
75}
76
77#[turbo_tasks::function]
78fn postcss_configs() -> Vc<Vec<RcStr>> {
79    Vc::cell(
80        [
81            ".postcssrc",
82            ".postcssrc.json",
83            ".postcssrc.yaml",
84            ".postcssrc.yml",
85            ".postcssrc.js",
86            ".postcssrc.mjs",
87            ".postcssrc.cjs",
88            ".config/postcssrc",
89            ".config/postcssrc.json",
90            ".config/postcssrc.yaml",
91            ".config/postcssrc.yml",
92            ".config/postcssrc.js",
93            ".config/postcssrc.mjs",
94            ".config/postcssrc.cjs",
95            "postcss.config.js",
96            "postcss.config.mjs",
97            "postcss.config.cjs",
98            "postcss.config.json",
99        ]
100        .into_iter()
101        .map(RcStr::from)
102        .collect(),
103    )
104}
105
106#[turbo_tasks::value]
107pub struct PostCssTransform {
108    evaluate_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        execution_context: ResolvedVc<ExecutionContext>,
120        config_location: PostCssConfigLocation,
121        source_maps: bool,
122    ) -> Vc<Self> {
123        PostCssTransform {
124            evaluate_context,
125            execution_context,
126            config_location,
127            source_maps,
128        }
129        .cell()
130    }
131}
132
133#[turbo_tasks::value_impl]
134impl SourceTransform for PostCssTransform {
135    #[turbo_tasks::function]
136    fn transform(&self, source: ResolvedVc<Box<dyn Source>>) -> Vc<Box<dyn Source>> {
137        Vc::upcast(
138            PostCssTransformedAsset {
139                evaluate_context: self.evaluate_context,
140                execution_context: self.execution_context,
141                config_location: self.config_location,
142                source,
143                source_map: self.source_maps,
144            }
145            .cell(),
146        )
147    }
148}
149
150#[turbo_tasks::value]
151struct PostCssTransformedAsset {
152    evaluate_context: ResolvedVc<Box<dyn AssetContext>>,
153    execution_context: ResolvedVc<ExecutionContext>,
154    config_location: PostCssConfigLocation,
155    source: ResolvedVc<Box<dyn Source>>,
156    source_map: bool,
157}
158
159#[turbo_tasks::value_impl]
160impl Source for PostCssTransformedAsset {
161    #[turbo_tasks::function]
162    fn ident(&self) -> Vc<AssetIdent> {
163        self.source.ident()
164    }
165}
166
167#[turbo_tasks::value_impl]
168impl Asset for PostCssTransformedAsset {
169    #[turbo_tasks::function]
170    async fn content(self: ResolvedVc<Self>) -> Result<Vc<AssetContent>> {
171        let this = self.await?;
172        Ok(*transform_process_operation(self)
173            .issue_file_path(this.source.ident().path(), "PostCSS processing")
174            .await?
175            .connect()
176            .await?
177            .content)
178    }
179}
180
181#[turbo_tasks::function(operation)]
182fn transform_process_operation(
183    asset: ResolvedVc<PostCssTransformedAsset>,
184) -> Vc<ProcessPostCssResult> {
185    asset.process()
186}
187
188#[turbo_tasks::value]
189struct ProcessPostCssResult {
190    content: ResolvedVc<AssetContent>,
191    assets: Vec<ResolvedVc<VirtualSource>>,
192}
193
194#[turbo_tasks::function]
195async fn config_changed(
196    asset_context: Vc<Box<dyn AssetContext>>,
197    postcss_config_path: Vc<FileSystemPath>,
198) -> Result<Vc<Completion>> {
199    let config_asset = asset_context
200        .process(
201            Vc::upcast(FileSource::new(postcss_config_path)),
202            Value::new(ReferenceType::Internal(
203                InnerAssets::empty().to_resolved().await?,
204            )),
205        )
206        .module();
207
208    Ok(Vc::<Completions>::cell(vec![
209        any_content_changed_of_module(config_asset)
210            .to_resolved()
211            .await?,
212        extra_configs_changed(asset_context, postcss_config_path)
213            .to_resolved()
214            .await?,
215    ])
216    .completed())
217}
218
219#[turbo_tasks::function]
220async fn extra_configs_changed(
221    asset_context: Vc<Box<dyn AssetContext>>,
222    postcss_config_path: Vc<FileSystemPath>,
223) -> Result<Vc<Completion>> {
224    let parent_path = postcss_config_path.parent();
225
226    let config_paths = [
227        parent_path.join("tailwind.config.js".into()),
228        parent_path.join("tailwind.config.mjs".into()),
229        parent_path.join("tailwind.config.ts".into()),
230    ];
231
232    let configs = config_paths
233        .into_iter()
234        .map(|path| async move {
235            Ok(
236                if matches!(&*path.get_type().await?, FileSystemEntryType::File) {
237                    match *asset_context
238                        .process(
239                            Vc::upcast(FileSource::new(path)),
240                            Value::new(ReferenceType::Internal(
241                                InnerAssets::empty().to_resolved().await?,
242                            )),
243                        )
244                        .try_into_module()
245                        .await?
246                    {
247                        Some(module) => {
248                            Some(any_content_changed_of_module(*module).to_resolved().await?)
249                        }
250                        None => None,
251                    }
252                } else {
253                    None
254                },
255            )
256        })
257        .try_flat_join()
258        .await?;
259
260    Ok(Vc::<Completions>::cell(configs).completed())
261}
262
263#[turbo_tasks::value]
264pub struct JsonSource {
265    pub path: ResolvedVc<FileSystemPath>,
266    pub key: ResolvedVc<Option<RcStr>>,
267    pub allow_json5: bool,
268}
269
270#[turbo_tasks::value_impl]
271impl JsonSource {
272    #[turbo_tasks::function]
273    pub fn new(
274        path: ResolvedVc<FileSystemPath>,
275        key: ResolvedVc<Option<RcStr>>,
276        allow_json5: bool,
277    ) -> Vc<Self> {
278        JsonSource {
279            path,
280            key,
281            allow_json5,
282        }
283        .cell()
284    }
285}
286
287#[turbo_tasks::value_impl]
288impl Source for JsonSource {
289    #[turbo_tasks::function]
290    async fn ident(&self) -> Result<Vc<AssetIdent>> {
291        match &*self.key.await? {
292            Some(key) => Ok(AssetIdent::from_path(
293                self.path
294                    .append(".".into())
295                    .append(key.clone())
296                    .append(".json".into()),
297            )),
298            None => Ok(AssetIdent::from_path(self.path.append(".json".into()))),
299        }
300    }
301}
302
303#[turbo_tasks::value_impl]
304impl Asset for JsonSource {
305    #[turbo_tasks::function]
306    async fn content(&self) -> Result<Vc<AssetContent>> {
307        let file_type = &*self.path.get_type().await?;
308        match file_type {
309            FileSystemEntryType::File => {
310                let json = if self.allow_json5 {
311                    self.path.read_json5().content().await?
312                } else {
313                    self.path.read_json().content().await?
314                };
315                let value = match &*self.key.await? {
316                    Some(key) => {
317                        let Some(value) = json.get(&**key) else {
318                            anyhow::bail!("Invalid file type {:?}", file_type)
319                        };
320                        value
321                    }
322                    None => &*json,
323                };
324                Ok(AssetContent::file(File::from(value.to_string()).into()))
325            }
326            FileSystemEntryType::NotFound => {
327                Ok(AssetContent::File(FileContent::NotFound.resolved_cell()).cell())
328            }
329            _ => Err(anyhow::anyhow!("Invalid file type {:?}", file_type)),
330        }
331    }
332}
333
334#[turbo_tasks::function]
335pub(crate) async fn config_loader_source(
336    project_path: Vc<FileSystemPath>,
337    postcss_config_path: Vc<FileSystemPath>,
338) -> Result<Vc<Box<dyn Source>>> {
339    let postcss_config_path_value = &*postcss_config_path.await?;
340    let postcss_config_path_filename = postcss_config_path_value.file_name();
341
342    if postcss_config_path_filename == "package.json" {
343        return Ok(Vc::upcast(JsonSource::new(
344            postcss_config_path,
345            Vc::cell(Some("postcss".into())),
346            false,
347        )));
348    }
349
350    if postcss_config_path_value.path.ends_with(".json")
351        || postcss_config_path_filename == ".postcssrc"
352    {
353        return Ok(Vc::upcast(JsonSource::new(
354            postcss_config_path,
355            Vc::cell(None),
356            true,
357        )));
358    }
359
360    // We can only load js files with `import()`.
361    if !postcss_config_path_value.path.ends_with(".js") {
362        return Ok(Vc::upcast(FileSource::new(postcss_config_path)));
363    }
364
365    let Some(config_path) = project_path
366        .await?
367        .get_relative_path_to(postcss_config_path_value)
368    else {
369        bail!("Unable to get relative path to postcss config");
370    };
371
372    // We don't want to bundle the config file, so we load it with `import()`.
373    // Bundling would break the ability to use `require.resolve` in the config file.
374    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".into()),
393        AssetContent::file(File::from(code).into()),
394    )))
395}
396
397#[turbo_tasks::function]
398async fn postcss_executor(
399    asset_context: Vc<Box<dyn AssetContext>>,
400    project_path: Vc<FileSystemPath>,
401    postcss_config_path: Vc<FileSystemPath>,
402) -> Result<Vc<ProcessResult>> {
403    let config_asset = asset_context
404        .process(
405            config_loader_source(project_path, postcss_config_path),
406            Value::new(ReferenceType::Entry(EntryReferenceSubType::Undefined)),
407        )
408        .module()
409        .to_resolved()
410        .await?;
411
412    Ok(asset_context.process(
413        Vc::upcast(FileSource::new(embed_file_path(
414            "transforms/postcss.ts".into(),
415        ))),
416        Value::new(ReferenceType::Internal(ResolvedVc::cell(fxindexmap! {
417            "CONFIG".into() => config_asset
418        }))),
419    ))
420}
421
422async fn find_config_in_location(
423    project_path: Vc<FileSystemPath>,
424    location: PostCssConfigLocation,
425    source: Vc<Box<dyn Source>>,
426) -> Result<Option<Vc<FileSystemPath>>> {
427    if let FindContextFileResult::Found(config_path, _) = *find_context_file_or_package_key(
428        project_path,
429        postcss_configs(),
430        Value::new("postcss".into()),
431    )
432    .await?
433    {
434        return Ok(Some(*config_path));
435    }
436
437    if matches!(location, PostCssConfigLocation::ProjectPathOrLocalPath) {
438        if let FindContextFileResult::Found(config_path, _) = *find_context_file_or_package_key(
439            source.ident().path().parent(),
440            postcss_configs(),
441            Value::new("postcss".into()),
442        )
443        .await?
444        {
445            return Ok(Some(*config_path));
446        }
447    }
448
449    Ok(None)
450}
451
452#[turbo_tasks::value_impl]
453impl GenerateSourceMap for PostCssTransformedAsset {
454    #[turbo_tasks::function]
455    async fn generate_source_map(&self) -> Result<Vc<OptionStringifiedSourceMap>> {
456        let source = Vc::try_resolve_sidecast::<Box<dyn GenerateSourceMap>>(*self.source).await?;
457        match source {
458            Some(source) => Ok(source.generate_source_map()),
459            None => Ok(Vc::cell(None)),
460        }
461    }
462}
463
464#[turbo_tasks::value_impl]
465impl PostCssTransformedAsset {
466    #[turbo_tasks::function]
467    async fn process(&self) -> Result<Vc<ProcessPostCssResult>> {
468        let ExecutionContext {
469            project_path,
470            chunking_context,
471            env,
472        } = &*self.execution_context.await?;
473
474        // For this postcss transform, there is no gaurantee that looking up for the
475        // source path will arrives specific project config for the postcss.
476        // i.e, this is possible
477        // - root
478        //  - node_modules
479        //     - somepkg/(some.module.css, postcss.config.js) // this could be symlinked local, or
480        //       actual remote pkg or anything
481        //  - packages // root of workspace pkgs
482        //     - pkg1/(postcss.config.js) // The actual config we're looking for
483        //
484        // We look for the config in the project path first, then the source path
485        let Some(config_path) =
486            find_config_in_location(**project_path, self.config_location, *self.source).await?
487        else {
488            return Ok(ProcessPostCssResult {
489                content: self.source.content().to_resolved().await?,
490                assets: Vec::new(),
491            }
492            .cell());
493        };
494
495        let source_content = self.source.content();
496        let AssetContent::File(file) = *source_content.await? else {
497            bail!("PostCSS transform only support transforming files");
498        };
499        let FileContent::Content(content) = &*file.await? else {
500            return Ok(ProcessPostCssResult {
501                content: AssetContent::File(FileContent::NotFound.resolved_cell()).resolved_cell(),
502                assets: Vec::new(),
503            }
504            .cell());
505        };
506        let content = content.content().to_str()?;
507        let evaluate_context = self.evaluate_context;
508        let source_map = self.source_map;
509
510        // This invalidates the transform when the config changes.
511        let config_changed = config_changed(*evaluate_context, config_path)
512            .to_resolved()
513            .await?;
514
515        let postcss_executor = postcss_executor(*evaluate_context, **project_path, config_path)
516            .module()
517            .to_resolved()
518            .await?;
519        let css_fs_path = self.source.ident().path();
520
521        // We need to get a path relative to the project because the postcss loader
522        // runs with the project as the current working directory.
523        let css_path = if let Some(css_path) = project_path
524            .await?
525            .get_relative_path_to(&*css_fs_path.await?)
526        {
527            css_path.into_owned()
528        } else {
529            // This shouldn't be an error since it can happen on virtual assets
530            "".into()
531        };
532
533        let config_value = evaluate_webpack_loader(WebpackLoaderContext {
534            module_asset: postcss_executor,
535            cwd: *project_path,
536            env: *env,
537            context_ident_for_issue: self.source.ident().to_resolved().await?,
538            asset_context: evaluate_context,
539            chunking_context: *chunking_context,
540            resolve_options_context: None,
541            args: vec![
542                ResolvedVc::cell(content.into()),
543                ResolvedVc::cell(css_path.into()),
544                ResolvedVc::cell(source_map.into()),
545            ],
546            additional_invalidation: config_changed,
547        })
548        .await?;
549
550        let SingleValue::Single(val) = config_value.try_into_single().await? else {
551            // An error happened, which has already been converted into an issue.
552            return Ok(ProcessPostCssResult {
553                content: AssetContent::File(FileContent::NotFound.resolved_cell()).resolved_cell(),
554                assets: Vec::new(),
555            }
556            .cell());
557        };
558        let processed_css: PostCssProcessingResult = parse_json_with_source_context(val.to_str()?)
559            .context("Unable to deserializate response from PostCSS transform operation")?;
560
561        // TODO handle SourceMap
562        let file = File::from(processed_css.css);
563        let assets = emitted_assets_to_virtual_sources(processed_css.assets).await?;
564        let content =
565            AssetContent::File(FileContent::Content(file).resolved_cell()).resolved_cell();
566        Ok(ProcessPostCssResult { content, assets }.cell())
567    }
568}
569
570#[turbo_tasks::value]
571struct PostCssTransformIssue {
572    source: ResolvedVc<FileSystemPath>,
573    description: RcStr,
574    severity: ResolvedVc<IssueSeverity>,
575    title: RcStr,
576}
577
578#[turbo_tasks::value_impl]
579impl Issue for PostCssTransformIssue {
580    #[turbo_tasks::function]
581    fn file_path(&self) -> Vc<FileSystemPath> {
582        *self.source
583    }
584
585    #[turbo_tasks::function]
586    fn title(&self) -> Vc<StyledString> {
587        StyledString::Text(self.title.clone()).cell()
588    }
589
590    #[turbo_tasks::function]
591    fn description(&self) -> Vc<OptionStyledString> {
592        Vc::cell(Some(
593            StyledString::Text(self.description.clone()).resolved_cell(),
594        ))
595    }
596
597    #[turbo_tasks::function]
598    fn severity(&self) -> Vc<IssueSeverity> {
599        *self.severity
600    }
601
602    #[turbo_tasks::function]
603    fn stage(&self) -> Vc<IssueStage> {
604        IssueStage::Transform.cell()
605    }
606}