From 8f01a4ae83a6ba60ac8fc2049a5ce990970dca0e Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Mon, 2 Mar 2020 05:58:47 -0500 Subject: [PATCH] Fix RenderOpts in `next-server` (#10776) * Correctly pass preview data * remove todo * re-do change * fix types * Prevent regression --- packages/next/next-server/server/api-utils.ts | 10 +++ .../next/next-server/server/next-server.ts | 25 +++++--- packages/next/next-server/server/render.tsx | 5 +- .../prerender-preview/test/index.test.js | 61 +++++++++++++++++++ 4 files changed, 90 insertions(+), 11 deletions(-) diff --git a/packages/next/next-server/server/api-utils.ts b/packages/next/next-server/server/api-utils.ts index 340d4babd7..3ccfba8773 100644 --- a/packages/next/next-server/server/api-utils.ts +++ b/packages/next/next-server/server/api-utils.ts @@ -260,6 +260,7 @@ const COOKIE_NAME_PRERENDER_BYPASS = `__prerender_bypass` const COOKIE_NAME_PRERENDER_DATA = `__next_preview_data` export const SYMBOL_PREVIEW_DATA = Symbol(COOKIE_NAME_PRERENDER_DATA) +const SYMBOL_CLEARED_COOKIES = Symbol(COOKIE_NAME_PRERENDER_BYPASS) export function tryGetPreviewData( req: IncomingMessage, @@ -405,6 +406,10 @@ function setPreviewData( } function clearPreviewData(res: NextApiResponse): NextApiResponse { + if (SYMBOL_CLEARED_COOKIES in res) { + return res + } + const { serialize } = require('cookie') as typeof import('cookie') const previous = res.getHeader('Set-Cookie') res.setHeader(`Set-Cookie`, [ @@ -432,6 +437,11 @@ function clearPreviewData(res: NextApiResponse): NextApiResponse { path: '/', }), ]) + + Object.defineProperty(res, SYMBOL_CLEARED_COOKIES, { + value: true, + enumerable: false, + }) return res } diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index 80b4f5e8a5..5108dea904 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -41,7 +41,7 @@ import pathMatch from './lib/path-match' import { recursiveReadDirSync } from './lib/recursive-readdir-sync' import { loadComponents, LoadComponentsReturnType } from './load-components' import { normalizePagePath } from './normalize-page-path' -import { renderToHTML } from './render' +import { RenderOpts, RenderOptsPartial, renderToHTML } from './render' import { getPagePath } from './require' import Router, { DynamicRoutes, @@ -115,6 +115,7 @@ export default class Server { documentMiddlewareEnabled: boolean hasCssMode: boolean dev?: boolean + previewProps: __ApiPreviewProps } private compression?: Middleware private onErrorMiddleware?: ({ err }: { err: Error }) => Promise @@ -165,6 +166,7 @@ export default class Server { staticMarkup, buildId: this.buildId, generateEtags, + previewProps: this.getPreviewProps(), } // Only the `publicRuntimeConfig` key is exposed to the client side @@ -668,13 +670,12 @@ export default class Server { } } - const previewProps = this.getPreviewProps() await apiResolver( req, res, query, pageModule, - { ...previewProps }, + this.renderOpts.previewProps, this.onErrorMiddleware ) return true @@ -869,7 +870,7 @@ export default class Server { res: ServerResponse, pathname: string, { components, query }: FindComponentsResult, - opts: any + opts: RenderOptsPartial ): Promise { // we need to ensure the status code if /404 is visited directly if (pathname === '/404') { @@ -890,7 +891,7 @@ export default class Server { const hasStaticPaths = !!components.getStaticPaths // Toggle whether or not this is a Data request - const isDataReq = query._nextDataReq + const isDataReq = !!query._nextDataReq delete query._nextDataReq // Serverless requests need its URL transformed back into the original @@ -958,8 +959,11 @@ export default class Server { }) } - const previewProps = this.getPreviewProps() - const previewData = tryGetPreviewData(req, res, { ...previewProps }) + const previewData = tryGetPreviewData( + req, + res, + this.renderOpts.previewProps + ) const isPreviewMode = previewData !== false // Compute the SPR cache key @@ -1017,15 +1021,16 @@ export default class Server { pageData = renderResult.renderOpts.pageData sprRevalidate = renderResult.renderOpts.revalidate } else { - const renderOpts = { + const renderOpts: RenderOpts = { ...components, ...opts, } renderResult = await renderToHTML(req, res, pathname, query, renderOpts) html = renderResult - pageData = renderOpts.pageData - sprRevalidate = renderOpts.revalidate + // TODO: change this to a different passing mechanism + pageData = (renderOpts as any).pageData + sprRevalidate = (renderOpts as any).revalidate } return { html, pageData, sprRevalidate } diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index b92a19bc5e..e4b0d81dd3 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -124,7 +124,7 @@ function render( return { html, head } } -type RenderOpts = LoadComponentsReturnType & { +export type RenderOptsPartial = { staticMarkup: boolean buildId: string canonicalBase: string @@ -147,6 +147,8 @@ type RenderOpts = LoadComponentsReturnType & { previewProps: __ApiPreviewProps } +export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial + function renderDocument( Document: DocumentType, { @@ -517,6 +519,7 @@ export async function renderToHTML( props.pageProps = data.props // pass up revalidate and props for export + // TODO: change this to a different passing mechanism ;(renderOpts as any).revalidate = data.revalidate ;(renderOpts as any).pageData = props } diff --git a/test/integration/prerender-preview/test/index.test.js b/test/integration/prerender-preview/test/index.test.js index 51046b0d59..89e72e411b 100644 --- a/test/integration/prerender-preview/test/index.test.js +++ b/test/integration/prerender-preview/test/index.test.js @@ -8,6 +8,7 @@ import { findPort, initNextServerScript, killApp, + launchApp, nextBuild, nextStart, renderViaHTTP, @@ -201,6 +202,66 @@ const startServerlessEmulator = async (dir, port) => { } describe('Prerender Preview Mode', () => { + describe('Development Mode', () => { + beforeAll(async () => { + await fs.remove(nextConfigPath) + }) + + let appPort, app + it('should start development application', async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + + let previewCookieString + it('should enable preview mode', async () => { + const res = await fetchViaHTTP(appPort, '/api/preview', { lets: 'goooo' }) + expect(res.status).toBe(200) + + const cookies = res.headers + .get('set-cookie') + .split(',') + .map(cookie.parse) + + expect(cookies.length).toBe(2) + previewCookieString = + cookie.serialize('__prerender_bypass', cookies[0].__prerender_bypass) + + '; ' + + cookie.serialize('__next_preview_data', cookies[1].__next_preview_data) + }) + + it('should return cookies to be expired after dev server reboot', async () => { + await killApp(app) + app = await launchApp(appDir, appPort) + + const res = await fetchViaHTTP( + appPort, + '/', + {}, + { headers: { Cookie: previewCookieString } } + ) + expect(res.status).toBe(200) + + const body = await res.text() + // "err":{"name":"TypeError","message":"Cannot read property 'previewModeId' of undefined" + expect(body).not.toContain('err') + expect(body).not.toContain('TypeError') + expect(body).not.toContain('previewModeId') + + const cookies = res.headers + .get('set-cookie') + .replace(/(=\w{3}),/g, '$1') + .split(',') + .map(cookie.parse) + + expect(cookies.length).toBe(2) + }) + + afterAll(async () => { + await killApp(app) + }) + }) + describe('Server Mode', () => { beforeAll(async () => { await fs.remove(nextConfigPath)