diff --git a/docs/api-reference/next.config.js/basepath.md b/docs/api-reference/next.config.js/basepath.md new file mode 100644 index 0000000000000..21bb8b032dbd6 --- /dev/null +++ b/docs/api-reference/next.config.js/basepath.md @@ -0,0 +1,43 @@ +--- +description: Learn more about setting a base path in Next.js +--- + +# Base Path + +To deploy a Next.js application under a sub-path of a domain you can use the `basePath` option. + +`basePath` allows you to set a path prefix for the application. For example `/docs` instead of `/` (the default). + +For example, to set the base path to `/docs`, set the following configuration in `next.config.js`: + +```js +module.exports = { + basePath: '/docs', +} +``` + +## Links + +When linking to other pages using `next/link` and `next/router` the `basePath` will be automatically applied. + +For example using `/about` will automatically become `/docs/about` when `basePath` is set to `/docs`. + +```js +export default function HomePage() { + return ( + <> + + About Page + + + ) +} +``` + +Output html: + +```html +About Page +``` + +This makes sure that you don't have to change all links in your application when changing the `basePath` value. diff --git a/docs/api-reference/next.config.js/headers.md b/docs/api-reference/next.config.js/headers.md new file mode 100644 index 0000000000000..dad6ace1eb820 --- /dev/null +++ b/docs/api-reference/next.config.js/headers.md @@ -0,0 +1,91 @@ +--- +description: Add custom HTTP headers to your Next.js app. +--- + +# Headers + +Headers allow you to set custom HTTP headers for an incoming request path. + +To set custom HTTP headers you can use the `headers` key in `next.config.js`: + +```js +module.exports = { + async headers() { + return [ + { + source: '/about', + headers: [ + { + key: 'x-custom-header', + value: 'my custom header value', + }, + { + key: 'x-another-custom-header', + value: 'my other custom header value', + }, + ], + }, + , + ] + }, +} +``` + +`headers` is an async function that expects an array to be returned holding objects with `source` and `headers` properties: + +- `source` is the incoming request path pattern. +- `headers` is an array of header objects with the `key` and `value` properties. + +## Path Matching + +Path matches are allowed, for example `/blog/:slug` will match `/blog/hello-world` (no nested paths): + +```js +module.exports = { + async headers() { + return [ + { + source: '/blog/:slug', + headers: [ + { + key: 'x-slug', + value: ':slug', // Matched parameters can be used in the value + }, + { + key: 'x-slug-:slug', // Matched parameters can be used in the key + value: 'my other custom header value', + }, + ], + }, + , + ] + }, +} +``` + +### Wildcard Path Matching + +To match a wildcard path you can use `*` after a parameter, for example `/blog/:slug*` will match `/blog/a/b/c/d/hello-world`: + +```js +module.exports = { + async headers() { + return [ + { + source: '/blog/:slug*', + headers: [ + { + key: 'x-slug', + value: ':slug*', // Matched parameters can be used in the value + }, + { + key: 'x-slug-:slug*', // Matched parameters can be used in the key + value: 'my other custom header value', + }, + ], + }, + , + ] + }, +} +``` diff --git a/docs/api-reference/next.config.js/redirects.md b/docs/api-reference/next.config.js/redirects.md new file mode 100644 index 0000000000000..bcc93e24c8a34 --- /dev/null +++ b/docs/api-reference/next.config.js/redirects.md @@ -0,0 +1,67 @@ +--- +description: Add redirects to your Next.js app. +--- + +# Redirects + +Redirects allow you to redirect an incoming request path to a different destination path. + +Redirects are only available on the Node.js environment and do not affect client-side routing. + +To use Redirects you can use the `redirects` key in `next.config.js`: + +```js +module.exports = { + async redirects() { + return [ + { + source: '/about', + destination: '/', + permanent: true, + }, + ] + }, +} +``` + +`redirects` is an async function that expects an array to be returned holding objects with `source`, `destination`, and `permanent` properties: + +- `source` is the incoming request path pattern. +- `destination` is the path you want to route to. +- `permanent` if the redirect is permanent or not. + +## Path Matching + +Path matches are allowed, for example `/old-blog/:slug` will match `/old-blog/hello-world` (no nested paths): + +```js +module.exports = { + async redirects() { + return [ + { + source: '/old-blog/:slug', + destination: '/news/:slug', // Matched parameters can be used in the destination + permanent: true, + }, + ] + }, +} +``` + +### Wildcard Path Matching + +To match a wildcard path you can use `*` after a parameter, for example `/blog/:slug*` will match `/blog/a/b/c/d/hello-world`: + +```js +module.exports = { + async redirects() { + return [ + { + source: '/blog/:slug*', + destination: '/news/:slug*', // Matched parameters can be used in the destination + permanent: true, + }, + ] + }, +} +``` diff --git a/docs/api-reference/next.config.js/rewrites.md b/docs/api-reference/next.config.js/rewrites.md new file mode 100644 index 0000000000000..1e4fa48213e2e --- /dev/null +++ b/docs/api-reference/next.config.js/rewrites.md @@ -0,0 +1,112 @@ +--- +description: Add rewrites to your Next.js app. +--- + +# Rewrites + +Rewrites allow you to map an incoming request path to a different destination path. + +Rewrites are only available on the Node.js environment and do not affect client-side routing. + +To use rewrites you can use the `rewrites` key in `next.config.js`: + +```js +module.exports = { + async rewrites() { + return [ + { + source: '/about', + destination: '/', + }, + ] + }, +} +``` + +`rewrites` is an async function that expects an array to be returned holding objects with `source` and `destination` properties: + +- `source` is the incoming request path pattern. +- `destination` is the path you want to route to. + +## Path Matching + +Path matches are allowed, for example `/blog/:slug` will match `/blog/hello-world` (no nested paths): + +```js +module.exports = { + async rewrites() { + return [ + { + source: '/blog/:slug', + destination: '/news/:slug', // Matched parameters can be used in the destination + }, + ] + }, +} +``` + +### Wildcard Path Matching + +To match a wildcard path you can use `*` after a parameter, for example `/blog/:slug*` will match `/blog/a/b/c/d/hello-world`: + +```js +module.exports = { + async rewrites() { + return [ + { + source: '/blog/:slug*', + destination: '/news/:slug*', // Matched parameters can be used in the destination + }, + ] + }, +} +``` + +## Rewriting to an external URL + +
+ Examples + +
+ +Rewrites allow you to rewrite to an external url. This is especially useful for incrementally adopting Next.js. + +```js +module.exports = { + async rewrites() { + return [ + { + source: '/blog/:slug', + destination: 'https://example.com/blog/:slug', // Matched parameters can be used in the destination + }, + ] + }, +} +``` + +### Incremental adoption of Next.js + +You can also make Next.js check the application routes before falling back to proxying to the previous website. + +This way you don't have to change the rewrites configuration when migrating more pages to Next.js + +```js +module.exports = { + async rewrites() { + return [ + // we need to define a no-op rewrite to trigger checking + // all pages/static files before we attempt proxying + { + source: '/:path*', + destination: '/:path*', + }, + { + source: '/:path*', + destination: `https://custom-routes-proxying-endpoint.vercel.app/:path*`, + }, + ] + }, +} +``` diff --git a/docs/manifest.json b/docs/manifest.json index 5db0aef99ed25..e4ff3d7a01605 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -227,6 +227,22 @@ "title": "Environment Variables", "path": "/docs/api-reference/next.config.js/environment-variables.md" }, + { + "title": "Base Path", + "path": "/docs/api-reference/next.config.js/basepath.md" + }, + { + "title": "Rewrites", + "path": "/docs/api-reference/next.config.js/rewrites.md" + }, + { + "title": "Redirects", + "path": "/docs/api-reference/next.config.js/redirects.md" + }, + { + "title": "Custom Headers", + "path": "/docs/api-reference/next.config.js/headers.md" + }, { "title": "Custom Page Extensions", "path": "/docs/api-reference/next.config.js/custom-page-extensions.md" diff --git a/examples/custom-routes-proxying/package.json b/examples/custom-routes-proxying/package.json index 748db774f52f1..352eb4ab81731 100644 --- a/examples/custom-routes-proxying/package.json +++ b/examples/custom-routes-proxying/package.json @@ -8,7 +8,7 @@ }, "dependencies": { "next": "latest", - "react": "16.8.6", - "react-dom": "16.8.6" + "react": "^16.13.1", + "react-dom": "^16.13.1" } } diff --git a/examples/dynamic-routing/package.json b/examples/dynamic-routing/package.json index 0df90de82e529..7b4cb1b6cd7dc 100644 --- a/examples/dynamic-routing/package.json +++ b/examples/dynamic-routing/package.json @@ -9,7 +9,7 @@ "license": "ISC", "dependencies": { "next": "latest", - "react": "16.8.6", - "react-dom": "16.8.6" + "react": "^16.13.1", + "react-dom": "^16.13.1" } } diff --git a/examples/with-http2/package.json b/examples/with-http2/package.json index 85ee868e57dfc..1c1e4ae63a6a3 100644 --- a/examples/with-http2/package.json +++ b/examples/with-http2/package.json @@ -8,8 +8,8 @@ }, "dependencies": { "next": "latest", - "react": "16.8.6", - "react-dom": "16.8.6" + "react": "^16.13.1", + "react-dom": "^16.13.1" }, "license": "ISC" } diff --git a/examples/with-linaria/package.json b/examples/with-linaria/package.json index 8c3ef14f7f388..105fbd56aa31e 100644 --- a/examples/with-linaria/package.json +++ b/examples/with-linaria/package.json @@ -10,8 +10,8 @@ "@zeit/next-css": "^1.0.1", "linaria": "2.0.0-alpha.5", "next": "latest", - "react": "16.8.3", - "react-dom": "16.8.3" + "react": "^16.13.1", + "react-dom": "^16.13.1" }, "license": "ISC" } diff --git a/examples/with-rbx-bulma-pro/package.json b/examples/with-rbx-bulma-pro/package.json index a3067ba7b6ddd..0ebc7974352f3 100644 --- a/examples/with-rbx-bulma-pro/package.json +++ b/examples/with-rbx-bulma-pro/package.json @@ -9,7 +9,7 @@ "bulma-pro": "^0.1.7", "next": "^9.1.8-canary.11", "rbx": "^2.2.0", - "react": "16.8.6", - "react-dom": "16.8.6" + "react": "^16.13.1", + "react-dom": "^16.13.1" } } diff --git a/examples/with-stitches-styled/css/index.js b/examples/with-stitches-styled/css/index.js index 2b6c855ed5529..82ca04e9d4b7b 100644 --- a/examples/with-stitches-styled/css/index.js +++ b/examples/with-stitches-styled/css/index.js @@ -1,17 +1,12 @@ -import { createConfig } from '@stitches/css' +import { createCss } from '@stitches/css' import { createStyled } from '@stitches/styled' -const config = createConfig({ +export const css = createCss({ tokens: { colors: { RED: 'tomato', }, }, }) -/* - With Typescript: - const { Provider, styled, useCss } = createStyled() -*/ -const { Provider, styled, useCss } = createStyled() -export { config, Provider, styled, useCss } +export const styled = createStyled(css) diff --git a/examples/with-stitches-styled/package.json b/examples/with-stitches-styled/package.json index 9f7688677478b..f02cd6ad3cb8b 100644 --- a/examples/with-stitches-styled/package.json +++ b/examples/with-stitches-styled/package.json @@ -8,8 +8,8 @@ "start": "next start" }, "dependencies": { - "@stitches/css": "2.0.6", - "@stitches/styled": "2.0.6", + "@stitches/css": "3.0.0", + "@stitches/styled": "3.0.0", "next": "9.3.5", "react": "16.13.1", "react-dom": "16.13.1" diff --git a/examples/with-stitches-styled/pages/_app.js b/examples/with-stitches-styled/pages/_app.js deleted file mode 100644 index 3a02350e7434f..0000000000000 --- a/examples/with-stitches-styled/pages/_app.js +++ /dev/null @@ -1,18 +0,0 @@ -import App from 'next/app' -import { createCss } from '@stitches/css' -import { Provider, config } from '../css' - -/* - With Typescript: - export default class MyApp extends App<{ serverCss: TCss }> { -*/ -export default class MyApp extends App { - render() { - const { Component, pageProps, serverCss } = this.props - return ( - - - - ) - } -} diff --git a/examples/with-stitches-styled/pages/_document.js b/examples/with-stitches-styled/pages/_document.js index 37446a385eae7..df3b228357357 100644 --- a/examples/with-stitches-styled/pages/_document.js +++ b/examples/with-stitches-styled/pages/_document.js @@ -1,25 +1,28 @@ import Document from 'next/document' -import { createCss } from '@stitches/css' -import { config } from '../css' +import { css } from '../css' export default class MyDocument extends Document { static async getInitialProps(ctx) { - const css = createCss(config) const originalRenderPage = ctx.renderPage try { - ctx.renderPage = () => - originalRenderPage({ - enhanceApp: (App) => (props) => , - }) + let extractedStyles + ctx.renderPage = () => { + const { styles, result } = css.getStyles(originalRenderPage) + extractedStyles = styles + return result + } const initialProps = await Document.getInitialProps(ctx) + return { ...initialProps, styles: ( <> {initialProps.styles} - + {extractedStyles.map((content) => ( + + ))} ), } diff --git a/examples/with-stitches-styled/pages/index.js b/examples/with-stitches-styled/pages/index.js index 139f87aa79be4..46a2e51a0daf8 100644 --- a/examples/with-stitches-styled/pages/index.js +++ b/examples/with-stitches-styled/pages/index.js @@ -1,6 +1,8 @@ import { styled } from '../css' -const Header = styled.h1((css) => css.color('RED')) +const Header = styled.h1({ + color: 'RED', +}) export default function Home() { return
Hello world
diff --git a/examples/with-stitches/css/index.js b/examples/with-stitches/css/index.js index 810d0e4bfa19d..bf4ac7ac1acae 100644 --- a/examples/with-stitches/css/index.js +++ b/examples/with-stitches/css/index.js @@ -1,30 +1,9 @@ -import { createConfig } from '@stitches/css' -import * as React from 'react' +import { createCss } from '@stitches/css' -const config = createConfig({ +export const css = createCss({ tokens: { colors: { RED: 'tomato', }, }, }) - -/* - With Typescript: - const context = React.createContext>(null) -*/ -const context = React.createContext(null) - -/* - With Typescript: - const Provider = ({ css, children }: { css: TCss, children?: React.ReactNode }) => { - return {children} - } -*/ -const Provider = ({ css, children }) => { - return {children} -} - -const useCss = () => React.useContext(context) - -export { config, Provider, useCss } diff --git a/examples/with-stitches/package.json b/examples/with-stitches/package.json index 0487dc2e090c9..761c06c9d5fb5 100644 --- a/examples/with-stitches/package.json +++ b/examples/with-stitches/package.json @@ -8,7 +8,7 @@ "start": "next start" }, "dependencies": { - "@stitches/css": "2.0.6", + "@stitches/css": "3.0.0", "next": "9.3.5", "react": "16.13.1", "react-dom": "16.13.1" diff --git a/examples/with-stitches/pages/_app.js b/examples/with-stitches/pages/_app.js deleted file mode 100644 index 3b6955437658b..0000000000000 --- a/examples/with-stitches/pages/_app.js +++ /dev/null @@ -1,18 +0,0 @@ -import { createCss } from '@stitches/css' -import App from 'next/app' -import { config, Provider } from '../css' - -/* - With Typescript: - export default class MyApp extends App<{ serverCss: TCss }> { -*/ -export default class MyApp extends App { - render() { - const { Component, pageProps, serverCss } = this.props - return ( - - - - ) - } -} diff --git a/examples/with-stitches/pages/_document.js b/examples/with-stitches/pages/_document.js index 5de659632367e..df3b228357357 100644 --- a/examples/with-stitches/pages/_document.js +++ b/examples/with-stitches/pages/_document.js @@ -1,26 +1,27 @@ -import { createCss } from '@stitches/css' import Document from 'next/document' -import { config } from '../css' +import { css } from '../css' export default class MyDocument extends Document { static async getInitialProps(ctx) { - const css = createCss(config) const originalRenderPage = ctx.renderPage try { - ctx.renderPage = () => - originalRenderPage({ - enhanceApp: (App) => (props) => , - }) + let extractedStyles + ctx.renderPage = () => { + const { styles, result } = css.getStyles(originalRenderPage) + extractedStyles = styles + return result + } const initialProps = await Document.getInitialProps(ctx) + return { ...initialProps, styles: ( <> {initialProps.styles} - {css.getStyles().map((css, index) => ( - + {extractedStyles.map((content) => ( + ))} ), diff --git a/examples/with-stitches/pages/index.js b/examples/with-stitches/pages/index.js index aac79783121e0..039130840e4d4 100644 --- a/examples/with-stitches/pages/index.js +++ b/examples/with-stitches/pages/index.js @@ -1,6 +1,13 @@ -import { useCss } from '../css' +import { css } from '../css' export default function Home() { - const css = useCss() - return

Hello world

+ return ( +

+ Hello world +

+ ) } diff --git a/examples/with-zones/blog/package.json b/examples/with-zones/blog/package.json index b4f26bca0735e..f26dbba0f0a06 100644 --- a/examples/with-zones/blog/package.json +++ b/examples/with-zones/blog/package.json @@ -6,8 +6,8 @@ }, "dependencies": { "next": "latest", - "react": "16.8.6", - "react-dom": "16.8.6" + "react": "^16.13.1", + "react-dom": "^16.13.1" }, "license": "ISC" } diff --git a/examples/with-zones/home/package.json b/examples/with-zones/home/package.json index 10e8e1055d365..7ebc19d462400 100644 --- a/examples/with-zones/home/package.json +++ b/examples/with-zones/home/package.json @@ -6,8 +6,8 @@ }, "dependencies": { "next": "latest", - "react": "16.8.6", - "react-dom": "16.8.6" + "react": "^16.13.1", + "react-dom": "^16.13.1" }, "license": "ISC" } diff --git a/packages/next/README.md b/packages/next/README.md index d4d5cedd1b52e..9a401f10e64b7 100644 --- a/packages/next/README.md +++ b/packages/next/README.md @@ -29,9 +29,9 @@ Please see our [contributing.md](/contributing.md). ## Authors -- Arunoda Susiripala ([@arunoda](https://twitter.com/arunoda)) – [Vercel](https://vercel.com) -- Tim Neutkens ([@timneutkens](https://twitter.com/timneutkens)) – [Vercel](https://vercel.com) -- Naoyuki Kanezawa ([@nkzawa](https://twitter.com/nkzawa)) – [Vercel](https://vercel.com) +- Arunoda Susiripala ([@arunoda](https://twitter.com/arunoda)) – [Vercel](https://vercel.com/about/arunoda-zeit) +- Tim Neutkens ([@timneutkens](https://twitter.com/timneutkens)) – [Vercel](https://vercel.com/about/timneutkens) +- Naoyuki Kanezawa ([@nkzawa](https://twitter.com/nkzawa)) – [Vercel](https://vercel.com/about/nkzawa) - Tony Kovanen ([@tonykovanen](https://twitter.com/tonykovanen)) – [Vercel](https://vercel.com) -- Guillermo Rauch ([@rauchg](https://twitter.com/rauchg)) – [Vercel](https://vercel.com) +- Guillermo Rauch ([@rauchg](https://twitter.com/rauchg)) – [Vercel](https://vercel.com/about/rauchg) - Dan Zajdband ([@impronunciable](https://twitter.com/impronunciable)) – Knight-Mozilla / Coral Project diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 2c20abbc93034..ada23018f9fa8 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -863,6 +863,9 @@ export default async function getBaseWebpackConfig( 'process.env.__NEXT_REACT_MODE': JSON.stringify( config.experimental.reactMode ), + 'process.env.__NEXT_SCROLL_RESTORATION': JSON.stringify( + config.experimental.scrollRestoration + ), 'process.env.__NEXT_ROUTER_BASEPATH': JSON.stringify(config.basePath), ...(isServer ? { diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index a9208f0cb70c0..8cdd959d2748f 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -97,6 +97,11 @@ type ComponentLoadCancel = (() => void) | null type HistoryMethod = 'replaceState' | 'pushState' +const manualScrollRestoration = + process.env.__NEXT_SCROLL_RESTORATION && + typeof window !== 'undefined' && + 'scrollRestoration' in window.history + function fetchNextData( dataHref: string, isServerRender: boolean, @@ -250,6 +255,35 @@ export default class Router implements BaseRouter { } window.addEventListener('popstate', this.onPopState) + + // enable custom scroll restoration handling when available + // otherwise fallback to browser's default handling + if (process.env.__NEXT_SCROLL_RESTORATION) { + if (manualScrollRestoration) { + window.history.scrollRestoration = 'manual' + + let scrollDebounceTimeout: undefined | NodeJS.Timeout + + const debouncedScrollSave = () => { + if (scrollDebounceTimeout) clearTimeout(scrollDebounceTimeout) + + scrollDebounceTimeout = setTimeout(() => { + const { url, as: curAs, options } = history.state + this.changeState( + 'replaceState', + url, + curAs, + Object.assign({}, options, { + _N_X: window.scrollX, + _N_Y: window.scrollY, + }) + ) + }, 10) + } + + window.addEventListener('scroll', debouncedScrollSave) + } + } } } @@ -503,7 +537,13 @@ export default class Router implements BaseRouter { throw error } + if (process.env.__NEXT_SCROLL_RESTORATION) { + if (manualScrollRestoration && '_N_X' in options) { + window.scrollTo(options._N_X, options._N_Y) + } + } Router.events.emit('routeChangeComplete', as) + return resolve(true) }) }, diff --git a/packages/next/next-server/lib/utils.ts b/packages/next/next-server/lib/utils.ts index a3058b18253c9..6dee4015678c3 100644 --- a/packages/next/next-server/lib/utils.ts +++ b/packages/next/next-server/lib/utils.ts @@ -199,6 +199,12 @@ export interface NextApiRequest extends IncomingMessage { body: any env: Env + + preview?: boolean + /** + * Preview data set on the request, if any + * */ + previewData?: any } /** diff --git a/packages/next/next-server/server/api-utils.ts b/packages/next/next-server/server/api-utils.ts index e2ba57d721737..930412aeb20a6 100644 --- a/packages/next/next-server/server/api-utils.ts +++ b/packages/next/next-server/server/api-utils.ts @@ -46,7 +46,16 @@ export async function apiResolver( setLazyProp({ req: apiReq }, 'cookies', getCookieParser(req)) // Parsing query string setLazyProp({ req: apiReq, params }, 'query', getQueryParser(req)) - // // Parsing of body + // Parsing preview data + setLazyProp({ req: apiReq }, 'previewData', () => + tryGetPreviewData(req, res, apiContext) + ) + // Checking if preview mode is enabled + setLazyProp({ req: apiReq }, 'preview', () => + apiReq.previewData !== false ? true : undefined + ) + + // Parsing of body if (bodyParser) { apiReq.body = await parseBody( apiReq, diff --git a/packages/next/next-server/server/config.ts b/packages/next/next-server/server/config.ts index deb2e0104fa05..33f87a7ab2b27 100644 --- a/packages/next/next-server/server/config.ts +++ b/packages/next/next-server/server/config.ts @@ -53,6 +53,7 @@ const defaultConfig: { [key: string]: any } = { workerThreads: false, pageEnv: false, productionBrowserSourceMaps: false, + scrollRestoration: false, }, future: { excludeDefaultMomentLocales: false, diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index 3f27cbfc1663e..ae2aebe8f7b74 100644 --- a/packages/react-dev-overlay/package.json +++ b/packages/react-dev-overlay/package.json @@ -28,6 +28,7 @@ "strip-ansi": "6.0.0" }, "peerDependencies": { + "webpack": "^4|^5", "react": "^16.9.0", "react-dom": "^16.9.0" } diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 81498ecb60b05..60447961c7758 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -20,7 +20,7 @@ }, "peerDependencies": { "react-refresh": "0.8.3", - "webpack": "^4" + "webpack": "^4|^5" }, "devDependencies": { "react-refresh": "0.8.3" diff --git a/test/integration/prerender-preview/pages/api/read.js b/test/integration/prerender-preview/pages/api/read.js new file mode 100644 index 0000000000000..80a76dba09940 --- /dev/null +++ b/test/integration/prerender-preview/pages/api/read.js @@ -0,0 +1,7 @@ +export default (req, res) => { + const { preview, previewData } = req + res.json({ + preview, + previewData, + }) +} diff --git a/test/integration/prerender-preview/test/index.test.js b/test/integration/prerender-preview/test/index.test.js index f85c7a1d5d4c7..d0c78a8fa13b6 100644 --- a/test/integration/prerender-preview/test/index.test.js +++ b/test/integration/prerender-preview/test/index.test.js @@ -182,6 +182,27 @@ function runTests(startServer = nextStart) { expect(cookies[1]).not.toHaveProperty('Max-Age') }) + it('should pass undefined to API routes when not in preview', async () => { + const res = await fetchViaHTTP(appPort, `/api/read`) + const json = await res.json() + + expect(json).toMatchObject({}) + }) + it('should pass the preview data to API routes', async () => { + const res = await fetchViaHTTP( + appPort, + '/api/read', + {}, + { headers: { Cookie: previewCookieString } } + ) + const json = await res.json() + + expect(json).toMatchObject({ + preview: true, + previewData: { lets: 'goooo' }, + }) + }) + afterAll(async () => { await killApp(app) }) diff --git a/test/integration/scroll-back-restoration/next.config.js b/test/integration/scroll-back-restoration/next.config.js new file mode 100644 index 0000000000000..456258562b6d9 --- /dev/null +++ b/test/integration/scroll-back-restoration/next.config.js @@ -0,0 +1,5 @@ +module.exports = { + experimental: { + scrollRestoration: true, + }, +} diff --git a/test/integration/scroll-back-restoration/pages/another.js b/test/integration/scroll-back-restoration/pages/another.js new file mode 100644 index 0000000000000..a37308e7d8779 --- /dev/null +++ b/test/integration/scroll-back-restoration/pages/another.js @@ -0,0 +1,10 @@ +import Link from 'next/link' + +export default () => ( + <> +

hi from another

+ + to index + + +) diff --git a/test/integration/scroll-back-restoration/pages/index.js b/test/integration/scroll-back-restoration/pages/index.js new file mode 100644 index 0000000000000..edb974d3b7b5f --- /dev/null +++ b/test/integration/scroll-back-restoration/pages/index.js @@ -0,0 +1,50 @@ +import Link from 'next/link' + +const Page = ({ loaded }) => { + const link = ( + + + to another + + + ) + + if (typeof window !== 'undefined') { + window.didHydrate = true + } + + if (loaded) { + return ( + <> +
+ {link} +
the end
+ + ) + } + + return link +} + +export default Page + +export const getServerSideProps = () => { + return { + props: { + loaded: true, + }, + } +} diff --git a/test/integration/scroll-back-restoration/test/index.test.js b/test/integration/scroll-back-restoration/test/index.test.js new file mode 100644 index 0000000000000..4c3ea725fad81 --- /dev/null +++ b/test/integration/scroll-back-restoration/test/index.test.js @@ -0,0 +1,89 @@ +/* eslint-env jest */ + +import { join } from 'path' +import webdriver from 'next-webdriver' +import { + killApp, + findPort, + launchApp, + nextStart, + nextBuild, + check, +} from 'next-test-utils' + +jest.setTimeout(1000 * 60 * 2) + +const appDir = join(__dirname, '../') +let appPort +let app + +const runTests = () => { + it('should restore the scroll position on navigating back', async () => { + const browser = await webdriver(appPort, '/') + await browser.eval(() => + document.querySelector('#to-another').scrollIntoView() + ) + const scrollRestoration = await browser.eval( + () => window.history.scrollRestoration + ) + + expect(scrollRestoration).toBe('manual') + + const scrollX = Math.floor(await browser.eval(() => window.scrollX)) + const scrollY = Math.floor(await browser.eval(() => window.scrollY)) + + expect(scrollX).not.toBe(0) + expect(scrollY).not.toBe(0) + + await browser.eval(() => window.next.router.push('/another')) + + await check( + () => browser.eval(() => document.documentElement.innerHTML), + /hi from another/ + ) + await browser.eval(() => (window.didHydrate = false)) + + await browser.eval(() => window.history.back()) + await check(() => browser.eval(() => window.didHydrate), { + test(content) { + return content + }, + }) + + const newScrollX = Math.floor(await browser.eval(() => window.scrollX)) + const newScrollY = Math.floor(await browser.eval(() => window.scrollY)) + + console.log({ + scrollX, + scrollY, + newScrollX, + newScrollY, + }) + + expect(scrollX).toBe(newScrollX) + expect(scrollY).toBe(newScrollY) + }) +} + +describe('Scroll Restoration Support', () => { + describe('dev mode', () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTests() + }) + + describe('server mode', () => { + beforeAll(async () => { + await nextBuild(appDir) + appPort = await findPort() + app = await nextStart(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTests() + }) +}) diff --git a/test/integration/scroll-forward-restoration/next.config.js b/test/integration/scroll-forward-restoration/next.config.js new file mode 100644 index 0000000000000..456258562b6d9 --- /dev/null +++ b/test/integration/scroll-forward-restoration/next.config.js @@ -0,0 +1,5 @@ +module.exports = { + experimental: { + scrollRestoration: true, + }, +} diff --git a/test/integration/scroll-forward-restoration/pages/another.js b/test/integration/scroll-forward-restoration/pages/another.js new file mode 100644 index 0000000000000..a37308e7d8779 --- /dev/null +++ b/test/integration/scroll-forward-restoration/pages/another.js @@ -0,0 +1,10 @@ +import Link from 'next/link' + +export default () => ( + <> +

hi from another

+ + to index + + +) diff --git a/test/integration/scroll-forward-restoration/pages/index.js b/test/integration/scroll-forward-restoration/pages/index.js new file mode 100644 index 0000000000000..edb974d3b7b5f --- /dev/null +++ b/test/integration/scroll-forward-restoration/pages/index.js @@ -0,0 +1,50 @@ +import Link from 'next/link' + +const Page = ({ loaded }) => { + const link = ( + + + to another + + + ) + + if (typeof window !== 'undefined') { + window.didHydrate = true + } + + if (loaded) { + return ( + <> +
+ {link} +
the end
+ + ) + } + + return link +} + +export default Page + +export const getServerSideProps = () => { + return { + props: { + loaded: true, + }, + } +} diff --git a/test/integration/scroll-forward-restoration/test/index.test.js b/test/integration/scroll-forward-restoration/test/index.test.js new file mode 100644 index 0000000000000..ebb867457f179 --- /dev/null +++ b/test/integration/scroll-forward-restoration/test/index.test.js @@ -0,0 +1,97 @@ +/* eslint-env jest */ + +import { join } from 'path' +import webdriver from 'next-webdriver' +import { + killApp, + findPort, + launchApp, + nextStart, + nextBuild, + check, +} from 'next-test-utils' + +jest.setTimeout(1000 * 60 * 2) + +const appDir = join(__dirname, '../') +let appPort +let app + +const runTests = () => { + it('should restore the scroll position on navigating forward', async () => { + const browser = await webdriver(appPort, '/another') + await browser.elementByCss('#to-index').click() + + await check( + () => browser.eval(() => document.documentElement.innerHTML), + /the end/ + ) + + await browser.eval(() => + document.querySelector('#to-another').scrollIntoView() + ) + const scrollRestoration = await browser.eval( + () => window.history.scrollRestoration + ) + + expect(scrollRestoration).toBe('manual') + + const scrollX = Math.floor(await browser.eval(() => window.scrollX)) + const scrollY = Math.floor(await browser.eval(() => window.scrollY)) + + expect(scrollX).not.toBe(0) + expect(scrollY).not.toBe(0) + + await browser.eval(() => window.history.back()) + + await check( + () => browser.eval(() => document.documentElement.innerHTML), + /hi from another/ + ) + + await browser.eval(() => (window.didHydrate = false)) + await browser.eval(() => window.history.forward()) + + await check(() => browser.eval(() => window.didHydrate), { + test(content) { + return content + }, + }) + + const newScrollX = Math.floor(await browser.eval(() => window.scrollX)) + const newScrollY = Math.floor(await browser.eval(() => window.scrollY)) + + console.log({ + scrollX, + scrollY, + newScrollX, + newScrollY, + }) + + expect(scrollX).toBe(newScrollX) + expect(scrollY).toBe(newScrollY) + }) +} + +describe('Scroll Restoration Support', () => { + describe('dev mode', () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTests() + }) + + describe('server mode', () => { + beforeAll(async () => { + await nextBuild(appDir) + appPort = await findPort() + app = await nextStart(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTests() + }) +}) diff --git a/yarn.lock b/yarn.lock index c81f338b6c31b..226b2d14e26a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15948,7 +15948,7 @@ webpack-sources@1.4.3, webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-s source-list-map "^2.0.0" source-map "~0.6.1" -webpack@4.43.0, webpack@^4.42.1: +webpack@4.43.0: version "4.43.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.43.0.tgz#c48547b11d563224c561dad1172c8aa0b8a678e6" integrity sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g==