Repository: vercel/next.js
Pull request: Turbopack: improve Lightning CSS error handling

Don't return the `Result:Err`, but turn into an issue and return `Unparseable`

Also fixes the location translation: Lightning CSS uses 1-based columns, while Turbopack uses 0-based column indices.

Closes PACK-5234

Changed files:
filename: test/development/acceptance-app/ReactRefreshLogBox.test.ts
patch: @@ -415,13 +415,13 @@ describe('ReactRefreshLogBox app', () => {
     if (isTurbopack) {
       await expect(browser).toDisplayRedbox(`
        {
-         "description": "Parsing css source code failed",
+         "description": "Parsing CSS source code failed",
          "environmentLabel": null,
          "label": "Build Error",
-         "source": "./index.module.css (1:9)
-       Parsing css source code failed
+         "source": "./index.module.css (1:8)
+       Parsing CSS source code failed
        > 1 | .button
-           |         ^",
+           |        ^",
          "stack": [],
        }
       `)
@@ -447,11 +447,11 @@ describe('ReactRefreshLogBox app', () => {
     if (isTurbopack) {
       await expect(browser).toDisplayRedbox(`
        {
-         "description": "Parsing css source code failed",
+         "description": "Transforming CSS failed",
          "environmentLabel": null,
          "label": "Build Error",
          "source": "./index.module.css
-       Parsing css source code failed
+       Transforming CSS failed
        Selector "button" is not pure. Pure selectors must contain at least one local class or id.
        Import traces:
          Client Component Browser:

filename: test/development/acceptance/ReactRefreshLogBox.test.ts
patch: @@ -578,13 +578,13 @@ describe('ReactRefreshLogBox', () => {
     if (isTurbopack) {
       await expect(browser).toDisplayRedbox(`
        {
-         "description": "Parsing css source code failed",
+         "description": "Parsing CSS source code failed",
          "environmentLabel": null,
          "label": "Build Error",
-         "source": "./index.module.css (1:9)
-       Parsing css source code failed
+         "source": "./index.module.css (1:8)
+       Parsing CSS source code failed
        > 1 | .button
-           |         ^",
+           |        ^",
          "stack": [],
        }
       `)
@@ -610,11 +610,11 @@ describe('ReactRefreshLogBox', () => {
     if (isTurbopack) {
       await expect(browser).toDisplayRedbox(`
        {
-         "description": "Parsing css source code failed",
+         "description": "Transforming CSS failed",
          "environmentLabel": null,
          "label": "Build Error",
          "source": "./index.module.css
-       Parsing css source code failed
+       Transforming CSS failed
        Selector "button" is not pure. Pure selectors must contain at least one local class or id.
        Import traces:
          Browser:

filename: test/development/sass-error/index.test.ts
patch: @@ -27,7 +27,7 @@ describe('app dir - css', () => {
           // css-loader does not report an error for this case
           expect(source).toMatchInlineSnapshot(`
            "./app/global.scss.css (45:1)
-           Parsing css source code failed
+           Parsing CSS source code failed
              43 | }
              44 |
            > 45 | input.defaultCheckbox::before path {
@@ -36,7 +36,7 @@ describe('app dir - css', () => {
              47 | }
              48 |
 
-           Pseudo-elements like '::before' or '::after' can't be followed by selectors like 'Ident("path")' at [project]/app/global.scss.css:0:884
+           Pseudo-elements like '::before' or '::after' can't be followed by selectors like 'Ident("path")'
 
            Import trace:
              Client Component Browser:

filename: turbopack/crates/turbopack-core/src/issue/mod.rs
patch: @@ -767,8 +767,8 @@ async fn into_plain_trace(traces: Vec<Vec<ReadRef<AssetIdent>>>) -> Result<Vec<P
     Ok(plain_traces)
 }
 
-#[turbo_tasks::value(shared, serialization = "none")]
-#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
+#[turbo_tasks::value(shared)]
+#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash)]
 pub enum IssueStage {
     Config,
     AppStructure,

filename: turbopack/crates/turbopack-css/src/process.rs
patch: @@ -1,6 +1,6 @@
 use std::sync::{Arc, RwLock};
 
-use anyhow::{Context, Result, bail};
+use anyhow::{Result, bail};
 use lightningcss::{
     css_modules::{CssModuleExport, CssModuleExports, Pattern, Segment},
     stylesheet::{MinifyOptions, ParserOptions, PrinterOptions, StyleSheet, ToCssResult},
@@ -466,8 +466,8 @@ async fn process_content(
 
                 // We need to collect here because we need to avoid holding the lock while calling
                 // `.await` in the loop.
-                let warngins = warnings.read().unwrap().iter().cloned().collect::<Vec<_>>();
-                for err in warngins.iter() {
+                let warnings = warnings.read().unwrap().iter().cloned().collect::<Vec<_>>();
+                for err in warnings.iter() {
                     match err.kind {
                         lightningcss::error::ParserError::UnexpectedToken(_)
                         | lightningcss::error::ParserError::UnexpectedImportRule
@@ -477,15 +477,16 @@ async fn process_content(
                                 Some(loc) => {
                                     let pos = SourcePos {
                                         line: loc.line as _,
-                                        column: loc.column as _,
+                                        column: (loc.column - 1) as _,
                                     };
                                     IssueSource::from_line_col(source, pos, pos)
                                 }
                                 None => IssueSource::from_source_only(source),
                             };
 
                             ParsingIssue {
-                                msg: err.to_string().into(),
+                                msg: err.kind.to_string().into(),
+                                stage: IssueStage::Parse,
                                 source,
                             }
                             .resolved_cell()
@@ -506,13 +507,30 @@ async fn process_content(
                 // minify() is actually transform, and it performs operations like CSS modules
                 // handling.
                 //
-                //
                 // See: https://github.com/parcel-bundler/lightningcss/issues/935#issuecomment-2739325537
-                ss.minify(MinifyOptions {
+                if let Err(e) = ss.minify(MinifyOptions {
                     targets,
                     ..Default::default()
-                })
-                .context("failed to transform css")?;
+                }) {
+                    let source = match &e.loc {
+                        Some(loc) => {
+                            let pos = SourcePos {
+                                line: loc.line as _,
+                                column: (loc.column - 1) as _,
+                            };
+                            IssueSource::from_line_col(source, pos, pos)
+                        }
+                        None => IssueSource::from_source_only(source),
+                    };
+                    ParsingIssue {
+                        msg: e.kind.to_string().into(),
+                        stage: IssueStage::Transform,
+                        source,
+                    }
+                    .resolved_cell()
+                    .emit();
+                    return Ok(ParseCssResult::Unparsable.cell());
+                }
 
                 stylesheet_into_static(&ss, without_warnings(config.clone()))
             }
@@ -521,14 +539,15 @@ async fn process_content(
                     Some(loc) => {
                         let pos = SourcePos {
                             line: loc.line as _,
-                            column: loc.column as _,
+                            column: (loc.column - 1) as _,
                         };
                         IssueSource::from_line_col(source, pos, pos)
                     }
                     None => IssueSource::from_source_only(source),
                 };
                 ParsingIssue {
-                    msg: e.to_string().into(),
+                    msg: e.kind.to_string().into(),
+                    stage: IssueStage::Parse,
                     source,
                 }
                 .resolved_cell()
@@ -581,6 +600,7 @@ impl CssError {
                          least one local class or id."
                     )
                     .into(),
+                    stage: IssueStage::Transform,
                     // TODO: This should include the location of the selector in the file.
                     source: IssueSource::from_source_only(source),
                 }
@@ -680,6 +700,7 @@ fn generate_css_source_map(source_map: &parcel_sourcemap::SourceMap) -> Result<R
 #[turbo_tasks::value]
 struct ParsingIssue {
     msg: RcStr,
+    stage: IssueStage,
     source: IssueSource,
 }
 
@@ -692,12 +713,17 @@ impl Issue for ParsingIssue {
 
     #[turbo_tasks::function]
     fn stage(&self) -> Vc<IssueStage> {
-        IssueStage::Parse.cell()
+        self.stage.clone().cell()
     }
 
     #[turbo_tasks::function]
     fn title(&self) -> Vc<StyledString> {
-        StyledString::Text(rcstr!("Parsing css source code failed")).cell()
+        StyledString::Text(match self.stage {
+            IssueStage::Parse => rcstr!("Parsing CSS source code failed"),
+            IssueStage::Transform => rcstr!("Transforming CSS failed"),
+            _ => rcstr!("CSS processing failed"),
+        })
+        .cell()
     }
 
     #[turbo_tasks::function]