diff --git a/docs/advanced-features/codemods.md b/docs/advanced-features/codemods.md index 02b39bdadf69..dfe8d18657c7 100644 --- a/docs/advanced-features/codemods.md +++ b/docs/advanced-features/codemods.md @@ -45,7 +45,7 @@ export default function Page() { ### `next-image-to-legacy-image` -Safely migrates existing Next.js 10, 11, 12 applications importing `next/image` to the renamed `next/legacy/image` import in Next.js 13. +This codemod safely migrates existing Next.js 10, 11, 12 applications importing `next/image` to the renamed `next/legacy/image` import in Next.js 13. For example: @@ -81,7 +81,7 @@ export default function Home() { ### `next-image-experimental` (experimental) -Dangerously migrates from `next/legacy/image` to the new `next/image` by adding inline styles and removing unused props. +This codemod dangerously migrates from `next/legacy/image` to the new `next/image` by adding inline styles and removing unused props. Please note this codemod is experimental and only covers static usage (such as ``) but not dynamic usage (such as ``). - Removes `layout` prop and adds `style` - Removes `objectFit` prop and adds `style` diff --git a/docs/api-reference/next/font.md b/docs/api-reference/next/font.md index 6d02fc1d8652..ddc6836b7ca3 100644 --- a/docs/api-reference/next/font.md +++ b/docs/api-reference/next/font.md @@ -91,7 +91,7 @@ A string value to define the CSS variable name to be used if the style is applie ### Font function arguments -For usage, review [Local Fonts](/docs/optimizing/fonts#local-fonts). +For usage, review [Local Fonts](/docs/basic-features/font-optimization.md#local-fonts). | Key | Example | Data type | Required | | ------------------------------------------- | ----------------------------------------------------------- | -------------------------------------- | -------- | diff --git a/docs/basic-features/font-optimization.md b/docs/basic-features/font-optimization.md index 3d78e78295ad..ac5119f92372 100644 --- a/docs/basic-features/font-optimization.md +++ b/docs/basic-features/font-optimization.md @@ -28,8 +28,9 @@ Import the font you would like to use from `@next/font/google` as a function. We To use the font in all your pages, add it to [`_app.js` file](https://nextjs.org/docs/advanced-features/custom-app) under `/pages` as shown below: -```js:pages/_app.js -import { Inter } from '@next/font/google'; +```js +// pages/_app.js +import { Inter } from '@next/font/google' // If loading a variable font, you don't need to specify the font weight const inter = Inter() @@ -45,8 +46,9 @@ export default function MyApp({ Component, pageProps }) { If you can't use a variable font, you will **need to specify a weight**: -```js:pages/_app.js -import { Roboto } from '@next/font/google'; +```js +// pages/_app.js +import { Roboto } from '@next/font/google' const roboto = Roboto({ weight: '400', @@ -65,10 +67,11 @@ export default function MyApp({ Component, pageProps }) { You can also use the font without a wrapper and `className` by injecting it inside the `` as follows: -```js:pages/_app.js -import { Inter } from '@next/font/google'; +```js +// pages/_app.js +import { Inter } from '@next/font/google' -const inter = Inter(); +const inter = Inter() export default function MyApp({ Component, pageProps }) { return ( @@ -110,8 +113,9 @@ This can be done in 2 ways: - On a font per font basis by adding it to the function call - ```js:pages/_app.js - const inter = Inter({ subsets: ["latin"] }); + ```js + // pages/_app.js + const inter = Inter({ subsets: ['latin'] }) ``` - Globally for all your fonts in your `next.config.js` @@ -135,11 +139,12 @@ View the [Font API Reference](/docs/api-reference/next/font.md#nextfontgoogle) f Import `@next/font/local` and specify the `src` of your local font file. We recommend using [**variable fonts**](https://fonts.google.com/variablefonts) for the best performance and flexibility. -```js:pages/_app.js -import localFont from '@next/font/local'; +```js +// pages/_app.js +import localFont from '@next/font/local' // Font files can be colocated inside of `pages` -const myFont = localFont({ src: './my-font.woff2' }); +const myFont = localFont({ src: './my-font.woff2' }) export default function MyApp({ Component, pageProps }) { return ( diff --git a/docs/routing/introduction.md b/docs/routing/introduction.md index 662c395937ff..c19a59486c9b 100644 --- a/docs/routing/introduction.md +++ b/docs/routing/introduction.md @@ -68,7 +68,7 @@ The example above uses multiple links. Each one maps a path (`href`) to a known - `/about` → `pages/about.js` - `/blog/hello-world` → `pages/blog/[slug].js` -Any `` in the viewport (initially or through scroll) will be prefetched by default (including the corresponding data) for pages using [Static Generation](/docs/basic-features/data-fetching/get-static-props.md). The corresponding data for [server-rendered](/docs/basic-features/data-fetching/get-server-side-props.md) routes is fetched _only when_ the is clicked. +Any `` in the viewport (initially or through scroll) will be prefetched by default (including the corresponding data) for pages using [Static Generation](/docs/basic-features/data-fetching/get-static-props.md). The corresponding data for [server-rendered](/docs/basic-features/data-fetching/get-server-side-props.md) routes is fetched _only when_ the `` is clicked. ### Linking to dynamic paths diff --git a/docs/testing.md b/docs/testing.md index 7dca84b8879a..7414761d37c3 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -293,7 +293,7 @@ module.exports = createJestConfig(customJestConfig) Under the hood, `next/jest` is automatically configuring Jest for you, including: - Setting up `transform` using [SWC](https://nextjs.org/docs/advanced-features/compiler) -- Auto mocking stylesheets (`.css`, `.module.css`, and their scss variants) and image imports +- Auto mocking stylesheets (`.css`, `.module.css`, and their scss variants), image imports and [`@next/font`](https://nextjs.org/docs/basic-features/font-optimization) - Loading `.env` (and all variants) into `process.env` - Ignoring `node_modules` from test resolving and transforms - Ignoring `.next` from test resolving diff --git a/docs/upgrading.md b/docs/upgrading.md index bd7539b659a0..86f1cd626c6f 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -34,16 +34,16 @@ You can continue using `pages` with new features that work in both directories, ### `` Component -Next.js 12 introduced new improvements to the Image Component with a temporary import: `next/future/image`. These improvements included less client-side JavaScript, easier ways to extend and style images, better accessibility, and native browser lazy loading. +Next.js 12 introduced many improvements to the Image Component with a temporary import: `next/future/image`. These improvements included less client-side JavaScript, easier ways to extend and style images, better accessibility, and native browser lazy loading. -In version 13, this new behavior is now the default for `next/image`. +Starting in Next.js 13, this new behavior is now the default for `next/image`. There are two codemods to help you migrate to the new Image Component: -- [**`next-image-to-legacy-image` codemod**](/docs/advanced-features/codemods.md#rename-instances-of-nextimage): Safely and automatically renames `next/image` imports to `next/legacy/image`. Existing components will maintain the same behavior. -- [**`next-image-experimental` codemod**](/docs/advanced-features/codemods.md#migrate-next-image-experimental-experimental): Dangerously adds inline styles and removes unused props using the experimental. This will change the behavior of existing components to match the new defaults. To use this codemod, you need to run the `next-image-to-legacy-image` codemod first. +- [next-image-to-legacy-image](/docs/advanced-features/codemods.md#rename-instances-of-nextimage): This codemod will safely and automatically rename `next/image` imports to `next/legacy/image` to maintain the same behavior as Next.js 12. We recommend running this codemod to quickly update to Next.js 13 automatically. +- [next-image-experimental](/docs/advanced-features/codemods.md#next-image-experimental-experimental): After running the previous codemod, you can optionally run this experimental codemod to upgrade `next/legacy/image` to the new `next/image`, which will remove unused props and add inline styles. Please note this codemod is experimental and only covers static usage (such as ``) but not dynamic usage (such as ``). -Alternatively, you can manually update props by following the [`next/future/image` migration guide](/docs/api-reference/next/image.md#migration). This will change the behavior of existing components to match the new defaults. +Alternatively, you can manually update by following the [migration guide](/docs/advanced-features/codemods.md#next-image-experimental-experimental) and also see the [legacy comparison](/docs/api-reference/next/legacy/image.md#comparison). ### `` Component diff --git a/errors/next-image-upgrade-to-13.md b/errors/next-image-upgrade-to-13.md index eeb42566da13..3553cc3e3a40 100644 --- a/errors/next-image-upgrade-to-13.md +++ b/errors/next-image-upgrade-to-13.md @@ -31,7 +31,7 @@ After running this codemod, you can optionally upgrade `next/legacy/image` to th npx @next/codemod next-image-experimental . ``` -Please note this second codemod is experimental and only covers static usage, not dynamic usage (such ``). +Please note this second codemod is experimental and only covers static usage (such as ``) but not dynamic usage (such as ``). ### Useful Links diff --git a/examples/with-turbopack/app/hooks/[categorySlug]/[subCategorySlug]/page.tsx b/examples/with-turbopack/app/hooks/[categorySlug]/[subCategorySlug]/page.tsx index 17332209bc76..59b42fd7fec9 100644 --- a/examples/with-turbopack/app/hooks/[categorySlug]/[subCategorySlug]/page.tsx +++ b/examples/with-turbopack/app/hooks/[categorySlug]/[subCategorySlug]/page.tsx @@ -4,7 +4,7 @@ import { SkeletonCard } from '@/ui/SkeletonCard'; export default function Page({ params }: PageProps) { const category = use( - fetchSubCategory(params.categorySlug, params.subCategory), + fetchSubCategory(params.categorySlug, params.subCategorySlug), ); if (!category) return null; return ( diff --git a/jest.config.js b/jest.config.js index a99f0afcea12..177949779f05 100644 --- a/jest.config.js +++ b/jest.config.js @@ -14,6 +14,9 @@ const customJestConfig = { globals: { AbortSignal: global.AbortSignal, }, + moduleNameMapper: { + '@next/font/(.*)': '@next/font/$1', + }, } // createJestConfig is exported in this way to ensure that next/jest can load the Next.js config which is async diff --git a/packages/font/src/google/loader.ts b/packages/font/src/google/loader.ts index b835c439ade0..a4692077d364 100644 --- a/packages/font/src/google/loader.ts +++ b/packages/font/src/google/loader.ts @@ -49,27 +49,21 @@ const downloadGoogleFonts: FontLoader = async ({ ) } - let fontFaceDeclarations = '' - for (const weight of weights) { - for (const style of styles) { - const fontAxes = getFontAxes( - fontFamily, - weight, - style, - selectedVariableAxes - ) - const url = getUrl(fontFamily, fontAxes, display) + const fontAxes = getFontAxes( + fontFamily, + weights, + styles, + selectedVariableAxes + ) + const url = getUrl(fontFamily, fontAxes, display) - let cachedCssRequest = cssCache.get(url) - const fontFaceDeclaration = - cachedCssRequest ?? (await fetchCSSFromGoogleFonts(url, fontFamily)) - if (!cachedCssRequest) { - cssCache.set(url, fontFaceDeclaration) - } else { - cssCache.delete(url) - } - fontFaceDeclarations += `${fontFaceDeclaration}\n` - } + let cachedCssRequest = cssCache.get(url) + const fontFaceDeclarations = + cachedCssRequest ?? (await fetchCSSFromGoogleFonts(url, fontFamily)) + if (!cachedCssRequest) { + cssCache.set(url, fontFaceDeclarations) + } else { + cssCache.delete(url) } // Find font files to download diff --git a/packages/font/src/google/utils.ts b/packages/font/src/google/utils.ts index 5060ebcc29fa..16baec887f90 100644 --- a/packages/font/src/google/utils.ts +++ b/packages/font/src/google/utils.ts @@ -126,25 +126,50 @@ export function validateData(functionName: string, data: any): FontOptions { export function getUrl( fontFamily: string, - axes: [string, string][], + axes: { + wght: string[] + ital: string[] + variableAxes?: [string, string][] + }, display: string ) { + // Variants are all combinations of weight and style, each variant will result in a separate font file + const variants: Array<[string, string][]> = [] + for (const wgth of axes.wght) { + if (axes.ital.length === 0) { + variants.push([['wght', wgth], ...(axes.variableAxes ?? [])]) + } else { + for (const ital of axes.ital) { + variants.push([ + ['ital', ital], + ['wght', wgth], + ...(axes.variableAxes ?? []), + ]) + } + } + } + // Google api requires the axes to be sorted, starting with lowercase words - axes.sort(([a], [b]) => { - const aIsLowercase = a.charCodeAt(0) > 96 - const bIsLowercase = b.charCodeAt(0) > 96 - if (aIsLowercase && !bIsLowercase) return -1 - if (bIsLowercase && !aIsLowercase) return 1 + if (axes.variableAxes) { + variants.forEach((variant) => { + variant.sort(([a], [b]) => { + const aIsLowercase = a.charCodeAt(0) > 96 + const bIsLowercase = b.charCodeAt(0) > 96 + if (aIsLowercase && !bIsLowercase) return -1 + if (bIsLowercase && !aIsLowercase) return 1 - return a > b ? 1 : -1 - }) + return a > b ? 1 : -1 + }) + }) + } return `https://fonts.googleapis.com/css2?family=${fontFamily.replace( / /g, '+' - )}:${axes.map(([key]) => key).join(',')}@${axes - .map(([, val]) => val) - .join(',')}&display=${display}` + )}:${variants[0].map(([key]) => key).join(',')}@${variants + .map((variant) => variant.map(([, val]) => val).join(',')) + .sort() + .join(';')}&display=${display}` } export async function fetchCSSFromGoogleFonts(url: string, fontFamily: string) { @@ -192,17 +217,23 @@ export async function fetchFontFile(url: string) { export function getFontAxes( fontFamily: string, - weight: string, - style: string, + weights: string[], + styles: string[], selectedVariableAxes?: string[] -): [string, string][] { +): { + wght: string[] + ital: string[] + variableAxes?: [string, string][] +} { const allAxes: Array<{ tag: string; min: number; max: number }> = ( fontData as any )[fontFamily].axes - const italicAxis: [string, string][] = - style === 'italic' ? [['ital', '1']] : [] + const hasItalic = styles.includes('italic') + const hasNormal = styles.includes('normal') + const ital = hasItalic ? [...(hasNormal ? ['0'] : []), '1'] : [] - if (weight === 'variable') { + // Weights will always contain one element if it's a variable font + if (weights[0] === 'variable') { if (selectedVariableAxes) { const defineAbleAxes: string[] = allAxes .map(({ tag }) => tag) @@ -228,14 +259,25 @@ export function getFontAxes( }) } - const variableAxes: [string, string][] = allAxes - .filter( - ({ tag }) => tag === 'wght' || selectedVariableAxes?.includes(tag) - ) - .map(({ tag, min, max }) => [tag, `${min}..${max}`]) + let weightAxis: string + const variableAxes: [string, string][] = [] + for (const { tag, min, max } of allAxes) { + if (tag === 'wght') { + weightAxis = `${min}..${max}` + } else if (selectedVariableAxes?.includes(tag)) { + variableAxes.push([tag, `${min}..${max}`]) + } + } - return [...italicAxis, ...variableAxes] + return { + wght: [weightAxis!], + ital, + variableAxes, + } } else { - return [...italicAxis, ['wght', weight]] + return { + ital, + wght: weights, + } } } diff --git a/packages/next/build/jest/__mocks__/nextFontMock.js b/packages/next/build/jest/__mocks__/nextFontMock.js new file mode 100644 index 000000000000..f076797502d8 --- /dev/null +++ b/packages/next/build/jest/__mocks__/nextFontMock.js @@ -0,0 +1,12 @@ +module.exports = new Proxy( + {}, + { + get: function getter() { + return () => ({ + className: 'className', + variable: 'variable', + style: { fontFamily: 'fontFamily' }, + }) + }, + } +) diff --git a/packages/next/build/jest/jest.ts b/packages/next/build/jest/jest.ts index ef17dc10a189..8b1e704bc1b0 100644 --- a/packages/next/build/jest/jest.ts +++ b/packages/next/build/jest/jest.ts @@ -115,6 +115,9 @@ export default function nextJest(options: { dir?: string } = {}) { // Keep .svg to it's own rule to make overriding easy '^.+\\.(svg)$': require.resolve(`./__mocks__/fileMock.js`), + // Handle @next/font + '@next/font/(.*)': require.resolve('./__mocks__/nextFontMock.js'), + // custom config comes last to ensure the above rules are matched, // fixes the case where @pages/(.*) -> src/pages/$! doesn't break // CSS/image mocks diff --git a/packages/next/build/swc/options.js b/packages/next/build/swc/options.js index 6dfb154cd0e1..673d4a0dd279 100644 --- a/packages/next/build/swc/options.js +++ b/packages/next/build/swc/options.js @@ -33,7 +33,6 @@ function getBaseSWCOptions({ jsConfig, swcCacheDir, isServerLayer, - relativeFilePathFromRoot, hasServerComponents, }) { const parserConfig = getParserOptions({ filename, jsConfig }) @@ -131,15 +130,6 @@ function getBaseSWCOptions({ isServer: !!isServerLayer, } : false, - fontLoaders: - nextConfig?.experimental?.fontLoaders && relativeFilePathFromRoot - ? { - fontLoaders: nextConfig.experimental.fontLoaders.map( - ({ loader }) => loader - ), - relativeFilePathFromRoot, - } - : null, } } @@ -255,6 +245,15 @@ export function getLoaderSWCOptions({ hasServerComponents, }) + if (nextConfig?.experimental?.fontLoaders && relativeFilePathFromRoot) { + baseOptions.fontLoaders = { + fontLoaders: nextConfig.experimental.fontLoaders.map( + ({ loader }) => loader + ), + relativeFilePathFromRoot, + } + } + const isNextDist = nextDistPath.test(filename) if (isServer) { diff --git a/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts b/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts index bb781e778b3b..c048c8b90817 100644 --- a/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts @@ -189,7 +189,7 @@ export class FlightClientEntryPlugin { dependency: layoutOrPageDependency, }) - const isAbsoluteRequest = layoutOrPageRequest[0] === '/' + const isAbsoluteRequest = path.isAbsolute(layoutOrPageRequest) // Next.js internals are put into a separate entry. if (!isAbsoluteRequest) { diff --git a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts index 99859a5bc437..00aefb985d64 100644 --- a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts @@ -201,7 +201,7 @@ export class FlightManifestPlugin { }, } } else { - // It is possible that there are mtuliepl modules with the same resouce, + // It is possible that there are multiple modules with the same resource, // e.g. extracted by mini-css-extract-plugin. In that case we need to // merge the chunks. manifest[resource].default.chunks = [ diff --git a/test/e2e/next-font/app/pages/with-google-fonts.js b/test/e2e/next-font/app/pages/with-google-fonts.js index cd6428a4ec3a..39ede912aa31 100644 --- a/test/e2e/next-font/app/pages/with-google-fonts.js +++ b/test/e2e/next-font/app/pages/with-google-fonts.js @@ -1,7 +1,16 @@ -import { Fraunces, Indie_Flower } from '@next/font/google' +import { Fraunces, Indie_Flower, Roboto } from '@next/font/google' -const indieFlower = Indie_Flower({ weight: '400' }) -const fraunces = Fraunces({ weight: '400' }) +const indieFlower = Indie_Flower({ weight: '400', preload: false }) +const fraunces = Fraunces({ weight: '400', preload: false }) + +const robotoMultiple = Roboto({ + weight: ['900', '100'], + style: ['normal', 'italic'], +}) +const frauncesMultiple = Fraunces({ + style: ['italic', 'normal'], + axes: ['SOFT', 'WONK', 'opsz'], +}) export default function WithFonts() { return ( @@ -12,6 +21,12 @@ export default function WithFonts() {
{JSON.stringify(fraunces)}
+
+ {JSON.stringify(robotoMultiple)} +
+
+ {JSON.stringify(frauncesMultiple)} +
) } diff --git a/test/e2e/next-font/app/pages/with-local-fonts.js b/test/e2e/next-font/app/pages/with-local-fonts.js index ac78e3065b26..37f218859d24 100644 --- a/test/e2e/next-font/app/pages/with-local-fonts.js +++ b/test/e2e/next-font/app/pages/with-local-fonts.js @@ -99,6 +99,31 @@ const robotoVar2 = localFont({ ], }) +const robotoWithPreload = localFont({ + src: [ + { + path: '../fonts/roboto/roboto-100.woff2', + weight: '100', + style: 'normal', + }, + { + path: '../fonts/roboto/roboto-900-italic.woff2', + weight: '900', + style: 'italic', + }, + { + path: '../fonts/roboto/roboto-100.woff2', + weight: '100', + style: 'normal', + }, + { + path: '../fonts/roboto/roboto-100-italic.woff2', + weight: '900', + style: 'italic', + }, + ], +}) + export default function WithFonts() { return ( <> @@ -117,6 +142,12 @@ export default function WithFonts() {
{JSON.stringify(robotoVar2)}
+
+ {JSON.stringify(robotoWithPreload)} +
) } diff --git a/test/e2e/next-font/google-font-mocked-responses.js b/test/e2e/next-font/google-font-mocked-responses.js index 40b9da03a2d1..f1fa0f4f3843 100644 --- a/test/e2e/next-font/google-font-mocked-responses.js +++ b/test/e2e/next-font/google-font-mocked-responses.js @@ -543,4 +543,314 @@ module.exports = { )}) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }`, + 'https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght,SOFT,WONK@0,9..144,100..900,0..100,0..1;1,9..144,100..900,0..100,0..1&display=optional': ` + /* vietnamese */ +@font-face { + font-family: 'Fraunces'; + font-style: italic; + font-weight: 100 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/fraunces/v24/6NUT8FyLNQOQZAnv9ZwNpOQkzP9Ddt2Wew.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Fraunces'; + font-style: italic; + font-weight: 100 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/fraunces/v24/6NUT8FyLNQOQZAnv9ZwNpOUkzP9Ddt2Wew.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Fraunces'; + font-style: italic; + font-weight: 100 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/fraunces/v24/6NUT8FyLNQOQZAnv9ZwNpOskzP9Ddt0.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* vietnamese */ +@font-face { + font-family: 'Fraunces'; + font-style: normal; + font-weight: 100 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/fraunces/v24/6NUV8FyLNQOQZAnv9ZwHlOkuy91BRtw.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Fraunces'; + font-style: normal; + font-weight: 100 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/fraunces/v24/6NUV8FyLNQOQZAnv9ZwGlOkuy91BRtw.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Fraunces'; + font-style: normal; + font-weight: 100 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/fraunces/v24/6NUV8FyLNQOQZAnv9ZwIlOkuy91B.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + `, + 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,900;1,100;1,900&display=optional': ` + /* cyrillic-ext */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 100; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEz0dL-vwnYh2eg.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 100; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEzQdL-vwnYh2eg.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 100; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEzwdL-vwnYh2eg.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 100; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEzMdL-vwnYh2eg.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 100; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEz8dL-vwnYh2eg.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 100; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEz4dL-vwnYh2eg.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 100; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOiCnqEu92Fr1Mu51QrEzAdL-vwnYg.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOjCnqEu92Fr1Mu51TLBCc3CsTYl4BOQ3o.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOjCnqEu92Fr1Mu51TLBCc-CsTYl4BOQ3o.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOjCnqEu92Fr1Mu51TLBCc2CsTYl4BOQ3o.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOjCnqEu92Fr1Mu51TLBCc5CsTYl4BOQ3o.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOjCnqEu92Fr1Mu51TLBCc1CsTYl4BOQ3o.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOjCnqEu92Fr1Mu51TLBCc0CsTYl4BOQ3o.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOjCnqEu92Fr1Mu51TLBCc6CsTYl4BO.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 100; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxFIzIXKMnyrYk.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 100; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxMIzIXKMnyrYk.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 100; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxEIzIXKMnyrYk.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 100; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxLIzIXKMnyrYk.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 100; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxHIzIXKMnyrYk.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 100; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxGIzIXKMnyrYk.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 100; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOkCnqEu92Fr1MmgVxIIzIXKMny.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmYUtfCRc4AMP6lbBP.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmYUtfABc4AMP6lbBP.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmYUtfCBc4AMP6lbBP.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmYUtfBxc4AMP6lbBP.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmYUtfCxc4AMP6lbBP.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmYUtfChc4AMP6lbBP.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 900; + font-display: optional; + src: url(https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmYUtfBBc4AMP6lQ.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + `, } diff --git a/test/e2e/next-font/index.test.ts b/test/e2e/next-font/index.test.ts index 8768d8a966a4..d4a933b524bf 100644 --- a/test/e2e/next-font/index.test.ts +++ b/test/e2e/next-font/index.test.ts @@ -335,23 +335,43 @@ describe('@next/font/google', () => { expect($('link[rel="preconnect"]').length).toBe(0) // Preload - expect($('link[as="font"]').length).toBe(2) - // _app - expect($('link[as="font"]').get(0).attribs).toEqual({ - as: 'font', - crossorigin: 'anonymous', - href: '/_next/static/media/0812efcfaefec5ea.p.woff2', - rel: 'preload', - type: 'font/woff2', - }) - // with-local-fonts - expect($('link[as="font"]').get(1).attribs).toEqual({ - as: 'font', - crossorigin: 'anonymous', - href: '/_next/static/media/ab6fdae82d1a8d92.p.woff2', - rel: 'preload', - type: 'font/woff2', - }) + expect($('link[as="font"]').length).toBe(5) + expect( + Array.from($('link[as="font"]')) + .map((el) => el.attribs.href) + .sort() + ).toEqual([ + '/_next/static/media/02205c9944024f15.p.woff2', + '/_next/static/media/0812efcfaefec5ea.p.woff2', + '/_next/static/media/1deec1af325840fd.p.woff2', + '/_next/static/media/ab6fdae82d1a8d92.p.woff2', + '/_next/static/media/d55edb6f37902ebf.p.woff2', + ]) + }) + + test('google fonts with multiple weights/styles', async () => { + const html = await renderViaHTTP(next.url, '/with-google-fonts') + const $ = cheerio.load(html) + + // Preconnect + expect($('link[rel="preconnect"]').length).toBe(0) + + // Preload + expect($('link[as="font"]').length).toBe(7) + + expect( + Array.from($('link[as="font"]')) + .map((el) => el.attribs.href) + .sort() + ).toEqual([ + '/_next/static/media/0812efcfaefec5ea.p.woff2', + '/_next/static/media/4f3dcdf40b3ca86d.p.woff2', + '/_next/static/media/560a6db6ac485cb1.p.woff2', + '/_next/static/media/686d1702f12625fe.p.woff2', + '/_next/static/media/86d92167ff02c708.p.woff2', + '/_next/static/media/c9baea324111137d.p.woff2', + '/_next/static/media/fb68b4558e2a718e.p.woff2', + ]) }) }) diff --git a/test/production/jest/index.test.ts b/test/production/jest/index.test.ts index 5b831c902609..67db89fb5db3 100644 --- a/test/production/jest/index.test.ts +++ b/test/production/jest/index.test.ts @@ -21,6 +21,11 @@ describe('next/jest', () => { import Image from "next/image"; import img from "../public/vercel.svg"; import styles from "../styles/index.module.css"; + import localFont from "@next/font/local"; + import { Inter } from "@next/font/google"; + + const inter = Inter(); + const myFont = localFont({ src: "./my-font.woff2" }); const Comp = dynamic(() => import("../components/comp"), { loading: () =>

Loading...

, @@ -32,6 +37,7 @@ describe('next/jest', () => { logo logo 2

hello world

+

hello world

} `, @@ -118,8 +124,10 @@ describe('next/jest', () => { expect(router.push._isMockFunction).toBeTruthy() }) `, + 'pages/my-font.woff2': 'fake font', }, dependencies: { + '@next/font': 'canary', jest: '27.4.7', '@testing-library/jest-dom': '5.16.1', '@testing-library/react': '12.1.2', diff --git a/test/unit/google-font-loader.test.ts b/test/unit/google-font-loader.test.ts index cbd690989c8f..3a5ff41620e5 100644 --- a/test/unit/google-font-loader.test.ts +++ b/test/unit/google-font-loader.test.ts @@ -73,6 +73,30 @@ describe('@next/font/google loader', () => { { weight: '400' }, 'https://fonts.googleapis.com/css2?family=Molle:ital,wght@1,400&display=optional', ], + [ + 'Roboto', + { weight: ['500', '300', '400'], style: ['normal', 'italic'] }, + 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=optional', + ], + [ + 'Roboto Mono', + { style: ['italic', 'normal'] }, + 'https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=optional', + ], + [ + 'Fraunces', + { + style: ['normal', 'italic'], + axes: ['WONK', 'opsz', 'SOFT'], + }, + 'https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght,SOFT,WONK@0,9..144,100..900,0..100,0..1;1,9..144,100..900,0..100,0..1&display=optional', + ], + + [ + 'Poppins', + { weight: ['900', '400', '100'] }, + 'https://fonts.googleapis.com/css2?family=Poppins:wght@100;400;900&display=optional', + ], ])('%s', async (functionName: string, data: any, url: string) => { fetch.mockResolvedValue({ ok: true, @@ -88,66 +112,10 @@ describe('@next/font/google loader', () => { isServer: true, variableName: 'myFont', }) - expect(css).toBe('OK\n') + expect(css).toBe('OK') expect(fetch).toHaveBeenCalledTimes(1) expect(fetch).toHaveBeenCalledWith(url, expect.any(Object)) }) - - test('Multiple weights and styles', async () => { - let i = 1 - fetch.mockResolvedValue({ - ok: true, - text: async () => `${i++}`, - }) - - const { css } = await loader({ - functionName: 'Roboto', - data: [ - { - weight: ['300', '400', '500'], - style: ['normal', 'italic'], - }, - ], - config: { subsets: [] }, - emitFontFile: jest.fn(), - resolve: jest.fn(), - fs: {} as any, - isServer: true, - variableName: 'myFont', - }) - expect(css).toBe('1\n2\n3\n4\n5\n6\n') - expect(fetch).toHaveBeenCalledTimes(6) - expect(fetch).toHaveBeenNthCalledWith( - 1, - 'https://fonts.googleapis.com/css2?family=Roboto:wght@300&display=optional', - expect.any(Object) - ) - expect(fetch).toHaveBeenNthCalledWith( - 2, - 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@1,300&display=optional', - expect.any(Object) - ) - expect(fetch).toHaveBeenNthCalledWith( - 3, - 'https://fonts.googleapis.com/css2?family=Roboto:wght@400&display=optional', - expect.any(Object) - ) - expect(fetch).toHaveBeenNthCalledWith( - 4, - 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@1,400&display=optional', - expect.any(Object) - ) - expect(fetch).toHaveBeenNthCalledWith( - 5, - 'https://fonts.googleapis.com/css2?family=Roboto:wght@500&display=optional', - expect.any(Object) - ) - expect(fetch).toHaveBeenNthCalledWith( - 6, - 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@1,500&display=optional', - expect.any(Object) - ) - }) }) describe('Errors', () => {