Repository: vercel/next.js
Pull request: Fix Turbopack local font `font-family` declaration

This fixes #85912, as the logic of injecting the `font-family` property should be mirrored from the Webpack implementation.

Missing feature from https://github.com/vercel/next.js/pull/85051

Changed files:
filename: crates/next-core/src/next_font/local/stylesheet.rs
patch: @@ -62,11 +62,17 @@ pub(super) async fn build_font_face_definitions(
         };
         let query_str = qstring::QString::from(serde_json::to_string(&query)?.as_str());
 
+        // Check if `font-family` is explicitly defined in `declarations`
+        let has_custom_font_family = options.declarations.as_ref().is_some_and(|declarations| {
+            declarations
+                .iter()
+                .any(|declaration| declaration.prop == "font-family")
+        });
+
         definitions.push_str(&formatdoc!(
             r#"
                 @font-face {{
-                    {}
-                    font-family: '{}';
+                    {}{}
                     src: url('@vercel/turbopack-next/internal/font/local/font?{}') format('{}');
                     font-display: {};
                     {}{}
@@ -79,20 +85,24 @@ pub(super) async fn build_font_face_definitions(
                     .map(|declaration| format!("{}: {};", declaration.prop, declaration.value))
                     .join("\n")
             ),
-            scoped_font_family,
+            if has_custom_font_family {
+                "".to_owned()
+            } else {
+                format!("\nfont-family: '{}';", scoped_font_family)
+            },
             query_str,
             ext_to_format(&font.ext)?,
             options.display,
             &font
                 .weight
                 .as_ref()
                 .or(options.default_weight.as_ref())
-                .map_or_else(|| "".to_owned(), |w| format!("font-weight: {w};")),
+                .map_or_else(|| "".to_owned(), |w| format!("\nfont-weight: {w};")),
             &font
                 .style
                 .as_ref()
                 .or(options.default_style.as_ref())
-                .map_or_else(|| "".to_owned(), |s| format!("font-style: {s};")),
+                .map_or_else(|| "".to_owned(), |s| format!("\nfont-style: {s};")),
         ));
     }
 

filename: test/e2e/next-font/app/pages/with-local-fonts.js
patch: @@ -13,6 +13,15 @@ const myFont2 = localFont({
   preload: false,
   variable: '--my-font',
 })
+const myFont1Override = localFont({
+  src: '../fonts/my-font.woff2',
+  declarations: [
+    {
+      prop: 'font-family',
+      value: 'foobar',
+    },
+  ],
+})
 
 const roboto = localFont({
   preload: false,
@@ -134,6 +143,9 @@ export default function WithFonts() {
       <div id="second-local-font" className={myFont2.className}>
         {JSON.stringify(myFont2)}
       </div>
+      <div id="first-local-font-override" className={myFont1Override.className}>
+        {JSON.stringify(myFont1Override)}
+      </div>
       <div id="roboto-local-font" className={roboto.className}>
         {JSON.stringify(roboto)}
       </div>

filename: test/e2e/next-font/index.test.ts
patch: @@ -650,20 +650,25 @@ describe('next/font', () => {
       // Get all stylesheets
       const stylesheets = await browser.eval(`
         Array.from(document.styleSheets)
-          .map(sheet => {
+          .flatMap(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%')
+      expect(stylesheets).toContainEqual(
+        expect.stringContaining('ascent-override: 90%;')
+      )
+
+      // Check that the custom declaration is included in the CSS and overrides the default family
+      expect(stylesheets).toContainEqual(
+        expect.stringContaining('font-family: foobar;')
+      )
     })
   })
 })