Repository: vercel/next.js
Pull request: Turbopack: Implement next/font/local declarations option

## What?

Fixes #82822. Implements `declarations` which was missing in the implementation of `next/font/local`

Closes PACK-5711

Changed files:
filename: crates/next-core/src/next_font/local/options.rs
patch: @@ -9,6 +9,7 @@ use super::request::{
     AdjustFontFallback, NextFontLocalRequest, NextFontLocalRequestArguments, SrcDescriptor,
     SrcRequest,
 };
+use crate::next_font::local::request::NextFontLocalDeclaration;
 
 /// A normalized, Vc-friendly struct derived from validating and transforming
 /// [[NextFontLocalRequest]]
@@ -32,6 +33,8 @@ pub(super) struct NextFontLocalOptions {
     /// The name of the variable assigned to the results of calling the
     /// `localFont` function. This is used as the font family's base name.
     pub variable_name: RcStr,
+    /// A list of custom properties to be included in the @font-face declaration.
+    pub declarations: Option<Vec<NextFontLocalDeclaration>>,
 }
 
 impl NextFontLocalOptions {
@@ -174,6 +177,7 @@ pub(super) fn options_from_request(request: &NextFontLocalRequest) -> Result<Nex
         src,
         adjust_font_fallback,
         variable,
+        declarations,
     } = &request.arguments.0;
 
     let fonts = match src {
@@ -202,6 +206,15 @@ pub(super) fn options_from_request(request: &NextFontLocalRequest) -> Result<Nex
         variable_name: request.variable_name.to_owned(),
         default_weight: weight.as_ref().and_then(|s| s.parse().ok()),
         default_style: style.to_owned(),
+        declarations: declarations.as_ref().map(|decls| {
+            decls
+                .iter()
+                .map(|decl| NextFontLocalDeclaration {
+                    prop: decl.prop.clone(),
+                    value: decl.value.clone(),
+                })
+                .collect()
+        }),
     })
 }
 
@@ -248,7 +261,8 @@ mod tests {
                 fallback: None,
                 adjust_font_fallback: AdjustFontFallback::Arial,
                 variable: None,
-                variable_name: rcstr!("myFont")
+                variable_name: rcstr!("myFont"),
+                declarations: None,
             },
         );
 
@@ -303,7 +317,8 @@ mod tests {
                 fallback: None,
                 adjust_font_fallback: AdjustFontFallback::Arial,
                 variable: None,
-                variable_name: rcstr!("myFont")
+                variable_name: rcstr!("myFont"),
+                declarations: None,
             },
         );
 
@@ -377,7 +392,8 @@ mod tests {
                 fallback: Some(vec![rcstr!("Fallback")]),
                 adjust_font_fallback: AdjustFontFallback::TimesNewRoman,
                 variable: Some(rcstr!("myvar")),
-                variable_name: rcstr!("myFont")
+                variable_name: rcstr!("myFont"),
+                declarations: None,
             },
         );
 

filename: crates/next-core/src/next_font/local/request.rs
patch: @@ -12,6 +12,25 @@ pub(super) struct NextFontLocalRequest {
     pub variable_name: RcStr,
 }
 
+#[derive(
+    Clone,
+    Debug,
+    PartialEq,
+    Eq,
+    PartialOrd,
+    Ord,
+    Hash,
+    TaskInput,
+    Serialize,
+    Deserialize,
+    TraceRawVcs,
+    NonLocalValue,
+)]
+pub(super) struct NextFontLocalDeclaration {
+    pub prop: RcStr,
+    pub value: RcStr,
+}
+
 #[derive(Debug, Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub(super) struct NextFontLocalRequestArguments {
@@ -29,6 +48,7 @@ pub(super) struct NextFontLocalRequestArguments {
     )]
     pub adjust_font_fallback: AdjustFontFallback,
     pub variable: Option<RcStr>,
+    pub declarations: Option<Vec<NextFontLocalDeclaration>>,
 }
 
 #[derive(Debug, Deserialize)]

filename: crates/next-core/src/next_font/local/stylesheet.rs
patch: @@ -1,5 +1,6 @@
 use anyhow::{Result, bail};
 use indoc::formatdoc;
+use itertools::Itertools;
 use turbo_rcstr::RcStr;
 use turbo_tasks::Vc;
 
@@ -64,12 +65,20 @@ pub(super) async fn build_font_face_definitions(
         definitions.push_str(&formatdoc!(
             r#"
                 @font-face {{
+                    {}
                     font-family: '{}';
                     src: url('@vercel/turbopack-next/internal/font/local/font?{}') format('{}');
                     font-display: {};
                     {}{}
                 }}
             "#,
+            options.declarations.as_ref().map_or_else(
+                || "".to_owned(),
+                |declarations| declarations
+                    .iter()
+                    .map(|declaration| format!("{}: {};", declaration.prop, declaration.value))
+                    .join("\n")
+            ),
             scoped_font_family,
             query_str,
             ext_to_format(&font.ext)?,

filename: test/e2e/next-font/app/pages/with-local-fonts.js
patch: @@ -6,6 +6,7 @@ const myFont1 = localFont({
   weight: '100',
   fallback: ['system-ui'],
   adjustFontFallback: 'Times New Roman',
+  declarations: [{ prop: 'ascent-override', value: '90%' }],
 })
 const myFont2 = localFont({
   src: '../fonts/my-other-font.woff2',

filename: test/e2e/next-font/index.test.ts
patch: @@ -642,4 +642,28 @@ describe('next/font', () => {
       })
     })
   })
+
+  describe('custom declarations', () => {
+    test('local font with custom declarations', async () => {
+      const browser = await webdriver(next.url, '/with-local-fonts')
+
+      // Get all stylesheets
+      const stylesheets = await browser.eval(`
+        Array.from(document.styleSheets)
+          .map(sheet => {
+            try {
+              return Array.from(sheet.cssRules || sheet.rules || [])
+                .map(rule => rule.cssText)
+                .join('\\n')
+            } catch (e) {
+              return ''
+            }
+          })
+          .join('\\n')
+      `)
+
+      // Check that the custom declaration is included in the CSS
+      expect(stylesheets).toContain('ascent-override: 90%')
+    })
+  })
 })