diff --git a/.eslintignore b/.eslintignore index 6a558257c0a76c1..02dd2b05059b4c9 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,4 +3,4 @@ node_modules **/_next/** **/dist/** examples/with-ioc/** -examples/with-kea/** +examples/with-kea/** \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 5c51653d42d7c7f..3701997c2bbfccc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,4 @@ node_modules **/.next/** **/_next/** -**/dist/** +**/dist/** \ No newline at end of file diff --git a/.prettierignore_staged b/.prettierignore_staged index cead30ab8514cc6..7278db6b0605e4a 100644 --- a/.prettierignore_staged +++ b/.prettierignore_staged @@ -1,3 +1,3 @@ **/.next/** **/_next/** -**/dist/** +**/dist/** \ No newline at end of file diff --git a/docs/advanced-features/preview-mode.md b/docs/advanced-features/preview-mode.md index f47f88b3ba505f7..2dd68ea1972284c 100644 --- a/docs/advanced-features/preview-mode.md +++ b/docs/advanced-features/preview-mode.md @@ -153,7 +153,7 @@ https:///api/preview?secret=&slug= Take a look at the following examples to learn more: -- [DatoCMS Example](https://github.com/zeit/next.js/tree/canary/examples/cms-datocms) +- [DatoCMS Example](https://github.com/zeit/next.js/tree/canary/examples/cms-datocms) ([Demo](https://next-blog-datocms.now.sh/)) - [TakeShape Example](https://github.com/zeit/next.js/tree/canary/examples/cms-takeshape) ## More Details diff --git a/docs/basic-features/data-fetching.md b/docs/basic-features/data-fetching.md index 995499c890433a5..959e854cb165ba9 100644 --- a/docs/basic-features/data-fetching.md +++ b/docs/basic-features/data-fetching.md @@ -152,8 +152,8 @@ The `paths` key determines which paths will be pre-rendered. For example, suppos ```js return { paths: [ - { params: { id: 1 } }, - { params: { id: 2 } } + { params: { id: '1' } }, + { params: { id: '2' } } ], fallback: ... } @@ -229,7 +229,7 @@ If `fallback` is `true`, then the behavior of `getStaticProps` changes: In the “fallback” version of a page: - The page’s props will be empty. -- Using the [router](/docs/api-reference/next/router.md)), you can detect if the fallback is being rendered, `router.isFallback` will be `true`. +- Using the [router](/docs/api-reference/next/router.md), you can detect if the fallback is being rendered, `router.isFallback` will be `true`. Here’s an example that uses `isFallback`: @@ -366,7 +366,7 @@ export default Page ### When should I use `getServerSideProps`? -You should use `getServerSideProps` only if you need to pre-render a page whose data must be fetched at request time. Time to first byte (TTFB) will be slower than `getStaticProps` because the server must compute the result on every request, and the result cannot be cached by a CDN without extra . +You should use `getServerSideProps` only if you need to pre-render a page whose data must be fetched at request time. Time to first byte (TTFB) will be slower than `getStaticProps` because the server must compute the result on every request, and the result cannot be cached by a CDN without extra configuration. If you don’t need to pre-render the data, then you should consider fetching data on the client side. [Click here to learn more](#fetching-data-on-the-client-side). @@ -389,7 +389,7 @@ export const getServerSideProps: GetServerSideProps = async context => { `getServerSideProps` only runs on server-side and never runs on the browser. If a page uses `getServerSideProps` , then: - When you request this page directly, `getServerSideProps` runs at the request time, and this page will be pre-rendered with the returned props. -- When you request this page on client-side page transitions through `next/link` ([documentation](/docs/api-reference/next/link.md)), Next.js sends an API request to server, which runs `getServerSideProps`. It’ll return a JSON that contains the result of running `getServerSideProps`, and the JSON will be used to render the page. All this work will be handled automatically by Next.js, so you don’t need to do anything extra as long as you have `getServerSideProps` defined. +- When you request this page on client-side page transitions through `next/link` ([documentation](/docs/api-reference/next/link.md)) or `next/router` ([documentation](/docs/api-reference/next/router.md)), Next.js sends an API request to the server, which runs `getServerSideProps`. It’ll return JSON that contains the result of running `getServerSideProps`, and the JSON will be used to render the page. All this work will be handled automatically by Next.js, so you don’t need to do anything extra as long as you have `getServerSideProps` defined. #### Only allowed in a page @@ -428,7 +428,7 @@ function Profile() { Take a look at the following examples to learn more: -- [DatoCMS Example](https://github.com/zeit/next.js/tree/canary/examples/cms-datocms) +- [DatoCMS Example](https://github.com/zeit/next.js/tree/canary/examples/cms-datocms) ([Demo](https://next-blog-datocms.now.sh/)) - [TakeShape Example](https://github.com/zeit/next.js/tree/canary/examples/cms-takeshape) ## Learn more diff --git a/docs/basic-features/pages.md b/docs/basic-features/pages.md index 9c4c7bb4d023c4f..848b92be5116a02 100644 --- a/docs/basic-features/pages.md +++ b/docs/basic-features/pages.md @@ -246,7 +246,7 @@ We've discussed two forms of pre-rendering for Next.js. Take a look at the following examples to learn more: -- [DatoCMS Example](https://github.com/zeit/next.js/tree/canary/examples/cms-datocms) +- [DatoCMS Example](https://github.com/zeit/next.js/tree/canary/examples/cms-datocms) ([Demo](https://next-blog-datocms.now.sh/)) - [TakeShape Example](https://github.com/zeit/next.js/tree/canary/examples/cms-takeshape) ## Learn more diff --git a/errors/built-in-css-disabled.md b/errors/built-in-css-disabled.md new file mode 100644 index 000000000000000..4f6311769a31be0 --- /dev/null +++ b/errors/built-in-css-disabled.md @@ -0,0 +1,18 @@ +# Built-in CSS Support Disabled + +#### Why This Error Occurred + +Custom CSS configuration was added in `next.config.js` which disables the built-in CSS/SCSS support to prevent conflicting configuration. + +A legacy plugin such as `@zeit/next-css` being added in `next.config.js` can cause this message. + +#### Possible Ways to Fix It + +If you would like to leverage the built-in CSS/SCSS support you can remove any custom CSS configuration or any plugins like `@zeit/next-css` or `@zeit/next-sass` in your `next.config.js`. + +If you would prefer not to leverage the built-in support you can ignore this message. + +### Useful Links + +- [Built-in CSS Support docs](https://nextjs.org/docs/basic-features/built-in-css-support) +- [Custom webpack config docs](https://nextjs.org/docs/api-reference/next.config.js/custom-webpack-config) diff --git a/errors/install-sass.md b/errors/install-sass.md new file mode 100644 index 000000000000000..91b7d1301f39562 --- /dev/null +++ b/errors/install-sass.md @@ -0,0 +1,19 @@ +# Install `sass` to Use Built-In Sass Support + +#### Why This Error Occurred + +Using Next.js' [built-in Sass support](https://nextjs.org/docs/basic-features/built-in-css-support#sass-support) requires that you bring your own version of Sass. + +#### Possible Ways to Fix It + +Please install the `sass` package in your project. + +```bash +npm i sass +# or +yarn add sass +``` + +### Useful Links + +- [Sass Support in Documentation](https://nextjs.org/docs/basic-features/built-in-css-support#sass-support) diff --git a/examples/amp-first/pages/index.js b/examples/amp-first/pages/index.js index 36ea2f07cd18def..6518d49e8d108c5 100644 --- a/examples/amp-first/pages/index.js +++ b/examples/amp-first/pages/index.js @@ -234,10 +234,10 @@ const Home = props => ( ) // amp-script requires absolute URLs, so we create a property `host` which we can use to calculate the script URL. -Home.getInitialProps = async ({ req }) => { +export async function getServerSideProps({ req }) { // WARNING: This is a generally unsafe application unless you're deploying to a managed platform like ZEIT Now. // Be sure your load balancer is configured to not allow spoofed host headers. - return { host: `${getProtocol(req)}://${req.headers.host}` } + return { props: { host: `${getProtocol(req)}://${req.headers.host}` } } } function getProtocol(req) { diff --git a/examples/analyze-bundles/package.json b/examples/analyze-bundles/package.json index 44577da10f31566..49db7282fda0e2f 100644 --- a/examples/analyze-bundles/package.json +++ b/examples/analyze-bundles/package.json @@ -12,8 +12,8 @@ "cross-env": "^6.0.3", "faker": "^4.1.0", "next": "latest", - "react": "^16.7.0", - "react-dom": "^16.7.0" + "react": "^16.8.0", + "react-dom": "^16.8.0" }, "license": "ISC" } diff --git a/examples/analyze-bundles/pages/index.js b/examples/analyze-bundles/pages/index.js index c5e13c7e8f9cc01..c40a24da07b1892 100644 --- a/examples/analyze-bundles/pages/index.js +++ b/examples/analyze-bundles/pages/index.js @@ -1,31 +1,28 @@ import React from 'react' import Link from 'next/link' +import faker from 'faker' -export default class Index extends React.Component { - static getInitialProps({ req }) { - if (req) { - // Runs only in the server - const faker = require('faker') - const name = faker.name.findName() - return { name } - } - - // Runs only in the client - return { name: 'Arunoda' } - } - - render() { - const { name } = this.props - return ( +const Index = ({ name }) => { + return ( +
+

Home Page

+

Welcome, {name}

-

Home Page

-

Welcome, {name}

-
- - About Page - -
+ + About Page +
- ) +
+ ) +} + +export default Index + +export async function getStaticProps() { + // The name will be generated at build time only + const name = faker.name.findName() + + return { + props: { name }, } } diff --git a/examples/api-routes/components/Person.js b/examples/api-routes/components/Person.js index 16c2bf63a523c91..d43584b1668bc37 100644 --- a/examples/api-routes/components/Person.js +++ b/examples/api-routes/components/Person.js @@ -2,7 +2,7 @@ import Link from 'next/link' export default ({ person }) => (
  • - + {person.name}
  • diff --git a/examples/api-routes/package.json b/examples/api-routes/package.json index 5eeb8a1552fb2a4..f74abdd7e0fca8d 100644 --- a/examples/api-routes/package.json +++ b/examples/api-routes/package.json @@ -7,8 +7,8 @@ "start": "next start" }, "dependencies": { - "isomorphic-unfetch": "3.0.0", "next": "latest", + "node-fetch": "2.6.0", "react": "^16.8.6", "react-dom": "^16.8.6" }, diff --git a/examples/api-routes/pages/index.js b/examples/api-routes/pages/index.js index 4f1b5c97d411e1a..bfabbc5ced50e70 100644 --- a/examples/api-routes/pages/index.js +++ b/examples/api-routes/pages/index.js @@ -1,5 +1,5 @@ import Person from '../components/Person' -import fetch from 'isomorphic-unfetch' +import fetch from 'node-fetch' const Index = ({ people }) => (
      @@ -9,11 +9,11 @@ const Index = ({ people }) => (
    ) -Index.getInitialProps = async () => { +export async function getServerSideProps() { const response = await fetch('http://localhost:3000/api/people') const people = await response.json() - return { people } + return { props: { people } } } export default Index diff --git a/examples/api-routes/pages/person.js b/examples/api-routes/pages/person/[id].js similarity index 81% rename from examples/api-routes/pages/person.js rename to examples/api-routes/pages/person/[id].js index ec15c7a3786c02c..34d63cce840c512 100644 --- a/examples/api-routes/pages/person.js +++ b/examples/api-routes/pages/person/[id].js @@ -1,4 +1,4 @@ -import fetch from 'isomorphic-unfetch' +import fetch from 'node-fetch' const Person = ({ data, status }) => status === 200 ? ( @@ -30,11 +30,16 @@ const Person = ({ data, status }) =>

    {data.message}

    ) -Person.getInitialProps = async ({ query }) => { - const response = await fetch(`http://localhost:3000/api/people/${query.id}`) - +export async function getServerSideProps({ params }) { + const response = await fetch(`http://localhost:3000/api/people/${params.id}`) const data = await response.json() - return { data, status: response.status } + + return { + props: { + data, + status: response.status, + }, + } } export default Person diff --git a/examples/cms-datocms/README.md b/examples/cms-datocms/README.md index 0dffcfa1d8b42cd..ef71e5c4b3747f9 100644 --- a/examples/cms-datocms/README.md +++ b/examples/cms-datocms/README.md @@ -2,6 +2,10 @@ This example showcases Next.js's [Static Generation](/docs/basic-features/pages.md) feature using [DatoCMS](https://www.datocms.com/) as the data source. +## Demo + +### [https://next-blog-datocms.now.sh/](https://next-blog-datocms.now.sh/) + ## How to use ### Using `create-next-app` diff --git a/examples/cms-datocms/package.json b/examples/cms-datocms/package.json index ab883a9f5797129..8be37cb0d18576a 100644 --- a/examples/cms-datocms/package.json +++ b/examples/cms-datocms/package.json @@ -10,7 +10,7 @@ "classnames": "2.2.6", "date-fns": "2.10.0", "isomorphic-unfetch": "3.0.0", - "next": "9.2.3-canary.26", + "next": "9.3.0", "react": "^16.13.0", "react-datocms": "1.1.0", "react-dom": "^16.13.0", diff --git a/examples/cms-datocms/pages/index.js b/examples/cms-datocms/pages/index.js index e789b602ee15a88..f2b4e4f3e7a98bf 100644 --- a/examples/cms-datocms/pages/index.js +++ b/examples/cms-datocms/pages/index.js @@ -36,7 +36,7 @@ export default function Index({ allPosts }) { } export async function getStaticProps({ preview }) { - const allPosts = await getAllPostsForHome(preview) + const allPosts = (await getAllPostsForHome(preview)) || [] return { props: { allPosts }, } diff --git a/examples/cms-datocms/pages/posts/[slug].js b/examples/cms-datocms/pages/posts/[slug].js index bcfa6683eec7ee0..d00288b803ee2ea 100644 --- a/examples/cms-datocms/pages/posts/[slug].js +++ b/examples/cms-datocms/pages/posts/[slug].js @@ -50,7 +50,7 @@ export default function Post({ post, morePosts, preview }) { ) } -export async function getStaticProps({ params, preview }) { +export async function getStaticProps({ params, preview = null }) { const data = await getPostAndMorePosts(params.slug, preview) const content = await markdownToHtml(data?.post?.content || '') diff --git a/examples/cms-datocms/public/images/author.jpg b/examples/cms-datocms/public/images/author.jpg deleted file mode 100644 index a3794bc99380042..000000000000000 Binary files a/examples/cms-datocms/public/images/author.jpg and /dev/null differ diff --git a/examples/cms-datocms/public/images/image.jpg b/examples/cms-datocms/public/images/image.jpg deleted file mode 100644 index 3a119fd96f97d86..000000000000000 Binary files a/examples/cms-datocms/public/images/image.jpg and /dev/null differ diff --git a/examples/custom-server-express/package.json b/examples/custom-server-express/package.json index 6c17a925470a747..3fa6f68b09cdf6f 100644 --- a/examples/custom-server-express/package.json +++ b/examples/custom-server-express/package.json @@ -10,7 +10,7 @@ "cross-env": "^5.2.0", "express": "^4.14.0", "next": "latest", - "react": "^16.7.0", - "react-dom": "^16.7.0" + "react": "^16.13.0", + "react-dom": "^16.13.0" } } diff --git a/examples/custom-server-express/pages/index.js b/examples/custom-server-express/pages/index.js index 958fffabb6361c3..2dfad58236b7892 100644 --- a/examples/custom-server-express/pages/index.js +++ b/examples/custom-server-express/pages/index.js @@ -13,10 +13,5 @@ export default () => ( b -
  • - - post #2 - -
  • ) diff --git a/examples/custom-server-express/pages/posts.js b/examples/custom-server-express/pages/posts.js deleted file mode 100644 index 6c300ddb00363e3..000000000000000 --- a/examples/custom-server-express/pages/posts.js +++ /dev/null @@ -1,19 +0,0 @@ -import React, { Component } from 'react' - -export default class extends Component { - static getInitialProps({ query: { id } }) { - return { postId: id } - } - - render() { - return ( -
    -

    My blog post #{this.props.postId}

    -

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do - eiusmod tempor incididunt ut labore et dolore magna aliqua. -

    -
    - ) - } -} diff --git a/examples/custom-server-express/server.js b/examples/custom-server-express/server.js index 802c0726ff92038..f5f4ac20263c761 100644 --- a/examples/custom-server-express/server.js +++ b/examples/custom-server-express/server.js @@ -17,10 +17,6 @@ app.prepare().then(() => { return app.render(req, res, '/b', req.query) }) - server.get('/posts/:id', (req, res) => { - return app.render(req, res, '/posts', { id: req.params.id }) - }) - server.all('*', (req, res) => { return handle(req, res) }) diff --git a/examples/custom-server-fastify/package.json b/examples/custom-server-fastify/package.json index 8b8ad5b148f81f6..c4ef63115d86a8b 100644 --- a/examples/custom-server-fastify/package.json +++ b/examples/custom-server-fastify/package.json @@ -7,10 +7,10 @@ "start": "cross-env NODE_ENV=production node ./server.js" }, "dependencies": { - "cross-env": "^5.2.0", - "fastify": "2.1.0", + "cross-env": "^7.0.2", + "fastify": "^2.12.1", "next": "latest", - "react": "^16.8.4", - "react-dom": "^16.8.4" + "react": "^16.13.0", + "react-dom": "^16.13.0" } } diff --git a/examples/custom-server-fastify/server.js b/examples/custom-server-fastify/server.js index a381d324926d4dd..256e3756031e437 100644 --- a/examples/custom-server-fastify/server.js +++ b/examples/custom-server-fastify/server.js @@ -6,6 +6,7 @@ const dev = process.env.NODE_ENV !== 'production' fastify.register((fastify, opts, next) => { const app = Next({ dev }) + const handle = app.getRequestHandler() app .prepare() .then(() => { @@ -30,7 +31,7 @@ fastify.register((fastify, opts, next) => { }) fastify.all('/*', (req, reply) => { - return app.handleRequest(req.req, reply.res).then(() => { + return handle(req.req, reply.res).then(() => { reply.sent = true }) }) diff --git a/examples/data-fetch/README.md b/examples/data-fetch/README.md index bbbd445a5fba318..272091d25d99443 100644 --- a/examples/data-fetch/README.md +++ b/examples/data-fetch/README.md @@ -3,7 +3,7 @@ Next.js was conceived to make it easy to create universal apps. That's why fetching data on the server and the client when necessary is so easy with Next. -Using `getInitialProps` fetches data on the server for SSR and then on the client when the component is re-mounted (not on the first paint). +Using `getStaticProps` fetches data at built time from a page, Next.js will pre-render this page at build time. ## Deploy your own diff --git a/examples/data-fetch/package.json b/examples/data-fetch/package.json index ce6b96d2187181f..55d94b79a3ea614 100644 --- a/examples/data-fetch/package.json +++ b/examples/data-fetch/package.json @@ -7,8 +7,8 @@ "start": "next start" }, "dependencies": { - "isomorphic-unfetch": "^3.0.0", "next": "latest", + "node-fetch": "^2.6.0", "react": "^16.8.4", "react-dom": "^16.8.4" }, diff --git a/examples/data-fetch/pages/index.js b/examples/data-fetch/pages/index.js index b9aee6825d7dbaa..a6d2c2b2edba0e8 100644 --- a/examples/data-fetch/pages/index.js +++ b/examples/data-fetch/pages/index.js @@ -1,11 +1,11 @@ import React from 'react' import Link from 'next/link' -import fetch from 'isomorphic-unfetch' +import fetch from 'node-fetch' -function Index(props) { +function Index({ stars }) { return (
    -

    Next.js has {props.stars} ⭐️

    +

    Next.js has {stars} ⭐️

    How about preact? @@ -13,10 +13,14 @@ function Index(props) { ) } -Index.getInitialProps = async () => { +export async function getStaticProps() { const res = await fetch('https://api.github.com/repos/zeit/next.js') const json = await res.json() // better use it inside try .. catch - return { stars: json.stargazers_count } + return { + props: { + stars: json.stargazers_count, + }, + } } export default Index diff --git a/examples/data-fetch/pages/preact.js b/examples/data-fetch/pages/preact.js index 1a9d707863297f0..9c4df30454e8bee 100644 --- a/examples/data-fetch/pages/preact.js +++ b/examples/data-fetch/pages/preact.js @@ -1,11 +1,11 @@ import React from 'react' import Link from 'next/link' -import fetch from 'isomorphic-unfetch' +import fetch from 'node-fetch' -function Preact(props) { +function Preact({ stars }) { return (
    -

    Preact has {props.stars} ⭐

    +

    Preact has {stars} ⭐

    I bet Next.js has more stars (?) @@ -13,10 +13,14 @@ function Preact(props) { ) } -Preact.getInitialProps = async () => { +export async function getStaticProps() { const res = await fetch('https://api.github.com/repos/developit/preact') const json = await res.json() // better use it inside try .. catch - return { stars: json.stargazers_count } + return { + props: { + stars: json.stargazers_count, + }, + } } export default Preact diff --git a/examples/form-handler/components/index.js b/examples/form-handler/components/index.js deleted file mode 100644 index 526b37a09f1dbc2..000000000000000 --- a/examples/form-handler/components/index.js +++ /dev/null @@ -1,37 +0,0 @@ -import React, { Component } from 'react' -import Head from 'next/head' -import { Col, Row } from 'react-bootstrap' - -import Header from './Header' -import DisplayForm from './DisplayForm' - -import UserForm from './UserForm' -import Social from './Social' - -class Main extends Component { - render() { - return ( -
    - - Form Handler - - -
    - - - - - - - - - -
    - ) - } -} - -export default Main diff --git a/examples/form-handler/package.json b/examples/form-handler/package.json index ae631e78b4273c5..ac38f593e977ae4 100644 --- a/examples/form-handler/package.json +++ b/examples/form-handler/package.json @@ -5,14 +5,13 @@ "main": "index.js", "scripts": { "start": "next start", - "dev": "node server.js", + "dev": "next", "build": "next build" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { - "express": "^4.16.4", "next": "latest", "next-redux-wrapper": "^2.1.0", "react": "^16.8.4", diff --git a/examples/form-handler/pages/_app.js b/examples/form-handler/pages/_app.js index fbb59e4c98b6786..7d795471c626586 100644 --- a/examples/form-handler/pages/_app.js +++ b/examples/form-handler/pages/_app.js @@ -1,28 +1,14 @@ -import App from 'next/app' import React from 'react' import { Provider } from 'react-redux' import withRedux from 'next-redux-wrapper' import { initStore } from '../store' -class MyApp extends App { - static async getInitialProps({ Component, router, ctx }) { - let pageProps = {} - - if (Component.getInitialProps) { - pageProps = await Component.getInitialProps(ctx) - } - - return { pageProps } - } - - render() { - const { Component, pageProps, store } = this.props - return ( - - - - ) - } +const App = ({ Component, pageProps, store }) => { + return ( + + + + ) } -export default withRedux(initStore)(MyApp) +export default withRedux(initStore)(App) diff --git a/examples/form-handler/pages/index.js b/examples/form-handler/pages/index.js index fbf022aaf2daf51..bdbc2e76f1035ff 100644 --- a/examples/form-handler/pages/index.js +++ b/examples/form-handler/pages/index.js @@ -1,13 +1,41 @@ import React, { Component } from 'react' +import Head from 'next/head' +import { Col, Row, Grid } from 'react-bootstrap' -import { connect } from 'react-redux' +import Header from '../components/Header' +import DisplayForm from '../components/DisplayForm' -import Main from '../components' +import UserForm from '../components/UserForm' +import Social from '../components/Social' class Index extends Component { render() { - return
    + return ( +
    + + Form Handler + + + +
    + + + + + + + + + + +
    + ) } } -export default connect()(Index) +export default Index diff --git a/examples/form-handler/server.js b/examples/form-handler/server.js deleted file mode 100644 index 37b0bdb46ade622..000000000000000 --- a/examples/form-handler/server.js +++ /dev/null @@ -1,29 +0,0 @@ -const express = require('express') -const next = require('next') - -const dev = process.env.NODE_ENV !== 'production' -const app = next({ dev }) -const handle = app.getRequestHandler() - -app - .prepare() - .then(() => { - const server = express() - - server.get('/', (req, res) => { - return handle(req, res) - }) - - server.get('*', (req, res) => { - return handle(req, res) - }) - - server.listen(3000, err => { - if (err) throw err - console.log('> Ready on http://localhost:3000') - }) - }) - .catch(ex => { - console.error(ex.stack) - process.exit(1) - }) diff --git a/examples/ssr-caching/pages/blog.js b/examples/ssr-caching/pages/blog.js deleted file mode 100644 index f70cd07a6316662..000000000000000 --- a/examples/ssr-caching/pages/blog.js +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' - -export default class extends React.Component { - static getInitialProps({ query: { id } }) { - return { id } - } - - render() { - return ( -
    -

    My {this.props.id} blog post

    -

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do - eiusmod tempor incididunt ut labore et dolore magna aliqua. -

    -
    - ) - } -} diff --git a/examples/ssr-caching/pages/blog/[id].js b/examples/ssr-caching/pages/blog/[id].js new file mode 100644 index 000000000000000..44606a200e1304d --- /dev/null +++ b/examples/ssr-caching/pages/blog/[id].js @@ -0,0 +1,28 @@ +import React from 'react' + +export default function(props) { + return ( +
    +

    My {props.id} blog post

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. +

    +
    + ) +} + +export async function getStaticProps({ params: { id } }) { + return { props: { id } } +} + +export async function getStaticPaths() { + return { + paths: [ + { params: { id: 'first' } }, + { params: { id: 'second' } }, + { params: { id: 'last' } }, + ], + fallback: true, + } +} diff --git a/examples/with-loading/README.md b/examples/with-loading/README.md index 7573404122cd3b8..82ab5b88fd68734 100644 --- a/examples/with-loading/README.md +++ b/examples/with-loading/README.md @@ -1,6 +1,6 @@ # Example app with page loading indicator -Sometimes when switching between pages, Next.js needs to download pages(chunks) from the server before rendering the page. And it may also need to wait for the data. So while doing these tasks, browser might be non responsive. +Sometimes when switching between pages, Next.js needs to download pages(chunks) from the server before rendering the page. And it may also need to wait for the data. So while doing these tasks, the browser might be non responsive. We can simply fix this issue by showing a loading indicator. That's what this examples shows. diff --git a/examples/with-loading/package.json b/examples/with-loading/package.json index 0ed321dce58843a..ab0f1b9cfb82d33 100644 --- a/examples/with-loading/package.json +++ b/examples/with-loading/package.json @@ -1,7 +1,6 @@ { "name": "with-loading", "version": "1.0.0", - "description": "This example features:", "main": "index.js", "scripts": { "dev": "next", diff --git a/examples/with-loading/pages/_app.js b/examples/with-loading/pages/_app.js index 282d54900f3d536..ebc53649086a791 100644 --- a/examples/with-loading/pages/_app.js +++ b/examples/with-loading/pages/_app.js @@ -1,9 +1,7 @@ -import React from 'react' -import App from 'next/app' -import Link from 'next/link' -import NProgress from 'nprogress' import Router from 'next/router' +import Link from 'next/link' import Head from 'next/head' +import NProgress from 'nprogress' Router.events.on('routeChangeStart', url => { console.log(`Loading: ${url}`) @@ -12,34 +10,31 @@ Router.events.on('routeChangeStart', url => { Router.events.on('routeChangeComplete', () => NProgress.done()) Router.events.on('routeChangeError', () => NProgress.done()) -export default class MyApp extends App { - render() { - const { Component, pageProps } = this.props - return ( - <> - - {/* Import CSS for nprogress */} - - - - - - ) - } +export default function App({ Component, pageProps }) { + return ( + <> + + {/* Import CSS for nprogress */} + + + + + + ) } diff --git a/examples/with-loading/pages/about.js b/examples/with-loading/pages/about.js index 39942733aae5c3c..ffa87104a122983 100644 --- a/examples/with-loading/pages/about.js +++ b/examples/with-loading/pages/about.js @@ -1,12 +1,10 @@ -import React from 'react' - const AboutPage = () =>

    This is about Next.js!

    -AboutPage.getInitialProps = async () => { +export async function getServerSideProps() { await new Promise(resolve => { setTimeout(resolve, 500) }) - return {} + return { props: {} } } export default AboutPage diff --git a/examples/with-loading/pages/forever.js b/examples/with-loading/pages/forever.js index 6d6c43815f56f77..f36f4ba4d4a0068 100644 --- a/examples/with-loading/pages/forever.js +++ b/examples/with-loading/pages/forever.js @@ -1,12 +1,10 @@ -import React from 'react' - const ForeverPage = () =>

    This page was rendered for a while!

    -ForeverPage.getInitialProps = async () => { +export async function getServerSideProps() { await new Promise(resolve => { setTimeout(resolve, 3000) }) - return {} + return { props: {} } } export default ForeverPage diff --git a/examples/with-loading/pages/index.js b/examples/with-loading/pages/index.js index dc2940c298f9ca0..5070538cdf800ab 100644 --- a/examples/with-loading/pages/index.js +++ b/examples/with-loading/pages/index.js @@ -1,5 +1,3 @@ -import React from 'react' - const IndexPage = () =>

    Hello Next.js!

    export default IndexPage diff --git a/examples/with-next-sass/README.md b/examples/with-next-sass/README.md index 60ef149c54ac712..4df3a58463dd8d5 100644 --- a/examples/with-next-sass/README.md +++ b/examples/with-next-sass/README.md @@ -1,6 +1,6 @@ # Example app with next-sass -This example uses next-sass without css-modules. The config can be found in `next.config.js`, change `withSass()` to `withSass({cssModules: true})` if you use css-modules. Then in the code, you import the stylesheet as `import style from '../styles/style.scss'` and use it like `
    `. [Learn more](https://github.com/zeit/next-plugins/tree/master/packages/next-sass). +This example demonstrates how to use Next.js' built-in Global Sass/Scss imports and Component-Level Sass/Scss modules support. ## Deploy your own diff --git a/examples/with-next-sass/components/hello-world.js b/examples/with-next-sass/components/hello-world.js new file mode 100644 index 000000000000000..2a478662183d392 --- /dev/null +++ b/examples/with-next-sass/components/hello-world.js @@ -0,0 +1,7 @@ +import styles from './hello-world.module.scss' + +export default () => ( +
    + Hello World, I am being styled using SCSS Modules! +
    +) diff --git a/examples/with-next-sass/components/hello-world.module.scss b/examples/with-next-sass/components/hello-world.module.scss new file mode 100644 index 000000000000000..5b1000dcd9c613e --- /dev/null +++ b/examples/with-next-sass/components/hello-world.module.scss @@ -0,0 +1,5 @@ +$color: red; + +.hello { + color: $color; +} diff --git a/examples/with-next-sass/next.config.js b/examples/with-next-sass/next.config.js deleted file mode 100644 index ed73b1374f1bd56..000000000000000 --- a/examples/with-next-sass/next.config.js +++ /dev/null @@ -1,2 +0,0 @@ -const withSass = require('@zeit/next-sass') -module.exports = withSass() diff --git a/examples/with-next-sass/package.json b/examples/with-next-sass/package.json index da5a8dcd941d7ba..3511b5b825603b4 100644 --- a/examples/with-next-sass/package.json +++ b/examples/with-next-sass/package.json @@ -5,10 +5,9 @@ "start": "next start" }, "dependencies": { - "@zeit/next-sass": "^1.0.0", "next": "latest", - "node-sass": "4.7.2", "react": "^16.7.0", - "react-dom": "^16.7.0" + "react-dom": "^16.7.0", + "sass": "1.26.3" } } diff --git a/examples/with-next-sass/pages/_app.js b/examples/with-next-sass/pages/_app.js new file mode 100644 index 000000000000000..569ae43d200dad8 --- /dev/null +++ b/examples/with-next-sass/pages/_app.js @@ -0,0 +1,7 @@ +import '../styles.scss' + +function MyApp({ Component, pageProps }) { + return +} + +export default MyApp diff --git a/examples/with-next-sass/pages/index.js b/examples/with-next-sass/pages/index.js index bfb3f3abf7c5496..55d4550097ccbbb 100644 --- a/examples/with-next-sass/pages/index.js +++ b/examples/with-next-sass/pages/index.js @@ -1,3 +1,7 @@ -import '../styles/style.scss' +import HelloWorld from '../components/hello-world' -export default () =>
    Hello World!
    +export default () => ( +
    + +
    +) diff --git a/examples/with-next-sass/styles.scss b/examples/with-next-sass/styles.scss new file mode 100644 index 000000000000000..f1a1f1957ed9bd3 --- /dev/null +++ b/examples/with-next-sass/styles.scss @@ -0,0 +1,5 @@ +$backgroundColor: #2ecc71; + +.app { + background-color: $backgroundColor; +} diff --git a/examples/with-next-sass/styles/style.scss b/examples/with-next-sass/styles/style.scss deleted file mode 100644 index 73cc2e0e844ba3d..000000000000000 --- a/examples/with-next-sass/styles/style.scss +++ /dev/null @@ -1,4 +0,0 @@ -$color: #2ecc71; -.example { - background-color: $color; -} diff --git a/examples/with-react-relay-network-modern/.babelrc b/examples/with-react-relay-network-modern/.babelrc index 8fad94a215c1e96..8a782ec48561c39 100644 --- a/examples/with-react-relay-network-modern/.babelrc +++ b/examples/with-react-relay-network-modern/.babelrc @@ -3,6 +3,6 @@ "next/babel" ], "plugins": [ - "relay" + ["relay", { artifactDirectory: "__generated__" }] ] -} \ No newline at end of file +} diff --git a/examples/with-react-relay-network-modern/components/BlogPosts.js b/examples/with-react-relay-network-modern/components/BlogPosts.js index c341f62c486eab1..30ff74963e2072c 100644 --- a/examples/with-react-relay-network-modern/components/BlogPosts.js +++ b/examples/with-react-relay-network-modern/components/BlogPosts.js @@ -6,9 +6,10 @@ const BlogPosts = props => { return (

    Blog posts

    - {props.viewer.allBlogPosts.edges.map(({ node }) => ( - - ))} + {props.viewer.allBlogPosts && + props.viewer.allBlogPosts.edges.map(({ node }) => ( + + ))}
    ) } diff --git a/examples/with-react-relay-network-modern/package.json b/examples/with-react-relay-network-modern/package.json index 82cb57a3bc3d4a3..23f4685fa8ee47d 100644 --- a/examples/with-react-relay-network-modern/package.json +++ b/examples/with-react-relay-network-modern/package.json @@ -7,7 +7,7 @@ "dev": "next", "build": "next build", "start": "next start", - "relay": "relay-compiler --src ./ --exclude '**/.next/**' '**/node_modules/**' '**/test/**' '**/__generated__/**' --exclude '**/schema/**' --schema ./schema/schema.graphql", + "relay": "relay-compiler --src ./ --exclude '**/.next/**' '**/node_modules/**' '**/test/**' '**/__generated__/**' --exclude '**/schema/**' --schema ./schema/schema.graphql --artifactDirectory __generated__", "schema": "graphql get-schema -e dev" }, "author": "", @@ -15,19 +15,19 @@ "dependencies": { "dotenv": "^8.0.0", "dotenv-webpack": "^1.5.4", - "graphql": "^14.3.0", + "graphql": "^14.6.0", "isomorphic-fetch": "^2.2.1", "next": "latest", - "react": "^16.7.0", - "react-dom": "^16.7.0", - "react-relay": "^5.0.0", - "react-relay-network-modern": "^4.0.0", - "react-relay-network-modern-ssr": "^1.2.2" + "react": "^16.13.0", + "react-dom": "^16.13.0", + "react-relay": "^9.0.0", + "react-relay-network-modern": "^4.5.0", + "react-relay-network-modern-ssr": "^1.4.0" }, "devDependencies": { - "babel-plugin-relay": "^5.0.0", + "babel-plugin-relay": "^9.0.0", "graphcool": "^1.2.1", "graphql-cli": "^3.0.11", - "relay-compiler": "^5.0.0" + "relay-compiler": "^9.0.0" } } diff --git a/examples/with-react-relay-network-modern/pages/_app.js b/examples/with-react-relay-network-modern/pages/_app.js index 07e62e6bf690f37..79b6b48da6a7629 100644 --- a/examples/with-react-relay-network-modern/pages/_app.js +++ b/examples/with-react-relay-network-modern/pages/_app.js @@ -35,7 +35,7 @@ export default class App extends NextApp { const environment = createEnvironment( relayData, JSON.stringify({ - queryID: Component.query ? Component.query().params.name : undefined, + queryID: Component.query ? Component.query.params.name : undefined, variables, }) ) diff --git a/examples/with-relay-modern-server-express/.babelrc b/examples/with-relay-modern-server-express/.babelrc index 8fad94a215c1e96..c7eb72f9da57075 100644 --- a/examples/with-relay-modern-server-express/.babelrc +++ b/examples/with-relay-modern-server-express/.babelrc @@ -3,6 +3,6 @@ "next/babel" ], "plugins": [ - "relay" + ["relay", { artifactDirectory: "__generated__" }] ] } \ No newline at end of file diff --git a/examples/with-relay-modern-server-express/package.json b/examples/with-relay-modern-server-express/package.json index 70b4a5ba8cb6f8b..a4519dd2c9c7593 100644 --- a/examples/with-relay-modern-server-express/package.json +++ b/examples/with-relay-modern-server-express/package.json @@ -8,25 +8,25 @@ "build": "next build", "prestart": "npm run build", "start": "NODE_ENV=production node server", - "relay": "relay-compiler --src ./ --exclude '**/.next/**' '**/node_modules/**' '**/test/**' '**/__generated__/**' '**/server/**' --schema ./server/schema.graphql --verbose" + "relay": "relay-compiler --src ./ --exclude '**/.next/**' '**/node_modules/**' '**/test/**' '**/__generated__/**' '**/server/**' --schema ./server/schema.graphql --artifactDirectory __generated__ --verbose" }, "author": "", "license": "ISC", "dependencies": { "dotenv": "^4.0.0", "dotenv-webpack": "^1.5.4", - "express-graphql": "^0.7.1", - "graphql": "^14.1.1", + "express-graphql": "^0.9.0", + "graphql": "^14.6.0", "graphql-relay": "^0.6.0", "isomorphic-unfetch": "^3.0.0", "next": "latest", - "react": "^16.7.0", - "react-dom": "^16.7.0", - "react-relay": "^5.0.0" + "react": "^16.13.0", + "react-dom": "^16.13.0", + "react-relay": "^9.0.0" }, "devDependencies": { - "babel-plugin-relay": "^2.0.0", + "babel-plugin-relay": "^9.0.0", "graphql-cli": "^1.0.0-beta.4", - "relay-compiler": "^2.0.0" + "relay-compiler": "^9.0.0" } } diff --git a/examples/with-relay-modern/package.json b/examples/with-relay-modern/package.json index 014bc7dde2541b3..2187db4ad0b3274 100644 --- a/examples/with-relay-modern/package.json +++ b/examples/with-relay-modern/package.json @@ -15,17 +15,17 @@ "dependencies": { "dotenv": "^8.2.0", "dotenv-webpack": "^1.7.0", - "graphql": "^14.5.8", + "graphql": "^14.6.0", "isomorphic-unfetch": "^3.0.0", "next": "latest", - "react": "^16.12.0", - "react-dom": "^16.12.0", - "react-relay": "^8.0.0" + "react": "^16.13.0", + "react-dom": "^16.13.0", + "react-relay": "^9.0.0" }, "devDependencies": { - "babel-plugin-relay": "^8.0.0", + "babel-plugin-relay": "^9.0.0", "graphcool": "^1.4.0", "graphql-cli": "^3.0.14", - "relay-compiler": "^8.0.0" + "relay-compiler": "^9.0.0" } } diff --git a/examples/with-zeit-fetch/pages/index.js b/examples/with-zeit-fetch/pages/index.js index 45154170f08a947..3a0f26b58229a6e 100644 --- a/examples/with-zeit-fetch/pages/index.js +++ b/examples/with-zeit-fetch/pages/index.js @@ -2,10 +2,10 @@ import React from 'react' import Link from 'next/link' import fetch from '../fetch' -function Index(props) { +export default function Index({ stars }) { return (
    -

    Next.js has {props.stars} ⭐️

    +

    Next.js has {stars} ⭐️

    How about preact? @@ -13,10 +13,10 @@ function Index(props) { ) } -Index.getInitialProps = async () => { +export async function getStaticProps() { const res = await fetch('https://api.github.com/repos/zeit/next.js') const json = await res.json() // better use it inside try .. catch - return { stars: json.stargazers_count } + return { + props: { stars: json.stargazers_count }, + } } - -export default Index diff --git a/examples/with-zeit-fetch/pages/preact.js b/examples/with-zeit-fetch/pages/preact.js index e89e8a23257e127..80c2ff9b94cecc1 100644 --- a/examples/with-zeit-fetch/pages/preact.js +++ b/examples/with-zeit-fetch/pages/preact.js @@ -2,10 +2,10 @@ import React from 'react' import Link from 'next/link' import fetch from '../fetch' -function Preact(props) { +export default function Preact({ stars }) { return (
    -

    Preact has {props.stars} ⭐

    +

    Preact has {stars} ⭐

    I bet Next.js has more stars (?) @@ -13,10 +13,10 @@ function Preact(props) { ) } -Preact.getInitialProps = async () => { +export async function getStaticProps() { const res = await fetch('https://api.github.com/repos/developit/preact') const json = await res.json() // better use it inside try .. catch - return { stars: json.stargazers_count } + return { + props: { stars: json.stargazers_count }, + } } - -export default Preact diff --git a/lerna.json b/lerna.json index 4c767fbbff9ec56..f3be2ef7a6f3df8 100644 --- a/lerna.json +++ b/lerna.json @@ -12,5 +12,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "9.2.3-canary.28" + "version": "9.3.1-canary.5" } diff --git a/package.json b/package.json index 5a958e468991f39..f6512307ff35c31 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "tree-kill": "1.2.1", "typescript": "3.7.3", "wait-port": "0.2.2", - "webpack-bundle-analyzer": "3.3.2" + "webpack-bundle-analyzer": "3.6.1" }, "resolutions": { "browserslist": "^4.8.3", diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index de37f8403d95ba5..69b2a61061b8fb8 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.28", + "version": "9.3.1-canary.5", "keywords": [ "react", "next", diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index eab9e5543ab20a2..120773cae0027c3 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.28", + "version": "9.3.1-canary.5", "main": "index.js", "license": "MIT", "repository": { @@ -8,6 +8,6 @@ "directory": "packages/next-bundle-analyzer" }, "dependencies": { - "webpack-bundle-analyzer": "3.3.2" + "webpack-bundle-analyzer": "3.6.1" } } diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 8d38268afbad40a..3cc9aa9034aaae8 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.28", + "version": "9.3.1-canary.5", "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 3171592310e4021..67bebdb5384b243 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.28", + "version": "9.3.1-canary.5", "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 b9af7c59eebf827..60d4e3f5c96f2d1 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.28", + "version": "9.3.1-canary.5", "nextjs": { "name": "Material UI", "required-env": [] diff --git a/packages/next-plugin-sentry/package.json b/packages/next-plugin-sentry/package.json index ae5ca8c624693cc..6cfca3e9206fd74 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.28", + "version": "9.3.1-canary.5", "nextjs": { "name": "Sentry", "required-env": [ diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 21e9cdee9a11c4f..85b397bbe0b4038 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.28", + "version": "9.3.1-canary.5", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", @@ -12,7 +12,6 @@ "core-js": "3.6.4", "microbundle": "0.11.0", "object-assign": "4.1.1", - "promise-polyfill": "8.1.3", "url-polyfill": "1.1.8", "whatwg-fetch": "3.0.0" } diff --git a/packages/next-polyfill-nomodule/src/index.js b/packages/next-polyfill-nomodule/src/index.js index 5b4552e10a8fcfc..51f97f54997fd25 100644 --- a/packages/next-polyfill-nomodule/src/index.js +++ b/packages/next-polyfill-nomodule/src/index.js @@ -1,66 +1,52 @@ -import 'core-js/modules/es6.array.copy-within' -import 'core-js/modules/es6.array.fill' -import 'core-js/modules/es6.array.find' -import 'core-js/modules/es6.array.find-index' -import 'core-js/modules/es7.array.flat-map' -import 'core-js/modules/es6.array.from' -import 'core-js/modules/es7.array.includes' -import 'core-js/modules/es6.array.iterator' -import 'core-js/modules/es6.array.of' -import 'core-js/modules/es6.array.species' -import 'core-js/modules/es6.function.has-instance' -import 'core-js/modules/es6.map' -import 'core-js/modules/es6.number.constructor' -import 'core-js/modules/es6.number.epsilon' -import 'core-js/modules/es6.number.is-finite' -import 'core-js/modules/es6.number.is-integer' -import 'core-js/modules/es6.number.is-nan' -import 'core-js/modules/es6.number.is-safe-integer' -import 'core-js/modules/es6.number.max-safe-integer' -import 'core-js/modules/es6.number.min-safe-integer' -import 'core-js/modules/es7.object.entries' -import 'core-js/modules/es7.object.get-own-property-descriptors' -import 'core-js/modules/es6.object.is' -import 'core-js/modules/es7.object.values' -import 'core-js/modules/es6.reflect.apply' -import 'core-js/modules/es6.reflect.construct' -import 'core-js/modules/es6.reflect.define-property' -import 'core-js/modules/es6.reflect.delete-property' -import 'core-js/modules/es6.reflect.get' -import 'core-js/modules/es6.reflect.get-own-property-descriptor' -import 'core-js/modules/es6.reflect.get-prototype-of' -import 'core-js/modules/es6.reflect.has' -import 'core-js/modules/es6.reflect.is-extensible' -import 'core-js/modules/es6.reflect.own-keys' -import 'core-js/modules/es6.reflect.prevent-extensions' -import 'core-js/modules/es6.reflect.set' -import 'core-js/modules/es6.reflect.set-prototype-of' -import 'core-js/modules/es6.regexp.constructor' -import 'core-js/modules/es6.regexp.flags' -import 'core-js/modules/es6.regexp.match' -import 'core-js/modules/es6.regexp.replace' -import 'core-js/modules/es6.regexp.split' -import 'core-js/modules/es6.regexp.search' -import 'core-js/modules/es6.set' -import 'core-js/modules/es6.symbol' -import 'core-js/modules/es7.symbol.async-iterator' -import 'core-js/modules/es6.string.code-point-at' -import 'core-js/modules/es6.string.ends-with' -import 'core-js/modules/es6.string.from-code-point' -import 'core-js/modules/es6.string.includes' -import 'core-js/modules/es6.string.iterator' -import 'core-js/modules/es7.string.pad-start' -import 'core-js/modules/es7.string.pad-end' -import 'core-js/modules/es6.string.raw' -import 'core-js/modules/es6.string.repeat' -import 'core-js/modules/es6.string.starts-with' -import 'core-js/modules/es7.string.trim-left' -import 'core-js/modules/es7.string.trim-right' -import 'core-js/modules/es6.weak-map' -import 'core-js/modules/es6.weak-set' +import 'core-js/features/array/copy-within' +import 'core-js/features/array/fill' +import 'core-js/features/array/find' +import 'core-js/features/array/find-index' +import 'core-js/features/array/flat-map' +import 'core-js/features/array/flat' +import 'core-js/features/array/from' +import 'core-js/features/array/includes' +import 'core-js/features/array/iterator' +import 'core-js/features/array/of' +import 'core-js/features/array/species' +import 'core-js/features/function/has-instance' +import 'core-js/features/map' +import 'core-js/features/number/constructor' +import 'core-js/features/number/epsilon' +import 'core-js/features/number/is-finite' +import 'core-js/features/number/is-integer' +import 'core-js/features/number/is-nan' +import 'core-js/features/number/is-safe-integer' +import 'core-js/features/number/max-safe-integer' +import 'core-js/features/number/min-safe-integer' +import 'core-js/features/object/entries' +import 'core-js/features/object/get-own-property-descriptors' +import 'core-js/features/object/is' +import 'core-js/features/object/values' +import 'core-js/features/reflect' +import 'core-js/features/regexp' +import 'core-js/features/set' +import 'core-js/features/symbol' +import 'core-js/features/symbol/async-iterator' +import 'core-js/features/string/code-point-at' +import 'core-js/features/string/ends-with' +import 'core-js/features/string/from-code-point' +import 'core-js/features/string/includes' +import 'core-js/features/string/iterator' +import 'core-js/features/string/pad-start' +import 'core-js/features/string/pad-end' +import 'core-js/features/string/raw' +import 'core-js/features/string/repeat' +import 'core-js/features/string/starts-with' +import 'core-js/features/string/trim-left' +import 'core-js/features/string/trim-right' +import 'core-js/features/weak-map' +import 'core-js/features/weak-set' +import 'core-js/features/promise' +import 'core-js/features/promise/all-settled' +import 'core-js/features/promise/finally' // Specialized Packages: -import 'promise-polyfill/src/polyfill' import 'whatwg-fetch' import 'url-polyfill' import assign from 'object-assign' diff --git a/packages/next/build/babel/preset.ts b/packages/next/build/babel/preset.ts index a244e4e00ae6bf7..95fa36aecfd5dca 100644 --- a/packages/next/build/babel/preset.ts +++ b/packages/next/build/babel/preset.ts @@ -157,9 +157,7 @@ module.exports = ( helpers: true, regenerator: true, useESModules: supportsESM && presetEnvConfig.modules !== 'commonjs', - absoluteRuntime: (process.versions as any).pnp - ? __dirname - : undefined, + absoluteRuntime: process.versions.pnp ? __dirname : undefined, ...options['transform-runtime'], }, ], diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 39b3bcfd36468b7..a63780edc904442 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -64,7 +64,7 @@ import createSpinner from './spinner' import { collectPages, getPageSizeInKb, - hasCustomAppGetInitialProps, + hasCustomGetInitialProps, isPageStatic, PageInfo, printCustomRoutes, @@ -229,6 +229,7 @@ export default async function build(dir: string, conf = null): Promise { const hasPages404 = Boolean( mappedPages['/404'] && mappedPages['/404'].startsWith('private-next-pages') ) + let hasNonStaticErrorPage: boolean if (hasPublicDir) { try { @@ -456,6 +457,24 @@ export default async function build(dir: string, conf = null): Promise { staticCheckWorkers.getStdout().pipe(process.stdout) staticCheckWorkers.getStderr().pipe(process.stderr) + const runtimeEnvConfig = { + publicRuntimeConfig: config.publicRuntimeConfig, + serverRuntimeConfig: config.serverRuntimeConfig, + } + + hasNonStaticErrorPage = + hasCustomErrorPage && + (await hasCustomGetInitialProps( + path.join( + distDir, + ...(isLikeServerless + ? ['serverless', 'pages'] + : ['server', 'static', buildId, 'pages']), + '_error.js' + ), + runtimeEnvConfig + )) + const analysisBegin = process.hrtime() await Promise.all( pageKeys.map(async page => { @@ -485,14 +504,10 @@ export default async function build(dir: string, conf = null): Promise { pagesManifest[page] = bundleRelative.replace(/\\/g, '/') - const runtimeEnvConfig = { - publicRuntimeConfig: config.publicRuntimeConfig, - serverRuntimeConfig: config.serverRuntimeConfig, - } const nonReservedPage = !page.match(/^\/(_app|_error|_document|api)/) if (nonReservedPage && customAppGetInitialProps === undefined) { - customAppGetInitialProps = hasCustomAppGetInitialProps( + customAppGetInitialProps = hasCustomGetInitialProps( isLikeServerless ? serverBundle : path.join( @@ -549,12 +564,12 @@ export default async function build(dir: string, conf = null): Promise { } if (hasPages404 && page === '/404') { - if (!result.isStatic) { + if (!result.isStatic && !result.hasStaticProps) { throw new Error(PAGES_404_GET_INITIAL_PROPS_ERROR) } // we need to ensure the 404 lambda is present since we use // it when _app has getInitialProps - if (customAppGetInitialProps) { + if (customAppGetInitialProps && !result.hasStaticProps) { staticPages.delete(page) } } @@ -618,7 +633,7 @@ export default async function build(dir: string, conf = null): Promise { // Since custom _app.js can wrap the 404 page we have to opt-out of static optimization if it has getInitialProps // Only export the static 404 when there is no /_error present const useStatic404 = - !customAppGetInitialProps && (!hasCustomErrorPage || hasPages404) + !customAppGetInitialProps && (!hasNonStaticErrorPage || hasPages404) if (invalidPages.size > 0) { throw new Error( @@ -907,6 +922,7 @@ export default async function build(dir: string, conf = null): Promise { distPath: distDir, buildId: buildId, pagesDir, + useStatic404, pageExtensions: config.pageExtensions, buildManifest, isModern: config.experimental.modern, diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index e0e9ae3d605ffa7..999f29c56ad1eee 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -57,6 +57,7 @@ export async function printTreeView( pageExtensions, buildManifest, isModern, + useStatic404, }: { distPath: string buildId: string @@ -64,6 +65,7 @@ export async function printTreeView( pageExtensions: string[] buildManifest: BuildManifestShape isModern: boolean + useStatic404: boolean } ) { const getPrettySize = (_size: number): string => { @@ -87,6 +89,14 @@ export async function printTreeView( const hasCustomApp = await findPageFile(pagesDir, '/_app', pageExtensions) const hasCustomError = await findPageFile(pagesDir, '/_error', pageExtensions) + if (useStatic404) { + pageInfos.set('/404', { + ...(pageInfos.get('/404') || pageInfos.get('/_error')), + static: true, + } as any) + list = [...list, '/404'] + } + const pageList = list .slice() .filter( @@ -720,14 +730,14 @@ export async function isPageStatic( } } -export function hasCustomAppGetInitialProps( - _appBundle: string, +export function hasCustomGetInitialProps( + bundle: string, runtimeEnvConfig: any ): boolean { require('../next-server/lib/runtime-config').setConfig(runtimeEnvConfig) - let mod = require(_appBundle) + let mod = require(bundle) - if (_appBundle.endsWith('_app.js')) { + if (bundle.endsWith('_app.js') || bundle.endsWith('_error.js')) { mod = mod.default || mod } else { // since we don't output _app in serverless mode get it from a page diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 2df362d3984810d..106cd1bf379198e 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1,3 +1,4 @@ +import chalk from 'chalk' import crypto from 'crypto' import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin' import path from 'path' @@ -963,6 +964,17 @@ export default async function getBaseWebpackConfig( ) ?? false if (hasUserCssConfig) { + // only show warning for one build + if (isServer) { + console.warn( + chalk.yellow.bold('Warning: ') + + chalk.bold( + 'Built-in CSS support is being disabled due to custom CSS configuration being detected.\n' + ) + + 'See here for more info: https://err.sh/next.js/built-in-css-disabled\n' + ) + } + if (webpackConfig.module?.rules.length) { // Remove default CSS Loader webpackConfig.module.rules = webpackConfig.module.rules.filter( diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index c50f932c8bf56d4..7fcaf28c218005b 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -205,6 +205,7 @@ const nextServerlessLoader: loader.Loader = function() { 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 {sendPayload} = require('next/dist/next-server/server/send-payload'); const buildManifest = require('${buildManifest}'); const reactLoadableManifest = require('${reactLoadableManifest}'); const Document = require('${absoluteDocumentPath}').default; @@ -232,7 +233,8 @@ const nextServerlessLoader: loader.Loader = function() { export const config = ComponentInfo['confi' + 'g'] || {} export const _app = App - export async function renderReqToHTML(req, res, fromExport, _renderOpts, _params) { + export async function renderReqToHTML(req, res, renderMode, _renderOpts, _params) { + const fromExport = renderMode === 'export' || renderMode === true; ${ basePath ? ` @@ -327,21 +329,15 @@ const nextServerlessLoader: loader.Loader = function() { let result = await renderToHTML(req, res, "${page}", Object.assign({}, getStaticProps ? {} : parsedUrl.query, nowParams ? nowParams : params, _params, isFallback ? { __nextFallback: 'true' } : {}), renderOpts) - if (_nextData && !fromExport) { - const payload = JSON.stringify(renderOpts.pageData) - res.setHeader('Content-Type', 'application/json') - res.setHeader('Content-Length', Buffer.byteLength(payload)) - - res.setHeader( - 'Cache-Control', - isPreviewMode - ? \`private, no-cache, no-store, max-age=0, must-revalidate\` - : getServerSideProps - ? \`no-cache, no-store, must-revalidate\` - : \`s-maxage=\${renderOpts.revalidate}, stale-while-revalidate\` - ) - res.end(payload) - return null + if (!renderMode) { + if (_nextData || getStaticProps || getServerSideProps) { + sendPayload(res, _nextData ? JSON.stringify(renderOpts.pageData) : result, _nextData ? 'json' : 'html', { + private: isPreviewMode, + stateful: !!getServerSideProps, + revalidate: renderOpts.revalidate, + }) + return null + } } else if (isPreviewMode) { res.setHeader( 'Cache-Control', @@ -349,7 +345,7 @@ const nextServerlessLoader: loader.Loader = function() { ) } - if (fromExport) return { html: result, renderOpts } + if (renderMode) return { html: result, renderOpts } return result } catch (err) { if (err.code === 'ENOENT') { diff --git a/packages/next/client/dev/error-overlay/format-webpack-messages.js b/packages/next/client/dev/error-overlay/format-webpack-messages.js index 6ff01edca15be6e..c61845352ff6abb 100644 --- a/packages/next/client/dev/error-overlay/format-webpack-messages.js +++ b/packages/next/client/dev/error-overlay/format-webpack-messages.js @@ -31,8 +31,7 @@ function isLikelyASyntaxError(message) { } // Cleans up webpack error messages. -// eslint-disable-next-line no-unused-vars -function formatMessage(message, isError) { +function formatMessage(message) { let lines = message.split('\n') // Strip Webpack-added headers off errors/warnings @@ -58,9 +57,6 @@ function formatMessage(message, isError) { /SyntaxError\s+\((\d+):(\d+)\)\s*(.+?)\n/g, `${friendlySyntaxErrorLabel} $3 ($1:$2)\n` ) - // Remove columns from ESLint formatter output (we added these for more - // accurate syntax errors) - message = message.replace(/Line (\d+):\d+:/g, 'Line $1:') // Clean up export errors message = message.replace( /^.*export '(.+?)' was not found in '(.+?)'.*$/gm, @@ -93,6 +89,17 @@ function formatMessage(message, isError) { ] } + // Add helpful message for users trying to use Sass for the first time + if (lines[1] && lines[1].match(/Cannot find module.+node-sass/)) { + // ./file.module.scss (<>) => ./file.module.scss + lines[0] = lines[0].replace(/(.+) \(.+?(?=\?\?).+?\)/, '$1') + + lines[1] = + "To use Next.js' built-in Sass support, you first need to install `sass`.\n" + lines[1] += 'Run `npm i sass` or `yarn add sass` inside your workspace.\n' + lines[1] += '\nLearn more: https://err.sh/next.js/install-sass' + } + message = lines.join('\n') // Internal stacks are generally useless so we strip them... with the // exception of stacks containing `webpack:` because they're normally diff --git a/packages/next/client/dev/prerender-indicator.js b/packages/next/client/dev/prerender-indicator.js index c22b3163ea825af..234d9f3fdd0d64c 100644 --- a/packages/next/client/dev/prerender-indicator.js +++ b/packages/next/client/dev/prerender-indicator.js @@ -94,7 +94,7 @@ function createContainer(prefix) { - +
    diff --git a/packages/next/export/worker.js b/packages/next/export/worker.js index a5169bf8d62e06d..55770c11800c6e0 100644 --- a/packages/next/export/worker.js +++ b/packages/next/export/worker.js @@ -154,7 +154,13 @@ export default async function({ } renderMethod = mod.renderReqToHTML - const result = await renderMethod(req, res, true, { ampPath }, params) + const result = await renderMethod( + req, + res, + 'export', + { ampPath }, + params + ) curRenderOpts = result.renderOpts || {} html = result.html } @@ -227,7 +233,7 @@ export default async function({ let ampHtml if (serverless) { req.url += (req.url.includes('?') ? '&' : '?') + 'amp=1' - ampHtml = (await renderMethod(req, res, true)).html + ampHtml = (await renderMethod(req, res, 'export')).html } else { ampHtml = await renderMethod( req, diff --git a/packages/next/lib/check-custom-routes.ts b/packages/next/lib/check-custom-routes.ts index c3a538901323883..d51747c90335239 100644 --- a/packages/next/lib/check-custom-routes.ts +++ b/packages/next/lib/check-custom-routes.ts @@ -1,4 +1,4 @@ -import { match as regexpMatch } from 'path-to-regexp' +import * as pathToRegexp from 'path-to-regexp' import { PERMANENT_REDIRECT_STATUS, TEMPORARY_REDIRECT_STATUS, @@ -150,12 +150,15 @@ export default function checkCustomRoutes( invalidParts.push(...result.invalidParts) } + let sourceTokens: pathToRegexp.Token[] | undefined + if (typeof route.source === 'string' && route.source.startsWith('/')) { // only show parse error if we didn't already show error // for not being a string try { // Make sure we can parse the source properly - regexpMatch(route.source) + sourceTokens = pathToRegexp.parse(route.source) + pathToRegexp.tokensToRegexp(sourceTokens) } catch (err) { // If there is an error show our err.sh but still show original error or a formatted one if we can const errMatches = err.message.match(/at (\d{0,})/) @@ -179,6 +182,34 @@ export default function checkCustomRoutes( } } + // make sure no unnamed patterns are attempted to be used in the + // destination as this can cause confusion and is not allowed + if (typeof (route as Rewrite).destination === 'string') { + if ( + (route as Rewrite).destination.startsWith('/') && + Array.isArray(sourceTokens) + ) { + const unnamedInDest = new Set() + + for (const token of sourceTokens) { + if (typeof token === 'object' && typeof token.name === 'number') { + const unnamedIndex = `:${token.name}` + if ((route as Rewrite).destination.includes(unnamedIndex)) { + unnamedInDest.add(unnamedIndex) + } + } + } + + if (unnamedInDest.size > 0) { + invalidParts.push( + `\`destination\` has unnamed params ${[...unnamedInDest].join( + ', ' + )}` + ) + } + } + } + const hasInvalidKeys = invalidKeys.length > 0 const hasInvalidParts = invalidParts.length > 0 diff --git a/packages/next/lib/is-serializable-props.ts b/packages/next/lib/is-serializable-props.ts new file mode 100644 index 000000000000000..0a45620bcbff50b --- /dev/null +++ b/packages/next/lib/is-serializable-props.ts @@ -0,0 +1,145 @@ +const regexpPlainIdentifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/ + +function isPlainObject(value: any): boolean { + if (Object.prototype.toString.call(value) !== '[object Object]') { + return false + } + + const prototype = Object.getPrototypeOf(value) + return prototype === null || prototype === Object.prototype +} + +export function isSerializableProps( + page: string, + method: string, + input: any +): true { + if (!isPlainObject(input)) { + throw new SerializableError( + page, + method, + '', + `Props must be returned as a plain object from ${method}: \`{ props: { ... } }\`.` + ) + } + + function visit(visited: Map, value: any, path: string) { + if (visited.has(value)) { + throw new SerializableError( + page, + method, + path, + `Circular references cannot be expressed in JSON (references: \`${visited.get( + value + ) || '(self)'}\`).` + ) + } + + visited.set(value, path) + } + + function isSerializable( + refs: Map, + value: any, + path: string + ): true { + const type = typeof value + if ( + // `null` can be serialized, but not `undefined`. + value === null || + // n.b. `bigint`, `function`, `symbol`, and `undefined` cannot be + // serialized. + // + // `object` is special-cased below, as it may represent `null`, an Array, + // a plain object, a class, et al. + type === 'boolean' || + type === 'number' || + type === 'string' + ) { + return true + } + + if (type === 'undefined') { + throw new SerializableError( + page, + method, + path, + '`undefined` cannot be serialized as JSON. Please use `null` or omit this value all together.' + ) + } + + if (isPlainObject(value)) { + visit(refs, value, path) + + if ( + Object.entries(value).every(([key, value]) => { + const nextPath = regexpPlainIdentifier.test(key) + ? `${path}.${key}` + : `${path}[${JSON.stringify(key)}]` + + const newRefs = new Map(refs) + return ( + isSerializable(newRefs, key, nextPath) && + isSerializable(newRefs, value, nextPath) + ) + }) + ) { + return true + } + + throw new SerializableError( + page, + method, + path, + `invariant: Unknown error encountered in Object.` + ) + } + + if (Array.isArray(value)) { + visit(refs, value, path) + + const newRefs = new Map(refs) + if ( + value.every((value, index) => + isSerializable(newRefs, value, `${path}[${index}]`) + ) + ) { + return true + } + + throw new SerializableError( + page, + method, + path, + `invariant: Unknown error encountered in Array.` + ) + } + + // None of these can be expressed as JSON: + // const type: "bigint" | "symbol" | "object" | "function" + throw new SerializableError( + page, + method, + path, + '`' + + type + + '`' + + (type === 'object' + ? ` ("${Object.prototype.toString.call(value)}")` + : '') + + ' cannot be serialized as JSON. Please only return JSON serializable data types.' + ) + } + + return isSerializable(new Map(), input, '') +} + +export class SerializableError extends Error { + constructor(page: string, method: string, path: string, message: string) { + super( + path + ? `Error serializing \`${path}\` returned from \`${method}\` in "${page}".\nReason: ${message}` + : `Error serializing props returned from \`${method}\` in "${page}".\nReason: ${message}` + ) + } +} diff --git a/packages/next/next-server/server/lib/path-match.ts b/packages/next/next-server/server/lib/path-match.ts index 199268b13381baa..81e1fb5fe048bec 100644 --- a/packages/next/next-server/server/lib/path-match.ts +++ b/packages/next/next-server/server/lib/path-match.ts @@ -25,19 +25,13 @@ export default (customRoute = false) => { } if (customRoute) { - const newParams: { [k: string]: string } = {} for (const key of keys) { - // unnamed matches should always be a number while named - // should be a string + // unnamed params should be removed as they + // are not allowed to be used in the destination if (typeof key.name === 'number') { - newParams[key.name + 1 + ''] = (res.params as any)[key.name + ''] - delete (res.params as any)[key.name + ''] + delete (res.params as any)[key.name] } } - res.params = { - ...res.params, - ...newParams, - } } return { ...params, ...res.params } diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index d5869ef61b9023f..2fb9c66b5a39d4a 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -52,6 +52,7 @@ import Router, { Route, } from './router' import { sendHTML } from './send-html' +import { sendPayload } from './send-payload' import { serveStatic } from './serve-static' import { getFallback, @@ -488,7 +489,8 @@ export default class Server { fn: async (_req, res, params, _parsedUrl) => { const { parsedDestination } = prepareDestination( route.destination, - params + params, + true ) const updatedDestination = formatUrl(parsedDestination) @@ -925,17 +927,17 @@ export default class Server { const renderResult = await (components.Component as any).renderReqToHTML( req, res, - true + 'passthrough' ) sendPayload( res, JSON.stringify(renderResult?.renderOpts?.pageData), - 'application/json', + 'json', !this.renderOpts.dev ? { - revalidate: -1, - private: isPreviewMode, // Leave to user-land caching + private: isPreviewMode, + stateful: true, // non-SSG data request } : undefined ) @@ -954,11 +956,11 @@ export default class Server { sendPayload( res, JSON.stringify(props), - 'application/json', + 'json', !this.renderOpts.dev ? { - revalidate: -1, - private: isPreviewMode, // Leave to user-land caching + private: isPreviewMode, + stateful: true, // GSSP data request } : undefined ) @@ -970,11 +972,12 @@ export default class Server { ...opts, }) - if (html && isServerProps && isPreviewMode) { - sendPayload(res, html, 'text/html; charset=utf-8', { - revalidate: -1, + if (html && isServerProps) { + sendPayload(res, html, 'html', { private: isPreviewMode, + stateful: true, // GSSP request }) + return null } return html @@ -999,9 +1002,16 @@ export default class Server { sendPayload( res, data, - isDataReq ? 'application/json' : 'text/html; charset=utf-8', - cachedData.curRevalidate !== undefined && !this.renderOpts.dev - ? { revalidate: cachedData.curRevalidate, private: isPreviewMode } + isDataReq ? 'json' : 'html', + !this.renderOpts.dev + ? { + private: isPreviewMode, + stateful: false, // GSP response + revalidate: + cachedData.curRevalidate !== undefined + ? cachedData.curRevalidate + : /* default to minimum revalidate (this should be an invariant) */ 1, + } : undefined ) @@ -1028,7 +1038,7 @@ export default class Server { renderResult = await (components.Component as any).renderReqToHTML( req, res, - true + 'passthrough' ) html = renderResult.html @@ -1104,7 +1114,12 @@ export default class Server { query.__nextFallback = 'true' if (isLikeServerless) { prepareServerlessUrl(req, query) - html = await (components.Component as any).renderReqToHTML(req, res) + const renderResult = await (components.Component as any).renderReqToHTML( + req, + res, + 'passthrough' + ) + html = renderResult.html } else { html = (await renderToHTML(req, res, pathname, query, { ...components, @@ -1113,7 +1128,7 @@ export default class Server { } } - sendPayload(res, html, 'text/html; charset=utf-8') + sendPayload(res, html, 'html') } const { @@ -1124,9 +1139,13 @@ export default class Server { sendPayload( res, isDataReq ? JSON.stringify(pageData) : html, - isDataReq ? 'application/json' : 'text/html; charset=utf-8', + isDataReq ? 'json' : 'html', !this.renderOpts.dev - ? { revalidate: sprRevalidate, private: isPreviewMode } + ? { + private: isPreviewMode, + stateful: false, // GSP response + revalidate: sprRevalidate, + } : undefined ) } @@ -1347,38 +1366,6 @@ export default class Server { } } -function sendPayload( - res: ServerResponse, - payload: any, - type: string, - 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 (options != null) { - if (options?.private) { - res.setHeader( - 'Cache-Control', - `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=${options.revalidate}, stale-while-revalidate` - ) - } else if (options?.revalidate === false) { - res.setHeader( - 'Cache-Control', - `s-maxage=31536000, stale-while-revalidate` - ) - } - } - res.end(payload) -} - function prepareServerlessUrl(req: IncomingMessage, query: ParsedUrlQuery) { const curUrl = parseUrl(req.url!, true) req.url = formatUrl({ diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index 65a6d7e51213dff..920b6b70e130914 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -8,6 +8,7 @@ import { SERVER_PROPS_SSG_CONFLICT, SSG_GET_INITIAL_PROPS_CONFLICT, } from '../../lib/constants' +import { isSerializableProps } from '../../lib/is-serializable-props' import { isInAmpMode } from '../lib/amp' import { AmpStateContext } from '../lib/amp-context' import { @@ -329,6 +330,7 @@ export async function renderToHTML( delete query.__nextFallback const isSSG = !!getStaticProps + const isBuildTimeSSG = isSSG && renderOpts.nextExport const defaultAppGetInitialProps = App.getInitialProps === (App as any).origGetInitialProps @@ -416,7 +418,7 @@ export async function renderToHTML( renderOpts.nextExport = true } - if (pathname === '/404' && !isAutoExport) { + if (pathname === '/404' && (hasPageGetInitialProps || getServerSideProps)) { throw new Error(PAGES_404_GET_INITIAL_PROPS_ERROR) } } @@ -508,6 +510,16 @@ export async function renderToHTML( throw new Error(invalidKeysMsg('getStaticProps', invalidKeys)) } + if ( + (dev || isBuildTimeSSG) && + !isSerializableProps(pathname, 'getStaticProps', data.props) + ) { + // this fn should throw an error instead of ever returning `false` + throw new Error( + 'invariant: getStaticProps did not return valid props. Please report this.' + ) + } + if (typeof data.revalidate === 'number') { if (!Number.isInteger(data.revalidate)) { throw new Error( @@ -567,6 +579,16 @@ export async function renderToHTML( throw new Error(invalidKeysMsg('getServerSideProps', invalidKeys)) } + if ( + (dev || isBuildTimeSSG) && + !isSerializableProps(pathname, 'getServerSideProps', data.props) + ) { + // this fn should throw an error instead of ever returning `false` + throw new Error( + 'invariant: getServerSideProps did not return valid props. Please report this.' + ) + } + props.pageProps = data.props ;(renderOpts as any).pageData = props } diff --git a/packages/next/next-server/server/router.ts b/packages/next/next-server/server/router.ts index 73eced0696bdd3c..c738a23bab7fab3 100644 --- a/packages/next/next-server/server/router.ts +++ b/packages/next/next-server/server/router.ts @@ -1,7 +1,6 @@ import { IncomingMessage, ServerResponse } from 'http' import { parse as parseUrl, UrlWithParsedQuery } from 'url' import { compile as compilePathToRegex } from 'path-to-regexp' -import { stringify as stringifyQs } from 'querystring' import pathMatch from './lib/path-match' export const route = pathMatch() @@ -34,24 +33,53 @@ export type DynamicRoutes = Array<{ page: string; match: RouteMatch }> export type PageChecker = (pathname: string) => Promise -export const prepareDestination = (destination: string, params: Params) => { +export const prepareDestination = ( + destination: string, + params: Params, + isRedirect?: boolean +) => { const parsedDestination = parseUrl(destination, true) const destQuery = parsedDestination.query let destinationCompiler = compilePathToRegex( - `${parsedDestination.pathname!}${parsedDestination.hash || ''}` + `${parsedDestination.pathname!}${parsedDestination.hash || ''}`, + // we don't validate while compiling the destination since we should + // have already validated before we got to this point and validating + // breaks compiling destinations with named pattern params from the source + // e.g. /something:hello(.*) -> /another/:hello is broken with validation + // since compile validation is meant for reversing and not for inserting + // params from a separate path-regex into another + { validate: false } ) let newUrl - Object.keys(destQuery).forEach(key => { - const val = destQuery[key] + // update any params in query values + for (const [key, strOrArray] of Object.entries(destQuery)) { + let value = Array.isArray(strOrArray) ? strOrArray[0] : strOrArray + if (value) { + const queryCompiler = compilePathToRegex(value, { validate: false }) + value = queryCompiler(params) + } + destQuery[key] = value + } + + // add params to query + for (const [name, value] of Object.entries(params)) { if ( - typeof val === 'string' && - val.startsWith(':') && - params[val.substr(1)] + isRedirect && + new RegExp(`:${name}(?!\\w)`).test( + parsedDestination.pathname + (parsedDestination.hash || '') + ) ) { - destQuery[key] = params[val.substr(1)] + // Don't add segment to query if used in destination + // and it's a redirect so that we don't pollute the query + // with unwanted values + continue } - }) + + if (!(name in destQuery)) { + destQuery[name] = Array.isArray(value) ? value.join('/') : value + } + } try { newUrl = encodeURI(destinationCompiler(params)) @@ -59,8 +87,8 @@ export const prepareDestination = (destination: string, params: Params) => { const [pathname, hash] = newUrl.split('#') parsedDestination.pathname = pathname parsedDestination.hash = `${hash ? '#' : ''}${hash || ''}` - parsedDestination.search = stringifyQs(parsedDestination.query) parsedDestination.path = `${pathname}${parsedDestination.search}` + delete parsedDestination.search } catch (err) { if (err.message.match(/Expected .*? to not repeat, but got an array/)) { throw new Error( diff --git a/packages/next/next-server/server/send-payload.ts b/packages/next/next-server/server/send-payload.ts new file mode 100644 index 000000000000000..0a222ebdbb52c84 --- /dev/null +++ b/packages/next/next-server/server/send-payload.ts @@ -0,0 +1,50 @@ +import { ServerResponse } from 'http' +import { isResSent } from '../lib/utils' + +export function sendPayload( + res: ServerResponse, + payload: any, + type: 'html' | 'json', + options?: + | { private: true } + | { private: boolean; stateful: true } + | { private: boolean; stateful: false; revalidate: number | false } +): void { + if (isResSent(res)) { + return + } + + // TODO: ETag headers? + res.setHeader( + 'Content-Type', + type === 'json' ? 'application/json' : 'text/html; charset=utf-8' + ) + res.setHeader('Content-Length', Buffer.byteLength(payload)) + if (options != null) { + if (options.private || options.stateful) { + if (options.private || !res.hasHeader('Cache-Control')) { + res.setHeader( + 'Cache-Control', + `private, no-cache, no-store, max-age=0, must-revalidate` + ) + } + } else if (typeof options.revalidate === 'number') { + if (options.revalidate < 1) { + throw new Error( + `invariant: invalid Cache-Control duration provided: ${options.revalidate} < 1` + ) + } + + res.setHeader( + 'Cache-Control', + `s-maxage=${options.revalidate}, stale-while-revalidate` + ) + } else if (options.revalidate === false) { + res.setHeader( + 'Cache-Control', + `s-maxage=31536000, stale-while-revalidate` + ) + } + } + res.end(payload) +} diff --git a/packages/next/next-server/server/spr-cache.ts b/packages/next/next-server/server/spr-cache.ts index c449fcd331dbfff..65b0de02e47e076 100644 --- a/packages/next/next-server/server/spr-cache.ts +++ b/packages/next/next-server/server/spr-cache.ts @@ -11,6 +11,10 @@ const mkdirp = promisify(mkdirpOrig) const readFile = promisify(fs.readFile) const writeFile = promisify(fs.writeFile) +function toRoute(pathname: string): string { + return pathname.replace(/\/$/, '').replace(/\/index$/, '') || '/' +} + type SprCacheValue = { html: string pageData: any @@ -34,6 +38,8 @@ const getSeedPath = (pathname: string, ext: string): string => { } export const calculateRevalidate = (pathname: string): number | false => { + pathname = toRoute(pathname) + // in development we don't have a prerender-manifest // and default to always revalidating to allow easier debugging const curTime = new Date().getTime() diff --git a/packages/next/package.json b/packages/next/package.json index 5e25a032050c8fb..1e6a3e74e7ac997 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "9.2.3-canary.28", + "version": "9.3.1-canary.5", "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.28", + "@next/polyfill-nomodule": "9.3.1-canary.5", "amphtml-validator": "1.0.30", "async-retry": "1.2.3", "async-sema": "3.0.0", @@ -136,7 +136,7 @@ "string-hash": "1.1.3", "strip-ansi": "5.2.0", "style-loader": "1.0.0", - "styled-jsx": "3.2.4", + "styled-jsx": "3.2.5", "terser": "4.4.2", "thread-loader": "2.1.3", "unfetch": "4.1.0", diff --git a/packages/next/pages/_error.tsx b/packages/next/pages/_error.tsx index e52e207c19ebd25..1a9718984210754 100644 --- a/packages/next/pages/_error.tsx +++ b/packages/next/pages/_error.tsx @@ -14,20 +14,23 @@ export type ErrorProps = { title?: string } +function _getInitialProps({ + res, + err, +}: NextPageContext): Promise | ErrorProps { + const statusCode = + res && res.statusCode ? res.statusCode : err ? err.statusCode! : 404 + return { statusCode } +} + /** * `Error` component used for handling errors. */ export default class Error

    extends React.Component

    { static displayName = 'ErrorPage' - static getInitialProps({ - res, - err, - }: NextPageContext): Promise | ErrorProps { - const statusCode = - res && res.statusCode ? res.statusCode : err ? err.statusCode! : 404 - return { statusCode } - } + static getInitialProps = _getInitialProps + static origGetInitialProps = _getInitialProps render() { const { statusCode } = this.props diff --git a/packages/next/types/index.d.ts b/packages/next/types/index.d.ts index ae8bbe80665dc1b..08767b0d1956489 100644 --- a/packages/next/types/index.d.ts +++ b/packages/next/types/index.d.ts @@ -64,12 +64,14 @@ export { NextApiHandler, } -export type GetStaticProps = (ctx: { +export type GetStaticProps< + P extends { [key: string]: any } = { [key: string]: any } +> = (ctx: { params?: ParsedUrlQuery preview?: boolean previewData?: any }) => Promise<{ - props: { [key: string]: any } + props: P revalidate?: number | boolean }> @@ -78,13 +80,15 @@ export type GetStaticPaths = () => Promise<{ fallback: boolean }> -export type GetServerSideProps = (context: { +export type GetServerSideProps< + P extends { [key: string]: any } = { [key: string]: any } +> = (context: { req: IncomingMessage res: ServerResponse params?: ParsedUrlQuery query: ParsedUrlQuery preview?: boolean previewData?: any -}) => Promise<{ [key: string]: any }> +}) => Promise<{ props: P }> export default next diff --git a/test/integration/static-404/pages/_error.js b/test/integration/404-page-custom-error/pages/_error.js similarity index 100% rename from test/integration/static-404/pages/_error.js rename to test/integration/404-page-custom-error/pages/_error.js diff --git a/test/integration/404-page-custom-error/pages/err.js b/test/integration/404-page-custom-error/pages/err.js new file mode 100644 index 000000000000000..6d0f2c17817a3a7 --- /dev/null +++ b/test/integration/404-page-custom-error/pages/err.js @@ -0,0 +1,5 @@ +const page = () => 'err page' +page.getInitialProps = () => { + throw new Error('oops') +} +export default page diff --git a/test/integration/404-page-custom-error/pages/index.js b/test/integration/404-page-custom-error/pages/index.js new file mode 100644 index 000000000000000..f6c15d1f66e8a6d --- /dev/null +++ b/test/integration/404-page-custom-error/pages/index.js @@ -0,0 +1 @@ +export default () => 'hello from index' diff --git a/test/integration/404-page-custom-error/test/index.test.js b/test/integration/404-page-custom-error/test/index.test.js new file mode 100644 index 000000000000000..3caf5cd042de74f --- /dev/null +++ b/test/integration/404-page-custom-error/test/index.test.js @@ -0,0 +1,127 @@ +/* eslint-env jest */ +/* global jasmine */ +import fs from 'fs-extra' +import { join } from 'path' +import { + killApp, + findPort, + launchApp, + nextStart, + nextBuild, + renderViaHTTP, + fetchViaHTTP, +} from 'next-test-utils' + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2 + +const appDir = join(__dirname, '../') +const nextConfig = join(appDir, 'next.config.js') + +let appPort +let buildId +let app + +const runTests = mode => { + const isDev = mode === 'dev' + + it('should respond to 404 correctly', async () => { + const res = await fetchViaHTTP(appPort, '/404') + expect(res.status).toBe(404) + expect(await res.text()).toContain('This page could not be found') + }) + + it('should render error correctly', async () => { + const text = await renderViaHTTP(appPort, '/err') + expect(text).toContain(isDev ? 'oops' : 'Internal Server Error') + }) + + it('should render index page normal', async () => { + const html = await renderViaHTTP(appPort, '/') + expect(html).toContain('hello from index') + }) + + if (!isDev) { + it('should set pages404 in routes-manifest correctly', async () => { + const data = await fs.readJSON(join(appDir, '.next/routes-manifest.json')) + expect(data.pages404).toBe(true) + }) + + it('should have output 404.html', async () => { + expect( + await fs + .access( + join( + appDir, + '.next', + ...(mode === 'server' + ? ['server', 'static', buildId, 'pages'] + : ['serverless', 'pages']), + '404.html' + ) + ) + .then(() => true) + .catch(() => false) + ) + }) + } +} + +describe('Default 404 Page with custom _error', () => { + describe('server mode', () => { + afterAll(() => killApp(app)) + + it('should build successfully', async () => { + const { code } = await nextBuild(appDir, [], { + stderr: true, + stdout: true, + }) + + expect(code).toBe(0) + + appPort = await findPort() + + app = await nextStart(appDir, appPort) + buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') + }) + + runTests('server') + }) + + describe('serverless mode', () => { + afterAll(async () => { + await fs.remove(nextConfig) + await killApp(app) + }) + + it('should build successfully', async () => { + await fs.writeFile( + nextConfig, + ` + module.exports = { target: 'experimental-serverless-trace' } + ` + ) + const { code } = await nextBuild(appDir, [], { + stderr: true, + stdout: true, + }) + + expect(code).toBe(0) + + appPort = await findPort() + app = await nextStart(appDir, appPort) + buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') + }) + + runTests('serverless') + }) + + describe('dev mode', () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTests('dev') + }) +}) diff --git a/test/integration/404-page-ssg/next.config.js b/test/integration/404-page-ssg/next.config.js new file mode 100644 index 000000000000000..4ba52ba2c8df675 --- /dev/null +++ b/test/integration/404-page-ssg/next.config.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/test/integration/404-page-ssg/pages/404.js b/test/integration/404-page-ssg/pages/404.js new file mode 100644 index 000000000000000..3af534db8fc8966 --- /dev/null +++ b/test/integration/404-page-ssg/pages/404.js @@ -0,0 +1,6 @@ +export const getStaticProps = () => ({ + props: { hello: 'world', random: Math.random() }, +}) + +const page = ({ random }) => `custom 404 page ${random}` +export default page diff --git a/test/integration/404-page-ssg/pages/_app.js b/test/integration/404-page-ssg/pages/_app.js new file mode 100644 index 000000000000000..93e23041c80e84a --- /dev/null +++ b/test/integration/404-page-ssg/pages/_app.js @@ -0,0 +1,12 @@ +const App = ({ Component, pageProps }) => + +App.getInitialProps = async ({ Component, ctx }) => { + if (Component.getInitialProps) { + await Component.getInitialProps(ctx) + } + return { + pageProps: {}, + } +} + +export default App diff --git a/test/integration/404-page-ssg/pages/err.js b/test/integration/404-page-ssg/pages/err.js new file mode 100644 index 000000000000000..6d0f2c17817a3a7 --- /dev/null +++ b/test/integration/404-page-ssg/pages/err.js @@ -0,0 +1,5 @@ +const page = () => 'err page' +page.getInitialProps = () => { + throw new Error('oops') +} +export default page diff --git a/test/integration/404-page-ssg/pages/index.js b/test/integration/404-page-ssg/pages/index.js new file mode 100644 index 000000000000000..f6c15d1f66e8a6d --- /dev/null +++ b/test/integration/404-page-ssg/pages/index.js @@ -0,0 +1 @@ +export default () => 'hello from index' diff --git a/test/integration/404-page-ssg/test/index.test.js b/test/integration/404-page-ssg/test/index.test.js new file mode 100644 index 000000000000000..487ecae21109007 --- /dev/null +++ b/test/integration/404-page-ssg/test/index.test.js @@ -0,0 +1,180 @@ +/* eslint-env jest */ +/* global jasmine */ +import fs from 'fs-extra' +import { join } from 'path' +import { + killApp, + findPort, + launchApp, + nextStart, + nextBuild, + renderViaHTTP, + fetchViaHTTP, +} from 'next-test-utils' + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2 + +const appDir = join(__dirname, '../') +const nextConfig = join(appDir, 'next.config.js') +const gip404Err = /`pages\/404` can not have getInitialProps\/getServerSideProps/ + +let nextConfigContent +let stdout +let stderr +let buildId +let appPort +let app + +const runTests = isDev => { + it('should respond to 404 correctly', async () => { + const res = await fetchViaHTTP(appPort, '/404') + expect(res.status).toBe(404) + expect(await res.text()).toContain('custom 404 page') + }) + + it('should render error correctly', async () => { + const text = await renderViaHTTP(appPort, '/err') + expect(text).toContain(isDev ? 'oops' : 'An unexpected error has occurred') + }) + + it('should not show an error in the logs for 404 SSG', async () => { + await renderViaHTTP(appPort, '/non-existent') + expect(stderr).not.toMatch(gip404Err) + expect(stdout).not.toMatch(gip404Err) + }) + + it('should render index page normal', async () => { + const html = await renderViaHTTP(appPort, '/') + expect(html).toContain('hello from index') + }) + + if (!isDev) { + it('should not revalidate custom 404 page', async () => { + const res1 = await renderViaHTTP(appPort, '/non-existent') + const res2 = await renderViaHTTP(appPort, '/non-existent') + const res3 = await renderViaHTTP(appPort, '/non-existent') + const res4 = await renderViaHTTP(appPort, '/non-existent') + + expect(res1 === res2 && res2 === res3 && res3 === res4).toBe(true) + + expect(res1).toContain('custom 404 page') + }) + + it('should set pages404 in routes-manifest correctly', async () => { + const data = await fs.readJSON(join(appDir, '.next/routes-manifest.json')) + expect(data.pages404).toBe(true) + }) + + it('should have 404 page in prerender-manifest', async () => { + const data = await fs.readJSON( + join(appDir, '.next/prerender-manifest.json') + ) + expect(data.routes['/404']).toEqual({ + initialRevalidateSeconds: false, + srcRoute: null, + dataRoute: `/_next/data/${buildId}/404.json`, + }) + }) + } +} + +describe('404 Page Support SSG', () => { + describe('server mode', () => { + afterAll(() => killApp(app)) + + it('should build successfully', async () => { + nextConfigContent = await fs.readFile(nextConfig, 'utf8') + const { + code, + stderr: buildStderr, + stdout: buildStdout, + } = await nextBuild(appDir, [], { + stderr: true, + stdout: true, + }) + + expect(code).toBe(0) + expect(buildStderr).not.toMatch(gip404Err) + expect(buildStdout).not.toMatch(gip404Err) + + appPort = await findPort() + stderr = '' + stdout = '' + + app = await nextStart(appDir, appPort, { + onStdout(msg) { + stdout += msg + }, + onStderr(msg) { + stderr += msg + }, + }) + buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') + }) + + runTests() + }) + + describe('serverless mode', () => { + afterAll(async () => { + await fs.writeFile(nextConfig, nextConfigContent) + await killApp(app) + }) + + it('should build successfully', async () => { + nextConfigContent = await fs.readFile(nextConfig, 'utf8') + await fs.writeFile( + nextConfig, + ` + module.exports = { target: 'experimental-serverless-trace' } + ` + ) + const { + code, + stderr: buildStderr, + stdout: buildStdout, + } = await nextBuild(appDir, [], { + stderr: true, + stdout: true, + }) + + expect(code).toBe(0) + expect(buildStderr).not.toMatch(gip404Err) + expect(buildStdout).not.toMatch(gip404Err) + + appPort = await findPort() + stderr = '' + stdout = '' + app = await nextStart(appDir, appPort, { + onStdout(msg) { + stdout += msg + }, + onStderr(msg) { + stderr += msg + }, + }) + buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') + }) + + runTests() + }) + + describe('dev mode', () => { + beforeAll(async () => { + appPort = await findPort() + stderr = '' + stdout = '' + app = await launchApp(appDir, appPort, { + onStdout(msg) { + stdout += msg + }, + onStderr(msg) { + stderr += msg + }, + }) + }) + afterAll(() => killApp(app)) + + runTests(true) + }) +}) diff --git a/test/integration/404-page/test/index.test.js b/test/integration/404-page/test/index.test.js index 9029af04c2f982d..913304ede143892 100644 --- a/test/integration/404-page/test/index.test.js +++ b/test/integration/404-page/test/index.test.js @@ -19,6 +19,7 @@ const appDir = join(__dirname, '../') const pages404 = join(appDir, 'pages/404.js') const appPage = join(appDir, 'pages/_app.js') const nextConfig = join(appDir, 'next.config.js') +const gip404Err = /`pages\/404` can not have getInitialProps\/getServerSideProps/ let nextConfigContent let buildId @@ -148,9 +149,7 @@ describe('404 Page Support', () => { await fs.remove(pages404) await fs.move(`${pages404}.bak`, pages404) - expect(stderr).toContain( - `\`pages/404\` can not have getInitialProps/getServerSideProps, https://err.sh/zeit/next.js/404-get-initial-props` - ) + expect(stderr).toMatch(gip404Err) expect(code).toBe(1) }) @@ -180,51 +179,181 @@ describe('404 Page Support', () => { await fs.remove(pages404) await fs.move(`${pages404}.bak`, pages404) - const error = `\`pages/404\` can not have getInitialProps/getServerSideProps, https://err.sh/zeit/next.js/404-get-initial-props` + expect(stderr).toMatch(gip404Err) + }) + + it('does not show error with getStaticProps in pages/404 build', async () => { + await fs.move(pages404, `${pages404}.bak`) + await fs.writeFile( + pages404, + ` + const page = () => 'custom 404 page' + export const getStaticProps = () => ({ props: { a: 'b' } }) + export default page + ` + ) + const { stderr, code } = await nextBuild(appDir, [], { stderr: true }) + await fs.remove(pages404) + await fs.move(`${pages404}.bak`, pages404) + + expect(stderr).not.toMatch(gip404Err) + expect(code).toBe(0) + }) + + it('does not show error with getStaticProps in pages/404 dev', async () => { + await fs.move(pages404, `${pages404}.bak`) + await fs.writeFile( + pages404, + ` + const page = () => 'custom 404 page' + export const getStaticProps = () => ({ props: { a: 'b' } }) + export default page + ` + ) + + let stderr = '' + appPort = await findPort() + app = await launchApp(appDir, appPort, { + onStderr(msg) { + stderr += msg || '' + }, + }) + await renderViaHTTP(appPort, '/abc') + await waitFor(1000) + + await killApp(app) - expect(stderr).toContain(error) + await fs.remove(pages404) + await fs.move(`${pages404}.bak`, pages404) + + expect(stderr).not.toMatch(gip404Err) + }) + + it('shows error with getServerSideProps in pages/404 build', async () => { + await fs.move(pages404, `${pages404}.bak`) + await fs.writeFile( + pages404, + ` + const page = () => 'custom 404 page' + export const getServerSideProps = () => ({ props: { a: 'b' } }) + export default page + ` + ) + const { stderr, code } = await nextBuild(appDir, [], { stderr: true }) + await fs.remove(pages404) + await fs.move(`${pages404}.bak`, pages404) + + expect(stderr).toMatch(gip404Err) + expect(code).toBe(1) + }) + + it('shows error with getServerSideProps in pages/404 dev', async () => { + await fs.move(pages404, `${pages404}.bak`) + await fs.writeFile( + pages404, + ` + const page = () => 'custom 404 page' + export const getServerSideProps = () => ({ props: { a: 'b' } }) + export default page + ` + ) + + let stderr = '' + appPort = await findPort() + app = await launchApp(appDir, appPort, { + onStderr(msg) { + stderr += msg || '' + }, + }) + await renderViaHTTP(appPort, '/abc') + await waitFor(1000) + + await killApp(app) + + await fs.remove(pages404) + await fs.move(`${pages404}.bak`, pages404) + + expect(stderr).toMatch(gip404Err) }) describe('_app with getInitialProps', () => { - beforeAll(async () => { - await fs.writeFile( + beforeAll(() => + fs.writeFile( appPage, ` - import NextApp from 'next/app' - const App = ({ Component, pageProps }) => - App.getInitialProps = NextApp.getInitialProps - export default App - ` + import NextApp from 'next/app' + const App = ({ Component, pageProps }) => + App.getInitialProps = NextApp.getInitialProps + export default App + ` ) - await nextBuild(appDir) - appPort = await findPort() - app = await nextStart(appDir, appPort) - buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') - }) - afterAll(async () => { - await fs.remove(appPage) - await killApp(app) - }) + ) + afterAll(() => fs.remove(appPage)) - it('should not output static 404 if _app has getInitialProps', async () => { - expect( - await fs.exists( - join(appDir, '.next/server/static', buildId, 'pages/404.html') + describe('production mode', () => { + afterAll(() => killApp(app)) + + it('should build successfully', async () => { + const { code, stderr, stdout } = await nextBuild(appDir, [], { + stderr: true, + stdout: true, + }) + + expect(code).toBe(0) + expect(stderr).not.toMatch(gip404Err) + expect(stdout).not.toMatch(gip404Err) + + appPort = await findPort() + app = await nextStart(appDir, appPort) + buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') + }) + + it('should not output static 404 if _app has getInitialProps', async () => { + expect( + await fs.exists( + join(appDir, '.next/server/static', buildId, 'pages/404.html') + ) + ).toBe(false) + }) + + it('specify to use the 404 page still in the routes-manifest', async () => { + const manifest = await fs.readJSON( + join(appDir, '.next/routes-manifest.json') ) - ).toBe(false) - }) + expect(manifest.pages404).toBe(true) + }) - it('specify to use the 404 page still in the routes-manifest', async () => { - const manifest = await fs.readJSON( - join(appDir, '.next/routes-manifest.json') - ) - expect(manifest.pages404).toBe(true) + it('should still use 404 page', async () => { + const res = await fetchViaHTTP(appPort, '/abc') + expect(res.status).toBe(404) + expect(await res.text()).toContain('custom 404 page') + }) }) - it('should still use 404 page', async () => { - const res = await fetchViaHTTP(appPort, '/abc') - expect(res.status).toBe(404) - expect(await res.text()).toContain('custom 404 page') + describe('dev mode', () => { + let stderr = '' + let stdout = '' + + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort, { + onStderr(msg) { + stderr += msg + }, + onStdout(msg) { + stdout += msg + }, + }) + }) + afterAll(() => killApp(app)) + + it('should not show pages/404 GIP error if _app has GIP', async () => { + const res = await fetchViaHTTP(appPort, '/abc') + expect(res.status).toBe(404) + expect(await res.text()).toContain('custom 404 page') + expect(stderr).not.toMatch(gip404Err) + expect(stdout).not.toMatch(gip404Err) + }) }) }) }) diff --git a/test/integration/css-customization/test/index.test.js b/test/integration/css-customization/test/index.test.js index b09141c5def77fe..2d5d7d51db89a14 100644 --- a/test/integration/css-customization/test/index.test.js +++ b/test/integration/css-customization/test/index.test.js @@ -83,11 +83,15 @@ describe('Legacy Next-CSS Customization', () => { }) it('should compile successfully', async () => { - const { code, stdout } = await nextBuild(appDir, [], { + const { code, stdout, stderr } = await nextBuild(appDir, [], { stdout: true, + stderr: true, }) expect(code).toBe(0) expect(stdout).toMatch(/Compiled successfully/) + expect(stderr).toMatch( + /Built-in CSS support is being disabled due to custom CSS configuration being detected/ + ) }) it(`should've compiled and prefixed`, async () => { diff --git a/test/integration/custom-routes/next.config.js b/test/integration/custom-routes/next.config.js index 3ff2dca5755d5a6..ae854925d8dddd6 100644 --- a/test/integration/custom-routes/next.config.js +++ b/test/integration/custom-routes/next.config.js @@ -68,8 +68,8 @@ module.exports = { destination: '/api/hello', }, { - source: '/api-hello-regex/(.*)', - destination: '/api/hello?name=:1', + source: '/api-hello-regex/:first(.*)', + destination: '/api/hello?name=:first*', }, { source: '/api-hello-param/:name', @@ -83,6 +83,10 @@ module.exports = { source: '/:path/post-321', destination: '/with-params', }, + { + source: '/unnamed-params/nested/(.*)/:test/(.*)', + destination: '/with-params', + }, ] }, async redirects() { @@ -159,7 +163,7 @@ module.exports = { }, { source: '/unnamed/(first|second)/(.*)', - destination: '/:1/:2', + destination: '/got-unnamed', permanent: false, }, { @@ -172,6 +176,16 @@ module.exports = { destination: '/thank-you-next', permanent: false, }, + { + source: '/docs/:first(integrations|now-cli)/v2:second(.*)', + destination: '/:first/:second', + permanent: false, + }, + { + source: '/catchall-redirect/:path*', + destination: '/somewhere', + permanent: false, + }, ] }, diff --git a/test/integration/custom-routes/test/index.test.js b/test/integration/custom-routes/test/index.test.js index a3436c75ec52068..d574b1f46002e75 100644 --- a/test/integration/custom-routes/test/index.test.js +++ b/test/integration/custom-routes/test/index.test.js @@ -104,19 +104,39 @@ const runTests = (isDev = false) => { redirect: 'manual', } ) - const { pathname, hash } = url.parse(res.headers.get('location')) + const { pathname, hash, query } = url.parse( + res.headers.get('location'), + true + ) expect(res.status).toBe(301) expect(pathname).toBe('/docs/v2/network/status-codes') expect(hash).toBe('#500') + expect(query).toEqual({}) }) it('should redirect successfully with provided statusCode', async () => { const res = await fetchViaHTTP(appPort, '/redirect2', undefined, { redirect: 'manual', }) - const { pathname } = url.parse(res.headers.get('location')) + const { pathname, query } = url.parse(res.headers.get('location'), true) expect(res.status).toBe(301) expect(pathname).toBe('/') + expect(query).toEqual({}) + }) + + it('should redirect successfully with catchall', async () => { + const res = await fetchViaHTTP( + appPort, + '/catchall-redirect/hello/world', + undefined, + { + redirect: 'manual', + } + ) + const { pathname, query } = url.parse(res.headers.get('location'), true) + expect(res.status).toBe(307) + expect(pathname).toBe('/somewhere') + expect(query).toEqual({ path: 'hello/world' }) }) it('should server static files through a rewrite', async () => { @@ -158,7 +178,12 @@ const runTests = (isDev = false) => { const { pathname, query } = url.parse(res.headers.get('location'), true) expect(res.status).toBe(307) expect(pathname).toBe('/with-params') - expect(query).toEqual({ first: 'hello', second: 'world' }) + expect(query).toEqual({ + first: 'hello', + second: 'world', + name: 'world', + section: 'hello', + }) }) it('should overwrite param values correctly', async () => { @@ -260,7 +285,7 @@ const runTests = (isDev = false) => { it('should support proxying to external resource', async () => { const res = await fetchViaHTTP(appPort, '/proxy-me/first') expect(res.status).toBe(200) - expect([...externalServerHits]).toEqual(['/first']) + expect([...externalServerHits]).toEqual(['/first?path=first']) expect(await res.text()).toContain('hi from external') }) @@ -270,7 +295,7 @@ const runTests = (isDev = false) => { }) const { pathname } = url.parse(res.headers.get('location') || '') expect(res.status).toBe(307) - expect(pathname).toBe('/first/final') + expect(pathname).toBe('/got-unnamed') }) it('should support named like unnamed parameters correctly', async () => { @@ -303,7 +328,7 @@ const runTests = (isDev = false) => { it('should handle api rewrite with un-named param successfully', async () => { const data = await renderViaHTTP(appPort, '/api-hello-regex/hello/world') expect(JSON.parse(data)).toEqual({ - query: { '1': 'hello/world', name: 'hello/world' }, + query: { name: 'hello/world', first: 'hello/world' }, }) }) @@ -324,10 +349,41 @@ const runTests = (isDev = false) => { } ) - const { pathname, hostname } = url.parse(res.headers.get('location') || '') + const { pathname, hostname, query } = url.parse( + res.headers.get('location') || '', + true + ) expect(res.status).toBe(307) expect(pathname).toBe(encodeURI('/\\google.com/about')) expect(hostname).not.toBe('google.com') + expect(query).toEqual({}) + }) + + it('should handle unnamed parameters with multi-match successfully', async () => { + const html = await renderViaHTTP( + appPort, + '/unnamed-params/nested/first/second/hello/world' + ) + const params = JSON.parse( + cheerio + .load(html)('p') + .text() + ) + expect(params).toEqual({ test: 'hello' }) + }) + + it('should handle named regex parameters with multi-match successfully', async () => { + const res = await fetchViaHTTP( + appPort, + '/docs/integrations/v2-some/thing', + undefined, + { + redirect: 'manual', + } + ) + const { pathname } = url.parse(res.headers.get('location') || '') + expect(res.status).toBe(307) + expect(pathname).toBe('/integrations/-some/thing') }) if (!isDev) { @@ -439,7 +495,7 @@ const runTests = (isDev = false) => { statusCode: 307, }, { - destination: '/:1/:2', + destination: '/got-unnamed', regex: normalizeRegEx( '^\\/unnamed(?:\\/(first|second))(?:\\/(.*))$' ), @@ -458,6 +514,22 @@ const runTests = (isDev = false) => { source: '/redirect-override', statusCode: 307, }, + { + destination: '/:first/:second', + regex: normalizeRegEx( + '^\\/docs(?:\\/(integrations|now-cli))\\/v2(.*)$' + ), + source: '/docs/:first(integrations|now-cli)/v2:second(.*)', + statusCode: 307, + }, + { + destination: '/somewhere', + regex: normalizeRegEx( + '^\\/catchall-redirect(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))?$' + ), + source: '/catchall-redirect/:path*', + statusCode: 307, + }, ], headers: [ { @@ -591,9 +663,9 @@ const runTests = (isDev = false) => { source: '/api-hello', }, { - destination: '/api/hello?name=:1', + destination: '/api/hello?name=:first*', regex: normalizeRegEx('^\\/api-hello-regex(?:\\/(.*))$'), - source: '/api-hello-regex/(.*)', + source: '/api-hello-regex/:first(.*)', }, { destination: '/api/hello?hello=:name', @@ -610,6 +682,13 @@ const runTests = (isDev = false) => { regex: normalizeRegEx('^(?:\\/([^\\/]+?))\\/post-321$'), source: '/:path/post-321', }, + { + destination: '/with-params', + regex: normalizeRegEx( + '^\\/unnamed-params\\/nested(?:\\/(.*))(?:\\/([^\\/]+?))(?:\\/(.*))$' + ), + source: '/unnamed-params/nested/(.*)/:test/(.*)', + }, ], dynamicRoutes: [ { diff --git a/test/integration/getserversideprops-preview/pages/index.js b/test/integration/getserversideprops-preview/pages/index.js index 848b6a14138e384..d48204d469bf744 100644 --- a/test/integration/getserversideprops-preview/pages/index.js +++ b/test/integration/getserversideprops-preview/pages/index.js @@ -1,5 +1,13 @@ -export function getServerSideProps({ preview, previewData }) { - return { props: { hasProps: true, preview, previewData } } +export function getServerSideProps({ res, preview, previewData }) { + // test override in preview mode + res.setHeader('Cache-Control', 'public, max-age=3600') + return { + props: { + hasProps: true, + preview: !!preview, + previewData: previewData || null, + }, + } } export default function({ hasProps, preview, previewData }) { diff --git a/test/integration/getserversideprops-preview/test/index.test.js b/test/integration/getserversideprops-preview/test/index.test.js index d210e6611ed7ea9..289160dfa3df223 100644 --- a/test/integration/getserversideprops-preview/test/index.test.js +++ b/test/integration/getserversideprops-preview/test/index.test.js @@ -53,14 +53,14 @@ function runTests(startServer = nextStart) { const html = await renderViaHTTP(appPort, '/') const { nextData, pre } = getData(html) expect(nextData).toMatchObject({ isFallback: false }) - expect(pre).toBe('undefined and undefined') + expect(pre).toBe('false and null') }) it('should return page on second request', async () => { const html = await renderViaHTTP(appPort, '/') const { nextData, pre } = getData(html) expect(nextData).toMatchObject({ isFallback: false }) - expect(pre).toBe('undefined and undefined') + expect(pre).toBe('false and null') }) let previewCookieString @@ -198,9 +198,7 @@ function runTests(startServer = nextStart) { await browser.get(`http://localhost:${appPort}/`) await browser.waitForElementByCss('#props-pre') - expect(await browser.elementById('props-pre').text()).toBe( - 'undefined and undefined' - ) + expect(await browser.elementById('props-pre').text()).toBe('false and null') }) afterAll(async () => { @@ -219,7 +217,7 @@ const startServerlessEmulator = async (dir, port) => { return initNextServerScript(scriptPath, /ready on/i, env) } -describe('Prerender Preview Mode', () => { +describe('ServerSide Props Preview Mode', () => { describe('Development Mode', () => { beforeAll(async () => { await fs.remove(nextConfigPath) diff --git a/test/integration/getserversideprops/pages/custom-cache.js b/test/integration/getserversideprops/pages/custom-cache.js new file mode 100644 index 000000000000000..ddeb3bd7a11ef19 --- /dev/null +++ b/test/integration/getserversideprops/pages/custom-cache.js @@ -0,0 +1,12 @@ +import React from 'react' + +export async function getServerSideProps({ res }) { + res.setHeader('Cache-Control', 'public, max-age=3600') + return { + props: { world: 'world' }, + } +} + +export default ({ world }) => { + return

    hello: {world}

    +} diff --git a/test/integration/getserversideprops/pages/index.js b/test/integration/getserversideprops/pages/index.js index 80ce2375b131c27..7db7e529a58bfbf 100644 --- a/test/integration/getserversideprops/pages/index.js +++ b/test/integration/getserversideprops/pages/index.js @@ -14,6 +14,10 @@ const Page = ({ world, time }) => { <>

    hello {world}

    time: {time} + +
    to non-json + +
    to another diff --git a/test/integration/getserversideprops/pages/non-json.js b/test/integration/getserversideprops/pages/non-json.js new file mode 100644 index 000000000000000..dc418a32cf22b34 --- /dev/null +++ b/test/integration/getserversideprops/pages/non-json.js @@ -0,0 +1,11 @@ +export async function getServerSideProps() { + return { + props: { time: new Date() }, + } +} + +const Page = ({ time }) => { + return

    hello {time.toString()}

    +} + +export default Page diff --git a/test/integration/getserversideprops/test/index.test.js b/test/integration/getserversideprops/test/index.test.js index 613e58760a1c45f..283560deff2416a 100644 --- a/test/integration/getserversideprops/test/index.test.js +++ b/test/integration/getserversideprops/test/index.test.js @@ -1,21 +1,23 @@ /* eslint-env jest */ /* global jasmine */ -import fs from 'fs-extra' -import { join } from 'path' -import webdriver from 'next-webdriver' import cheerio from 'cheerio' import escapeRegex from 'escape-string-regexp' +import fs from 'fs-extra' import { - renderViaHTTP, + check, fetchViaHTTP, findPort, - launchApp, + getBrowserBodyText, killApp, - waitFor, + launchApp, nextBuild, nextStart, normalizeRegEx, + renderViaHTTP, + waitFor, } from 'next-test-utils' +import webdriver from 'next-webdriver' +import { join } from 'path' jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2 const appDir = join(__dirname, '..') @@ -64,6 +66,12 @@ const expectedManifestRoutes = () => [ ), page: '/catchall/[...path]', }, + { + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/custom-cache.json$` + ), + page: '/custom-cache', + }, { dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/default-revalidate.json$` @@ -76,6 +84,12 @@ const expectedManifestRoutes = () => [ ), page: '/invalid-keys', }, + { + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/non-json.json$` + ), + page: '/non-json', + }, { dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/something.json$` @@ -369,6 +383,23 @@ const runTests = (dev = false) => { `Keys that need to be moved: world, query, params, time, random` ) }) + + it('should show error for invalid JSON returned from getServerSideProps', async () => { + const html = await renderViaHTTP(appPort, '/non-json') + expect(html).toContain( + 'Error serializing `.time` returned from `getServerSideProps`' + ) + }) + + it('should show error for invalid JSON returned from getStaticProps on CST', async () => { + const browser = await webdriver(appPort, '/') + await browser.elementByCss('#non-json').click() + + await check( + () => getBrowserBodyText(browser), + /Error serializing `.time` returned from `getServerSideProps`/ + ) + }) } else { it('should not fetch data on mount', async () => { const browser = await webdriver(appPort, '/blog/post-100') @@ -389,12 +420,42 @@ const runTests = (dev = false) => { expect(dataRoutes).toEqual(expectedManifestRoutes()) }) - it('should set no-cache, no-store, must-revalidate header', async () => { - const res = await fetchViaHTTP( + it('should set default caching header', async () => { + const resPage = await fetchViaHTTP(appPort, `/something`) + expect(resPage.headers.get('cache-control')).toBe( + 'private, no-cache, no-store, max-age=0, must-revalidate' + ) + + const resData = await fetchViaHTTP( appPort, `/_next/data/${buildId}/something.json` ) - expect(res.headers.get('cache-control')).toContain('no-cache') + expect(resData.headers.get('cache-control')).toBe( + 'private, no-cache, no-store, max-age=0, must-revalidate' + ) + }) + + it('should respect custom caching header', async () => { + const resPage = await fetchViaHTTP(appPort, `/custom-cache`) + expect(resPage.headers.get('cache-control')).toBe('public, max-age=3600') + + const resData = await fetchViaHTTP( + appPort, + `/_next/data/${buildId}/custom-cache.json` + ) + expect(resData.headers.get('cache-control')).toBe('public, max-age=3600') + }) + + it('should not show error for invalid JSON returned from getServerSideProps', async () => { + const html = await renderViaHTTP(appPort, '/non-json') + expect(html).not.toContain('Error serializing') + expect(html).toContain('hello ') + }) + + it('should not show error for invalid JSON returned from getStaticProps on CST', async () => { + const browser = await webdriver(appPort, '/') + await browser.elementByCss('#non-json').click() + await check(() => getBrowserBodyText(browser), /hello /) }) } } diff --git a/test/integration/invalid-custom-routes/test/index.test.js b/test/integration/invalid-custom-routes/test/index.test.js index 92b0b166309e9d5..c9d731be9d71e26 100644 --- a/test/integration/invalid-custom-routes/test/index.test.js +++ b/test/integration/invalid-custom-routes/test/index.test.js @@ -59,6 +59,12 @@ const runTests = () => { destination: '/another', permanent: 'yes', }, + { + // unnamed in destination + source: '/hello/world/(.*)', + destination: '/:0', + permanent: true, + }, // invalid objects null, 'string', @@ -99,6 +105,10 @@ const runTests = () => { `\`permanent\` is not set to \`true\` or \`false\` for route {"source":"/hello","destination":"/another","permanent":"yes"}` ) + expect(stderr).toContain( + `\`destination\` has unnamed params :0 for route {"source":"/hello/world/(.*)","destination":"/:0","permanent":true}` + ) + expect(stderr).toContain( `The route null is not a valid object with \`source\` and \`destination\`` ) @@ -142,6 +152,11 @@ const runTests = () => { source: '/feedback/(?!general)', destination: '/feedback/general', }, + { + // unnamed in destination + source: '/hello/world/(.*)', + destination: '/:0', + }, // invalid objects null, 'string', @@ -174,6 +189,10 @@ const runTests = () => { `Error parsing \`/feedback/(?!general)\` https://err.sh/zeit/next.js/invalid-route-source` ) + expect(stderr).toContain( + `\`destination\` has unnamed params :0 for route {"source":"/hello/world/(.*)","destination":"/:0"}` + ) + expect(stderr).toContain( `The route null is not a valid object with \`source\` and \`destination\`` ) diff --git a/test/integration/prerender-no-revalidate/pages/index.js b/test/integration/prerender-no-revalidate/pages/index.js new file mode 100644 index 000000000000000..cf712032d6a2c6d --- /dev/null +++ b/test/integration/prerender-no-revalidate/pages/index.js @@ -0,0 +1,27 @@ +let runs = 0 + +export async function getStaticProps() { + if (runs++) { + throw new Error('GSP was re-run.') + } + + return { + props: { + world: 'world', + time: new Date().getTime(), + other: Math.random(), + }, + } +} + +const Page = ({ world, time, other }) => { + return ( +
    +

    hello {world}

    + time: {time} + other: {other} +
    + ) +} + +export default Page diff --git a/test/integration/prerender-no-revalidate/pages/named.js b/test/integration/prerender-no-revalidate/pages/named.js new file mode 100644 index 000000000000000..cf712032d6a2c6d --- /dev/null +++ b/test/integration/prerender-no-revalidate/pages/named.js @@ -0,0 +1,27 @@ +let runs = 0 + +export async function getStaticProps() { + if (runs++) { + throw new Error('GSP was re-run.') + } + + return { + props: { + world: 'world', + time: new Date().getTime(), + other: Math.random(), + }, + } +} + +const Page = ({ world, time, other }) => { + return ( +
    +

    hello {world}

    + time: {time} + other: {other} +
    + ) +} + +export default Page diff --git a/test/integration/prerender-no-revalidate/pages/nested/index.js b/test/integration/prerender-no-revalidate/pages/nested/index.js new file mode 100644 index 000000000000000..cf712032d6a2c6d --- /dev/null +++ b/test/integration/prerender-no-revalidate/pages/nested/index.js @@ -0,0 +1,27 @@ +let runs = 0 + +export async function getStaticProps() { + if (runs++) { + throw new Error('GSP was re-run.') + } + + return { + props: { + world: 'world', + time: new Date().getTime(), + other: Math.random(), + }, + } +} + +const Page = ({ world, time, other }) => { + return ( +
    +

    hello {world}

    + time: {time} + other: {other} +
    + ) +} + +export default Page diff --git a/test/integration/prerender-no-revalidate/pages/nested/named.js b/test/integration/prerender-no-revalidate/pages/nested/named.js new file mode 100644 index 000000000000000..cf712032d6a2c6d --- /dev/null +++ b/test/integration/prerender-no-revalidate/pages/nested/named.js @@ -0,0 +1,27 @@ +let runs = 0 + +export async function getStaticProps() { + if (runs++) { + throw new Error('GSP was re-run.') + } + + return { + props: { + world: 'world', + time: new Date().getTime(), + other: Math.random(), + }, + } +} + +const Page = ({ world, time, other }) => { + return ( +
    +

    hello {world}

    + time: {time} + other: {other} +
    + ) +} + +export default Page diff --git a/test/integration/prerender-no-revalidate/test/index.test.js b/test/integration/prerender-no-revalidate/test/index.test.js new file mode 100644 index 000000000000000..3779bd7595a8d36 --- /dev/null +++ b/test/integration/prerender-no-revalidate/test/index.test.js @@ -0,0 +1,129 @@ +/* eslint-env jest */ +/* global jasmine */ +import fs from 'fs-extra' +import { + findPort, + killApp, + nextBuild, + nextStart, + renderViaHTTP, + waitFor, +} from 'next-test-utils' +import { join } from 'path' + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2 +const appDir = join(__dirname, '..') +const nextConfig = join(appDir, 'next.config.js') +let app +let appPort +let buildId +let stderr + +function runTests(route, routePath, serverless) { + it(`[${route}] should not revalidate when set to false`, async () => { + const fileName = join( + appDir, + `.next`, + ...(serverless ? ['serverless'] : ['server', 'static', buildId]), + `pages/${routePath}.html` + ) + const initialHtml = await renderViaHTTP(appPort, route) + const initialFileHtml = await fs.readFile(fileName, 'utf8') + + let newHtml = await renderViaHTTP(appPort, route) + expect(initialHtml).toBe(newHtml) + expect(await fs.readFile(fileName, 'utf8')).toBe(initialFileHtml) + + await waitFor(500) + + newHtml = await renderViaHTTP(appPort, route) + expect(initialHtml).toBe(newHtml) + expect(await fs.readFile(fileName, 'utf8')).toBe(initialFileHtml) + + await waitFor(500) + + newHtml = await renderViaHTTP(appPort, route) + expect(initialHtml).toBe(newHtml) + expect(await fs.readFile(fileName, 'utf8')).toBe(initialFileHtml) + + expect(stderr).not.toContain('GSP was re-run') + }) + + it(`[${route}] should not revalidate /_next/data when set to false`, async () => { + const route = join(`/_next/data/${buildId}`, `${routePath}.json`) + const fileName = join( + appDir, + `.next`, + ...(serverless ? ['serverless'] : ['server', 'static', buildId]), + `pages/${routePath}.json` + ) + + const initialData = JSON.parse(await renderViaHTTP(appPort, route)) + const initialFileJson = await fs.readFile(fileName, 'utf8') + + expect(JSON.parse(await renderViaHTTP(appPort, route))).toEqual(initialData) + expect(await fs.readFile(fileName, 'utf8')).toBe(initialFileJson) + await waitFor(500) + + expect(JSON.parse(await renderViaHTTP(appPort, route))).toEqual(initialData) + expect(await fs.readFile(fileName, 'utf8')).toBe(initialFileJson) + await waitFor(500) + + expect(JSON.parse(await renderViaHTTP(appPort, route))).toEqual(initialData) + expect(await fs.readFile(fileName, 'utf8')).toBe(initialFileJson) + + expect(stderr).not.toContain('GSP was re-run') + }) +} + +describe('SSG Prerender No Revalidate', () => { + afterAll(() => fs.remove(nextConfig)) + + describe('serverless mode', () => { + beforeAll(async () => { + await fs.writeFile( + nextConfig, + `module.exports = { target: 'experimental-serverless-trace' }`, + 'utf8' + ) + await fs.remove(join(appDir, '.next')) + await nextBuild(appDir) + appPort = await findPort() + stderr = '' + app = await nextStart(appDir, appPort, { + onStderr: msg => { + stderr += msg + }, + }) + buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') + }) + afterAll(() => killApp(app)) + + runTests('/', '/index', true) + runTests('/named', '/named', true) + runTests('/nested', '/nested', true) + runTests('/nested/named', '/nested/named', true) + }) + + describe('production mode', () => { + beforeAll(async () => { + await fs.remove(nextConfig) + await fs.remove(join(appDir, '.next')) + await nextBuild(appDir, []) + appPort = await findPort() + stderr = '' + app = await nextStart(appDir, appPort, { + onStderr: msg => { + stderr += msg + }, + }) + buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') + }) + afterAll(() => killApp(app)) + + runTests('/', '/index') + runTests('/named', '/named') + runTests('/nested', '/nested') + runTests('/nested/named', '/nested/named') + }) +}) diff --git a/test/integration/prerender-preview/pages/index.js b/test/integration/prerender-preview/pages/index.js index 8fa4d03b6b519a8..26d417d3b2b5b96 100644 --- a/test/integration/prerender-preview/pages/index.js +++ b/test/integration/prerender-preview/pages/index.js @@ -1,5 +1,11 @@ export function getStaticProps({ preview, previewData }) { - return { props: { hasProps: true, preview, previewData } } + return { + props: { + hasProps: true, + preview: !!preview, + previewData: previewData || null, + }, + } } export default function({ hasProps, preview, previewData }) { diff --git a/test/integration/prerender-preview/test/index.test.js b/test/integration/prerender-preview/test/index.test.js index 9c40640c7921222..a8abd24ccfc71b3 100644 --- a/test/integration/prerender-preview/test/index.test.js +++ b/test/integration/prerender-preview/test/index.test.js @@ -53,14 +53,14 @@ function runTests(startServer = nextStart) { const html = await renderViaHTTP(appPort, '/') const { nextData, pre } = getData(html) expect(nextData).toMatchObject({ isFallback: false }) - expect(pre).toBe('undefined and undefined') + expect(pre).toBe('false and null') }) it('should return prerendered page on second request', async () => { const html = await renderViaHTTP(appPort, '/') const { nextData, pre } = getData(html) expect(nextData).toMatchObject({ isFallback: false }) - expect(pre).toBe('undefined and undefined') + expect(pre).toBe('false and null') }) it('should throw error when setting too large of preview data', async () => { @@ -198,9 +198,7 @@ function runTests(startServer = nextStart) { await browser.get(`http://localhost:${appPort}/`) await browser.waitForElementByCss('#props-pre') - expect(await browser.elementById('props-pre').text()).toBe( - 'undefined and undefined' - ) + expect(await browser.elementById('props-pre').text()).toBe('false and null') }) afterAll(async () => { diff --git a/test/integration/prerender/pages/index.js b/test/integration/prerender/pages/index.js index b1e2a18e4dab11d..59b333fddfe53d4 100644 --- a/test/integration/prerender/pages/index.js +++ b/test/integration/prerender/pages/index.js @@ -15,6 +15,10 @@ const Page = ({ world, time }) => { {/*
    idk
    */}

    hello {world}

    time: {time} + + to non-json + +
    to another diff --git a/test/integration/prerender/pages/non-json/[p].js b/test/integration/prerender/pages/non-json/[p].js new file mode 100644 index 000000000000000..e4f27b95f65c9bc --- /dev/null +++ b/test/integration/prerender/pages/non-json/[p].js @@ -0,0 +1,21 @@ +import { useRouter } from 'next/router' + +export async function getStaticProps() { + return { + props: { time: new Date() }, + } +} + +export async function getStaticPaths() { + return { paths: [], fallback: true } +} + +const Page = ({ time }) => { + const { isFallback } = useRouter() + + if (isFallback) return null + + return

    hello {time.toString()}

    +} + +export default Page diff --git a/test/integration/prerender/test/index.test.js b/test/integration/prerender/test/index.test.js index dc9a62a4521556f..13cff47a1ffc1c6 100644 --- a/test/integration/prerender/test/index.test.js +++ b/test/integration/prerender/test/index.test.js @@ -6,7 +6,9 @@ import fs from 'fs-extra' import { check, fetchViaHTTP, + File, findPort, + getBrowserBodyText, getReactErrorOverlayContent, initNextServerScript, killApp, @@ -682,6 +684,40 @@ const runTests = (dev = false, looseMode = false) => { const curRandom = await browser.elementByCss('#random').text() expect(curRandom).toBe(initialRandom + '') }) + + it('should show fallback before invalid JSON is returned from getStaticProps', async () => { + const html = await renderViaHTTP(appPort, '/non-json/foobar') + expect(html).toContain('"isFallback":true') + }) + + it('should show error for invalid JSON returned from getStaticProps on SSR', async () => { + const browser = await webdriver(appPort, '/non-json/direct') + + // FIXME: enable this + // expect(await getReactErrorOverlayContent(browser)).toMatch( + // /Error serializing `.time` returned from `getStaticProps`/ + // ) + + // FIXME: disable this + expect(await getReactErrorOverlayContent(browser)).toMatch( + /Failed to load static props/ + ) + }) + + it('should show error for invalid JSON returned from getStaticProps on CST', async () => { + const browser = await webdriver(appPort, '/') + await browser.elementByCss('#non-json').click() + + // FIXME: enable this + // expect(await getReactErrorOverlayContent(browser)).toMatch( + // /Error serializing `.time` returned from `getStaticProps`/ + // ) + + // FIXME: disable this + expect(await getReactErrorOverlayContent(browser)).toMatch( + /Failed to load static props/ + ) + }) } else { if (!looseMode) { it('should should use correct caching headers for a no-revalidate page', async () => { @@ -692,6 +728,18 @@ const runTests = (dev = false, looseMode = false) => { const initialHtml = await initialRes.text() expect(initialHtml).toMatch(/hello.*?world/) }) + + it('should not show error for invalid JSON returned from getStaticProps on SSR', async () => { + const browser = await webdriver(appPort, '/non-json/direct') + + await check(() => getBrowserBodyText(browser), /hello /) + }) + + it('should show error for invalid JSON returned from getStaticProps on CST', async () => { + const browser = await webdriver(appPort, '/') + await browser.elementByCss('#non-json').click() + await check(() => getBrowserBodyText(browser), /hello /) + }) } it('outputs dataRoutes in routes-manifest correctly', async () => { @@ -762,6 +810,14 @@ const runTests = (dev = false, looseMode = false) => { ), page: '/default-revalidate', }, + { + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/non\\-json\\/([^\\/]+?)\\.json$` + ), + page: '/non-json/[p]', + }, { dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/something.json$` @@ -817,6 +873,14 @@ const runTests = (dev = false, looseMode = false) => { '^\\/blog\\/([^\\/]+?)\\/([^\\/]+?)(?:\\/)?$' ), }, + '/non-json/[p]': { + dataRoute: `/_next/data/${buildId}/non-json/[p].json`, + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapedBuildId}\\/non\\-json\\/([^\\/]+?)\\.json$` + ), + fallback: '/non-json/[p].html', + routeRegex: normalizeRegEx('^\\/non\\-json\\/([^\\/]+?)(?:\\/)?$'), + }, '/user/[user]/profile': { fallback: '/user/[user]/profile.html', dataRoute: `/_next/data/${buildId}/user/[user]/profile.json`, @@ -1083,6 +1147,23 @@ describe('SSG Prerender', () => { 'You can not use getInitialProps with getStaticProps' ) }) + + it('should show serialization error during build', async () => { + await fs.remove(join(appDir, '.next')) + + const nonJsonPage = join(appDir, 'pages/non-json/[p].js') + const f = new File(nonJsonPage) + try { + f.replace('paths: []', `paths: [{ params: { p: 'testing' } }]`) + + const { stderr } = await nextBuild(appDir, [], { stderr: true }) + expect(stderr).toContain( + 'Error serializing `.time` returned from `getStaticProps` in "/non-json/[p]".' + ) + } finally { + f.restore() + } + }) }) describe('enumlated serverless mode', () => { diff --git a/test/integration/production/pages/bad-promise.js b/test/integration/production/pages/bad-promise.js new file mode 100644 index 000000000000000..ad186e3c1a009aa --- /dev/null +++ b/test/integration/production/pages/bad-promise.js @@ -0,0 +1,12 @@ +export default () => { + if (typeof window !== 'undefined') { + window.didRender = true + } + return ( + <> +