diff --git a/examples/next/getting-started/src/pages/[...pageUri].tsx b/examples/next/getting-started/src/pages/[...pageUri].tsx index 5fc44ce14..c47ba0d3a 100644 --- a/examples/next/getting-started/src/pages/[...pageUri].tsx +++ b/examples/next/getting-started/src/pages/[...pageUri].tsx @@ -2,7 +2,7 @@ import { getNextStaticProps, is404 } from '@faustjs/next'; import { Footer, Header, Hero } from 'components'; import { GetStaticPropsContext } from 'next'; import Head from 'next/head'; -import { client, Page as PageType } from 'client'; +import { client, Page as PageType } from 'client'; export interface PageProps { page: PageType | PageType['preview']['node'] | null | undefined; @@ -49,15 +49,10 @@ export default function Page() { } export async function getStaticProps(context: GetStaticPropsContext) { - if (await is404(client, context)) { - return { - notFound: true, - }; - } - return getNextStaticProps(context, { Page, client, + notFound: await is404(context, { client }), }); } diff --git a/examples/next/getting-started/src/pages/category/[categorySlug]/index.tsx b/examples/next/getting-started/src/pages/category/[categorySlug]/index.tsx index 9f88cd916..cd90bd2d6 100644 --- a/examples/next/getting-started/src/pages/category/[categorySlug]/index.tsx +++ b/examples/next/getting-started/src/pages/category/[categorySlug]/index.tsx @@ -53,15 +53,10 @@ export default function Page() { } export async function getStaticProps(context: GetStaticPropsContext) { - if (await is404(client, context)) { - return { - notFound: true, - }; - } - return getNextStaticProps(context, { Page, client, + notFound: await is404(context, { client }), }); } diff --git a/examples/next/getting-started/src/pages/posts/[postSlug]/index.tsx b/examples/next/getting-started/src/pages/posts/[postSlug]/index.tsx index 010165c67..22315d60b 100644 --- a/examples/next/getting-started/src/pages/posts/[postSlug]/index.tsx +++ b/examples/next/getting-started/src/pages/posts/[postSlug]/index.tsx @@ -49,15 +49,10 @@ export default function Page() { } export async function getStaticProps(context: GetStaticPropsContext) { - if (await is404(client, context)) { - return { - notFound: true, - }; - } - return getNextStaticProps(context, { Page, client, + notFound: await is404(context, { client }), }); } diff --git a/internal/website/docs/next/reference/handle-404s.mdx b/internal/website/docs/next/reference/handle-404s.mdx index abbadad82..5634e1e6d 100644 --- a/internal/website/docs/next/reference/handle-404s.mdx +++ b/internal/website/docs/next/reference/handle-404s.mdx @@ -48,15 +48,10 @@ export default function Page() { } export async function getStaticProps(context: GetStaticPropsContext) { - if (await is404(client, context)) { - return { - notFound: true, - }; - } - return getNextStaticProps(context, { Page, - client, + client, + notFound: await is404(context, { client }), }); } ``` diff --git a/packages/next/src/getProps.ts b/packages/next/src/getProps.ts index f8bddc196..38be27051 100644 --- a/packages/next/src/getProps.ts +++ b/packages/next/src/getProps.ts @@ -1,12 +1,13 @@ /* eslint-disable react/no-children-prop */ import { CategoryIdType, PageIdType, PostIdType } from '@faustjs/core'; -import { isObject } from 'lodash'; +import { isBoolean, isNumber, isObject } from 'lodash'; import isNil from 'lodash/isNil'; import { GetServerSidePropsContext, GetStaticPropsContext, GetStaticPropsResult, GetServerSidePropsResult, + Redirect, } from 'next'; import { RouterContext } from 'next/dist/next-server/lib/router-context'; @@ -25,22 +26,41 @@ import { export const CLIENT_CACHE_PROP = '__CLIENT_CACHE_PROP'; -export interface NextPropsConfig> { +export interface GetNextServerSidePropsConfig> { client: ReturnType; Page?: FunctionComponent | ComponentClass; props?: Props; + notFound?: boolean; + redirect?: Redirect; +} + +export interface GetNextStaticPropsConfig> { + client: ReturnType; + Page?: FunctionComponent | ComponentClass; + props?: Props; + revalidate?: number | boolean; + notFound?: boolean; + redirect?: Redirect; } export interface PageProps { props: Props & { [CLIENT_CACHE_PROP]: string | null }; } +export interface Is404Config { + client: ReturnType; +} + export async function getProps< Context extends GetStaticPropsContext | GetServerSidePropsContext, Props, >( context: Context, - { client, Page, props }: NextPropsConfig, + { + client, + Page, + props, + }: GetNextServerSidePropsConfig | GetNextStaticPropsConfig, ): Promise> { let cacheSnapshot: string | undefined; client.setAsRoot(); @@ -83,7 +103,7 @@ export async function getProps< export async function is404< Context extends GetStaticPropsContext | GetServerSidePropsContext, ->(client: ReturnType, { params }: Context): Promise { +>({ params }: Context, { client }: Is404Config): Promise { if (!params) { return false; } @@ -181,15 +201,43 @@ export async function is404< export async function getNextServerSideProps( context: GetServerSidePropsContext, - config: NextPropsConfig, + config: GetNextServerSidePropsConfig, ): Promise> { + const { notFound, redirect } = config; + + if (isBoolean(notFound) && notFound === true) { + return { + notFound, + }; + } + + if (isObject(redirect)) { + return { + redirect, + }; + } + return getProps(context, config); } export async function getNextStaticProps( context: GetStaticPropsContext, - config: NextPropsConfig, + config: GetNextStaticPropsConfig, ): Promise> { + const { notFound, redirect, revalidate } = config; + + if (isBoolean(notFound) && notFound === true) { + return { + notFound, + }; + } + + if (isObject(redirect)) { + return { + redirect, + }; + } + const pageProps: GetStaticPropsResult = await getProps( context, config, @@ -197,7 +245,7 @@ export async function getNextStaticProps( /* eslint-disable @typescript-eslint/no-explicit-any */ if (isObject(pageProps.props)) { - pageProps.revalidate = 1; + pageProps.revalidate = isNumber(revalidate) ? revalidate : 1; } /* eslint-enable @typescript-eslint/no-explicit-any */ diff --git a/packages/next/test/getProps.test.tsx b/packages/next/test/getProps.test.tsx new file mode 100644 index 000000000..d074f352f --- /dev/null +++ b/packages/next/test/getProps.test.tsx @@ -0,0 +1,213 @@ +import { expect } from '@jest/globals'; +import React from 'react'; +import { + mockedServerSidePropsContext, + mockedStaticPropsContext, +} from '../jest.setup'; +import { getNextStaticProps, getNextServerSideProps } from '../src'; +import { CLIENT_CACHE_PROP } from '../src/getProps'; + +describe('getNextStaticProps', () => { + let client = { + setAsRoot: () => {}, + prepareReactRender: () => {}, + }; + const setAsRootSpy = jest + .spyOn(client, 'setAsRoot') + .mockImplementation(() => {}); + const prepareReactRenderSpy = jest + .spyOn(client, 'prepareReactRender') + .mockImplementation(() => { + return { + cacheSnapshot: 'cache', + }; + }); + + const MockPage = () => { + return

My Page

; + }; + + test('getNextStaticProps with notFound should return notFound', async () => { + const context = mockedStaticPropsContext; + + const staticProps = await getNextStaticProps(context, { + // @ts-ignore + client, + Page: MockPage, + notFound: true, + }); + + const expectedProps = { + notFound: true, + }; + + expect(staticProps).toStrictEqual(expectedProps); + expect(setAsRootSpy).toBeCalledTimes(0); + expect(prepareReactRenderSpy).toBeCalledTimes(0); + }); + + test('getNextStaticProps with redirect should return the redirect object', async () => { + const context = mockedStaticPropsContext; + const redirect = { + destination: '/', + permanent: false, + }; + + const staticProps = await getNextStaticProps(context, { + // @ts-ignore + client, + Page: MockPage, + redirect, + }); + + expect(staticProps).toStrictEqual({ redirect }); + expect(setAsRootSpy).toBeCalledTimes(0); + expect(prepareReactRenderSpy).toBeCalledTimes(0); + }); + + test('getNextStaticProps without page should not call prepareReactRender', async () => { + const context = mockedStaticPropsContext; + + await getNextStaticProps(context, { + // @ts-ignore + client, + }); + + expect(prepareReactRenderSpy).toBeCalledTimes(0); + }); + + test('getNextStaticProps should return props with cache', async () => { + const context = mockedStaticPropsContext; + + const staticProps = await getNextStaticProps(context, { + // @ts-ignore + client, + Page: MockPage, + props: { + title: 'Hello World', + }, + }); + + const expectedProps = { + props: { + [CLIENT_CACHE_PROP]: 'cache', + title: 'Hello World', + }, + revalidate: 1, + }; + + expect(staticProps).toStrictEqual(expectedProps); + }); + + test('getNextStaticProps should allow for specifying revalidation', async () => { + const context = mockedStaticPropsContext; + + const staticProps = await getNextStaticProps(context, { + // @ts-ignore + client, + Page: MockPage, + revalidate: 30, + }); + + const expectedProps = { + props: { + [CLIENT_CACHE_PROP]: 'cache', + }, + revalidate: 30, + }; + + expect(staticProps).toStrictEqual(expectedProps); + }); +}); + +describe('getNextServerSideProps', () => { + let client = { + setAsRoot: () => {}, + prepareReactRender: () => {}, + }; + const setAsRootSpy = jest + .spyOn(client, 'setAsRoot') + .mockImplementation(() => {}); + const prepareReactRenderSpy = jest + .spyOn(client, 'prepareReactRender') + .mockImplementation(() => { + return { + cacheSnapshot: 'cache', + }; + }); + + const MockPage = () => { + return

My Page

; + }; + + test('getNextServerSideProps with notFound should return notFound', async () => { + const context = mockedServerSidePropsContext; + + const staticProps = await getNextServerSideProps(context, { + // @ts-ignore + client, + Page: MockPage, + notFound: true, + }); + + const expectedProps = { + notFound: true, + }; + + expect(staticProps).toStrictEqual(expectedProps); + expect(setAsRootSpy).toBeCalledTimes(0); + expect(prepareReactRenderSpy).toBeCalledTimes(0); + }); + + test('getNextServerSideProps with redirect should return the redirect object', async () => { + const context = mockedServerSidePropsContext; + const redirect = { + destination: '/', + permanent: false, + }; + + const staticProps = await getNextServerSideProps(context, { + // @ts-ignore + client, + Page: MockPage, + redirect, + }); + + expect(staticProps).toStrictEqual({ redirect }); + expect(setAsRootSpy).toBeCalledTimes(0); + expect(prepareReactRenderSpy).toBeCalledTimes(0); + }); + + test('getNextServerSideProps without page should not call prepareReactRender', async () => { + const context = mockedServerSidePropsContext; + + await getNextServerSideProps(context, { + // @ts-ignore + client, + }); + + expect(prepareReactRenderSpy).toBeCalledTimes(0); + }); + + test('getNextServerSideProps should return props with cache', async () => { + const context = mockedServerSidePropsContext; + + const staticProps = await getNextServerSideProps(context, { + // @ts-ignore + client, + Page: MockPage, + props: { + title: 'Hello World', + }, + }); + + const expectedProps = { + props: { + [CLIENT_CACHE_PROP]: 'cache', + title: 'Hello World', + }, + }; + + expect(staticProps).toStrictEqual(expectedProps); + }); +});