diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index 69885cff071c4..e68b65b11b536 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -72,9 +72,9 @@ const nextServerlessLoader: loader.Loader = function () { ` : '' - const collectDynamicRouteParams = pageIsDynamicRoute + const normalizeDynamicRouteParams = pageIsDynamicRoute ? ` - function collectDynamicRouteParams(query) { + function normalizeDynamicRouteParams(query) { return Object.keys(defaultRouteRegex.groups) .reduce((prev, key) => { let value = query[key] @@ -84,7 +84,14 @@ const nextServerlessLoader: loader.Loader = function () { // non-provided optional values should be undefined so normalize // them to undefined } - if(defaultRouteRegex.groups[key].optional && !value) { + if( + defaultRouteRegex.groups[key].optional && + (!value || ( + Array.isArray(value) && + value.length === 1 && + value[0] === 'index' + )) + ) { value = undefined delete query[key] } @@ -223,7 +230,7 @@ const nextServerlessLoader: loader.Loader = function () { ${defaultRouteRegex} - ${collectDynamicRouteParams} + ${normalizeDynamicRouteParams} ${handleRewrites} @@ -241,9 +248,11 @@ const nextServerlessLoader: loader.Loader = function () { const params = ${ pageIsDynamicRoute ? ` - trustQuery - ? collectDynamicRouteParams(parsedUrl.query) - : dynamicRouteMatcher(parsedUrl.pathname) + normalizeDynamicRouteParams( + trustQuery + ? parsedUrl.query + : dynamicRouteMatcher(parsedUrl.pathname) + ) ` : `{}` } @@ -316,7 +325,7 @@ const nextServerlessLoader: loader.Loader = function () { ${dynamicRouteMatcher} ${defaultRouteRegex} - ${collectDynamicRouteParams} + ${normalizeDynamicRouteParams} ${handleRewrites} export const config = ComponentInfo['confi' + 'g'] || {} @@ -394,9 +403,11 @@ const nextServerlessLoader: loader.Loader = function () { !getStaticProps && !getServerSideProps ) ? {} - : trustQuery - ? collectDynamicRouteParams(parsedUrl.query) - : dynamicRouteMatcher(parsedUrl.pathname) || {}; + : normalizeDynamicRouteParams( + trustQuery + ? parsedUrl.query + : dynamicRouteMatcher(parsedUrl.pathname) + ) ` : `const params = {};` } @@ -433,6 +444,7 @@ const nextServerlessLoader: loader.Loader = function () { ` : `const nowParams = null;` } + // make sure to set renderOpts to the correct params e.g. _params // if provided from worker or params if we're parsing them here renderOpts.params = _params || params diff --git a/test/integration/root-optional-revalidate/pages/[[...slug]].js b/test/integration/root-optional-revalidate/pages/[[...slug]].js new file mode 100644 index 0000000000000..4b80b91607436 --- /dev/null +++ b/test/integration/root-optional-revalidate/pages/[[...slug]].js @@ -0,0 +1,24 @@ +export default function Home(props) { + return
{JSON.stringify(props)}
+} + +export async function getStaticPaths() { + return { + paths: [ + { params: { slug: false } }, + { params: { slug: ['a'] } }, + { params: { slug: ['hello', 'world'] } }, + ], + fallback: false, + } +} + +export async function getStaticProps({ params }) { + return { + props: { + params, + random: Math.random(), + }, + revalidate: 1, + } +} diff --git a/test/integration/root-optional-revalidate/server.js b/test/integration/root-optional-revalidate/server.js new file mode 100644 index 0000000000000..aa513c139e055 --- /dev/null +++ b/test/integration/root-optional-revalidate/server.js @@ -0,0 +1,27 @@ +const path = require('path') +const http = require('http') + +const server = http.createServer(async (req, res) => { + const render = async (page) => { + const mod = require(`./${path.join('.next/serverless/pages/', page)}`) + try { + return await (mod.render || mod.default || mod)(req, res) + } catch (err) { + res.statusCode = 500 + return res.end('internal error') + } + } + + try { + await render('/[[...slug]].js') + } catch (err) { + console.error('failed to render', err) + res.statusCode = 500 + res.end('Internal Error') + } +}) + +const port = process.env.PORT || 3000 +server.listen(port, () => { + console.log('ready on', port) +}) diff --git a/test/integration/root-optional-revalidate/test/index.test.js b/test/integration/root-optional-revalidate/test/index.test.js new file mode 100644 index 0000000000000..75e453057ae51 --- /dev/null +++ b/test/integration/root-optional-revalidate/test/index.test.js @@ -0,0 +1,115 @@ +/* eslint-env jest */ + +import { join } from 'path' +import cheerio from 'cheerio' +import { + killApp, + findPort, + File, + nextBuild, + nextStart, + initNextServerScript, + renderViaHTTP, + waitFor, +} from 'next-test-utils' + +jest.setTimeout(1000 * 60 * 2) + +const appDir = join(__dirname, '../') +const nextConfig = new File(join(appDir, 'next.config.js')) +let app +let appPort + +const getProps = async (path, expected) => { + const html = await renderViaHTTP(appPort, path) + const $ = cheerio.load(html) + return JSON.parse($('#props').text()) +} + +const runTests = (rawServerless = false) => { + it('should render / correctly', async () => { + const props = await getProps('/', { params: {} }) + expect(props.params).toEqual({}) + + await waitFor(1000) + await getProps('/') + + const newProps = await getProps('/', { params: {} }) + expect(newProps.params).toEqual({}) + expect(props.random).not.toBe(newProps.random) + }) + + if (rawServerless) { + it('should render /index correctly', async () => { + const props = await getProps('/index') + expect(props.params).toEqual({}) + + await waitFor(1000) + await getProps('/index') + + const newProps = await getProps('/index') + expect(newProps.params).toEqual({}) + expect(props.random).not.toBe(newProps.random) + }) + } + + it('should render /a correctly', async () => { + const props = await getProps('/a') + expect(props.params).toEqual({ slug: ['a'] }) + + await waitFor(1000) + await getProps('/a') + + const newProps = await getProps('/a') + expect(newProps.params).toEqual({ slug: ['a'] }) + expect(props.random).not.toBe(newProps.random) + }) + + it('should render /hello/world correctly', async () => { + const props = await getProps('/hello/world') + expect(props.params).toEqual({ slug: ['hello', 'world'] }) + + await waitFor(1000) + await getProps('/hello/world') + + const newProps = await getProps('/hello/world') + expect(newProps.params).toEqual({ slug: ['hello', 'world'] }) + expect(props.random).not.toBe(newProps.random) + }) +} + +describe('Root Optional Catch-all Revalidate', () => { + describe('production mode', () => { + beforeAll(async () => { + await nextBuild(appDir) + appPort = await findPort() + app = await nextStart(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTests() + }) + + describe('raw serverless mode', () => { + beforeAll(async () => { + nextConfig.write(` + module.exports = { + target: 'experimental-serverless-trace' + } + `) + await nextBuild(appDir) + appPort = await findPort() + + app = await initNextServerScript(join(appDir, 'server.js'), /ready on/i, { + ...process.env, + PORT: appPort, + }) + }) + afterAll(async () => { + nextConfig.delete() + await killApp(app) + }) + + runTests(true) + }) +})