Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NextJS: Support @next/font #20291

Merged
merged 10 commits into from
Dec 20, 2022
Merged

NextJS: Support @next/font #20291

merged 10 commits into from
Dec 20, 2022

Conversation

valentinpalkovic
Copy link
Contributor

@valentinpalkovic valentinpalkovic commented Dec 15, 2022

Issue: #19711

What I did

Implemented support of @next/font in Storybook.

1. Custom babel plugin to transform @next/font usages and prepare them for the webpack loader

I need to transform "@next/font" imports and usages to a webpack loader-friendly format with parameters.
The plugin turns the following code:

// src/example.js
import { Inter, Roboto } from '@next/font/google'

import localFont from '@next/font/local'

const myFont = localFont({ src: './my-font.woff2' })

const inter = Inter({
  subsets: ['latin'],
});

const roboto = Roboto({
  weight: '400',
})

Into the following:

import inter from 'storybook-nextjs-font-loader?{filename: "src/example.js", source: "@next/font/google", fontFamily: "Inter", props: {"subsets":["latin"]}}!@next/font/google'
import roboto from 'storybook-nextjs-font-loader?{filename: "src/example.js", source: "@next/font/google", fontFamily: "Roboto", props: {"weight": "400"}}!@next/font/google'
import myFont from 'storybook-nextjs-font-loader?{filename: "src/example.js", source: "@next/font/local", props: {"src": "./my-font.woff2"}}!@next/font/local'

This Plugin tries to adopt the functionality provided by the nextjs swc plugin. Unfortunately, Next.js hasn't worked on a babel plugin. Indeed, if you opt out to use babel in Next.js, you are not allowed to use @next/font. Therefore this completely newly written plugin was necessary to adopt the same functionality in a babel environment.

2. Custom storybook-nextjs-font-loader webpack loader

The storybook-nextjs-font-loader does two things:

  1. It puts the necessary @font-face definitions into a style tag and places it into the head section of the document.
  2. It generates className, styles and variable outputs to make them usable in React Components, like it is described in the docs

The transformed import

import roboto from 'storybook-nextjs-font-loader?{filename: "src/example.js", source: "@next/font/google", fontFamily: "Roboto", props: {"weight": "400", "variable": "--font-roboto" }}!@next/font/google'

provides an object as a default export

import { Inter, Roboto } from '@next/font/google'

// This variable declaration will be turned into a named import after the babel plugin has transformed it
const roboto = Roboto({
  weight: '400',
  variable: '--font-roboto'
})

function ComponentUsesClassName() {
  return (
    <h1 className={roboto.className}>Component uses ClassName property</h1>
  )
}

function ComponentUsesStyle() {
  return (
    <h1 style={roboto.style}>Component uses ClassName property</h1>
  )
}

// Tailwind use case
function ComponentUsesVariable() {
  return (
    <div className={roboto.variable}>
       <h1 style={{ fontFamily: 'var(--font-roboto)' }}>Component uses CSS Variable<h1>
    </div>
  )
}

How to test

@next/font/google

  1. Bootstrap a nextjs/default-js sandbox
  2. Go to the Stories of `nextjs_default_js/Font
  3. Google Fonts are correctly loaded

@next/font/local

  1. Bootstrap a nextjs/default-js sandbox
  2. Go to .storybook/main.js and add the following config:
module.exports = {
  ...
  "staticDirs": [
    { 
      from: '../stories/frameworks/nextjs_default-js/fonts', 
      to: 'stories/frameworks/nextjs_default-js/fonts' 
    }
  ],
 }
  1. Custom Local Fonts are correctly loaded

Supported packages

  • @next/font/google
  • @next/font/local

Not supported features in the first iteration:

How to test

  • Is this testable with Jest or Chromatic screenshots?
  • Does this need an update to the documentation?

If your answer is yes to any of these, please make sure to include it in your PR.

@valentinpalkovic valentinpalkovic changed the title [WIP]: Support @next/font Support @next/font Dec 16, 2022
@valentinpalkovic valentinpalkovic marked this pull request as ready for review December 16, 2022 13:50
@valentinpalkovic valentinpalkovic force-pushed the valentin/next-font branch 2 times, most recently from ca793db to 43b371b Compare December 16, 2022 14:42
@valentinpalkovic valentinpalkovic added the ci:daily Run the CI jobs that normally run in the daily job. label Dec 16, 2022
@shilman shilman changed the title Support @next/font NextJS: Support @next/font Dec 16, 2022
@yannbf yannbf self-assigned this Dec 19, 2022
@valentinpalkovic valentinpalkovic removed the ci:daily Run the CI jobs that normally run in the daily job. label Dec 19, 2022
@ndelangen
Copy link
Member

Anything in the way from blocking this?

Copy link
Member

@yannbf yannbf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EDIT: turns out the issue is related to the export keyword. The AST transformation was not applied to ExportNamedDeclarations, probably not for default either

@valentinpalkovic it seems like if the fonts are imported in the .stories file, it's all good, but if they come from a separate file, it breaks:

image

Examples of separate file:

Button used in Button.stories.js:

import React from 'react';
import PropTypes from 'prop-types';
import './button.css';
import { Rubik } from '@next/font/google';
export const rubik = Rubik({ subsets: ['latin'], variable: '--font-latin-rubik', weight: '400' });

/**
 * Primary UI component for user interaction
 */
export const Button = ({ primary, backgroundColor, size, label, ...props }) => {
  const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
  return (
    <button
      type="button"
      className={[rubik.className, 'storybook-button', `storybook-button--${size}`, mode].join(' ')}
      {...props}
    >
      {label}
      <style jsx>{`
        button {
          background-color: ${backgroundColor};
        }
      `}</style>
    </button>
  );
};

a separate fonts.js file (I saw this documented somewhere, but don't remember):

import { Fira_Code } from '@next/font/google';

export const fira = Fira_Code({ subsets: ['latin']})

@valentinpalkovic
Copy link
Contributor Author

valentinpalkovic commented Dec 19, 2022

@yannbf Thank you! Indeed, if the Variable declaration was a ExportNamedDeclaration, the code broke:

import localFont from '@next/font/local';

// The export case was not considered in the babel transformation
export const localRubikStorm = localFont({
  src: '/fonts/RubikStorm-Regular.ttf',
  variable: '--font-rubik-storm',
});

This should now be fixed in the latest commit.

Now the above code will be transformed correctly to:

import localRubikStorm from "@next/font/local?localFont;{\"src\":\"/fonts/RubikStorm-Regular.ttf\",\"variable\":\"--font-rubik-storm\"}"; // The export case was not considered in the babel transformation

export { localRubikStorm };

Please delete the node_modules/.cache folder in your sandbox, after you have checked out the latest commit and you have rebuild the Nextjs framework code (yarn build nextjs). It seems, that changes in the babel plugin section require deleting the cache, otherwise, an old version of the plugin is loaded.

@yannbf
Copy link
Member

yannbf commented Dec 20, 2022

@valentinpalkovic that's awesome! I also tested it in Typescript and it works great.

There are two scenarios which will break with a similar error than before, but honestly I don't think people will do them, so I think it's fine to merge this!

First scenario: an object of fonts:

// fonts.ts
import { NextFontWithVariable } from '@next/font/dist/types';
import { Rubik_Puddles } from '@next/font/google';
import localFont from '@next/font/local';

export const fonts: Record<string, NextFontWithVariable> = {
  rubik: Rubik_Puddles({
    subsets: ['latin'],
    variable: '--font-latin-rubik',
    weight: '400',
  }),
  localRubikStorm: localFont({
    src: '/fonts/RubikStorm-Regular.ttf',
    variable: '--font-rubik-storm',
  })
}
// FontComponent.ts
import React from 'react';
import { fonts } from './fonts';

const { localRubikStorm, rubik } = fonts;

export default function Font({ variant }: {variant: string}) { ... }

Second scenario: using the as operator:

This scenario is even more unrealistic, I just wanted to make sure I tested them all

import { Rubik_Puddles } from '@next/font/google';
import localFont from '@next/font/local';
import { NextFontWithVariable } from '@next/font/dist/types';

// Works!
export const rubik: NextFontWithVariable = Rubik_Puddles({
  subsets: ['latin'],
  variable: '--font-latin-rubik',
  weight: '400',
});

// Does not work
export const localRubikStorm = localFont({
  src: '/fonts/RubikStorm-Regular.ttf',
  variable: '--font-rubik-storm',
}) as NextFontWithVariable;

I checked usage of next/font in different projects and the most common case is:

  1. fonts file with named exports
  2. fonts file with default export

All of which are fully supported by this PR!

@valentinpalkovic
Copy link
Contributor Author

valentinpalkovic commented Dec 20, 2022

@yannbf I am not sure whether the first scenario even works in Next.js. In their SWC code, they have a lot of if conditions where they throw errors, for example, if the import is aliased. Therefore I also think this is fine for now. Could you resolve the conflicts and merge the PR? (I think you can ignore the flaky windows unit tests.) I don't have unfortunately access to my machine right now.

@yannbf
Copy link
Member

yannbf commented Dec 20, 2022

@yannbf I am not sure whether the first scenario even works in Next.js. In their SWC code, they have a lot of if conditions where they throw errors, for example, if the import is aliased. Therefore I also think this is fine for now. Could you resolve the conflicts and merge the PR? (I think you can ignore the flaky windows unit tests.) I don't have unfortunately access to my machine right now.

You're right! It does fail in Next.js:
image

By switching it a little bit (moving to separate consts and exporting the fonts object, it works in Next.js and also in Storybook 🎉

@yannbf yannbf merged commit 111edc3 into next Dec 20, 2022
@yannbf yannbf deleted the valentin/next-font branch December 20, 2022 09:28
@valentinpalkovic
Copy link
Contributor Author

🎉

@ndelangen
Copy link
Member

🎉 INDEED! 👏

@hobadams
Copy link

hobadams commented Jan 4, 2023

Thank so much for the update here @valentinpalkovic

I'm using next 13.0.6 & Storybook 7.0.0-beta.19

This has now fixed the issue with Storybook not loading because of @next/font but my local fonts 404.

My font looks like this:

src/styles/fonts.ts

import localFont from '@next/font/local';

// @see https://nextjs.org/docs/api-reference/next/font
export const TTNorms = localFont({
  display: 'swap',
  fallback: ['sans-serif'],
  src: [
    {
      path: './fonts/TT_Norms_Pro_Regular.woff2',
      weight: '400',
      style: 'normal',
    },
    {
      path: './fonts/TT_Norms_Pro_Medium.woff2',
      weight: '500',
      style: 'normal',
    },
    {
      path: './fonts/TT_Norms_Pro_Light.woff2',
      weight: '300',
      style: 'italic',
    },
  ],
});

And in my components i use

import { TTNorms } from '@/styles/fonts';
...
<Component className={TTNorms.className}>

The file path actually looks correct but 404s.

GET http://localhost:6006/src/styles/fonts/TT_Norms_Pro_Medium.woff2 net::ERR_ABORTED 404 (Not Found)

Any ideas? Am i missing something?

Thanks so much for any help.

@valentinpalkovic
Copy link
Contributor Author

Hi @hobadams,

Have you set up the staticDirs correctly, as described here?
https://github.com/storybookjs/storybook/tree/next/code/frameworks/nextjs#nextfontlocal

@hobadams
Copy link

hobadams commented Jan 4, 2023

@valentinpalkovic - you are a hero, thank you so much. That fixed it! 🎉

@ndelangen
Copy link
Member

Can confirm @valentinpalkovic is a hero, for sure!

@patrick91
Copy link

hi @valentinpalkovic this change looks amazing, is there a way to use the fonts directly inside maybe preview.ts? I want to load all my fonts once and the use css to switch between them, without using them inside the components directly 😊

@valentinpalkovic
Copy link
Contributor Author

valentinpalkovic commented Feb 12, 2023

@patrick91 it shouldn’t be an issue. Just import the fonts, configure them and use for example a decorator, which sets the fonts onto a root element. Then use the classname or style property like described in the next font documentation.

@patrick91
Copy link

@valentinpalkovic thanks, I just got it working like this:

// preview.ts
import "../src/styles/globals.css";

import localFont from "@next/font/local";

const ranade = localFont({
  src: [
    {
      path: "../fonts/Ranade-Variable.ttf",
      style: "normal",
    },
    {
      path: "../fonts/Ranade-VariableItalic.ttf",
      style: "italic",
    },
  ],
  variable: "--font-ranade",
  // style: ["normal", "italic"],
});


document.body.classList.add(ranade.variable);
document.body.classList.add("font-sans");

export const parameters = {
  backgrounds: {
    default: "light",
  },
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
};

will try the other options in future! <3

@valentinpalkovic
Copy link
Contributor Author

@patrick91 Great! If you need further assistance, you can reach out to the Storybook Discord Group at any time. We will try to give you the help and assistance you need.

@Cayan
Copy link

Cayan commented Mar 6, 2023

EDIT: I found the problem for my case, I'm using the latest import from NextJs next/font instead of @next/font

Related: #21330 vercel/next.js#46159


I tried several options but I'm still stuck with the same error:
(0 , next_font_google__WEBPACK_IMPORTED_MODULE_3__.Inter) is not a function

and this is the error for local fonts:
next_font_local__WEBPACK_IMPORTED_MODULE_3___default(...) is not a function

using Storybook 7.0.0-beta.61 and Next 13.2.3

@valentinpalkovic
Copy link
Contributor Author

valentinpalkovic commented Mar 6, 2023

@Cayan next/font should be supported since 7.0.0-beta.60. Please make sure to install the latest @storybook/nextjs beta version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants