@@ -32,8 +32,9 @@ describe("next/font/google shim", () => {
3232 expect ( typeof Inter ) . toBe ( "function" ) ;
3333 } ) ;
3434
35- it ( "named export Inter returns className, style, variable" , async ( ) => {
36- const { Inter } = await import ( "../packages/vinext/src/shims/font-google.js" ) ;
35+ it ( "createFontLoader returns className, style, variable" , async ( ) => {
36+ const { createFontLoader } = await import ( "../packages/vinext/src/shims/font-google.js" ) ;
37+ const Inter = createFontLoader ( "Inter" ) ;
3738 const result = Inter ( { weight : [ "400" , "700" ] , subsets : [ "latin" ] } ) ;
3839 expect ( result . className ) . toMatch ( / ^ _ _ f o n t _ i n t e r _ \d + $ / ) ;
3940 expect ( result . style . fontFamily ) . toContain ( "Inter" ) ;
@@ -42,21 +43,24 @@ describe("next/font/google shim", () => {
4243 } ) ;
4344
4445 it ( "supports custom variable name" , async ( ) => {
45- const { Inter } = await import ( "../packages/vinext/src/shims/font-google.js" ) ;
46+ const { createFontLoader } = await import ( "../packages/vinext/src/shims/font-google.js" ) ;
47+ const Inter = createFontLoader ( "Inter" ) ;
4648 const result = Inter ( { weight : [ "400" ] , variable : "--my-font" } ) ;
4749 // variable returns a class name that sets the CSS variable, not the variable name itself
4850 expect ( result . variable ) . toMatch ( / ^ _ _ v a r i a b l e _ i n t e r _ \d + $ / ) ;
4951 } ) ;
5052
5153 it ( "supports custom fallback fonts" , async ( ) => {
52- const { Inter } = await import ( "../packages/vinext/src/shims/font-google.js" ) ;
54+ const { createFontLoader } = await import ( "../packages/vinext/src/shims/font-google.js" ) ;
55+ const Inter = createFontLoader ( "Inter" ) ;
5356 const result = Inter ( { weight : [ "400" ] , fallback : [ "Arial" , "Helvetica" ] } ) ;
5457 expect ( result . style . fontFamily ) . toContain ( "Arial" ) ;
5558 expect ( result . style . fontFamily ) . toContain ( "Helvetica" ) ;
5659 } ) ;
5760
5861 it ( "generates unique classNames for each call" , async ( ) => {
59- const { Inter } = await import ( "../packages/vinext/src/shims/font-google.js" ) ;
62+ const { createFontLoader } = await import ( "../packages/vinext/src/shims/font-google.js" ) ;
63+ const Inter = createFontLoader ( "Inter" ) ;
6064 const a = Inter ( { weight : [ "400" ] } ) ;
6165 const b = Inter ( { weight : [ "700" ] } ) ;
6266 expect ( a . className ) . not . toBe ( b . className ) ;
@@ -78,7 +82,8 @@ describe("next/font/google shim", () => {
7882 } ) ;
7983
8084 it ( "accepts _selfHostedCSS option for self-hosted mode" , async ( ) => {
81- const { Inter } = await import ( "../packages/vinext/src/shims/font-google.js" ) ;
85+ const { createFontLoader } = await import ( "../packages/vinext/src/shims/font-google.js" ) ;
86+ const Inter = createFontLoader ( "Inter" ) ;
8287 const fakeCSS = "@font-face { font-family: 'Inter'; src: url(/fonts/inter.woff2); }" ;
8388 const result = Inter ( { weight : [ "400" ] , _selfHostedCSS : fakeCSS } as any ) ;
8489 expect ( result . className ) . toBeDefined ( ) ;
@@ -145,44 +150,21 @@ describe("next/font/google shim", () => {
145150 expect ( styles2 . length ) . toBe ( styles . length ) ;
146151 } ) ;
147152
148- it ( "exports common font families as named exports " , async ( ) => {
153+ it ( "exports createFontLoader for ad-hoc font creation " , async ( ) => {
149154 const mod = await import ( "../packages/vinext/src/shims/font-google.js" ) ;
150- const names = [
151- "Inter" , "Roboto" , "Roboto_Mono" , "Open_Sans" , "Lato" ,
152- "Poppins" , "Montserrat" , "Geist" , "Geist_Mono" ,
153- "JetBrains_Mono" , "Fira_Code" ,
154- ] ;
155- for ( const name of names ) {
156- expect ( typeof ( mod as any ) [ name ] ) . toBe ( "function" ) ;
157- }
155+ expect ( typeof mod . createFontLoader ) . toBe ( "function" ) ;
156+ const loader = mod . createFontLoader ( "Inter" ) ;
157+ expect ( typeof loader ) . toBe ( "function" ) ;
158+ const result = loader ( { weight : [ "400" ] } ) ;
159+ expect ( result . className ) . toMatch ( / ^ _ _ f o n t _ i n t e r _ \d + $ / ) ;
160+ expect ( result . style . fontFamily ) . toContain ( "Inter" ) ;
158161 } ) ;
159162
160- it ( "exports all Google Fonts as named exports " , async ( ) => {
163+ it ( "proxy handles underscore-style names (e.g. Roboto_Mono) " , async ( ) => {
161164 const mod = await import ( "../packages/vinext/src/shims/font-google.js" ) ;
162- const fixturePath = path . join ( process . cwd ( ) , "tests/fixtures/google-fonts.json" ) ;
163- const fixture = JSON . parse ( fs . readFileSync ( fixturePath , "utf-8" ) ) as {
164- families : string [ ] ;
165- } ;
166- const toExportName = ( family : string ) : string =>
167- family
168- . replace ( / [ ^ 0 - 9 A - Z a - z ] + / g, "_" )
169- . replace ( / ^ _ + | _ + $ / g, "" )
170- . replace ( / _ + / g, "_" ) ;
171- const expected = fixture . families . map ( toExportName ) . sort ( ) ;
172- const nonFontExports = new Set ( [
173- "default" ,
174- "buildGoogleFontsUrl" ,
175- "getSSRFontLinks" ,
176- "getSSRFontStyles" ,
177- "getSSRFontPreloads" ,
178- ] ) ;
179- const actual = Object . keys ( mod )
180- . filter ( ( name ) => ! nonFontExports . has ( name ) )
181- . sort ( ) ;
182- expect ( actual ) . toEqual ( expected ) ;
183- for ( const name of actual ) {
184- expect ( typeof ( mod as any ) [ name ] ) . toBe ( "function" ) ;
185- }
165+ const fonts = mod . default as any ;
166+ const rm = fonts . Roboto_Mono ( { weight : [ "400" ] } ) ;
167+ expect ( rm . style . fontFamily ) . toContain ( "Roboto Mono" ) ;
186168 } ) ;
187169
188170 // ── Security: CSS injection via font family names ──
@@ -211,7 +193,7 @@ describe("next/font/google shim", () => {
211193
212194 it ( "sanitizes fallback font names with CSS injection attempts" , async ( ) => {
213195 const mod = await import ( "../packages/vinext/src/shims/font-google.js" ) ;
214- const { Inter } = mod ;
196+ const Inter = mod . createFontLoader ( "Inter" ) ;
215197 const result = Inter ( {
216198 weight : [ "400" ] ,
217199 fallback : [ "sans-serif" , "'); } body { color: red; } .x { font-family: ('" ] ,
@@ -231,7 +213,7 @@ describe("next/font/google shim", () => {
231213
232214 it ( "rejects invalid CSS variable names and falls back to auto-generated" , async ( ) => {
233215 const mod = await import ( "../packages/vinext/src/shims/font-google.js" ) ;
234- const { Inter } = mod ;
216+ const Inter = mod . createFontLoader ( "Inter" ) ;
235217 const beforeStyles = mod . getSSRFontStyles ( ) . length ;
236218 const result = Inter ( {
237219 weight : [ "400" ] ,
@@ -251,7 +233,7 @@ describe("next/font/google shim", () => {
251233
252234 it ( "accepts valid CSS variable names" , async ( ) => {
253235 const mod = await import ( "../packages/vinext/src/shims/font-google.js" ) ;
254- const { Inter } = mod ;
236+ const Inter = mod . createFontLoader ( "Inter" ) ;
255237 const beforeStyles = mod . getSSRFontStyles ( ) . length ;
256238 const result = Inter ( {
257239 weight : [ "400" ] ,
@@ -275,13 +257,18 @@ describe("vinext:google-fonts plugin", () => {
275257 expect ( plugin . enforce ) . toBe ( "pre" ) ;
276258 } ) ;
277259
278- it ( "is a no-op in dev mode (isBuild = false )" , async ( ) => {
260+ it ( "rewrites font imports in dev mode (no _selfHostedCSS )" , async ( ) => {
279261 const plugin = getGoogleFontsPlugin ( ) ;
280262 plugin . _isBuild = false ;
281263 const transform = unwrapHook ( plugin . transform ) ;
282264 const code = `import { Inter } from 'next/font/google';\nconst inter = Inter({ weight: ['400'] });` ;
283265 const result = await transform . call ( plugin , code , "/app/layout.tsx" ) ;
284- expect ( result ) . toBeNull ( ) ;
266+ // Import rewriting should happen even in dev mode
267+ expect ( result ) . not . toBeNull ( ) ;
268+ expect ( result . code ) . toContain ( "createFontLoader as __vinext_clf" ) ;
269+ expect ( result . code ) . toContain ( '__vinext_clf("Inter")' ) ;
270+ // But no self-hosted CSS in dev mode
271+ expect ( result . code ) . not . toContain ( "_selfHostedCSS" ) ;
285272 } ) ;
286273
287274 it ( "returns null for files without next/font/google imports" , async ( ) => {
@@ -321,14 +308,19 @@ describe("vinext:google-fonts plugin", () => {
321308 expect ( result ) . toBeNull ( ) ;
322309 } ) ;
323310
324- it ( "returns null when import exists but no font constructor call" , async ( ) => {
311+ it ( "rewrites import even when no constructor call exists " , async ( ) => {
325312 const plugin = getGoogleFontsPlugin ( ) ;
326313 plugin . _isBuild = true ;
327314 plugin . _cacheDir = path . join ( import . meta. dirname , ".test-font-cache" ) ;
328315 const transform = unwrapHook ( plugin . transform ) ;
329316 const code = `import { Inter } from 'next/font/google';\n// no call` ;
330317 const result = await transform . call ( plugin , code , "/app/layout.tsx" ) ;
331- expect ( result ) . toBeNull ( ) ;
318+ // Import rewriting should still happen even without a constructor call
319+ expect ( result ) . not . toBeNull ( ) ;
320+ expect ( result . code ) . toContain ( "createFontLoader as __vinext_clf" ) ;
321+ expect ( result . code ) . toContain ( '__vinext_clf("Inter")' ) ;
322+ // No constructor call, so no _selfHostedCSS
323+ expect ( result . code ) . not . toContain ( "_selfHostedCSS" ) ;
332324 } ) ;
333325
334326 it ( "transforms font call to include _selfHostedCSS during build" , async ( ) => {
@@ -346,6 +338,10 @@ describe("vinext:google-fonts plugin", () => {
346338
347339 const result = await transform . call ( plugin , code , "/app/layout.tsx" ) ;
348340 expect ( result ) . not . toBeNull ( ) ;
341+ // Should rewrite the import
342+ expect ( result . code ) . toContain ( "createFontLoader as __vinext_clf" ) ;
343+ expect ( result . code ) . toContain ( '__vinext_clf("Inter")' ) ;
344+ // Should inject self-hosted CSS
349345 expect ( result . code ) . toContain ( "_selfHostedCSS" ) ;
350346 expect ( result . code ) . toContain ( "@font-face" ) ;
351347 expect ( result . code ) . toContain ( "Inter" ) ;
@@ -386,6 +382,8 @@ describe("vinext:google-fonts plugin", () => {
386382
387383 const result = await transform . call ( plugin , code , "/app/layout.tsx" ) ;
388384 expect ( result ) . not . toBeNull ( ) ;
385+ // Should rewrite import and inject self-hosted CSS
386+ expect ( result . code ) . toContain ( "createFontLoader as __vinext_clf" ) ;
389387 expect ( result . code ) . toContain ( "_selfHostedCSS" ) ;
390388 // lgtm[js/incomplete-sanitization] — escaping quotes for test assertion, not sanitization
391389 expect ( result . code ) . toContain ( fakeCSS . replace ( / " / g, '\\"' ) ) ;
@@ -419,7 +417,9 @@ describe("vinext:google-fonts plugin", () => {
419417
420418 const result = await transform . call ( plugin , code , "/app/layout.tsx" ) ;
421419 expect ( result ) . not . toBeNull ( ) ;
422- // Both font calls should be transformed
420+ // Import should be rewritten
421+ expect ( result . code ) . toContain ( "createFontLoader as __vinext_clf" ) ;
422+ // Both font calls should have _selfHostedCSS injected
423423 const matches = result . code . match ( / _ s e l f H o s t e d C S S / g) ;
424424 expect ( matches ?. length ) . toBe ( 2 ) ;
425425
@@ -448,12 +448,71 @@ describe("vinext:google-fonts plugin", () => {
448448
449449 const result = await transform . call ( plugin , code , "/app/layout.tsx" ) ;
450450 expect ( result ) . not . toBeNull ( ) ;
451- // Only Inter should be transformed (1 match)
451+ // Import should be rewritten for Inter
452+ expect ( result . code ) . toContain ( "createFontLoader as __vinext_clf" ) ;
453+ // Only Inter should have _selfHostedCSS (1 match)
452454 const matches = result . code . match ( / _ s e l f H o s t e d C S S / g) ;
453455 expect ( matches ?. length ) . toBe ( 1 ) ;
454456
455457 plugin . _fontCache . clear ( ) ;
456458 } ) ;
459+
460+ it ( "rewrites aliased font imports (import { Inter as MyFont })" , async ( ) => {
461+ const plugin = getGoogleFontsPlugin ( ) ;
462+ plugin . _isBuild = false ;
463+ const transform = unwrapHook ( plugin . transform ) ;
464+ const code = `import { Inter as MyFont } from 'next/font/google';\nconst font = MyFont({ weight: ['400'] });` ;
465+ const result = await transform . call ( plugin , code , "/app/layout.tsx" ) ;
466+ expect ( result ) . not . toBeNull ( ) ;
467+ expect ( result . code ) . toContain ( "createFontLoader as __vinext_clf" ) ;
468+ // Should use the original name (Inter) for family and alias (MyFont) for local binding
469+ expect ( result . code ) . toContain ( 'const MyFont = /*#__PURE__*/ __vinext_clf("Inter")' ) ;
470+ } ) ;
471+
472+ it ( "handles multiple separate import statements from next/font/google" , async ( ) => {
473+ const plugin = getGoogleFontsPlugin ( ) ;
474+ plugin . _isBuild = false ;
475+ const transform = unwrapHook ( plugin . transform ) ;
476+ const code = [
477+ `import { Inter } from 'next/font/google';` ,
478+ `import { Roboto } from 'next/font/google';` ,
479+ `const inter = Inter({ weight: ['400'] });` ,
480+ `const roboto = Roboto({ weight: ['400'] });` ,
481+ ] . join ( "\n" ) ;
482+ const result = await transform . call ( plugin , code , "/app/layout.tsx" ) ;
483+ expect ( result ) . not . toBeNull ( ) ;
484+ // Both fonts should be transformed
485+ expect ( result . code ) . toContain ( '__vinext_clf("Inter")' ) ;
486+ expect ( result . code ) . toContain ( '__vinext_clf("Roboto")' ) ;
487+ // Second import should be removed/merged
488+ expect ( result . code ) . toContain ( "merged into first" ) ;
489+ } ) ;
490+
491+ it ( "handles font names with digits after underscore (e.g. Baloo_2)" , async ( ) => {
492+ const plugin = getGoogleFontsPlugin ( ) ;
493+ plugin . _isBuild = true ;
494+ plugin . _cacheDir = path . join ( import . meta. dirname , ".test-font-cache-digits" ) ;
495+ plugin . _fontCache . clear ( ) ;
496+
497+ // Pre-populate cache — URLSearchParams encodes "+" as "%2B"
498+ plugin . _fontCache . set (
499+ "https://fonts.googleapis.com/css2?family=Baloo%2B2%3Awght%40400&display=swap" ,
500+ "@font-face { font-family: 'Baloo 2'; src: url(/baloo.woff2); }" ,
501+ ) ;
502+
503+ const transform = unwrapHook ( plugin . transform ) ;
504+ const code = [
505+ `import { Baloo_2 } from 'next/font/google';` ,
506+ `const font = Baloo_2({ weight: '400' });` ,
507+ ] . join ( "\n" ) ;
508+ const result = await transform . call ( plugin , code , "/app/layout.tsx" ) ;
509+ expect ( result ) . not . toBeNull ( ) ;
510+ expect ( result . code ) . toContain ( '__vinext_clf("Baloo 2")' ) ;
511+ // Self-hosting should match the Baloo_2 call
512+ expect ( result . code ) . toContain ( "_selfHostedCSS" ) ;
513+
514+ plugin . _fontCache . clear ( ) ;
515+ } ) ;
457516} ) ;
458517
459518// ── fetchAndCacheFont integration ─────────────────────────────
0 commit comments