Skip to content

Commit

Permalink
Add @next/font Storybook loader
Browse files Browse the repository at this point in the history
  • Loading branch information
valentinpalkovic committed Dec 16, 2022
1 parent 926720d commit 4383d37
Show file tree
Hide file tree
Showing 18 changed files with 520 additions and 57 deletions.
5 changes: 3 additions & 2 deletions code/frameworks/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"prep": "../../../scripts/prepare/bundle.ts"
},
"dependencies": {
"@next/font": "^13.0.7",
"@storybook/builder-webpack5": "7.0.0-beta.10",
"@storybook/core-common": "7.0.0-beta.10",
"@storybook/node-logger": "7.0.0-beta.10",
Expand All @@ -82,7 +83,6 @@
"devDependencies": {
"@babel/core": "^7.20.5",
"@babel/types": "^7.20.5",
"@next/font": "^13.0.6",
"@storybook/addon-actions": "7.0.0-beta.10",
"@types/babel__core": "^7",
"next": "^13.0.5",
Expand Down Expand Up @@ -119,7 +119,8 @@
"./src/index.ts",
"./src/preset.ts",
"./src/preview.tsx",
"./src/next-image-loader-stub.ts"
"./src/next-image-loader-stub.ts",
"./src/font/webpack/loader/storybook-nextjs-font-loader.ts"
],
"platform": "node"
},
Expand Down
30 changes: 0 additions & 30 deletions code/frameworks/nextjs/src/font/babel.test.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export function removeTransformedVariableDeclarations(
/**
* Replaces `@next/font` import with a parameterized import
* @example
* // AST
* // AST of src/example.js
* import { Roboto, Inter } from '@next/font/google'
* const interName = Inter({
* subsets: ['latin'],
Expand All @@ -160,8 +160,8 @@ export function removeTransformedVariableDeclarations(
* })
*
* // Result
* import interName from '@next/font/google?Inter;{"subsets":["latin"]}'
* import robotoName from '@next/font/google?Roboto;{"weight":"400"}'
* import interName from 'storybook-nextjs-font-loader?{filename: "src/example.js", source: "@next/font/google", fontFamily: "Inter", props: {"subsets":["latin"]}}!@next/font/google'
* import robotoName from 'storybook-nextjs-font-loader?{filename: "src/example.js", source: "@next/font/google", fontFamily: "Roboto", props: {"weight": "400"}}!@next/font/google'
*
* // Following code will be removed from removeUnusedVariableDeclarations function
* const interName = Inter({
Expand All @@ -176,19 +176,21 @@ export function replaceImportWithParamterImport(
path: BabelCoreNamespace.NodePath<BabelCoreNamespace.types.ImportDeclaration>,
types: BabelTypes,
source: BabelCoreNamespace.types.StringLiteral,
metas: Array<VariableMeta>
metas: Array<VariableMeta>,
filename: string
) {
// Add an import for each specifier with parameters
path.replaceWithMultiple([
...metas.map((meta) => {
return types.importDeclaration(
[types.importDefaultSpecifier(types.identifier(meta.identifierName))],
types.stringLiteral(
// TODO
`${source.value}?${meta.functionName};${JSON.stringify(meta.properties).replace(
'\\"',
"'"
)}`
`storybook-nextjs-font-loader?${JSON.stringify({
source: source.value,
props: meta.properties,
fontFamily: meta.functionName,
filename,
})}!${source.value}`
)
);
}),
Expand Down
30 changes: 30 additions & 0 deletions code/frameworks/nextjs/src/font/babel/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { transform } from '@babel/core';
import TransformFontImports from '.';

const example = `
import { Inter, Roboto } from '@next/font/google'
import localFont from '@next/font/local'
const myFont = localFont({ src: './my-font.woff2' })
const roboto = Roboto({
weight: '400',
})
const inter = Inter({
subsets: ['latin'],
});
const randomObj = {}
`;

it('should transform AST properly', () => {
const { code } = transform(example, { plugins: [TransformFontImports] })!;
expect(code).toMatchInlineSnapshot(`
"import inter from \\"storybook-nextjs-font-loader?{\\\\\\"source\\\\\\":\\\\\\"@next/font/google\\\\\\",\\\\\\"props\\\\\\":{\\\\\\"subsets\\\\\\":[\\\\\\"latin\\\\\\"]},\\\\\\"fontFamily\\\\\\":\\\\\\"Inter\\\\\\"}!@next/font/google\\";
import roboto from \\"storybook-nextjs-font-loader?{\\\\\\"source\\\\\\":\\\\\\"@next/font/google\\\\\\",\\\\\\"props\\\\\\":{\\\\\\"weight\\\\\\":\\\\\\"400\\\\\\"},\\\\\\"fontFamily\\\\\\":\\\\\\"Roboto\\\\\\"}!@next/font/google\\";
import myFont from \\"storybook-nextjs-font-loader?{\\\\\\"source\\\\\\":\\\\\\"@next/font/local\\\\\\",\\\\\\"props\\\\\\":{\\\\\\"src\\\\\\":\\\\\\"./my-font.woff2\\\\\\"},\\\\\\"fontFamily\\\\\\":\\\\\\"localFont\\\\\\"}!@next/font/local\\";
const randomObj = {};"
`);
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import {
isDefined,
removeTransformedVariableDeclarations,
replaceImportWithParamterImport,
} from './babel.helpers';
} from './helpers';

type Babel = typeof BabelCoreNamespace;

/**
* Transforms "@next/font" imports and usages to a webpack loader friendly format with parameters
* @example
* // src/example.js
* // Turns this code:
* import { Inter, Roboto } from '@next/font/google'
* import localFont from '@next/font/local'
Expand All @@ -26,9 +27,9 @@ type Babel = typeof BabelCoreNamespace;
* });
*
* // Into this code:
* import inter from "@next/font/google?Inter;{\"subsets\":[\"latin\"]}";
* import roboto from "@next/font/google?Roboto;{\"weight\":\"400\"}";
* import myFont from "@next/font/local?localFont;{\"src\":\"./my-font.woff2\"}";
* 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 which is provided by the nextjs swc plugin
* https://github.com/vercel/next.js/pull/40221
Expand All @@ -37,9 +38,10 @@ export default function TransformFontImports({ types }: Babel): BabelCoreNamespa
return {
name: 'storybook-nextjs-font-imports',
visitor: {
ImportDeclaration(path) {
ImportDeclaration(path, state) {
const { node } = path;
const { source } = node;
const { filename = '' } = state;

if (source.value === '@next/font/local') {
const { specifiers } = node;
Expand All @@ -56,7 +58,7 @@ export default function TransformFontImports({ types }: Babel): BabelCoreNamespa
const variableMetas = getVariableMetasBySpecifier(program, types, specifier);

removeTransformedVariableDeclarations(path, types, variableMetas);
replaceImportWithParamterImport(path, types, source, variableMetas);
replaceImportWithParamterImport(path, types, source, variableMetas, filename);
}

if (source.value === '@next/font/google') {
Expand All @@ -75,7 +77,7 @@ export default function TransformFontImports({ types }: Babel): BabelCoreNamespa
.filter(isDefined);

removeTransformedVariableDeclarations(path, types, variableMetas);
replaceImportWithParamterImport(path, types, source, variableMetas);
replaceImportWithParamterImport(path, types, source, variableMetas, filename);
}
},
},
Expand Down
15 changes: 15 additions & 0 deletions code/frameworks/nextjs/src/font/webpack/configureNextFont.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Configuration } from 'webpack';
import VirtualLocalFontModulePlugin from './plugin/virtual-local-font-module';

export function configureNextFont(baseConfig: Configuration) {
baseConfig.plugins = [...(baseConfig.plugins || []), new VirtualLocalFontModulePlugin()];
baseConfig.resolveLoader = {
...baseConfig.resolveLoader,
alias: {
...baseConfig.resolveLoader?.alias,
'storybook-nextjs-font-loader': require.resolve(
'./font/webpack/loader/storybook-nextjs-font-loader'
),
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
fetchCSSFromGoogleFonts,
getFontAxes,
getUrl,
validateData,
} from '@next/font/dist/google/utils';
import loaderUtils from 'next/dist/compiled/loader-utils3';

import type { LoaderOptions } from '../types';

const cssCache = new Map<string, Promise<string>>();

export async function getFontFaceDeclarations(options: LoaderOptions) {
const { fontFamily, weights, styles, selectedVariableAxes, display, variable } = validateData(
options.fontFamily,
[options.props]
);

const fontAxes = getFontAxes(fontFamily, weights, styles, selectedVariableAxes);
const url = getUrl(fontFamily, fontAxes, display);

try {
const hasCachedCSS = cssCache.has(url);
const fontFaceCSS = hasCachedCSS
? cssCache.get(url)
: await fetchCSSFromGoogleFonts(url, fontFamily).catch(() => null);
if (!hasCachedCSS) {
cssCache.set(url, fontFaceCSS);
} else {
cssCache.delete(url);
}
if (fontFaceCSS === null) {
throw Error(`Failed to fetch \`${fontFamily}\` from Google Fonts.`);
}

return {
id: loaderUtils.getHashDigest(url, 'md5', 'hex', 6),
fontFamily,
fontFaceCSS,
weights,
styles,
variable,
};
} catch (error) {
throw new Error("Google Fonts couldn't be loaded.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import loaderUtils from 'next/dist/compiled/loader-utils3';
import { validateData } from '@next/font/dist/local/utils';
import path from 'path';

import type { LoaderOptions } from '../types';

type LocalFontSrc = string | Array<{ path: string; weight?: string; style?: string }>;

export async function getFontFaceDeclarations(options: LoaderOptions, rootContext: string) {
const localFontSrc = options.props.src as LocalFontSrc;

// Parent folder relative to the root context
const parentFolder = options.filename.split('/').slice(0, -1).join('/').replace(rootContext, '');

const { weight, style, variable } = validateData('', options.props);

const id = `font-${loaderUtils.getHashDigest(
Buffer.from(JSON.stringify(localFontSrc)),
'md5',
'hex',
6
)}`;

const getFontFaceCSS = () => {
if (typeof localFontSrc === 'string') {
const localFontPath = path.join(parentFolder, localFontSrc);

return `@font-face {
font-family: ${id};
src: url(${localFontPath});
}`;
}
return localFontSrc
.map((font) => {
const localFontPath = path.join(parentFolder, font.path);

return `@font-face {
font-family: ${id};
src: url(${localFontPath});
${font.weight ? `font-weight: ${font.weight};` : ''}
${font.style ? `font-style: ${font.style};` : ''}
}`;
})
.join('');
};

return {
id,
fontFamily: id,
fontFaceCSS: getFontFaceCSS(),
weights: weight ? [weight] : [],
styles: style ? [style] : [],
variable,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { getFontFaceDeclarations as getGoogleFontFaceDeclarations } from './google/get-font-face-declarations';
import { getFontFaceDeclarations as getLocalFontFaceDeclarations } from './local/get-font-face-declarations';
import type { LoaderOptions } from './types';
import { getCSSMeta } from './utils/get-css-meta';
import { setFontDeclarationsInHead } from './utils/set-font-declarations-in-head';

type FontFaceDeclaration = {
id: string;
fontFamily: string;
fontFaceCSS: any;
weights: string[];
styles: string[];
variable?: string;
};

export default async function storybookNextjsFontLoader(this: any) {
const options = this.getOptions() as LoaderOptions;

// get execution context
const rootCtx = this.rootContext;

let fontFaceDeclaration: FontFaceDeclaration | undefined;

if (options.source === '@next/font/google') {
fontFaceDeclaration = await getGoogleFontFaceDeclarations(options);
}

if (options.source === '@next/font/local') {
fontFaceDeclaration = await getLocalFontFaceDeclarations(options, rootCtx);
}

if (typeof fontFaceDeclaration !== 'undefined') {
const cssMeta = getCSSMeta(fontFaceDeclaration);

return `
${setFontDeclarationsInHead({
fontFaceCSS: cssMeta.fontFaceCSS,
id: fontFaceDeclaration.id,
classNamesCSS: cssMeta.classNamesCSS,
})}
module.exports = {
className: "${cssMeta.className}",
style: ${JSON.stringify(cssMeta.style)}
${cssMeta.variableClassName ? `, variable: "${cssMeta.variableClassName}"` : ''}
}
`;
}

return `module.exports = {}`;
}
18 changes: 18 additions & 0 deletions code/frameworks/nextjs/src/font/webpack/loader/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export type LoaderOptions = {
/**
* Initial import name. Can be `@next/font/google` or `@next/font/local`
*/
source: string;
/**
* Props passed to the `@next/font` function call
*/
props: Record<string, any>;
/**
* Font Family name
*/
fontFamily: string;
/**
* Filename of the issuer file, which imports `@next/font/google` or `@next/font/local
*/
filename: string;
};
Loading

0 comments on commit 4383d37

Please sign in to comment.