[PR #956] [MERGED] fix(fonts): stabilize next/font/google runtime registrations #982

Closed
opened 2026-05-06 13:11:23 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/956
Author: @NathanDrake2406
Created: 4/29/2026
Status: Merged
Merged: 4/29/2026
Merged by: @james-elicx

Base: mainHead: nathan/font-google-phase1


📝 Commits (4)

  • 29c561b fix(fonts): make Google font shim registrations stable
  • ba7126e fix(fonts): address Google font review feedback
  • f27c9cd fix(fonts): canonicalize Google font identity hashing
  • 2f8de11 fix(fonts): normalize default fallback font identities

📊 Changes

2 files changed (+203 additions, -37 deletions)

View changed files

📝 packages/vinext/src/shims/font-google-base.ts (+94 -24)
📝 tests/font-google.test.ts (+109 -13)

📄 Description

What this changes

Fixes #883.

The next/font/google runtime shim now gives each (family, options) registration a deterministic class identity instead of using a module-level counter. Repeated equivalent calls now reuse the same className and variable class, so dev HMR re-evaluation no longer appends a new pair of SSR style rules every time the app module reloads.

This also removes the shim-only :root { --font-... } emission. Google font variables now live only on the returned .variable class, matching Next.js behavior.

Why

The issue is not really three separate bugs. Counter runaway, stale head rules, and pinned :root variables all come from the same bad invariant: the shim treated every runtime call as a new font registration even when the call represented the same font and options.

Next.js avoids this shape entirely:

Approach

  • Replace classCounter++ with a deterministic FNV-based identity over the font family, effective font-family string, CSS variable name, public font options, and self-hosted CSS.
  • Reuse existing class/style dedupe sets by making equivalent calls produce equivalent class names.
  • Remove injectedRootVariables and the :root rule path.
  • Keep dynamic/proxy font calls on the existing runtime fallback. A full dev-time virtual CSS module transform can be a separate architecture PR.

Validation

  • Added regression tests for stable class identity, no :root font variable emission, and no SSR style growth for repeated equivalent calls.
  • Updated class-name shape assertions from counter-only suffixes to stable alphanumeric suffixes.
  • vp test run tests/font-google.test.ts
  • vp test run tests/shims.test.ts -t "next/font/google shim"
  • vp check
  • vp run vinext#build

Risks / follow-ups

Apps that accidentally relied on vinext injecting font variables at :root without applying the returned .variable class will need to apply font.variable, which is the Next.js-compatible usage. This PR intentionally does not attempt the larger Phase 2 architecture where dev font calls become Vite-owned virtual CSS modules.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/956 **Author:** [@NathanDrake2406](https://github.com/NathanDrake2406) **Created:** 4/29/2026 **Status:** ✅ Merged **Merged:** 4/29/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `nathan/font-google-phase1` --- ### 📝 Commits (4) - [`29c561b`](https://github.com/cloudflare/vinext/commit/29c561b0b5ab090dc44af8530abf2f7e45397852) fix(fonts): make Google font shim registrations stable - [`ba7126e`](https://github.com/cloudflare/vinext/commit/ba7126eae54b309c7fa05372eae639748f312011) fix(fonts): address Google font review feedback - [`f27c9cd`](https://github.com/cloudflare/vinext/commit/f27c9cdf56179bc75e32a8cdd07aa6706e29549b) fix(fonts): canonicalize Google font identity hashing - [`2f8de11`](https://github.com/cloudflare/vinext/commit/2f8de11e6dfc365281a08acf1e2d12784f405879) fix(fonts): normalize default fallback font identities ### 📊 Changes **2 files changed** (+203 additions, -37 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/shims/font-google-base.ts` (+94 -24) 📝 `tests/font-google.test.ts` (+109 -13) </details> ### 📄 Description ## What this changes Fixes #883. The `next/font/google` runtime shim now gives each `(family, options)` registration a deterministic class identity instead of using a module-level counter. Repeated equivalent calls now reuse the same `className` and `variable` class, so dev HMR re-evaluation no longer appends a new pair of SSR style rules every time the app module reloads. This also removes the shim-only `:root { --font-... }` emission. Google font variables now live only on the returned `.variable` class, matching Next.js behavior. ## Why The issue is not really three separate bugs. Counter runaway, stale head rules, and pinned `:root` variables all come from the same bad invariant: the shim treated every runtime call as a new font registration even when the call represented the same font and options. Next.js avoids this shape entirely: - SWC rewrites calls like `Inter({ subsets: ["latin"] })` into a font CSS module request: [`next-font-loader/index.ts` lines 18-30](https://github.com/vercel/next.js/blob/ae61573e062e900050b8e6b24626e450accc4570/packages/next/src/build/webpack/loaders/next-font-loader/index.ts#L18-L30). - The loader hashes the font CSS content for stable class identity: [`next-font-loader/index.ts` lines 128-134](https://github.com/vercel/next.js/blob/ae61573e062e900050b8e6b24626e450accc4570/packages/next/src/build/webpack/loaders/next-font-loader/index.ts#L128-L134). - css-loader turns `className` and `variable` exports into `__${exportName}_${fontFamilyHash}`: [`next-font.ts` lines 44-53](https://github.com/vercel/next.js/blob/ae61573e062e900050b8e6b24626e450accc4570/packages/next/src/build/webpack/config/blocks/css/loaders/next-font.ts#L44-L53). - The PostCSS plugin emits a `.className` rule and, when requested, a `.variable` rule. It does not emit a `:root` rule: [`postcss-next-font.ts` lines 138-174](https://github.com/vercel/next.js/blob/ae61573e062e900050b8e6b24626e450accc4570/packages/next/src/build/webpack/loaders/next-font-loader/postcss-next-font.ts#L138-L174). ## Approach - Replace `classCounter++` with a deterministic FNV-based identity over the font family, effective font-family string, CSS variable name, public font options, and self-hosted CSS. - Reuse existing class/style dedupe sets by making equivalent calls produce equivalent class names. - Remove `injectedRootVariables` and the `:root` rule path. - Keep dynamic/proxy font calls on the existing runtime fallback. A full dev-time virtual CSS module transform can be a separate architecture PR. ## Validation - Added regression tests for stable class identity, no `:root` font variable emission, and no SSR style growth for repeated equivalent calls. - Updated class-name shape assertions from counter-only suffixes to stable alphanumeric suffixes. - `vp test run tests/font-google.test.ts` - `vp test run tests/shims.test.ts -t "next/font/google shim"` - `vp check` - `vp run vinext#build` ## Risks / follow-ups Apps that accidentally relied on vinext injecting font variables at `:root` without applying the returned `.variable` class will need to apply `font.variable`, which is the Next.js-compatible usage. This PR intentionally does not attempt the larger Phase 2 architecture where dev font calls become Vite-owned virtual CSS modules. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:11:23 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#982
No description provided.