From 23497ae8ad040a68329e7f19cfbea4d391c40996 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Fri, 21 Feb 2020 21:51:32 -0500 Subject: [PATCH 1/5] Preview Mode Should Not Cache --- .../webpack/loaders/next-serverless-loader.ts | 7 ++++- .../next/next-server/server/next-server.ts | 31 +++++++++++++------ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index 7767ec05381f..e1710cb0a9f3 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -320,6 +320,9 @@ const nextServerlessLoader: loader.Loader = function() { const isFallback = parsedUrl.query.__nextFallback + const previewData = tryGetPreviewData(req, res, options.previewProps) + const isPreviewMode = previewData !== false + let result = await renderToHTML(req, res, "${page}", Object.assign({}, unstable_getStaticProps ? {} : parsedUrl.query, nowParams ? nowParams : params, _params, isFallback ? { __nextFallback: 'true' } : {}), renderOpts) if (_nextData && !fromExport) { @@ -329,7 +332,9 @@ const nextServerlessLoader: loader.Loader = function() { res.setHeader( 'Cache-Control', - unstable_getServerProps + isPreviewMode + ? \`private, no-cache, no-store, max-age=0, must-revalidate\` + : unstable_getServerProps ? \`no-cache, no-store, must-revalidate\` : \`s-maxage=\${renderOpts.revalidate}, stale-while-revalidate\` ) diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index c4166c6b7253..b1be3ee6581d 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -821,20 +821,25 @@ export default class Server { res: ServerResponse, payload: any, type: string, - revalidate?: number | false + options?: { revalidate: number | false; private: boolean } ) { // TODO: ETag? Cache-Control headers? Next-specific headers? res.setHeader('Content-Type', type) res.setHeader('Content-Length', Buffer.byteLength(payload)) if (!this.renderOpts.dev) { - if (revalidate) { + if (options?.private) { res.setHeader( 'Cache-Control', - revalidate < 0 + `private, no-cache, no-store, max-age=0, must-revalidate` + ) + } else if (options?.revalidate) { + res.setHeader( + 'Cache-Control', + options.revalidate < 0 ? `no-cache, no-store, must-revalidate` - : `s-maxage=${revalidate}, stale-while-revalidate` + : `s-maxage=${options.revalidate}, stale-while-revalidate` ) - } else if (revalidate === false) { + } else if (options?.revalidate === false) { res.setHeader( 'Cache-Control', `s-maxage=31536000, stale-while-revalidate` @@ -910,7 +915,10 @@ export default class Server { res, JSON.stringify(renderResult?.renderOpts?.pageData), 'application/json', - -1 + { + revalidate: -1, + private: false, // Leave to user-land caching + } ) return null } @@ -924,7 +932,10 @@ export default class Server { ...opts, isDataReq, }) - this.__sendPayload(res, JSON.stringify(props), 'application/json', -1) + this.__sendPayload(res, JSON.stringify(props), 'application/json', { + revalidate: -1, + private: false, // Leave to user-land caching + }) return null } @@ -957,7 +968,9 @@ export default class Server { res, data, isDataReq ? 'application/json' : 'text/html; charset=utf-8', - cachedData.curRevalidate + cachedData.curRevalidate !== undefined + ? { revalidate: cachedData.curRevalidate, private: isPreviewMode } + : undefined ) // Stop the request chain here if the data we sent was up-to-date @@ -1064,7 +1077,7 @@ export default class Server { res, isDataReq ? JSON.stringify(pageData) : html, isDataReq ? 'application/json' : 'text/html; charset=utf-8', - sprRevalidate + { revalidate: sprRevalidate, private: isPreviewMode } ) } From 8b0ad5feb79166eb5ea348202a72e95b811d04b8 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Fri, 21 Feb 2020 22:31:22 -0500 Subject: [PATCH 2/5] add import --- packages/next/build/webpack/loaders/next-serverless-loader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index e1710cb0a9f3..e2f6e9e488db 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -151,7 +151,7 @@ const nextServerlessLoader: loader.Loader = function() { } ${dynamicRouteImports} const { parse } = require('url') - const { apiResolver } = require('next/dist/next-server/server/api-utils') + const { tryGetPreviewData, apiResolver } = require('next/dist/next-server/server/api-utils') ${rewriteImports} ${dynamicRouteMatcher} From 7070d39c1b1103f5009e09b4d5390b2c8f2802fd Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sat, 22 Feb 2020 00:23:04 -0500 Subject: [PATCH 3/5] fix import --- .../next/build/webpack/loaders/next-serverless-loader.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index e2f6e9e488db..66d0c9ba3bab 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -151,7 +151,7 @@ const nextServerlessLoader: loader.Loader = function() { } ${dynamicRouteImports} const { parse } = require('url') - const { tryGetPreviewData, apiResolver } = require('next/dist/next-server/server/api-utils') + const { apiResolver } = require('next/dist/next-server/server/api-utils') ${rewriteImports} ${dynamicRouteMatcher} @@ -206,7 +206,8 @@ const nextServerlessLoader: loader.Loader = function() { } const {parse} = require('url') const {parse: parseQs} = require('querystring') - const {renderToHTML} =require('next/dist/next-server/server/render'); + const {renderToHTML} = require('next/dist/next-server/server/render'); + const { tryGetPreviewData } = require('next/dist/next-server/server/api-utils'); const {sendHTML} = require('next/dist/next-server/server/send-html'); const buildManifest = require('${buildManifest}'); const reactLoadableManifest = require('${reactLoadableManifest}'); From 30b753238a782604a0c87fc7c8a9cc572dd2e82f Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sat, 22 Feb 2020 00:45:29 -0500 Subject: [PATCH 4/5] add tests --- .../prerender-preview/test/index.test.js | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/integration/prerender-preview/test/index.test.js b/test/integration/prerender-preview/test/index.test.js index c7f7058d0255..511ecd1f93d4 100644 --- a/test/integration/prerender-preview/test/index.test.js +++ b/test/integration/prerender-preview/test/index.test.js @@ -20,6 +20,10 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2 const appDir = join(__dirname, '..') const nextConfigPath = join(appDir, 'next.config.js') +async function getBuildId() { + return fs.readFile(join(appDir, '.next', 'BUILD_ID'), 'utf8') +} + function getData(html) { const $ = cheerio.load(html) const nextData = $('#__NEXT_DATA__') @@ -91,10 +95,33 @@ function runTests() { const html = await res.text() const { nextData, pre } = getData(html) + expect(res.headers.get('cache-control')).toBe( + 'private, no-cache, no-store, max-age=0, must-revalidate' + ) expect(nextData).toMatchObject({ isFallback: false }) expect(pre).toBe('true and {"lets":"goooo"}') }) + it('should return correct caching headers for data preview request', async () => { + const res = await fetchViaHTTP( + appPort, + `/_next/data/${encodeURI(await getBuildId())}/index.json`, + {}, + { headers: { Cookie: previewCookieString } } + ) + const json = await res.json() + + expect(res.headers.get('cache-control')).toBe( + 'private, no-cache, no-store, max-age=0, must-revalidate' + ) + expect(json).toMatchObject({ + pageProps: { + preview: true, + previewData: { lets: 'goooo' }, + }, + }) + }) + it('should return cookies to be expired on reset request', async () => { const res = await fetchViaHTTP( appPort, From 5e80ddbc5115fdf0c4ec3012407c153efa2ea4b0 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sat, 22 Feb 2020 01:05:16 -0500 Subject: [PATCH 5/5] Add real serverless mode tests --- .../webpack/loaders/next-serverless-loader.ts | 5 +++ test/integration/prerender-preview/server.js | 41 +++++++++++++++++++ .../prerender-preview/test/index.test.js | 30 +++++++++++++- 3 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 test/integration/prerender-preview/server.js diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index 66d0c9ba3bab..38440685b423 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -341,6 +341,11 @@ const nextServerlessLoader: loader.Loader = function() { ) res.end(payload) return null + } else if (isPreviewMode) { + res.setHeader( + 'Cache-Control', + 'private, no-cache, no-store, max-age=0, must-revalidate' + ) } if (fromExport) return { html: result, renderOpts } diff --git a/test/integration/prerender-preview/server.js b/test/integration/prerender-preview/server.js new file mode 100644 index 000000000000..c35347a48951 --- /dev/null +++ b/test/integration/prerender-preview/server.js @@ -0,0 +1,41 @@ +const http = require('http') +const url = require('url') +const fs = require('fs') +const path = require('path') +const server = http.createServer((req, res) => { + let { pathname } = url.parse(req.url) + if (pathname.startsWith('/_next/data')) { + pathname = pathname + .replace(`/_next/data/${process.env.BUILD_ID}/`, '/') + .replace(/\.json$/, '') + } + console.log('serving', pathname) + + if (pathname === '/favicon.ico') { + res.statusCode = 404 + return res.end() + } + + if (pathname.startsWith('/_next/static/')) { + res.write( + fs.readFileSync( + path.join( + __dirname, + './.next/static/', + pathname.slice('/_next/static/'.length) + ), + 'utf8' + ) + ) + return res.end() + } else { + const re = require(`./.next/serverless/pages${pathname}`) + return typeof re.render === 'function' + ? re.render(req, res) + : re.default(req, res) + } +}) + +server.listen(process.env.PORT, () => { + console.log('ready on', process.env.PORT) +}) diff --git a/test/integration/prerender-preview/test/index.test.js b/test/integration/prerender-preview/test/index.test.js index 511ecd1f93d4..51046b0d59b7 100644 --- a/test/integration/prerender-preview/test/index.test.js +++ b/test/integration/prerender-preview/test/index.test.js @@ -6,6 +6,7 @@ import fs from 'fs-extra' import { fetchViaHTTP, findPort, + initNextServerScript, killApp, nextBuild, nextStart, @@ -31,7 +32,7 @@ function getData(html) { return { nextData: JSON.parse(nextData.html()), pre: preEl.text() } } -function runTests() { +function runTests(startServer = nextStart) { it('should compile successfully', async () => { await fs.remove(join(appDir, '.next')) const { code, stdout } = await nextBuild(appDir, [], { @@ -44,7 +45,7 @@ function runTests() { let appPort, app it('should start production application', async () => { appPort = await findPort() - app = await nextStart(appDir, appPort) + app = await startServer(appDir, appPort) }) it('should return prerendered page on first request', async () => { @@ -189,6 +190,16 @@ function runTests() { }) } +const startServerlessEmulator = async (dir, port) => { + const scriptPath = join(dir, 'server.js') + const env = Object.assign( + {}, + { ...process.env }, + { PORT: port, BUILD_ID: await getBuildId() } + ) + return initNextServerScript(scriptPath, /ready on/i, env) +} + describe('Prerender Preview Mode', () => { describe('Server Mode', () => { beforeAll(async () => { @@ -197,6 +208,7 @@ describe('Prerender Preview Mode', () => { runTests() }) + describe('Serverless Mode', () => { beforeAll(async () => { await fs.writeFile( @@ -210,4 +222,18 @@ describe('Prerender Preview Mode', () => { runTests() }) + + describe('Emulated Serverless Mode', () => { + beforeAll(async () => { + await fs.writeFile( + nextConfigPath, + `module.exports = { target: 'experimental-serverless-trace' }` + os.EOL + ) + }) + afterAll(async () => { + await fs.remove(nextConfigPath) + }) + + runTests(startServerlessEmulator) + }) })