diff --git a/.github/ISSUE_TEMPLATE/1.Bug_report.md b/.github/ISSUE_TEMPLATE/1.Bug_report.md index 6c10e9edc66b..ba86015e91e3 100644 --- a/.github/ISSUE_TEMPLATE/1.Bug_report.md +++ b/.github/ISSUE_TEMPLATE/1.Bug_report.md @@ -1,6 +1,6 @@ --- name: Bug report -about: Create a bug report for the Next.js core +about: Create a bug report for the Next.js core / examples --- # Bug report diff --git a/.github/ISSUE_TEMPLATE/3.Example_Bug_report.md b/.github/ISSUE_TEMPLATE/3.Example_Bug_report.md deleted file mode 100644 index a1640aee8e59..000000000000 --- a/.github/ISSUE_TEMPLATE/3.Example_Bug_report.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -name: Bug report for examples -about: Create a bug report for one of the Next.js examples ---- - -# Examples bug report - -## Example name - -Provide the example name - -## Describe the bug - -A clear and concise description of what the bug is. - -## To Reproduce - -Steps to reproduce the behavior, please provide code snippets or a repository: - -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -## Expected behavior - -A clear and concise description of what you expected to happen. - -## Screenshots - -If applicable, add screenshots to help explain your problem. - -## System information - -- OS: [e.g. macOS, Windows] -- Browser (if applies) [e.g. chrome, safari] -- Version of Next.js: [e.g. 6.0.2] - -## Additional context - -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/8.Question_about_next.md b/.github/ISSUE_TEMPLATE/8.Question_about_next.md deleted file mode 100644 index fdfd0c1e9ee1..000000000000 --- a/.github/ISSUE_TEMPLATE/8.Question_about_next.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: Question about Next.js -about: If you have a question related to Next.js or the examples. Reach out to the community on GitHub discussions ---- - -# Question about Next.js - -GitHub Issues are reserved for Bug reports and Feature requests. - -The best place to get your question answered is to post it on GitHub Discussions: https://github.com/zeit/next.js/discussions/new. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000000..3ea766fd6d3a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Ask a question + url: https://github.com/zeit/next.js/discussions + about: Ask questions and discuss with other community members diff --git a/errors/reserved-page-prop.md b/errors/reserved-page-prop.md new file mode 100644 index 000000000000..ae83aaababfe --- /dev/null +++ b/errors/reserved-page-prop.md @@ -0,0 +1,9 @@ +# Reserved Page Prop + +#### Why This Error Occurred + +In a page's `getInitialProps` a reserved prop was returned. Currently the only reserved page prop is `url` for legacy reasons. + +#### Possible Ways to Fix It + +Change the name of the prop returned from `getInitialProps` to any other name. diff --git a/examples/with-apollo/lib/apollo.js b/examples/with-apollo/lib/apollo.js index e13a68f240ab..17a40a6d309c 100644 --- a/examples/with-apollo/lib/apollo.js +++ b/examples/with-apollo/lib/apollo.js @@ -1,5 +1,6 @@ import React from 'react' import App from 'next/app' +import Head from 'next/head' import { ApolloProvider } from '@apollo/react-hooks' import createApolloClient from '../apolloClient' @@ -153,6 +154,10 @@ export const withApollo = ({ ssr = false } = {}) => PageComponent => { // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error console.error('Error while running `getDataFromTree`', error) } + + // getDataFromTree does not call componentWillUnmount + // head side effect therefore need to be cleared manually + Head.rewind() } } diff --git a/lerna.json b/lerna.json index 16f7b566a72b..d7ce38857e23 100644 --- a/lerna.json +++ b/lerna.json @@ -12,5 +12,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "9.2.3-canary.12" + "version": "9.2.3-canary.13" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 964c2db771de..6036cbedc0d7 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "9.2.3-canary.12", + "version": "9.2.3-canary.13", "keywords": [ "react", "next", diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 1fbd0f861d5d..9dc52445a1f5 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "9.2.3-canary.12", + "version": "9.2.3-canary.13", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 7e738971e6e1..63b50800e02c 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "9.2.3-canary.12", + "version": "9.2.3-canary.13", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-google-analytics/package.json b/packages/next-plugin-google-analytics/package.json index be4a9963fb26..27c703f307cb 100644 --- a/packages/next-plugin-google-analytics/package.json +++ b/packages/next-plugin-google-analytics/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-google-analytics", - "version": "9.2.3-canary.12", + "version": "9.2.3-canary.13", "nextjs": { "name": "Google Analytics", "required-env": [ diff --git a/packages/next-plugin-material-ui/package.json b/packages/next-plugin-material-ui/package.json index e6c68f5361de..64968e3e1954 100644 --- a/packages/next-plugin-material-ui/package.json +++ b/packages/next-plugin-material-ui/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-material-ui", - "version": "9.2.3-canary.12", + "version": "9.2.3-canary.13", "nextjs": { "name": "Material UI", "required-env": [] diff --git a/packages/next-plugin-sentry/package.json b/packages/next-plugin-sentry/package.json index 8856bcab3352..4f01cec1aede 100644 --- a/packages/next-plugin-sentry/package.json +++ b/packages/next-plugin-sentry/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-sentry", - "version": "9.2.3-canary.12", + "version": "9.2.3-canary.13", "nextjs": { "name": "Sentry", "required-env": [ diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 84df2a082b10..5c15bfa50511 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "9.2.3-canary.12", + "version": "9.2.3-canary.13", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next/README.md b/packages/next/README.md index eb677e47512d..1696181cd3d1 100644 --- a/packages/next/README.md +++ b/packages/next/README.md @@ -10,7 +10,7 @@ - +

diff --git a/packages/next/build/babel/plugins/next-ssg-transform.ts b/packages/next/build/babel/plugins/next-ssg-transform.ts index b4039bbe7616..2a7cea08e6b7 100644 --- a/packages/next/build/babel/plugins/next-ssg-transform.ts +++ b/packages/next/build/babel/plugins/next-ssg-transform.ts @@ -1,10 +1,12 @@ import { NodePath, PluginObj } from '@babel/core' import * as BabelTypes from '@babel/types' import { SERVER_PROPS_SSG_CONFLICT } from '../../../lib/constants' +import { + STATIC_PROPS_ID, + SERVER_PROPS_ID, +} from '../../../next-server/lib/constants' const pageComponentVar = '__NEXT_COMP' -const prerenderId = '__N_SSG' -const serverPropsId = '__N_SSP' export const EXPORT_NAME_GET_STATIC_PROPS = 'unstable_getStaticProps' export const EXPORT_NAME_GET_STATIC_PATHS = 'unstable_getStaticPaths' @@ -53,7 +55,7 @@ function decorateSsgExport( '=', t.memberExpression( t.identifier(pageComponentVar), - t.identifier(state.isPrerender ? prerenderId : serverPropsId) + t.identifier(state.isPrerender ? STATIC_PROPS_ID : SERVER_PROPS_ID) ), t.booleanLiteral(true) ), @@ -80,7 +82,7 @@ function decorateSsgExport( '=', t.memberExpression( t.identifier((defaultSpecifier as any).local.name), - t.identifier(state.isPrerender ? prerenderId : serverPropsId) + t.identifier(state.isPrerender ? STATIC_PROPS_ID : SERVER_PROPS_ID) ), t.booleanLiteral(true) ), diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 841f5472178b..849dff8b950b 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -569,12 +569,13 @@ export default async function build(dir: string, conf = null): Promise { ) staticCheckWorkers.end() - if (serverPropsPages.size > 0) { + if (serverPropsPages.size > 0 || ssgPages.size > 0) { // We update the routes manifest after the build with the - // serverProps routes since we can't determine this until after build - routesManifest.serverPropsRoutes = {} - - for (const page of serverPropsPages) { + // data routes since we can't determine these until after build + routesManifest.dataRoutes = getSortedRoutes([ + ...serverPropsPages, + ...ssgPages, + ]).map(page => { const pagePath = normalizePagePath(page) const dataRoute = path.posix.join( '/_next/data', @@ -582,7 +583,7 @@ export default async function build(dir: string, conf = null): Promise { `${pagePath}.json` ) - routesManifest.serverPropsRoutes[page] = { + return { page, dataRouteRegex: isDynamicRoute(page) ? getRouteRegex(dataRoute.replace(/\.json$/, '')).re.source.replace( @@ -597,7 +598,7 @@ export default async function build(dir: string, conf = null): Promise { )}$` ).source, } - } + }) await fsWriteFile( routesManifestPath, diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index 6d92d5092494..47ab3582dcf4 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -93,6 +93,7 @@ const nextServerlessLoader: loader.Loader = function() { const handleRewrites = ` const getCustomRouteMatcher = pathMatch(true) + const {prepareDestination} = require('next/dist/next-server/server/router') function handleRewrites(parsedUrl) { for (const rewrite of rewrites) { @@ -100,19 +101,14 @@ const nextServerlessLoader: loader.Loader = function() { const params = matcher(parsedUrl.pathname) if (params) { - parsedUrl.query = { - ...parsedUrl.query, - ...params - } - const parsedDest = parse(rewrite.destination) - const destCompiler = pathToRegexp.compile( - \`\${parsedDest.pathname}\${parsedDest.hash || ''}\` + const { parsedDestination } = prepareDestination( + rewrite.destination, + params ) - const newUrl = destCompiler(params) - const parsedNewUrl = parse(newUrl) + Object.assign(parsedUrl.query, parsedDestination.query, params) + delete parsedDestination.query - parsedUrl.pathname = parsedNewUrl.pathname - parsedUrl.hash = parsedNewUrl.hash + Object.assign(parsedUrl, parsedDestination) if (parsedUrl.pathname === '${page}'){ break @@ -170,7 +166,7 @@ const nextServerlessLoader: loader.Loader = function() { ` : '' } - const parsedUrl = parse(req.url, true) + const parsedUrl = handleRewrites(parse(req.url, true)) const params = ${ pageIsDynamicRoute diff --git a/packages/next/client/head-manager.js b/packages/next/client/head-manager.js index 5eec36503c0c..08b5c45a527a 100644 --- a/packages/next/client/head-manager.js +++ b/packages/next/client/head-manager.js @@ -5,90 +5,6 @@ const DOMAttributeNames = { httpEquiv: 'http-equiv', } -export default class HeadManager { - constructor() { - this.updatePromise = null - } - - updateHead = head => { - const promise = (this.updatePromise = Promise.resolve().then(() => { - if (promise !== this.updatePromise) return - - this.updatePromise = null - this.doUpdateHead(head) - })) - } - - doUpdateHead(head) { - const tags = {} - head.forEach(h => { - const components = tags[h.type] || [] - components.push(h) - tags[h.type] = components - }) - - this.updateTitle(tags.title ? tags.title[0] : null) - - const types = ['meta', 'base', 'link', 'style', 'script'] - types.forEach(type => { - this.updateElements(type, tags[type] || []) - }) - } - - updateTitle(component) { - let title = '' - if (component) { - const { children } = component.props - title = typeof children === 'string' ? children : children.join('') - } - if (title !== document.title) document.title = title - } - - updateElements(type, components) { - const headEl = document.getElementsByTagName('head')[0] - const headCountEl = headEl.querySelector('meta[name=next-head-count]') - if (process.env.NODE_ENV !== 'production') { - if (!headCountEl) { - console.error( - 'Warning: next-head-count is missing. https://err.sh/next.js/next-head-count-missing' - ) - return - } - } - - const headCount = Number(headCountEl.content) - const oldTags = [] - - for ( - let i = 0, j = headCountEl.previousElementSibling; - i < headCount; - i++, j = j.previousElementSibling - ) { - if (j.tagName.toLowerCase() === type) { - oldTags.push(j) - } - } - const newTags = components.map(reactElementToDOM).filter(newTag => { - for (let k = 0, len = oldTags.length; k < len; k++) { - const oldTag = oldTags[k] - if (oldTag.isEqualNode(newTag)) { - oldTags.splice(k, 1) - return false - } - } - return true - }) - - oldTags.forEach(t => t.parentNode.removeChild(t)) - newTags.forEach(t => headEl.insertBefore(t, headCountEl)) - headCountEl.content = ( - headCount - - oldTags.length + - newTags.length - ).toString() - } -} - function reactElementToDOM({ type, props }) { const el = document.createElement(type) for (const p in props) { @@ -110,3 +26,73 @@ function reactElementToDOM({ type, props }) { } return el } + +function updateElements(type, components) { + const headEl = document.getElementsByTagName('head')[0] + const headCountEl = headEl.querySelector('meta[name=next-head-count]') + if (process.env.NODE_ENV !== 'production') { + if (!headCountEl) { + console.error( + 'Warning: next-head-count is missing. https://err.sh/next.js/next-head-count-missing' + ) + return + } + } + + const headCount = Number(headCountEl.content) + const oldTags = [] + + for ( + let i = 0, j = headCountEl.previousElementSibling; + i < headCount; + i++, j = j.previousElementSibling + ) { + if (j.tagName.toLowerCase() === type) { + oldTags.push(j) + } + } + const newTags = components.map(reactElementToDOM).filter(newTag => { + for (let k = 0, len = oldTags.length; k < len; k++) { + const oldTag = oldTags[k] + if (oldTag.isEqualNode(newTag)) { + oldTags.splice(k, 1) + return false + } + } + return true + }) + + oldTags.forEach(t => t.parentNode.removeChild(t)) + newTags.forEach(t => headEl.insertBefore(t, headCountEl)) + headCountEl.content = (headCount - oldTags.length + newTags.length).toString() +} + +export default function initHeadManager() { + let updatePromise = null + + return head => { + const promise = (updatePromise = Promise.resolve().then(() => { + if (promise !== updatePromise) return + + updatePromise = null + const tags = {} + + head.forEach(h => { + const components = tags[h.type] || [] + components.push(h) + tags[h.type] = components + }) + + const titleComponent = tags.title ? tags.title[0] : null + let title = '' + if (titleComponent) { + const { children } = titleComponent.props + title = typeof children === 'string' ? children : children.join('') + } + if (title !== document.title) document.title = title + ;['meta', 'base', 'link', 'style', 'script'].forEach(type => { + updateElements(type, tags[type] || []) + }) + })) + } +} diff --git a/packages/next/client/index.js b/packages/next/client/index.js index 8cd2473aa15c..124b8ab35dc1 100644 --- a/packages/next/client/index.js +++ b/packages/next/client/index.js @@ -1,7 +1,7 @@ /* global location */ import React from 'react' import ReactDOM from 'react-dom' -import HeadManager from './head-manager' +import initHeadManager from './head-manager' import { createRouter, makePublicRouterInstance } from 'next/router' import mitt from '../next-server/lib/mitt' import { loadGetInitialProps, getURL, ST } from '../next-server/lib/utils' @@ -57,7 +57,7 @@ if (window.__NEXT_P) { window.__NEXT_P = [] window.__NEXT_P.push = register -const headManager = new HeadManager() +const updateHead = initHeadManager() const appElement = document.getElementById('__next') let lastAppProps @@ -397,7 +397,7 @@ function AppContainer({ children }) { } > - + {children} diff --git a/packages/next/next-server/lib/constants.ts b/packages/next/next-server/lib/constants.ts index d52a40da605a..40f97db6f7b4 100644 --- a/packages/next/next-server/lib/constants.ts +++ b/packages/next/next-server/lib/constants.ts @@ -34,3 +34,5 @@ export const ROUTE_NAME_REGEX = /^static[/\\][^/\\]+[/\\]pages[/\\](.*)\.js$/ export const SERVERLESS_ROUTE_NAME_REGEX = /^pages[/\\](.*)\.js$/ export const TEMPORARY_REDIRECT_STATUS = 307 export const PERMANENT_REDIRECT_STATUS = 308 +export const STATIC_PROPS_ID = '__N_SSG' +export const SERVER_PROPS_ID = '__N_SSP' diff --git a/packages/next/next-server/server/load-components.ts b/packages/next/next-server/server/load-components.ts index e4852ab52676..b088480e485d 100644 --- a/packages/next/next-server/server/load-components.ts +++ b/packages/next/next-server/server/load-components.ts @@ -5,6 +5,8 @@ import { CLIENT_STATIC_FILES_PATH, REACT_LOADABLE_MANIFEST, SERVER_DIRECTORY, + STATIC_PROPS_ID, + SERVER_PROPS_ID, } from '../lib/constants' import { join } from 'path' import { requirePage } from './require' @@ -16,6 +18,20 @@ export function interopDefault(mod: any) { return mod.default || mod } +function addComponentPropsId( + Component: any, + getStaticProps: any, + getServerProps: any +) { + // Mark the component with the SSG or SSP id here since we don't run + // the SSG babel transform for server mode + if (getStaticProps) { + Component[STATIC_PROPS_ID] = true + } else if (getServerProps) { + Component[SERVER_PROPS_ID] = true + } +} + export type ManifestItem = { id: number | string name: string @@ -66,11 +82,24 @@ export async function loadComponents( ): Promise { if (serverless) { const Component = await requirePage(pathname, distDir, serverless) + const { + unstable_getStaticProps, + unstable_getStaticPaths, + unstable_getServerProps, + } = Component + + addComponentPropsId( + Component, + unstable_getStaticProps, + unstable_getServerProps + ) + return { Component, pageConfig: Component.config || {}, - unstable_getStaticProps: Component.unstable_getStaticProps, - unstable_getStaticPaths: Component.unstable_getStaticPaths, + unstable_getStaticProps, + unstable_getStaticPaths, + unstable_getServerProps, } as LoadComponentsReturnType } const documentPath = join( @@ -111,6 +140,18 @@ export async function loadComponents( interopDefault(AppMod), ]) + const { + unstable_getServerProps, + unstable_getStaticProps, + unstable_getStaticPaths, + } = ComponentMod + + addComponentPropsId( + Component, + unstable_getStaticProps, + unstable_getServerProps + ) + return { App, Document, @@ -119,8 +160,8 @@ export async function loadComponents( DocumentMiddleware, reactLoadableManifest, pageConfig: ComponentMod.config || {}, - unstable_getServerProps: ComponentMod.unstable_getServerProps, - unstable_getStaticProps: ComponentMod.unstable_getStaticProps, - unstable_getStaticPaths: ComponentMod.unstable_getStaticPaths, + unstable_getServerProps, + unstable_getStaticProps, + unstable_getStaticPaths, } } diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index ae4d7ceefd57..8e848519c1c8 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -546,6 +546,19 @@ export async function renderToHTML( props.pageProps = data.props ;(renderOpts as any).pageData = props } + + if ( + !isSpr && // we only show this warning for legacy pages + !unstable_getServerProps && + process.env.NODE_ENV !== 'production' && + Object.keys(props?.pageProps || {}).includes('url') + ) { + console.warn( + `The prop \`url\` is a reserved prop in Next.js for legacy reasons and will be overridden on page ${pathname}\n` + + `See more info here: https://err.sh/zeit/next.js/reserved-page-prop` + ) + } + // We only need to do this if we want to support calling // _app's getInitialProps for getServerProps if not this can be removed if (isDataReq) return props diff --git a/packages/next/package.json b/packages/next/package.json index 3f8906b4f757..2ddb5038971f 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "9.2.3-canary.12", + "version": "9.2.3-canary.13", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -73,7 +73,7 @@ "@babel/preset-typescript": "7.7.2", "@babel/runtime": "7.7.2", "@babel/types": "7.7.4", - "@next/polyfill-nomodule": "9.2.3-canary.12", + "@next/polyfill-nomodule": "9.2.3-canary.13", "amphtml-validator": "1.0.30", "async-retry": "1.2.3", "async-sema": "3.0.0", diff --git a/packages/next/pages/_app.tsx b/packages/next/pages/_app.tsx index 3971441a2433..0132f5ab4825 100644 --- a/packages/next/pages/_app.tsx +++ b/packages/next/pages/_app.tsx @@ -42,8 +42,19 @@ export default class App

extends React.Component< render() { const { router, Component, pageProps } = this.props as AppProps - const url = createUrl(router) - return + + return ( + + ) } } diff --git a/test/integration/custom-routes/next.config.js b/test/integration/custom-routes/next.config.js index 27155c470423..3ff2dca5755d 100644 --- a/test/integration/custom-routes/next.config.js +++ b/test/integration/custom-routes/next.config.js @@ -73,7 +73,11 @@ module.exports = { }, { source: '/api-hello-param/:name', - destination: '/api/hello?name=:name', + destination: '/api/hello?hello=:name', + }, + { + source: '/api-dynamic-param/:name', + destination: '/api/dynamic/:name?hello=:name', }, { source: '/:path/post-321', diff --git a/test/integration/custom-routes/pages/api/dynamic/[slug].js b/test/integration/custom-routes/pages/api/dynamic/[slug].js new file mode 100644 index 000000000000..fd03963a3a95 --- /dev/null +++ b/test/integration/custom-routes/pages/api/dynamic/[slug].js @@ -0,0 +1 @@ +export default async (req, res) => res.json({ query: req.query }) diff --git a/test/integration/custom-routes/server.js b/test/integration/custom-routes/server.js new file mode 100644 index 000000000000..e07441ae678c --- /dev/null +++ b/test/integration/custom-routes/server.js @@ -0,0 +1,36 @@ +const path = require('path') +const http = require('http') + +const server = http.createServer((req, res) => { + const pagePath = page => path.join('.next/serverless/pages/', page) + const render = page => { + require(`./${pagePath(page)}`).render(req, res) + } + const apiCall = page => { + require(`./${pagePath(page)}`).default(req, res) + } + + switch (req.url) { + case '/blog/post-1': { + return render('/blog/[post]') + } + case '/query-rewrite/first/second': { + return render('/with-params') + } + case '/api-hello-param/first': { + return apiCall('/api/hello') + } + case '/api-dynamic-param/first': { + return apiCall('/api/dynamic/[slug]') + } + default: { + res.statusCode(404) + return res.end('404') + } + } +}) + +const port = process.env.PORT || 3000 +server.listen(port, () => { + console.log('ready on', port) +}) diff --git a/test/integration/custom-routes/test/index.test.js b/test/integration/custom-routes/test/index.test.js index e1769fddb9c9..a3436c75ec52 100644 --- a/test/integration/custom-routes/test/index.test.js +++ b/test/integration/custom-routes/test/index.test.js @@ -18,6 +18,7 @@ import { getBrowserBodyText, waitFor, normalizeRegEx, + initNextServerScript, } from 'next-test-utils' jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2 @@ -308,7 +309,9 @@ const runTests = (isDev = false) => { it('should handle api rewrite with param successfully', async () => { const data = await renderViaHTTP(appPort, '/api-hello-param/hello') - expect(JSON.parse(data)).toEqual({ query: { name: 'hello' } }) + expect(JSON.parse(data)).toEqual({ + query: { name: 'hello', hello: 'hello' }, + }) }) it('should handle encoded value in the pathname correctly', async () => { @@ -593,10 +596,15 @@ const runTests = (isDev = false) => { source: '/api-hello-regex/(.*)', }, { - destination: '/api/hello?name=:name', + destination: '/api/hello?hello=:name', regex: normalizeRegEx('^\\/api-hello-param(?:\\/([^\\/]+?))$'), source: '/api-hello-param/:name', }, + { + destination: '/api/dynamic/:name?hello=:name', + regex: normalizeRegEx('^\\/api-dynamic-param(?:\\/([^\\/]+?))$'), + source: '/api-dynamic-param/:name', + }, { destination: '/with-params', regex: normalizeRegEx('^(?:\\/([^\\/]+?))\\/post-321$'), @@ -608,6 +616,10 @@ const runTests = (isDev = false) => { page: '/another/[id]', regex: normalizeRegEx('^\\/another\\/([^\\/]+?)(?:\\/)?$'), }, + { + page: '/api/dynamic/[slug]', + regex: normalizeRegEx('^\\/api\\/dynamic\\/([^\\/]+?)(?:\\/)?$'), + }, { page: '/blog/[post]', regex: normalizeRegEx('^\\/blog\\/([^\\/]+?)(?:\\/)?$'), @@ -723,10 +735,77 @@ describe('Custom routes', () => { buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') }) afterAll(async () => { - await killApp(app) await fs.writeFile(nextConfigPath, nextConfigContent, 'utf8') + await killApp(app) }) runTests() }) + + describe('raw serverless mode', () => { + beforeAll(async () => { + nextConfigContent = await fs.readFile(nextConfigPath, 'utf8') + await fs.writeFile( + nextConfigPath, + nextConfigContent.replace(/\/\/ target/, 'target'), + 'utf8' + ) + await nextBuild(appDir) + + appPort = await findPort() + app = await initNextServerScript(join(appDir, 'server.js'), /ready on/, { + ...process.env, + PORT: appPort, + }) + }) + afterAll(async () => { + await fs.writeFile(nextConfigPath, nextConfigContent, 'utf8') + await killApp(app) + }) + + it('should apply rewrites in lambda correctly for page route', async () => { + const html = await renderViaHTTP(appPort, '/query-rewrite/first/second') + const data = JSON.parse( + cheerio + .load(html)('p') + .text() + ) + expect(data).toEqual({ + first: 'first', + second: 'second', + section: 'first', + name: 'second', + }) + }) + + it('should apply rewrites in lambda correctly for dynamic route', async () => { + const html = await renderViaHTTP(appPort, '/blog/post-1') + expect(html).toContain('post-2') + }) + + it('should apply rewrites in lambda correctly for API route', async () => { + const data = JSON.parse( + await renderViaHTTP(appPort, '/api-hello-param/first') + ) + expect(data).toEqual({ + query: { + name: 'first', + hello: 'first', + }, + }) + }) + + it('should apply rewrites in lambda correctly for dynamic API route', async () => { + const data = JSON.parse( + await renderViaHTTP(appPort, '/api-dynamic-param/first') + ) + expect(data).toEqual({ + query: { + slug: 'first', + name: 'first', + hello: 'first', + }, + }) + }) + }) }) diff --git a/test/integration/dynamic-routing/test/index.test.js b/test/integration/dynamic-routing/test/index.test.js index 77978c4e95cb..4c73f4430f55 100644 --- a/test/integration/dynamic-routing/test/index.test.js +++ b/test/integration/dynamic-routing/test/index.test.js @@ -15,6 +15,7 @@ import { normalizeRegEx, } from 'next-test-utils' import cheerio from 'cheerio' +import escapeRegex from 'escape-string-regexp' jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2 @@ -485,6 +486,10 @@ function runTests(dev) { route.regex = normalizeRegEx(route.regex) } + for (const route of manifest.dataRoutes) { + route.dataRouteRegex = normalizeRegEx(route.dataRouteRegex) + } + expect(manifest).toEqual({ version: 1, pages404: true, @@ -492,6 +497,32 @@ function runTests(dev) { headers: [], rewrites: [], redirects: [], + dataRoutes: [ + { + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/p1\\/p2\\/all\\-ssg\\/(.+?)\\.json$` + ), + page: '/p1/p2/all-ssg/[...rest]', + }, + { + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/p1\\/p2\\/nested\\-all\\-ssg\\/(.+?)\\.json$` + ), + page: '/p1/p2/nested-all-ssg/[...rest]', + }, + { + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/p1\\/p2\\/predefined\\-ssg\\/(.+?)\\.json$` + ), + page: '/p1/p2/predefined-ssg/[...rest]', + }, + ], dynamicRoutes: [ { page: '/blog/[name]/comment/[id]', diff --git a/test/integration/getserverprops/test/index.test.js b/test/integration/getserverprops/test/index.test.js index 1c932da181e2..84707e9eccf5 100644 --- a/test/integration/getserverprops/test/index.test.js +++ b/test/integration/getserverprops/test/index.test.js @@ -23,73 +23,74 @@ const nextConfig = join(appDir, 'next.config.js') let app let appPort let buildId +let stderr -const expectedManifestRoutes = () => ({ - '/something': { - page: '/something', +const expectedManifestRoutes = () => [ + { dataRouteRegex: normalizeRegEx( - `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/something.json$` + `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/index.json$` ), + page: '/', }, - '/blog/[post]': { - page: '/blog/[post]', + { dataRouteRegex: normalizeRegEx( - `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/blog\\/([^/]+?)\\.json$` + `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/another.json$` ), + page: '/another', }, - '/': { - page: '/', + { dataRouteRegex: normalizeRegEx( - `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/index.json$` + `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/blog.json$` ), + page: '/blog', }, - '/default-revalidate': { - page: '/default-revalidate', + { dataRouteRegex: normalizeRegEx( - `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/default-revalidate.json$` + `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/blog\\/([^\\/]+?)\\.json$` ), + page: '/blog/[post]', }, - '/catchall/[...path]': { - page: '/catchall/[...path]', + { dataRouteRegex: normalizeRegEx( - `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/catchall\\/(.+?)\\.json$` + `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/blog\\/([^\\/]+?)\\/([^\\/]+?)\\.json$` ), + page: '/blog/[post]/[comment]', }, - '/blog': { - page: '/blog', + { dataRouteRegex: normalizeRegEx( - `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/blog.json$` + `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/catchall\\/(.+?)\\.json$` ), + page: '/catchall/[...path]', }, - '/blog/[post]/[comment]': { - page: '/blog/[post]/[comment]', + { dataRouteRegex: normalizeRegEx( - `^\\/_next\\/data\\/${escapeRegex( - buildId - )}\\/blog\\/([^/]+?)\\/([^/]+?)\\.json$` + `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/default-revalidate.json$` ), + page: '/default-revalidate', }, - '/user/[user]/profile': { - page: '/user/[user]/profile', + { dataRouteRegex: normalizeRegEx( - `^\\/_next\\/data\\/${escapeRegex( - buildId - )}\\/user\\/([^/]+?)\\/profile\\.json$` + `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/invalid-keys.json$` ), + page: '/invalid-keys', }, - '/another': { - page: '/another', + { dataRouteRegex: normalizeRegEx( - `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/another.json$` + `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/something.json$` ), + page: '/something', }, - '/invalid-keys': { + { dataRouteRegex: normalizeRegEx( - `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/invalid-keys.json$` + `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/user\\/([^\\/]+?)\\/profile\\.json$` ), - page: '/invalid-keys', + page: '/user/[user]/profile', }, -}) +] const navigateTest = (dev = false) => { it('should navigate between pages successfully', async () => { @@ -208,7 +209,10 @@ const runTests = (dev = false) => { expect(JSON.parse(query)).toEqual({ path: ['first'] }) const data = JSON.parse( - await renderViaHTTP(appPort, `/_next/data/${buildId}/catchall/first.json`) + await renderViaHTTP( + appPort, + `/_next/data/${escapeRegex(buildId)}/catchall/first.json` + ) ) expect(data.pageProps.params).toEqual({ path: ['first'] }) @@ -216,7 +220,10 @@ const runTests = (dev = false) => { it('should return data correctly', async () => { const data = JSON.parse( - await renderViaHTTP(appPort, `/_next/data/${buildId}/something.json`) + await renderViaHTTP( + appPort, + `/_next/data/${escapeRegex(buildId)}/something.json` + ) ) expect(data.pageProps.world).toBe('world') }) @@ -225,7 +232,7 @@ const runTests = (dev = false) => { const data = JSON.parse( await renderViaHTTP( appPort, - `/_next/data/${buildId}/something.json?another=thing` + `/_next/data/${escapeRegex(buildId)}/something.json?another=thing` ) ) expect(data.pageProps.query.another).toBe('thing') @@ -233,7 +240,10 @@ const runTests = (dev = false) => { it('should return data correctly for dynamic page', async () => { const data = JSON.parse( - await renderViaHTTP(appPort, `/_next/data/${buildId}/blog/post-1.json`) + await renderViaHTTP( + appPort, + `/_next/data/${escapeRegex(buildId)}/blog/post-1.json` + ) ) expect(data.pageProps.post).toBe('post-1') }) @@ -322,6 +332,31 @@ const runTests = (dev = false) => { }) if (dev) { + it('should not show warning from url prop being returned', async () => { + const urlPropPage = join(appDir, 'pages/url-prop.js') + await fs.writeFile( + urlPropPage, + ` + export async function unstable_getServerProps() { + return { + props: { + url: 'something' + } + } + } + + export default ({ url }) =>

url: {url}

+ ` + ) + + const html = await renderViaHTTP(appPort, '/url-prop') + await fs.remove(urlPropPage) + expect(stderr).not.toMatch( + /The prop `url` is a reserved prop in Next.js for legacy reasons and will be overridden on page \/url-prop/ + ) + expect(html).toMatch(/url:.*?something/) + }) + it('should show error for extra keys returned from getServerProps', async () => { const html = await renderViaHTTP(appPort, '/invalid-keys') expect(html).toContain( @@ -341,15 +376,14 @@ const runTests = (dev = false) => { }) it('should output routes-manifest correctly', async () => { - const { serverPropsRoutes } = await fs.readJSON( + const { dataRoutes } = await fs.readJSON( join(appDir, '.next/routes-manifest.json') ) - for (const key of Object.keys(serverPropsRoutes)) { - const val = serverPropsRoutes[key].dataRouteRegex - serverPropsRoutes[key].dataRouteRegex = normalizeRegEx(val) + for (const route of dataRoutes) { + route.dataRouteRegex = normalizeRegEx(route.dataRouteRegex) } - expect(serverPropsRoutes).toEqual(expectedManifestRoutes()) + expect(dataRoutes).toEqual(expectedManifestRoutes()) }) it('should set no-cache, no-store, must-revalidate header', async () => { @@ -365,8 +399,13 @@ const runTests = (dev = false) => { describe('unstable_getServerProps', () => { describe('dev mode', () => { beforeAll(async () => { + stderr = '' appPort = await findPort() - app = await launchApp(appDir, appPort) + app = await launchApp(appDir, appPort, { + onStderr(msg) { + stderr += msg + }, + }) buildId = 'development' }) afterAll(() => killApp(app)) @@ -382,8 +421,13 @@ describe('unstable_getServerProps', () => { 'utf8' ) await nextBuild(appDir) + stderr = '' appPort = await findPort() - app = await nextStart(appDir, appPort) + app = await nextStart(appDir, appPort, { + onStderr(msg) { + stderr += msg + }, + }) buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') }) afterAll(() => killApp(app)) diff --git a/test/integration/prerender/test/index.test.js b/test/integration/prerender/test/index.test.js index f5431a853cd4..7d52eaed288e 100644 --- a/test/integration/prerender/test/index.test.js +++ b/test/integration/prerender/test/index.test.js @@ -428,6 +428,31 @@ const runTests = (dev = false, looseMode = false) => { // ) // }) + it('should not show warning from url prop being returned', async () => { + const urlPropPage = join(appDir, 'pages/url-prop.js') + await fs.writeFile( + urlPropPage, + ` + export async function unstable_getStaticProps() { + return { + props: { + url: 'something' + } + } + } + + export default ({ url }) =>

url: {url}

+ ` + ) + + const html = await renderViaHTTP(appPort, '/url-prop') + await fs.remove(urlPropPage) + expect(stderr).not.toMatch( + /The prop `url` is a reserved prop in Next.js for legacy reasons and will be overridden on page \/url-prop/ + ) + expect(html).toMatch(/url:.*?something/) + }) + it('should always show fallback for page not in getStaticPaths', async () => { const html = await renderViaHTTP(appPort, '/blog/post-321') const $ = cheerio.load(html) @@ -564,6 +589,83 @@ const runTests = (dev = false, looseMode = false) => { }) } + it('outputs dataRoutes in routes-manifest correctly', async () => { + const { dataRoutes } = JSON.parse( + await fs.readFile(join(appDir, '.next/routes-manifest.json'), 'utf8') + ) + + for (const route of dataRoutes) { + route.dataRouteRegex = normalizeRegEx(route.dataRouteRegex) + } + + expect(dataRoutes).toEqual([ + { + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/index.json$` + ), + page: '/', + }, + { + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/another.json$` + ), + page: '/another', + }, + { + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/blog.json$` + ), + page: '/blog', + }, + { + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/blog\\/([^\\/]+?)\\.json$` + ), + page: '/blog/[post]', + }, + { + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/blog\\/([^\\/]+?)\\/([^\\/]+?)\\.json$` + ), + page: '/blog/[post]/[comment]', + }, + { + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/catchall\\/(.+?)\\.json$` + ), + page: '/catchall/[...slug]', + }, + { + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/default-revalidate.json$` + ), + page: '/default-revalidate', + }, + { + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/something.json$` + ), + page: '/something', + }, + { + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/user\\/([^\\/]+?)\\/profile\\.json$` + ), + page: '/user/[user]/profile', + }, + ]) + }) + it('outputs a prerender-manifest correctly', async () => { const manifest = JSON.parse( await fs.readFile(join(appDir, '.next/prerender-manifest.json'), 'utf8') diff --git a/test/integration/size-limit/test/index.test.js b/test/integration/size-limit/test/index.test.js index 0d36408825aa..1700dafa5efe 100644 --- a/test/integration/size-limit/test/index.test.js +++ b/test/integration/size-limit/test/index.test.js @@ -80,7 +80,7 @@ describe('Production response size', () => { ) // These numbers are without gzip compression! - const delta = responseSizesBytes - 238 * 1024 + const delta = responseSizesBytes - 237 * 1024 expect(delta).toBeLessThanOrEqual(1024) // don't increase size more than 1kb expect(delta).toBeGreaterThanOrEqual(-1024) // don't decrease size more than 1kb without updating target })