Repository: vercel/next.js
Pull request: Turbopack next/font: Use a custom enum instead of `Result` for failed local font files

Previously, we used `anyhow::Result` for this, but this conflicts with how we use `Error` in Turbo Tasks as system-level exceptions.

Test Plan: `pnpm test-dev-turbo test/e2e/app-dir/next-font/next-font.test.ts`


Closes PACK-4569

Changed files:
filename: crates/next-core/src/next_font/font_fallback.rs
patch: @@ -60,7 +60,7 @@ impl FontFallback {
 }
 
 #[turbo_tasks::value(transparent)]
-pub(crate) struct FontFallbacks(Vec<ResolvedVc<FontFallback>>);
+pub(crate) struct FontFallbacks(pub Vec<ResolvedVc<FontFallback>>);
 
 #[turbo_tasks::value_impl]
 impl FontFallbacks {

filename: crates/next-core/src/next_font/local/errors.rs
patch: @@ -1,8 +1,19 @@
-use thiserror::Error;
+use std::fmt::Display;
+
+use serde::{Deserialize, Serialize};
 use turbo_rcstr::RcStr;
+use turbo_tasks::{trace::TraceRawVcs, NonLocalValue};
+
+pub(crate) enum FontResult<T> {
+    Ok(T),
+    FontFileNotFound(FontFileNotFound),
+}
+
+#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, NonLocalValue, TraceRawVcs)]
+pub(crate) struct FontFileNotFound(pub RcStr);
 
-#[derive(Debug, Error)]
-pub enum FontError {
-    #[error("could not find font file")]
-    FontFileNotFound(RcStr),
+impl Display for FontFileNotFound {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "Font file not found: Can't resolve {}'", self.0)
+    }
 }

filename: crates/next-core/src/next_font/local/font_fallback.rs
patch: @@ -7,6 +7,7 @@ use turbo_tasks::{ResolvedVc, Vc};
 use turbo_tasks_fs::{FileContent, FileSystemPath};
 
 use super::{
+    errors::{FontFileNotFound, FontResult},
     options::{FontDescriptor, FontDescriptors, FontWeight, NextFontLocalOptions},
     request::AdjustFontFallback,
 };
@@ -15,10 +16,15 @@ use crate::next_font::{
         AutomaticFontFallback, DefaultFallbackFont, FontAdjustment, FontFallback, FontFallbacks,
         DEFAULT_SANS_SERIF_FONT, DEFAULT_SERIF_FONT,
     },
-    local::errors::FontError,
     util::{get_scoped_font_family, FontFamilyType},
 };
 
+#[turbo_tasks::value(shared)]
+pub(crate) enum FontFallbackResult {
+    Ok(ResolvedVc<FontFallbacks>),
+    FontFileNotFound(FontFileNotFound),
+}
+
 // From
 // https://github.com/vercel/next.js/blob/7457be0c74e64b4d0617943ed27f4d557cc916be/packages/font/src/local/get-fallback-metrics-from-font-file.ts#L34
 static AVG_CHARACTERS: &str = "aaabcdeeeefghiijklmnnoopqrrssttuvwxyz      ";
@@ -29,56 +35,76 @@ static BOLD_WEIGHT: f64 = 700.0;
 pub(super) async fn get_font_fallbacks(
     lookup_path: Vc<FileSystemPath>,
     options_vc: Vc<NextFontLocalOptions>,
-) -> Result<Vc<FontFallbacks>> {
+) -> Result<Vc<FontFallbackResult>> {
     let options = &*options_vc.await?;
-    let mut font_fallbacks = vec![];
     let scoped_font_family =
         get_scoped_font_family(FontFamilyType::Fallback.cell(), options_vc.font_family());
 
+    let mut font_fallbacks = vec![];
     match options.adjust_font_fallback {
-        AdjustFontFallback::Arial => font_fallbacks.push(
-            FontFallback::Automatic(AutomaticFontFallback {
-                scoped_font_family: scoped_font_family.to_resolved().await?,
-                local_font_family: ResolvedVc::cell("Arial".into()),
-                adjustment: Some(
-                    get_font_adjustment(lookup_path, options_vc, &DEFAULT_SANS_SERIF_FONT).await?,
+        AdjustFontFallback::Arial => {
+            let adjustment =
+                get_font_adjustment(lookup_path, options_vc, &DEFAULT_SANS_SERIF_FONT).await?;
+
+            match adjustment {
+                FontResult::Ok(adjustment) => font_fallbacks.push(
+                    FontFallback::Automatic(AutomaticFontFallback {
+                        scoped_font_family: scoped_font_family.to_resolved().await?,
+                        local_font_family: ResolvedVc::cell("Arial".into()),
+                        adjustment: Some(adjustment),
+                    })
+                    .resolved_cell(),
                 ),
-            })
-            .resolved_cell(),
-        ),
-        AdjustFontFallback::TimesNewRoman => font_fallbacks.push(
-            FontFallback::Automatic(AutomaticFontFallback {
-                scoped_font_family: scoped_font_family.to_resolved().await?,
-                local_font_family: ResolvedVc::cell("Times New Roman".into()),
-                adjustment: Some(
-                    get_font_adjustment(lookup_path, options_vc, &DEFAULT_SERIF_FONT).await?,
+                FontResult::FontFileNotFound(err) => {
+                    return Ok(FontFallbackResult::FontFileNotFound(err).cell())
+                }
+            };
+        }
+        AdjustFontFallback::TimesNewRoman => {
+            let adjustment =
+                get_font_adjustment(lookup_path, options_vc, &DEFAULT_SERIF_FONT).await?;
+
+            match adjustment {
+                FontResult::Ok(adjustment) => font_fallbacks.push(
+                    FontFallback::Automatic(AutomaticFontFallback {
+                        scoped_font_family: scoped_font_family.to_resolved().await?,
+                        local_font_family: ResolvedVc::cell("Times New Roman".into()),
+                        adjustment: Some(adjustment),
+                    })
+                    .resolved_cell(),
                 ),
-            })
-            .resolved_cell(),
-        ),
+                FontResult::FontFileNotFound(err) => {
+                    return Ok(FontFallbackResult::FontFileNotFound(err).cell())
+                }
+            };
+        }
         AdjustFontFallback::None => (),
     };
 
     if let Some(fallback) = &options.fallback {
         font_fallbacks.push(FontFallback::Manual(fallback.clone()).resolved_cell());
     }
 
-    Ok(Vc::cell(font_fallbacks))
+    Ok(FontFallbackResult::Ok(FontFallbacks(font_fallbacks).resolved_cell()).cell())
 }
 
 async fn get_font_adjustment(
     lookup_path: Vc<FileSystemPath>,
     options: Vc<NextFontLocalOptions>,
     fallback_font: &DefaultFallbackFont,
-) -> Result<FontAdjustment> {
+) -> Result<FontResult<FontAdjustment>> {
     let options = &*options.await?;
     let main_descriptor = pick_font_for_fallback_generation(&options.fonts)?;
     let font_file = &*lookup_path
         .join(main_descriptor.path.clone())
         .read()
         .await?;
     let font_file_rope = match font_file {
-        FileContent::NotFound => bail!(FontError::FontFileNotFound(main_descriptor.path.clone())),
+        FileContent::NotFound => {
+            return Ok(FontResult::FontFileNotFound(FontFileNotFound(
+                main_descriptor.path.clone(),
+            )));
+        }
         FileContent::Content(file) => file.content(),
     };
 
@@ -106,12 +132,12 @@ async fn get_font_adjustment(
         None => 1.0,
     };
 
-    Ok(FontAdjustment {
+    Ok(FontResult::Ok(FontAdjustment {
         ascent: font.hhea_table.ascender as f64 / (units_per_em * size_adjust),
         descent: font.hhea_table.descender as f64 / (units_per_em * size_adjust),
         line_gap: font.hhea_table.line_gap as f64 / (units_per_em * size_adjust),
         size_adjust,
-    })
+    }))
 }
 
 fn calc_average_width(font: &mut Font<DynamicFontTableProvider>) -> Option<f32> {

filename: crates/next-core/src/next_font/local/mod.rs
patch: @@ -1,4 +1,5 @@
 use anyhow::{bail, Context, Result};
+use font_fallback::FontFallbackResult;
 use indoc::formatdoc;
 use serde::{Deserialize, Serialize};
 use turbo_rcstr::RcStr;
@@ -31,12 +32,12 @@ use super::{
 use crate::{
     next_app::metadata::split_extension,
     next_font::{
-        local::{errors::FontError, options::FontWeight},
+        local::options::FontWeight,
         util::{get_request_hash, get_request_id},
     },
 };
 
-mod errors;
+pub mod errors;
 pub mod font_fallback;
 pub mod options;
 pub mod request;
@@ -106,33 +107,26 @@ impl BeforeResolvePlugin for NextFontLocalResolvePlugin {
                 let request_hash = get_request_hash(&query).await?;
                 let qstr = qstring::QString::from(query.as_str());
                 let options_vc = font_options_from_query_map(**query_vc);
-                let font_fallbacks = get_font_fallbacks(lookup_path, options_vc);
-                let properties = get_font_css_properties(options_vc, font_fallbacks).await;
 
+                let font_fallbacks = &*get_font_fallbacks(lookup_path, options_vc).await?;
                 let lookup_path = lookup_path.to_resolved().await?;
-                if let Err(e) = &properties {
-                    for source_error in e.chain() {
-                        if let Some(FontError::FontFileNotFound(font_path)) =
-                            source_error.downcast_ref::<FontError>()
-                        {
-                            FontResolvingIssue {
-                                origin_path: lookup_path,
-                                font_path: ResolvedVc::cell(font_path.clone()),
-                            }
-                            .resolved_cell()
-                            .emit();
-
-                            return Ok(ResolveResultOption::some(*ResolveResult::primary(
-                                ResolveResultItem::Error(ResolvedVc::cell(
-                                    format!("Font file not found: Can't resolve {}'", font_path)
-                                        .into(),
-                                )),
-                            )));
+                let font_fallbacks = match font_fallbacks {
+                    FontFallbackResult::FontFileNotFound(err) => {
+                        FontResolvingIssue {
+                            origin_path: lookup_path,
+                            font_path: ResolvedVc::cell(err.0.clone()),
                         }
+                        .resolved_cell()
+                        .emit();
+
+                        return Ok(ResolveResultOption::some(*ResolveResult::primary(
+                            ResolveResultItem::Error(ResolvedVc::cell(err.to_string().into())),
+                        )));
                     }
-                }
+                    FontFallbackResult::Ok(font_fallbacks) => *font_fallbacks,
+                };
 
-                let properties = properties?;
+                let properties = get_font_css_properties(options_vc, *font_fallbacks).await?;
                 let file_content = formatdoc!(
                     r#"
                     import cssModule from "@vercel/turbopack-next/internal/font/local/cssmodule.module.css?{}";
@@ -194,7 +188,22 @@ impl BeforeResolvePlugin for NextFontLocalResolvePlugin {
                     )
                     .into(),
                 );
-                let fallback = get_font_fallbacks(lookup_path, options);
+                let fallback = &*get_font_fallbacks(lookup_path, options).await?;
+                let fallback = match fallback {
+                    FontFallbackResult::FontFileNotFound(err) => {
+                        FontResolvingIssue {
+                            origin_path: lookup_path.to_resolved().await?,
+                            font_path: ResolvedVc::cell(err.0.clone()),
+                        }
+                        .resolved_cell()
+                        .emit();
+
+                        return Ok(ResolveResultOption::some(*ResolveResult::primary(
+                            ResolveResultItem::Error(ResolvedVc::cell(err.to_string().into())),
+                        )));
+                    }
+                    FontFallbackResult::Ok(font_fallbacks) => **font_fallbacks,
+                };
 
                 let stylesheet = build_stylesheet(
                     font_options_from_query_map(**query_vc),