fwouts / previewjs

Preview UI components in your IDE instantly
https://previewjs.com
Other
1.84k stars 45 forks source link

Support NextJS 13 font resolvers #2021

Open jordanarldt opened 1 year ago

jordanarldt commented 1 year ago

Is your feature request related to a problem? Please describe. Hey again @fwouts :-) it's been a while.

I'm using PreviewJS with my NextJS app and I want my __previewjs__/Wrapper.tsx file to wrap components in the NextJS layout component, but it seems to be failing because I'm attempting to use a Google Font with the new NextJS fonts API.

Here's my layout file:

import "./globals.scss";
import { Raleway } from "next/font/google";

const raleway = Raleway({ subsets: ["latin"] });

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}): JSX.Element {
  return (
    <html lang="en">
      <body className={raleway.className}>{children}</body>
    </html>
  );
}

and here's my Wrapper.tsx file:

import "@/app/globals.scss";

import Layout from "@/app/layout";

export default function Wrapper({ children }: React.PropsWithChildren): JSX.Element {
  return (
    <div>
      <Layout>{children}</Layout>
    </div>
  );
}

Describe the solution you'd like It would be cool if the PreviewJS preview was able to automatically resolve these fonts so that they display correctly in the preview. Perhaps you could leverage the Google API and the name of the requested font from the NextJS import and use that to inject the font family tags into the preview body.

Describe alternatives you've considered Manually setting the font in the wrapper file, but this won't be feasible as my project grows.

Additional context Any more support for NextJS 13 will be awesome. Thanks again for the great work!

fwouts commented 1 year ago

Thanks for reporting this @jordanarldt, I didn't even know NextJS fonts API was a thing. Will look into it.

jordanarldt commented 1 year ago

Thanks for reporting this @jordanarldt, I didn't even know NextJS fonts API was a thing. Will look into it.

I believe it is a new thing since version 13 👍 still very cool though, and will be super useful in PreviewJS!

fwouts commented 1 year ago

So I found the logic that makes this work in Next.js: https://github.com/vercel/next.js/blob/c56f9f4ff906fb2ad4f1acb126071cb876d4363c/packages/next/src/build/webpack/loaders/next-font-loader/index.ts#L16

This is close to magic, and it has various weird edge cases. For example the following will fail:

import * as googleFonts from "next/font/google";

const inter = googleFonts.Inter({ subsets: ["latin"] });

I agree that making this work would be convenient, but I'm not quite sure how to proceed yet. I don't really want to hack Next.js-specific code in Preview.js, as it adds a maintenance burden in the future (whenever this API changes in future versions of Next.js, Preview.js needs to be updated to support all current and future versions, just like React itself).

In the meantime, here's a workaround:

module.exports = { vite: { resolve: { alias: { "next/font/google": path.resolve(__dirname, "./google-font-mock.js"), }, }, }, };


- add the following code in the corresponding `google-font-mock.js` file:

```js
export const Inter = () => ({});

This won't load the correct font, but at least it won't crash!

jordanarldt commented 1 year ago

@fwouts Awesome! Nice find, by the way! The way NextJS does it definitely seems like magic, and of course I wouldn't expect the PreviewJS implementation to be the same way they do it. I have a suggestion on an implementation that may help you.

So typically in a NextJS project you'll have a main file to contain all the font imports (e.g. fonts.ts), and this file will be used when you need to import a font as a style, classname, or font family.

Here's an example:

// fonts.ts
import { Plus_Jakarta_Sans } from "next/font/google";

export const jakartaSans = Plus_Jakarta_Sans({ subsets: ["latin"] });

And if i console.log(jakartaSans) here is the output:

{
  style: {
    fontFamily: "'__Plus_Jakarta_Sans_d21556', '__Plus_Jakarta_Sans_Fallback_d21556'",
    fontStyle: 'normal'
  },
  className: '__className_d21556'
}

So all we would really need PreviewJS to do is to be aware of the fonts we want to import, and then you could populate the Preview HTML with the necessary tags / class names. For example, here's the Google Font API URL for Plus Jakarta Sans: https://fonts.googleapis.com/css?family=Plus%20Jakarta%20Sans

That will give you all of the font face information (replaced with the font family name from the font data above), and then you would just need to make the styles, for example:

/* latin - Fetched from Google*/
@font-face {
  font-family: '__Plus_Jakarta_Sans_d21556'; // use the family name from the font output above
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/plusjakartasans/v8/LDIbaomQNQcsA88c7O9yZ4KMCoOg4IA6-91aHEjcWuA_qU79TR_VMq2oRsWk.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

.__className_d21556 {
  font-family: '__Plus_Jakarta_Sans_d21556';
}

Theoretically, this would work, but maybe PreviewJS would be able to control what the returned classnames are? I might have to try this out in my wrapper file to see if it works.

What do you think?

jordanarldt commented 1 year ago

@fwouts I actually made a working proof of concept, this may help you come up with a broader solution :-)

I mock my fonts.ts module:

// preview.config.js
module.exports = {
  alias: {
    "@/fonts": "__previewjs__/mocks/fonts.js",
  },
};

Here is the mock file:

// mock/fonts.js

export const jakartaSans = {
  style: {
    fontFamily: "'Plus Jakarta Sans'",
    fontStyle: 'normal'
  },
  className: '__previewjs_jakarta_sans',
};

And here is my wrapper:

// Wrapper.tsx

import { useEffect, useState } from "react";

import "@/app/globals.scss";

function useMockFonts() {
  const [fontStyles, setFontStyles] = useState("");

  useEffect(() => {
    (async () => {
      const styles = await fetch(
        "https://fonts.googleapis.com/css?family=Plus%20Jakarta%20Sans",
      ).then(res => res.text());

      setFontStyles(styles);
    })();
  });

  return fontStyles;
}

export function Wrapper({ children }: React.PropsWithChildren): JSX.Element {
  const style = useMockFonts();
  const css = `
  .__previewjs_jakarta_sans {
    font-family: "Plus Jakarta Sans";
  }`;

  return (
    <div>
      <style>{style}</style>
      <style>{css}</style>
      {children}
    </div>
  );
}
fwouts commented 1 year ago

That's neat, thanks.

For anyone reading this issue, @jordanarldt's solution is the recommended approach until we come to a more permanent solution ☝️