diff --git a/docs/advanced-features/custom-error-page.md b/docs/advanced-features/custom-error-page.md index 9b2971d0765e2..243a76ce5164b 100644 --- a/docs/advanced-features/custom-error-page.md +++ b/docs/advanced-features/custom-error-page.md @@ -2,6 +2,8 @@ description: Override and extend the built-in Error page to handle custom errors. --- +# Custom Error Page + ## 404 Page A 404 page may be accessed very often. Server-rendering an error page for every visit increases the load of the Next.js server. This can result in increased costs and slow experiences. diff --git a/docs/advanced-features/static-html-export.md b/docs/advanced-features/static-html-export.md index becfd255d69ed..969a2d20a7c25 100644 --- a/docs/advanced-features/static-html-export.md +++ b/docs/advanced-features/static-html-export.md @@ -21,7 +21,7 @@ The exported app supports almost every feature of Next.js, including dynamic rou > > If you're looking to make a hybrid site where only _some_ pages are prerendered to static HTML, Next.js already does that automatically for you! Read up on [Automatic Static Optimization](/docs/advanced-features/automatic-static-optimization.md) for details. > -> `next export` also causes features like Incremental Static Generation and Regeneration to be disabled, as they require `next start` or a serverless deployment to function. +> `next export` also causes features like [Incremental Static Generation](/docs/basic-features/data-fetching.md#fallback-true) and [Regeneration](/docs/basic-features/data-fetching.md#incremental-static-regeneration) to be disabled, as they require [`next start`](/docs/api-reference/cli.md#production) or a serverless deployment to function. ## How to use it @@ -53,7 +53,9 @@ For more advanced scenarios, you can define a parameter called [`exportPathMap`] ## Deployment -You can read about deploying your Next.js application in the [deployment section](/docs/deployment.md). +By default, `next export` will generate an `out` directory, which can be served by any static hosting service or CDN. + +> We strongly recommend using [Vercel](https://vercel.com/) even if your Next.js app is fully static. [Vercel](https://vercel.com/) is optimized to make static Next.js apps blazingly fast. `next export` works with Zero Config deployments on Vercel. ## Caveats @@ -62,11 +64,10 @@ You can read about deploying your Next.js application in the [deployment section - `getInitialProps` cannot be used alongside `getStaticProps` or `getStaticPaths` on any given page. If you have dynamic routes, instead of using `getStaticPaths` you'll need to configure the [`exportPathMap`](/docs/api-reference/next.config.js/exportPathMap.md) parameter in your [`next.config.js`](/docs/api-reference/next.config.js/introduction.md) file to let the exporter know which HTML files it should output. - When `getInitialProps` is called during export, the `req` and `res` fields of its [`context`](/docs/api-reference/data-fetching/getInitialProps.md#context-object) parameter will be empty objects, since during export there is no server running. - `getInitialProps` **will be called on every client-side navigation**, if you'd like to only fetch data at build-time, switch to `getStaticProps`. - - `getInitialProps` cannot use Node.js-specific libraries or the file system like `getStaticProps` can. `getInitialProps` must fetch from an API. + - `getInitialProps` should fetch from an API and cannot use Node.js-specific libraries or the file system like `getStaticProps` can. - For static export, the `getStaticProps` API is always preferred over `getInitialProps`: it's recommended you convert your pages to use the `getStaticProps` if possible. + It's recommended to use and migrate towards `getStaticProps` over `getInitialProps` whenever possible. -- The `fallback: true` mode of `getStaticPaths` is not supported when using `next export`. -- You won't be able to render HTML dynamically when static exporting, as the HTML files are pre-build. Your application can be a hybrid of [Static Generation](/docs/basic-features/pages.md#static-generation) and [Server-Side Rendering](/docs/basic-features/pages.md#server-side-rendering) when you don't use `next export`. You can learn more about it in the [pages section](/docs/basic-features/pages.md). +- The [`fallback: true`](/docs/basic-features/data-fetching.md#fallback-true) mode of `getStaticPaths` is not supported when using `next export`. - [API Routes](/docs/api-routes/introduction.md) are not supported by this method because they can't be prerendered to HTML. - [`getServerSideProps`](/docs/basic-features/data-fetching.md#getserversideprops-server-side-rendering) cannot be used within pages because the method requires a server. Consider using [`getStaticProps`](/docs/basic-features/data-fetching.md#getstaticprops-static-generation) instead. diff --git a/docs/api-reference/cli.md b/docs/api-reference/cli.md index 5b63772e85a2d..bd9959252349e 100644 --- a/docs/api-reference/cli.md +++ b/docs/api-reference/cli.md @@ -48,7 +48,7 @@ NODE_OPTIONS='--inspect' next The first load is colored green, yellow, or red. Aim for green for performant applications. -You can enable production profiling for React with the `--profile` flag in `next build`. This requires Next.js 9.5: +You can enable production profiling for React with the `--profile` flag in `next build`. This requires [Next.js 9.5](https://nextjs.org/blog/next-9-5): ```bash next build --profile diff --git a/docs/api-reference/data-fetching/getInitialProps.md b/docs/api-reference/data-fetching/getInitialProps.md index 6e428820ecd96..cc0d690119a27 100644 --- a/docs/api-reference/data-fetching/getInitialProps.md +++ b/docs/api-reference/data-fetching/getInitialProps.md @@ -4,19 +4,11 @@ description: Enable Server-Side Rendering in a page and do initial data populati # getInitialProps -> **Recommended: [`getStaticProps`](/docs/basic-features/data-fetching.md#getstaticprops-static-generation) or [`getServerSideProps`](/docs/basic-features/data-fetching.md#getserversideprops-server-side-rendering)** +> **Recommended: [`getStaticProps`](/docs/basic-features/data-fetching.md#getstaticprops-static-generation) or [`getServerSideProps`](/docs/basic-features/data-fetching.md#getserversideprops-server-side-rendering)**. > > If you're using Next.js 9.3 or newer, we recommend that you use `getStaticProps` or `getServerSideProps` instead of `getInitialProps`. > -> These new data fetching methods allow you to have a granular choice between static generation and server-side rendering. -> Learn more on the documentation for [Pages](/docs/basic-features/pages.md) and [Data fetching](/docs/basic-features/data-fetching.md): - -
- Examples - -
+> These new data fetching methods allow you to have a granular choice between static generation and server-side rendering. Learn more on the documentation for [Pages](/docs/basic-features/pages.md) and [Data fetching](/docs/basic-features/data-fetching.md). `getInitialProps` enables [server-side rendering](/docs/basic-features/pages.md#server-side-rendering) in a page and allows you to do **initial data population**, it means sending the [page](/docs/basic-features/pages.md) with the data already populated from the server. This is especially useful for [SEO](https://en.wikipedia.org/wiki/Search_engine_optimization). diff --git a/docs/api-reference/next.config.js/environment-variables.md b/docs/api-reference/next.config.js/environment-variables.md index 9a3536fa0c9b3..a0766ef442dbc 100644 --- a/docs/api-reference/next.config.js/environment-variables.md +++ b/docs/api-reference/next.config.js/environment-variables.md @@ -4,7 +4,7 @@ description: Learn to add and access environment variables in your Next.js appli # Environment Variables -> Since the release of Next.js 9.4 we now have a more intuitive and ergonomic experience for [adding environment variables](/docs/basic-features/environment-variables.md). Give it a try! +> Since the release of [Next.js 9.4](https://nextjs.org/blog/next-9-4) we now have a more intuitive and ergonomic experience for [adding environment variables](/docs/basic-features/environment-variables.md). Give it a try!
Examples diff --git a/docs/api-routes/dynamic-api-routes.md b/docs/api-routes/dynamic-api-routes.md index 8c49755693a6b..272f68025f7e2 100644 --- a/docs/api-routes/dynamic-api-routes.md +++ b/docs/api-routes/dynamic-api-routes.md @@ -31,7 +31,7 @@ Now, a request to `/api/post/abc` will respond with the text: `Post: abc`. A very common RESTful pattern is to set up routes like this: -- `GET api/posts/` - gets a list of posts, probably paginated +- `GET api/posts` - gets a list of posts, probably paginated - `GET api/posts/12345` - gets post id 12345 We can model this in two ways: @@ -40,11 +40,10 @@ We can model this in two ways: - `/api/posts.js` - `/api/posts/[postId].js` - Option 2: - - `/api/posts/index.js` - `/api/posts/[postId].js` -Both are equivalent. A third option of only using `/api/posts/[postId].js` is not valid because Dynamic Routes (including Catch-all routes - see below) do not have an `undefined` state and `GET api/posts/` will not match `/api/posts/[postId].js` under any circumstances. +Both are equivalent. A third option of only using `/api/posts/[postId].js` is not valid because Dynamic Routes (including Catch-all routes - see below) do not have an `undefined` state and `GET api/posts` will not match `/api/posts/[postId].js` under any circumstances. ### Catch all API routes diff --git a/docs/api-routes/introduction.md b/docs/api-routes/introduction.md index eac86c6ab3db0..9d71dc4aca031 100644 --- a/docs/api-routes/introduction.md +++ b/docs/api-routes/introduction.md @@ -17,7 +17,7 @@ description: Next.js supports API Routes, which allow you to build your API with API routes provide a straightforward solution to build your **API** with Next.js. -Any file inside the folder `pages/api` is mapped to `/api/*` and will be treated as an API endpoint instead of a `page`. +Any file inside the folder `pages/api` is mapped to `/api/*` and will be treated as an API endpoint instead of a `page`. They are server-side only bundles and won't increase your client-side bundle size. For example, the following API route `pages/api/user.js` handles a `json` response: @@ -48,9 +48,10 @@ export default (req, res) => { To fetch API endpoints, take a look into any of the examples at the start of this section. -> API Routes [do not specify CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS), meaning they are **same-origin only** by default. You can customize such behavior by wrapping the request handler with the [cors middleware](/docs/api-routes/api-middlewares.md#connectexpress-middleware-support). +## Caveats -> API Routes do not increase your client-side bundle size. They are server-side only bundles. +- API Routes [do not specify CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS), meaning they are **same-origin only** by default. You can customize such behavior by wrapping the request handler with the [cors middleware](/docs/api-routes/api-middlewares.md#connectexpress-middleware-support). +- API Routes can't be used with [`next export`](/docs/advanced-features/static-html-export.md) ## Related diff --git a/docs/basic-features/fast-refresh.md b/docs/basic-features/fast-refresh.md index 0d6040eb9050b..5df2d35968a98 100644 --- a/docs/basic-features/fast-refresh.md +++ b/docs/basic-features/fast-refresh.md @@ -108,5 +108,5 @@ with an empty array of dependencies would still re-run once during Fast Refresh. However, writing code resilient to occasional re-running of `useEffect` is a good practice even without Fast Refresh. It will make it easier for you to introduce new dependencies to it later on -and it's enforced by [React Strict Mode](/docs/api-reference/next.config.js/react-strict-mode), +and it's enforced by [React Strict Mode](/docs/api-reference/next.config.js/react-strict-mode.md), which we highly recommend enabling. diff --git a/docs/basic-features/supported-browsers-features.md b/docs/basic-features/supported-browsers-features.md index e073c9433608f..9da03b356a6b1 100644 --- a/docs/basic-features/supported-browsers-features.md +++ b/docs/basic-features/supported-browsers-features.md @@ -26,7 +26,7 @@ In addition to `fetch()` on the client side, Next.js polyfills `fetch()` in the If your own code or any external npm dependencies require features not supported by your target browsers, you need to add polyfills yourself. -In this case, you should add a top-level import for the **specific polyfill** you need in your [Custom ``](docs/advanced-features/custom-app.md) or the individual component. +In this case, you should add a top-level import for the **specific polyfill** you need in your [Custom ``](/docs/advanced-features/custom-app.md) or the individual component. ## JavaScript Language Features diff --git a/docs/deployment.md b/docs/deployment.md index eddcc9acd5949..fc1a806e9ccf8 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -72,6 +72,4 @@ Make sure your `package.json` has the `"build"` and `"start"` scripts: ### Static HTML Export -If you’d like to do a static HTML export of your Next.js app, follow the directions on [our documentation](/docs/advanced-features/static-html-export.md). By default, `next export` will generate an `out` directory, which can be served by any static hosting service or CDN. - -> We strongly recommend using [Vercel](https://vercel.com/) even if your Next.js app is fully static. [Vercel](https://vercel.com/) is optimized to make static Next.js apps blazingly fast. `next export` works with Zero Config deployments on Vercel. +If you’d like to do a static HTML export of your Next.js app, follow the directions on [our documentation](/docs/advanced-features/static-html-export.md). diff --git a/docs/faq.md b/docs/faq.md index d13c94b2ff984..8593eed265d7f 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -74,6 +74,6 @@ description: Get to know more about Next.js with the frequently asked questions.
- Can I make a Next.js Progressive Web App? -

Yes! Here's an example.

+ Can I make a Next.js Progressive Web App (PWA)? +

Yes! Check out our PWA Example to see how it works.

diff --git a/errors/missing-document-component.md b/errors/missing-document-component.md new file mode 100644 index 0000000000000..89fcdb1081eb0 --- /dev/null +++ b/errors/missing-document-component.md @@ -0,0 +1,13 @@ +# Missing Document Components + +#### Why This Error Occurred + +In your custom `pages/_document` an expected sub-component was not rendered. + +#### Possible Ways to Fix It + +Make sure to import and render all of the expected `Document` components. + +### Useful Links + +- [Custom Document Docs](https://nextjs.org/docs/advanced-features/custom-document) diff --git a/examples/data-fetch/README.md b/examples/data-fetch/README.md index d7146d911d652..6dc5c58e7ed8c 100644 --- a/examples/data-fetch/README.md +++ b/examples/data-fetch/README.md @@ -1,9 +1,9 @@ # Data fetch example 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. +on the server and the client when necessary is so easy with Next.js. -Using `getStaticProps` fetches data at build time from a page, Next.js will pre-render this page at build time. +By using `getStaticProps` Next.js will fetch data at build time from a page, and pre-render the page to static assets. ## Deploy your own @@ -13,8 +13,6 @@ Deploy the example using [Vercel](https://vercel.com): ## How to use -### Using `create-next-app` - Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: ```bash @@ -23,23 +21,4 @@ npx create-next-app --example data-fetch data-fetch-app yarn create next-app --example data-fetch data-fetch-app ``` -### Download manually - -Download the example: - -```bash -curl https://codeload.github.com/vercel/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/data-fetch -cd data-fetch -``` - -Install it and run: - -```bash -npm install -npm run dev -# or -yarn -yarn dev -``` - Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). diff --git a/examples/data-fetch/package.json b/examples/data-fetch/package.json index 55d94b79a3ea6..17aacdb9fe651 100644 --- a/examples/data-fetch/package.json +++ b/examples/data-fetch/package.json @@ -8,9 +8,8 @@ }, "dependencies": { "next": "latest", - "node-fetch": "^2.6.0", - "react": "^16.8.4", - "react-dom": "^16.8.4" + "react": "^16.13.1", + "react-dom": "^16.13.1" }, - "license": "ISC" + "license": "MIT" } diff --git a/examples/data-fetch/pages/index.js b/examples/data-fetch/pages/index.js index 90dc176afd3cd..d668c07e8fd6c 100644 --- a/examples/data-fetch/pages/index.js +++ b/examples/data-fetch/pages/index.js @@ -1,5 +1,4 @@ import Link from 'next/link' -import fetch from 'node-fetch' function Index({ stars }) { return ( diff --git a/examples/data-fetch/pages/preact-stars.js b/examples/data-fetch/pages/preact-stars.js index a8435ed70c3ff..0db33607dde37 100644 --- a/examples/data-fetch/pages/preact-stars.js +++ b/examples/data-fetch/pages/preact-stars.js @@ -1,5 +1,4 @@ import Link from 'next/link' -import fetch from 'node-fetch' function PreactStars({ stars }) { return ( diff --git a/examples/with-sentry/next.config.js b/examples/with-sentry/next.config.js index 272fef9c62884..7d5c90f380c5a 100644 --- a/examples/with-sentry/next.config.js +++ b/examples/with-sentry/next.config.js @@ -21,6 +21,7 @@ const COMMIT_SHA = VERCEL_BITBUCKET_COMMIT_SHA process.env.SENTRY_DSN = SENTRY_DSN +const basePath = '' module.exports = withSourceMaps({ serverRuntimeConfig: { @@ -63,12 +64,12 @@ module.exports = withSourceMaps({ include: '.next', ignore: ['node_modules'], stripPrefix: ['webpack://_N_E/'], - urlPrefix: '~/_next', + urlPrefix: `~${basePath}/_next`, release: COMMIT_SHA, }) ) } - return config }, + basePath, }) diff --git a/examples/z-experimental-refresh/.gitignore b/examples/z-experimental-refresh/.gitignore deleted file mode 100644 index 1437c53f70bc2..0000000000000 --- a/examples/z-experimental-refresh/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env.local -.env.development.local -.env.test.local -.env.production.local - -# vercel -.vercel diff --git a/examples/z-experimental-refresh/README.md b/examples/z-experimental-refresh/README.md deleted file mode 100644 index 078c5d65e90b5..0000000000000 --- a/examples/z-experimental-refresh/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# z-experimental-refresh - -This is an **experimental** demo of React Fast Refresh. -Please do not use these features in your application or project (yet). - -## Usage - -Run the following command to get started: - -```bash -yarn dev -# or -npm run dev -``` diff --git a/examples/z-experimental-refresh/components/ClickCount.js b/examples/z-experimental-refresh/components/ClickCount.js deleted file mode 100644 index 283266612179e..0000000000000 --- a/examples/z-experimental-refresh/components/ClickCount.js +++ /dev/null @@ -1,15 +0,0 @@ -import { useCallback, useState } from 'react' -import styles from './ClickCount.module.css' - -export default function ClickCount() { - const [count, setCount] = useState(0) - const increment = useCallback(() => { - setCount((v) => v + 1) - }, [setCount]) - - return ( - - ) -} diff --git a/examples/z-experimental-refresh/components/ClickCount.module.css b/examples/z-experimental-refresh/components/ClickCount.module.css deleted file mode 100644 index 3cba6f351a456..0000000000000 --- a/examples/z-experimental-refresh/components/ClickCount.module.css +++ /dev/null @@ -1,32 +0,0 @@ -button.btn { - margin: 0; - border: 1px solid #d1d1d1; - border-radius: 5px; - padding: 0.5em; - vertical-align: middle; - white-space: normal; - background: none; - line-height: 1; - font-size: 1rem; - font-family: inherit; - transition: all 0.2s ease; -} - -button.btn { - padding: 0.65em 1em; - background: #0076ff; - color: #fff; - border: none; - cursor: pointer; - transition: all 0.2s ease; -} -button.btn:focus { - outline: 0; - border-color: #0076ff; -} -button.btn:hover { - background: rgba(0, 118, 255, 0.8); -} -button.btn:focus { - box-shadow: 0 0 0 2px rgba(0, 118, 255, 0.5); -} diff --git a/examples/z-experimental-refresh/global.css b/examples/z-experimental-refresh/global.css deleted file mode 100644 index e28fd3dfe7a37..0000000000000 --- a/examples/z-experimental-refresh/global.css +++ /dev/null @@ -1,41 +0,0 @@ -body { - font-family: 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', 'Helvetica', - 'Arial', sans-serif; - padding: 20px 20px 60px; - max-width: 680px; - margin: 0 auto; - font-size: 16px; - line-height: 1.65; - word-break: break-word; - font-kerning: auto; - font-variant: normal; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - text-rendering: optimizeLegibility; - hyphens: auto; -} - -h2, -h3, -h4 { - margin-top: 1.5em; -} - -code, -pre { - font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, - Bitstream Vera Sans Mono, Courier New, monospace, serif; - font-size: 0.92em; - color: #d400ff; -} - -code:before, -code:after { - content: '`'; -} - -hr { - border: none; - border-bottom: 1px solid #efefef; - margin: 5em auto; -} diff --git a/examples/z-experimental-refresh/package.json b/examples/z-experimental-refresh/package.json deleted file mode 100644 index 97e0becd5a630..0000000000000 --- a/examples/z-experimental-refresh/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "z-experimental-refresh", - "version": "1.0.0", - "scripts": { - "dev": "next", - "build": "next build", - "start": "next start" - }, - "dependencies": { - "next": "canary", - "react": "^16.9.0", - "react-dom": "^16.9.0" - }, - "license": "MIT" -} diff --git a/examples/z-experimental-refresh/pages/_app.js b/examples/z-experimental-refresh/pages/_app.js deleted file mode 100644 index d305f97bab7f1..0000000000000 --- a/examples/z-experimental-refresh/pages/_app.js +++ /dev/null @@ -1,5 +0,0 @@ -import '../global.css' - -export default function MyApp({ Component, pageProps }) { - return -} diff --git a/examples/z-experimental-refresh/pages/index.js b/examples/z-experimental-refresh/pages/index.js deleted file mode 100644 index 6346c5e117737..0000000000000 --- a/examples/z-experimental-refresh/pages/index.js +++ /dev/null @@ -1,60 +0,0 @@ -import { useCallback, useEffect, useState } from 'react' -import ClickCount from '../components/ClickCount' -import styles from '../components/ClickCount.module.css' - -function a() { - console.log( - // hello - document.body() - ) -} - -function foo() { - a() -} - -function Home() { - const [count, setCount] = useState(0) - const increment = useCallback(() => { - setCount((v) => v + 1) - }, [setCount]) - - useEffect(() => { - const r = setInterval(() => { - increment() - }, 250) - return () => { - clearInterval(r) - } - }, [increment]) - - return ( -
-

Home

-
-

Auto Incrementing Value

-

Current value: {count}

-
-
-
-

Component with State

- -
-
-
- -
-
- ) -} - -export default Home diff --git a/lerna.json b/lerna.json index e88cb15d77dfa..7b24d1e9cddbb 100644 --- a/lerna.json +++ b/lerna.json @@ -17,5 +17,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "9.5.3-canary.20" + "version": "9.5.3-canary.21" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index a79c458755614..ee505f6569a9e 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.5.3-canary.20", + "version": "9.5.3-canary.21", "keywords": [ "react", "next", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index b2b7f7c76dcff..5b6b487205796 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "9.5.3-canary.20", + "version": "9.5.3-canary.21", "description": "ESLint plugin for NextJS.", "main": "lib/index.js", "license": "MIT", diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 62d53ada27dc3..928d386bd50f8 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.5.3-canary.20", + "version": "9.5.3-canary.21", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 5c7df344fbd14..829625db0d60e 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "9.5.3-canary.20", + "version": "9.5.3-canary.21", "license": "MIT", "dependencies": { "chalk": "4.1.0", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 875e4ae23e593..b3c71a342fe13 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "9.5.3-canary.20", + "version": "9.5.3-canary.21", "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 c35ad8a73d85c..a1cd6adcbef2c 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.5.3-canary.20", + "version": "9.5.3-canary.21", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-google-analytics" diff --git a/packages/next-plugin-sentry/package.json b/packages/next-plugin-sentry/package.json index 63c9b9e29de71..537b3d93b754b 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.5.3-canary.20", + "version": "9.5.3-canary.21", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-sentry" diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index f9f9921d9f126..b40824e7a8998 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "9.5.3-canary.20", + "version": "9.5.3-canary.21", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 4b05c023bb0a2..4afafe1384f51 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.5.3-canary.20", + "version": "9.5.3-canary.21", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next/build/babel/plugins/no-anonymous-default-export.ts b/packages/next/build/babel/plugins/no-anonymous-default-export.ts index 32aa89b6815bb..1d21b759fa030 100644 --- a/packages/next/build/babel/plugins/no-anonymous-default-export.ts +++ b/packages/next/build/babel/plugins/no-anonymous-default-export.ts @@ -48,6 +48,10 @@ export default function NoAnonymousDefaultExport({ chalk.bold('After'), chalk.cyan('const Named = () =>
;'), chalk.cyan('export default Named;'), + '', + `A codemod is available to fix the most common cases: ${chalk.cyan( + 'https://nextjs.link/codemod-ndc' + )}`, ].join('\n') ) break @@ -67,6 +71,10 @@ export default function NoAnonymousDefaultExport({ '', chalk.bold('After'), chalk.cyan('export default function Named() { /* ... */ }'), + '', + `A codemod is available to fix the most common cases: ${chalk.cyan( + 'https://nextjs.link/codemod-ndc' + )}`, ].join('\n') ) } diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index acfece465c8b5..31ee7da60ec97 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -238,7 +238,7 @@ export default async function getBaseWebpackConfig( pagesDir, cwd: dir, // Webpack 5 has a built-in loader cache - cache: !config.experimental.unstable_webpack5cache, + cache: !isWebpack5, babelPresetPlugins, hasModern: !!config.experimental.modern, development: dev, @@ -1107,9 +1107,9 @@ export default async function getBaseWebpackConfig( } if (isWebpack5) { - // On by default: + // futureEmitAssets is on by default in webpack 5 delete webpackConfig.output?.futureEmitAssets - // No longer polyfills Node.js modules: + // webpack 5 no longer polyfills Node.js modules: if (webpackConfig.node) delete webpackConfig.node.setImmediate if (dev) { @@ -1119,13 +1119,65 @@ export default async function getBaseWebpackConfig( webpackConfig.optimization.usedExports = false } - // Enable webpack 5 caching - if (config.experimental.unstable_webpack5cache) { - webpackConfig.cache = { - type: 'filesystem', - cacheDirectory: path.join(dir, '.next', 'cache', 'webpack'), + const nextPublicVariables = Object.keys(process.env).reduce( + (prev: string, key: string) => { + if (key.startsWith('NEXT_PUBLIC_')) { + return `${prev}|${key}=${process.env[key]}` + } + return prev + }, + '' + ) + const nextEnvVariables = Object.keys(config.env).reduce( + (prev: string, key: string) => { + return `${prev}|${key}=${config.env[key]}` + }, + '' + ) + + const configVars = JSON.stringify({ + crossOrigin: config.crossOrigin, + pageExtensions: config.pageExtensions, + trailingSlash: config.trailingSlash, + modern: config.experimental.modern, + buildActivity: config.devIndicators.buildActivity, + autoPrerender: config.devIndicators.autoPrerender, + plugins: config.experimental.plugins, + reactStrictMode: config.reactStrictMode, + reactMode: config.experimental.reactMode, + optimizeFonts: config.experimental.optimizeFonts, + optimizeImages: config.experimental.optimizeImages, + scrollRestoration: config.experimental.scrollRestoration, + basePath: config.basePath, + pageEnv: config.experimental.pageEnv, + excludeDefaultMomentLocales: config.future.excludeDefaultMomentLocales, + assetPrefix: config.assetPrefix, + target, + reactProductionProfiling, + }) + + const cache: any = { + type: 'filesystem', + // Includes: + // - Next.js version + // - NEXT_PUBLIC_ variable values (they affect caching) TODO: make this module usage only + // - next.config.js `env` key + // - next.config.js keys that affect compilation + version: `${process.env.__NEXT_VERSION}|${nextPublicVariables}|${nextEnvVariables}|${configVars}`, + cacheDirectory: path.join(dir, '.next', 'cache', 'webpack'), + } + + // Adds `next.config.js` as a buildDependency when custom webpack config is provided + if (config.webpack && config.configFile) { + cache.buildDependencies = { + config: [config.configFile], } } + + webpackConfig.cache = cache + + // @ts-ignore TODO: remove ignore when webpack 5 is stable + webpackConfig.optimization.realContentHash = false } webpackConfig = await buildConfiguration(webpackConfig, { diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index 2b0ffc7301e00..8c18f4ec7f488 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -399,9 +399,7 @@ const nextServerlessLoader: loader.Loader = function () { pageIsDynamicRoute ? ` const params = ( - fromExport && - !getStaticProps && - !getServerSideProps + fromExport ) ? {} : normalizeDynamicRouteParams( trustQuery diff --git a/packages/next/build/webpack/plugins/react-loadable-plugin.ts b/packages/next/build/webpack/plugins/react-loadable-plugin.ts index bb6810c094dfe..c2f3400b4fa56 100644 --- a/packages/next/build/webpack/plugins/react-loadable-plugin.ts +++ b/packages/next/build/webpack/plugins/react-loadable-plugin.ts @@ -22,9 +22,9 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWAR // Modified to strip out unneeded results for Next's specific use case import webpack, { - Compiler, // eslint-disable-next-line @typescript-eslint/no-unused-vars compilation as CompilationType, + Compiler, } from 'webpack' import sources from 'webpack-sources' @@ -65,7 +65,12 @@ function buildManifest( chunkGroup.chunks.forEach((chunk: any) => { chunk.files.forEach((file: string) => { - if (!file.match(/\.js$/) || !file.match(/^static\/chunks\//)) { + if ( + !( + (file.endsWith('.js') || file.endsWith('.css')) && + file.match(/^static\/(chunks|css)\//) + ) + ) { return } @@ -85,10 +90,7 @@ function buildManifest( continue } - manifest[request].push({ - id, - file, - }) + manifest[request].push({ id, file }) } }) }) diff --git a/packages/next/next-server/lib/utils.ts b/packages/next/next-server/lib/utils.ts index ca0f1e0d28312..93b53fe693c1c 100644 --- a/packages/next/next-server/lib/utils.ts +++ b/packages/next/next-server/lib/utils.ts @@ -166,6 +166,12 @@ export type DocumentInitialProps = RenderPageResult & { export type DocumentProps = DocumentInitialProps & { __NEXT_DATA__: NEXT_DATA dangerousAsPath: string + docComponentsRendered: { + Html?: boolean + Main?: boolean + Head?: boolean + NextScript?: boolean + } buildManifest: BuildManifest ampPath: string inAmpMode: boolean diff --git a/packages/next/next-server/server/config.ts b/packages/next/next-server/server/config.ts index c9eefdca419a6..3de166a548902 100644 --- a/packages/next/next-server/server/config.ts +++ b/packages/next/next-server/server/config.ts @@ -54,7 +54,6 @@ const defaultConfig: { [key: string]: any } = { optimizeFonts: false, optimizeImages: false, scrollRestoration: false, - unstable_webpack5cache: false, }, future: { excludeDefaultMomentLocales: false, @@ -277,7 +276,11 @@ export default function loadConfig( ) } - return assignDefaults({ configOrigin: CONFIG_FILE, ...userConfig }) + return assignDefaults({ + configOrigin: CONFIG_FILE, + configFile: path, + ...userConfig, + }) } else { const configBaseName = basename(CONFIG_FILE, extname(CONFIG_FILE)) const nonJsPath = findUp.sync( diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index c473467d6b875..5172d948a07a2 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -41,6 +41,7 @@ import { loadGetInitialProps, NextComponentType, RenderPage, + DocumentProps, } from '../lib/utils' import { tryGetPreviewData, __ApiPreviewProps } from './api-utils' import { denormalizePagePath } from './denormalize-page-path' @@ -158,6 +159,7 @@ function renderDocument( Document: DocumentType, { buildManifest, + docComponentsRendered, props, docProps, pathname, @@ -188,6 +190,7 @@ function renderDocument( devOnlyCacheBusterQueryString, }: RenderOpts & { props: any + docComponentsRendered: DocumentProps['docComponentsRendered'] docProps: DocumentInitialProps pathname: string query: ParsedUrlQuery @@ -233,6 +236,7 @@ function renderDocument( appGip, // whether the _app has getInitialProps }, buildManifest, + docComponentsRendered, dangerousAsPath, canonicalBase, ampPath, @@ -402,7 +406,7 @@ export async function renderToHTML( ) } - if (isAutoExport) { + if (isAutoExport || isFallback) { // remove query values except ones that will be set during export query = { ...(query.amp @@ -759,8 +763,11 @@ export async function renderToHTML( renderOpts.inAmpMode = inAmpMode renderOpts.hybridAmp = hybridAmp + const docComponentsRendered: DocumentProps['docComponentsRendered'] = {} + let html = renderDocument(Document, { ...renderOpts, + docComponentsRendered, buildManifest: filteredBuildManifest, // Only enabled in production as development mode has features relying on HMR (style injection for example) unstable_runtimeJS: @@ -787,6 +794,29 @@ export async function renderToHTML( devOnlyCacheBusterQueryString, }) + if (process.env.NODE_ENV !== 'production') { + const nonRenderedComponents = [] + const expectedDocComponents = ['Main', 'Head', 'NextScript', 'Html'] + + for (const comp of expectedDocComponents) { + if (!(docComponentsRendered as any)[comp]) { + nonRenderedComponents.push(comp) + } + } + const plural = nonRenderedComponents.length !== 1 ? 's' : '' + + if (nonRenderedComponents.length) { + console.warn( + `Expected Document Component${plural} ${nonRenderedComponents.join( + ', ' + )} ${ + plural ? 'were' : 'was' + } not rendered. Make sure you render them in your custom \`_document\`\n` + + `See more info here https://err.sh/next.js/missing-document-component` + ) + } + } + if (inAmpMode && html) { // inject HTML to AMP_RENDER_TARGET to allow rendering // directly to body in AMP mode diff --git a/packages/next/package.json b/packages/next/package.json index 6bcf76fc4617c..f43c2483e78c8 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "9.5.3-canary.20", + "version": "9.5.3-canary.21", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -77,8 +77,8 @@ "@babel/preset-typescript": "7.9.0", "@babel/runtime": "7.9.6", "@babel/types": "7.9.6", - "@next/react-dev-overlay": "9.5.3-canary.20", - "@next/react-refresh-utils": "9.5.3-canary.20", + "@next/react-dev-overlay": "9.5.3-canary.21", + "@next/react-refresh-utils": "9.5.3-canary.21", "ast-types": "0.13.2", "babel-plugin-syntax-jsx": "6.18.0", "babel-plugin-transform-define": "2.0.0", @@ -123,7 +123,7 @@ "react-dom": "^16.6.0" }, "devDependencies": { - "@next/polyfill-nomodule": "9.5.3-canary.20", + "@next/polyfill-nomodule": "9.5.3-canary.21", "@taskr/clear": "1.1.0", "@taskr/esnext": "1.1.0", "@taskr/watch": "1.1.0", diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx index 96b16f7385b12..8b90a4846ef44 100644 --- a/packages/next/pages/_document.tsx +++ b/packages/next/pages/_document.tsx @@ -25,9 +25,9 @@ export type OriginProps = { crossOrigin?: string } -function dedupe(bundles: any[]): any[] { - const files = new Set() - const kept = [] +function dedupe(bundles: T[]): T[] { + const files = new Set() + const kept: T[] = [] for (const bundle of bundles) { if (files.has(bundle.file)) continue @@ -123,7 +123,12 @@ export function Html( HTMLHtmlElement > ) { - const { inAmpMode } = useContext(DocumentComponentContext) + const { inAmpMode, docComponentsRendered } = useContext( + DocumentComponentContext + ) + + docComponentsRendered.Html = true + return ( getCssLinks(files: DocumentFiles): JSX.Element[] | null { - const { assetPrefix, devOnlyCacheBusterQueryString } = this.context + const { + assetPrefix, + devOnlyCacheBusterQueryString, + dynamicImports, + } = this.context const cssFiles = files.allFiles.filter((f) => f.endsWith('.css')) const sharedFiles = new Set(files.sharedFiles) + let dynamicCssFiles = dedupe( + dynamicImports.filter((f) => f.file.endsWith('.css')) + ).map((f) => f.file) + if (dynamicCssFiles.length) { + const existing = new Set(cssFiles) + dynamicCssFiles = dynamicCssFiles.filter( + (f) => !(existing.has(f) || sharedFiles.has(f)) + ) + cssFiles.push(...dynamicCssFiles) + } + const cssLinkElements: JSX.Element[] = [] cssFiles.forEach((file) => { const isSharedFile = sharedFiles.has(file) @@ -188,7 +208,6 @@ export class Head extends Component< /> ) }) - return cssLinkElements.length === 0 ? null : cssLinkElements } @@ -201,7 +220,7 @@ export class Head extends Component< return ( dedupe(dynamicImports) - .map((bundle: any) => { + .map((bundle) => { // `dynamicImports` will contain both `.js` and `.module.js` when the // feature is enabled. This clause will filter down to the modern // variants only. @@ -288,6 +307,8 @@ export class Head extends Component< } = this.context const disableRuntimeJS = unstable_runtimeJS === false + this.context.docComponentsRendered.Head = true + let { head } = this.context let children = this.props.children // show a warning if Head contains (only in development) @@ -501,7 +522,12 @@ export class Head extends Component< } export function Main() { - const { inAmpMode, html } = useContext(DocumentComponentContext) + const { inAmpMode, html, docComponentsRendered } = useContext( + DocumentComponentContext + ) + + docComponentsRendered.Main = true + if (inAmpMode) return <>{AMP_RENDER_TARGET}</> return <div id="__next" dangerouslySetInnerHTML={{ __html: html }} /> } @@ -528,7 +554,7 @@ export class NextScript extends Component<OriginProps> { devOnlyCacheBusterQueryString, } = this.context - return dedupe(dynamicImports).map((bundle: any) => { + return dedupe(dynamicImports).map((bundle) => { let modernProps = {} if (process.env.__NEXT_MODERN_BUILD) { modernProps = bundle.file.endsWith('.module.js') @@ -642,10 +668,13 @@ export class NextScript extends Component<OriginProps> { inAmpMode, buildManifest, unstable_runtimeJS, + docComponentsRendered, devOnlyCacheBusterQueryString, } = this.context const disableRuntimeJS = unstable_runtimeJS === false + docComponentsRendered.NextScript = true + if (inAmpMode) { if (process.env.NODE_ENV === 'production') { return null diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index 87a32842f60c7..e602a80e40e7c 100644 --- a/packages/react-dev-overlay/package.json +++ b/packages/react-dev-overlay/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-dev-overlay", - "version": "9.5.3-canary.20", + "version": "9.5.3-canary.21", "description": "A development-only overlay for developing React applications.", "repository": { "url": "vercel/next.js", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index ae163a90c7fdd..01668d624eab0 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "9.5.3-canary.20", + "version": "9.5.3-canary.21", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/test/integration/fallback-route-params/pages/[slug].js b/test/integration/fallback-route-params/pages/[slug].js new file mode 100644 index 0000000000000..8259ea56dc20c --- /dev/null +++ b/test/integration/fallback-route-params/pages/[slug].js @@ -0,0 +1,34 @@ +import { useRouter } from 'next/router' + +export const getStaticProps = () => { + return { + props: { + world: 'world', + }, + } +} + +export const getStaticPaths = () => { + return { + paths: [], + fallback: true, + } +} + +export default function Page({ world }) { + const router = useRouter() + + if (typeof window !== 'undefined' && !window.setInitialSlug) { + window.setInitialSlug = true + window.initialSlug = router.query.slug + } + + if (router.isFallback) return 'Loading...' + + return ( + <> + <p>hello {world}</p> + <p id="query">{JSON.stringify(router.query)}</p> + </> + ) +} diff --git a/test/integration/fallback-route-params/test/index.test.js b/test/integration/fallback-route-params/test/index.test.js new file mode 100644 index 0000000000000..877f0b0c1f120 --- /dev/null +++ b/test/integration/fallback-route-params/test/index.test.js @@ -0,0 +1,89 @@ +/* eslint-env jest */ + +import fs from 'fs-extra' +import { join } from 'path' +import cheerio from 'cheerio' +import webdriver from 'next-webdriver' +import { + killApp, + findPort, + nextBuild, + nextStart, + renderViaHTTP, + launchApp, + File, +} from 'next-test-utils' + +jest.setTimeout(1000 * 60 * 2) + +const appDir = join(__dirname, '../') +const nextConfig = new File(join(appDir, 'next.config.js')) +let appPort +let app + +const runTests = () => { + it('should have correct fallback query (skeleton)', async () => { + const html = await renderViaHTTP(appPort, '/first') + const $ = cheerio.load(html) + const { query } = JSON.parse($('#__NEXT_DATA__').text()) + expect(query).toEqual({}) + }) + + it('should have correct fallback query (hydration)', async () => { + const browser = await webdriver(appPort, '/second') + const initialSlug = await browser.eval(() => window.initialSlug) + expect(initialSlug).toBe(null) + + await browser.waitForElementByCss('#query') + + const hydratedQuery = JSON.parse( + await browser.elementByCss('#query').text() + ) + expect(hydratedQuery).toEqual({ slug: 'second' }) + }) +} + +describe('Fallback Dynamic Route Params', () => { + describe('dev mode', () => { + beforeAll(async () => { + await fs.remove(join(appDir, '.next')) + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTests() + }) + + describe('production mode', () => { + beforeAll(async () => { + await fs.remove(join(appDir, '.next')) + await nextBuild(appDir, []) + appPort = await findPort() + app = await nextStart(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTests() + }) + + describe('serverless mode', () => { + beforeAll(async () => { + nextConfig.write(` + module.exports = { + target: 'experimental-serverless-trace' + } + `) + await fs.remove(join(appDir, '.next')) + await nextBuild(appDir, [], { stdout: true }) + appPort = await findPort() + app = await nextStart(appDir, appPort) + }) + afterAll(async () => { + await killApp(app) + nextConfig.delete() + }) + + runTests() + }) +}) diff --git a/test/integration/missing-document-component-error/pages/index.js b/test/integration/missing-document-component-error/pages/index.js new file mode 100644 index 0000000000000..71d6a609df67c --- /dev/null +++ b/test/integration/missing-document-component-error/pages/index.js @@ -0,0 +1,3 @@ +export default function Index() { + return <p>Index page</p> +} diff --git a/test/integration/missing-document-component-error/test/index.test.js b/test/integration/missing-document-component-error/test/index.test.js new file mode 100644 index 0000000000000..5502a0f628d6d --- /dev/null +++ b/test/integration/missing-document-component-error/test/index.test.js @@ -0,0 +1,159 @@ +/* eslint-env jest */ + +import fs from 'fs-extra' +import { join } from 'path' +import { + findPort, + killApp, + launchApp, + check, + renderViaHTTP, +} from 'next-test-utils' + +jest.setTimeout(1000 * 60 * 2) + +const appDir = join(__dirname, '..') +const docPath = join(appDir, 'pages/_document.js') +let appPort +let app + +const checkMissing = async (missing = [], docContent) => { + await fs.writeFile(docPath, docContent) + let stderr = '' + + appPort = await findPort() + app = await launchApp(appDir, appPort, { + onStderr(msg) { + stderr += msg || '' + }, + }) + + await renderViaHTTP(appPort, '/') + + await check(() => stderr, new RegExp(`missing-document-component`)) + await check(() => stderr, new RegExp(`${missing.join(', ')}`)) + + await killApp(app) + await fs.remove(docPath) +} + +describe('Missing _document components error', () => { + it('should detect missing Html component', async () => { + await checkMissing( + ['Html'], + ` + import Document, { Head, Main, NextScript } from 'next/document' + + class MyDocument extends Document { + render() { + return ( + <html> + <Head /> + <body> + <Main /> + <NextScript /> + </body> + </html> + ) + } + } + + export default MyDocument + ` + ) + }) + + it('should detect missing Head component', async () => { + await checkMissing( + ['Head'], + ` + import Document, { Html, Main, NextScript } from 'next/document' + + class MyDocument extends Document { + render() { + return ( + <Html> + <body> + <Main /> + <NextScript /> + </body> + </Html> + ) + } + } + + export default MyDocument + ` + ) + }) + + it('should detect missing Main component', async () => { + await checkMissing( + ['Main'], + ` + import Document, { Html, Head, NextScript } from 'next/document' + + class MyDocument extends Document { + render() { + return ( + <Html> + <Head /> + <body> + <NextScript /> + </body> + </Html> + ) + } + } + + export default MyDocument + ` + ) + }) + + it('should detect missing NextScript component', async () => { + await checkMissing( + ['NextScript'], + ` + import Document, { Html, Head, Main } from 'next/document' + + class MyDocument extends Document { + render() { + return ( + <Html> + <body> + <Main /> + </body> + </Html> + ) + } + } + + export default MyDocument + ` + ) + }) + + it('should detect multiple missing document components', async () => { + await checkMissing( + ['Head', 'NextScript'], + ` + import Document, { Html, Main } from 'next/document' + + class MyDocument extends Document { + render() { + return ( + <Html> + <body> + <Main /> + </body> + </Html> + ) + } + } + + export default MyDocument + ` + ) + }) +}) diff --git a/test/integration/production/components/dynamic-css/many-imports/with-css-1.js b/test/integration/production/components/dynamic-css/many-imports/with-css-1.js new file mode 100644 index 0000000000000..1df8d47c21e5f --- /dev/null +++ b/test/integration/production/components/dynamic-css/many-imports/with-css-1.js @@ -0,0 +1,3 @@ +import styles from './with-css-1.module.css' + +export default () => <p className={styles.content}>With CSS 1</p> diff --git a/test/integration/production/components/dynamic-css/many-imports/with-css-1.module.css b/test/integration/production/components/dynamic-css/many-imports/with-css-1.module.css new file mode 100644 index 0000000000000..69fda78f79397 --- /dev/null +++ b/test/integration/production/components/dynamic-css/many-imports/with-css-1.module.css @@ -0,0 +1,3 @@ +.content { + color: inherit; +} diff --git a/test/integration/production/components/dynamic-css/many-imports/with-css-2.js b/test/integration/production/components/dynamic-css/many-imports/with-css-2.js new file mode 100644 index 0000000000000..1112809cb0b39 --- /dev/null +++ b/test/integration/production/components/dynamic-css/many-imports/with-css-2.js @@ -0,0 +1,3 @@ +import styles from './with-css-2.module.css' + +export default () => <p className={styles.content}>With CSS 2</p> diff --git a/test/integration/production/components/dynamic-css/many-imports/with-css-2.module.css b/test/integration/production/components/dynamic-css/many-imports/with-css-2.module.css new file mode 100644 index 0000000000000..69fda78f79397 --- /dev/null +++ b/test/integration/production/components/dynamic-css/many-imports/with-css-2.module.css @@ -0,0 +1,3 @@ +.content { + color: inherit; +} diff --git a/test/integration/production/components/dynamic-css/many-imports/with-css-3.js b/test/integration/production/components/dynamic-css/many-imports/with-css-3.js new file mode 100644 index 0000000000000..fa4f705cbac5e --- /dev/null +++ b/test/integration/production/components/dynamic-css/many-imports/with-css-3.js @@ -0,0 +1,3 @@ +import styles from './with-css-3.module.css' + +export default () => <p className={styles.content}>With CSS 3</p> diff --git a/test/integration/production/components/dynamic-css/many-imports/with-css-3.module.css b/test/integration/production/components/dynamic-css/many-imports/with-css-3.module.css new file mode 100644 index 0000000000000..69fda78f79397 --- /dev/null +++ b/test/integration/production/components/dynamic-css/many-imports/with-css-3.module.css @@ -0,0 +1,3 @@ +.content { + color: inherit; +} diff --git a/test/integration/production/components/dynamic-css/many-modules/with-css-2.module.css b/test/integration/production/components/dynamic-css/many-modules/with-css-2.module.css new file mode 100644 index 0000000000000..3efa99e6fb171 --- /dev/null +++ b/test/integration/production/components/dynamic-css/many-modules/with-css-2.module.css @@ -0,0 +1,3 @@ +.text { + color: red; +} diff --git a/test/integration/production/components/dynamic-css/many-modules/with-css.js b/test/integration/production/components/dynamic-css/many-modules/with-css.js new file mode 100644 index 0000000000000..fc11238a63deb --- /dev/null +++ b/test/integration/production/components/dynamic-css/many-modules/with-css.js @@ -0,0 +1,8 @@ +import styles from './with-css.module.css' +import styles2 from './with-css-2.module.css' + +export default () => ( + <div className={styles.content}> + <p className={styles2.text}>With CSS</p> + </div> +) diff --git a/test/integration/production/components/dynamic-css/many-modules/with-css.module.css b/test/integration/production/components/dynamic-css/many-modules/with-css.module.css new file mode 100644 index 0000000000000..69fda78f79397 --- /dev/null +++ b/test/integration/production/components/dynamic-css/many-modules/with-css.module.css @@ -0,0 +1,3 @@ +.content { + color: inherit; +} diff --git a/test/integration/production/components/dynamic-css/nested/Nested.jsx b/test/integration/production/components/dynamic-css/nested/Nested.jsx new file mode 100644 index 0000000000000..7cc93b468ad9d --- /dev/null +++ b/test/integration/production/components/dynamic-css/nested/Nested.jsx @@ -0,0 +1,3 @@ +import styles2 from './with-css-2.module.css' + +export default () => <p className={styles2.text}>With CSS</p> diff --git a/test/integration/production/components/dynamic-css/nested/with-css-2.module.css b/test/integration/production/components/dynamic-css/nested/with-css-2.module.css new file mode 100644 index 0000000000000..3efa99e6fb171 --- /dev/null +++ b/test/integration/production/components/dynamic-css/nested/with-css-2.module.css @@ -0,0 +1,3 @@ +.text { + color: red; +} diff --git a/test/integration/production/components/dynamic-css/nested/with-css.js b/test/integration/production/components/dynamic-css/nested/with-css.js new file mode 100644 index 0000000000000..a513d6e9d10b3 --- /dev/null +++ b/test/integration/production/components/dynamic-css/nested/with-css.js @@ -0,0 +1,8 @@ +import styles from './with-css.module.css' +import Nested from './Nested' + +export default () => ( + <div className={styles.content}> + <Nested /> + </div> +) diff --git a/test/integration/production/components/dynamic-css/nested/with-css.module.css b/test/integration/production/components/dynamic-css/nested/with-css.module.css new file mode 100644 index 0000000000000..69fda78f79397 --- /dev/null +++ b/test/integration/production/components/dynamic-css/nested/with-css.module.css @@ -0,0 +1,3 @@ +.content { + color: inherit; +} diff --git a/test/integration/production/components/dynamic-css/no-css.js b/test/integration/production/components/dynamic-css/no-css.js new file mode 100644 index 0000000000000..380145b1902c9 --- /dev/null +++ b/test/integration/production/components/dynamic-css/no-css.js @@ -0,0 +1 @@ +export default () => <p>Without CSS</p> diff --git a/test/integration/production/components/dynamic-css/shared-css-module/with-css-2.js b/test/integration/production/components/dynamic-css/shared-css-module/with-css-2.js new file mode 100644 index 0000000000000..f8514fa12614b --- /dev/null +++ b/test/integration/production/components/dynamic-css/shared-css-module/with-css-2.js @@ -0,0 +1,8 @@ +import styles from './with-css-2.module.css' +import stylesShared from './with-css-shared.module.css' + +export default () => ( + <div className={styles.content}> + <p className={stylesShared.test}>With CSS</p> + </div> +) diff --git a/test/integration/production/components/dynamic-css/shared-css-module/with-css-2.module.css b/test/integration/production/components/dynamic-css/shared-css-module/with-css-2.module.css new file mode 100644 index 0000000000000..69fda78f79397 --- /dev/null +++ b/test/integration/production/components/dynamic-css/shared-css-module/with-css-2.module.css @@ -0,0 +1,3 @@ +.content { + color: inherit; +} diff --git a/test/integration/production/components/dynamic-css/shared-css-module/with-css-shared.module.css b/test/integration/production/components/dynamic-css/shared-css-module/with-css-shared.module.css new file mode 100644 index 0000000000000..3efa99e6fb171 --- /dev/null +++ b/test/integration/production/components/dynamic-css/shared-css-module/with-css-shared.module.css @@ -0,0 +1,3 @@ +.text { + color: red; +} diff --git a/test/integration/production/components/dynamic-css/shared-css-module/with-css.js b/test/integration/production/components/dynamic-css/shared-css-module/with-css.js new file mode 100644 index 0000000000000..d0397a8c33d5b --- /dev/null +++ b/test/integration/production/components/dynamic-css/shared-css-module/with-css.js @@ -0,0 +1,8 @@ +import styles from './with-css.module.css' +import stylesShared from './with-css-shared.module.css' + +export default () => ( + <div className={styles.content}> + <p className={stylesShared.test}>With CSS</p> + </div> +) diff --git a/test/integration/production/components/dynamic-css/shared-css-module/with-css.module.css b/test/integration/production/components/dynamic-css/shared-css-module/with-css.module.css new file mode 100644 index 0000000000000..69fda78f79397 --- /dev/null +++ b/test/integration/production/components/dynamic-css/shared-css-module/with-css.module.css @@ -0,0 +1,3 @@ +.content { + color: inherit; +} diff --git a/test/integration/production/components/dynamic-css/with-css.js b/test/integration/production/components/dynamic-css/with-css.js new file mode 100644 index 0000000000000..d988353142973 --- /dev/null +++ b/test/integration/production/components/dynamic-css/with-css.js @@ -0,0 +1,3 @@ +import styles from './with-css.module.css' + +export default () => <p className={styles.content}>With CSS</p> diff --git a/test/integration/production/components/dynamic-css/with-css.module.css b/test/integration/production/components/dynamic-css/with-css.module.css new file mode 100644 index 0000000000000..69fda78f79397 --- /dev/null +++ b/test/integration/production/components/dynamic-css/with-css.module.css @@ -0,0 +1,3 @@ +.content { + color: inherit; +} diff --git a/test/integration/production/pages/dynamic/css.js b/test/integration/production/pages/dynamic/css.js new file mode 100644 index 0000000000000..116e142e80af7 --- /dev/null +++ b/test/integration/production/pages/dynamic/css.js @@ -0,0 +1,5 @@ +import dynamic from 'next/dynamic' + +const Hello = dynamic(import('../../components/dynamic-css/with-css')) + +export default Hello diff --git a/test/integration/production/pages/dynamic/many-css-modules.js b/test/integration/production/pages/dynamic/many-css-modules.js new file mode 100644 index 0000000000000..4f77b5b90b668 --- /dev/null +++ b/test/integration/production/pages/dynamic/many-css-modules.js @@ -0,0 +1,7 @@ +import dynamic from 'next/dynamic' + +const Component = dynamic( + import('../../components/dynamic-css/many-modules/with-css') +) + +export default Component diff --git a/test/integration/production/pages/dynamic/many-dynamic-css.js b/test/integration/production/pages/dynamic/many-dynamic-css.js new file mode 100644 index 0000000000000..817164b8090b8 --- /dev/null +++ b/test/integration/production/pages/dynamic/many-dynamic-css.js @@ -0,0 +1,21 @@ +import dynamic from 'next/dynamic' + +const First = dynamic( + import('../../components/dynamic-css/many-imports/with-css-1') +) +const Second = dynamic( + import('../../components/dynamic-css/many-imports/with-css-2') +) +const Third = dynamic( + import('../../components/dynamic-css/many-imports/with-css-3') +) + +export default function Page() { + return ( + <div> + <First /> + <Second /> + <Third /> + </div> + ) +} diff --git a/test/integration/production/pages/dynamic/nested-css.js b/test/integration/production/pages/dynamic/nested-css.js new file mode 100644 index 0000000000000..9efdb6d0dcb10 --- /dev/null +++ b/test/integration/production/pages/dynamic/nested-css.js @@ -0,0 +1,7 @@ +import dynamic from 'next/dynamic' + +const Component = dynamic( + import('../../components/dynamic-css/nested/with-css') +) + +export default Component diff --git a/test/integration/production/pages/dynamic/no-css.js b/test/integration/production/pages/dynamic/no-css.js new file mode 100644 index 0000000000000..bb67592264e20 --- /dev/null +++ b/test/integration/production/pages/dynamic/no-css.js @@ -0,0 +1,5 @@ +import dynamic from 'next/dynamic' + +const Hello = dynamic(import('../../components/dynamic-css/no-css')) + +export default Hello diff --git a/test/integration/production/pages/dynamic/shared-css-module.js b/test/integration/production/pages/dynamic/shared-css-module.js new file mode 100644 index 0000000000000..e90b1dd138ae2 --- /dev/null +++ b/test/integration/production/pages/dynamic/shared-css-module.js @@ -0,0 +1,17 @@ +import dynamic from 'next/dynamic' + +const First = dynamic( + import('../../components/dynamic-css/shared-css-module/with-css') +) +const Second = dynamic( + import('../../components/dynamic-css/shared-css-module/with-css-2') +) + +export default function Page() { + return ( + <div> + <First /> + <Second /> + </div> + ) +} diff --git a/test/integration/production/test/dynamic.js b/test/integration/production/test/dynamic.js index 510e7f9dde30f..61e4cfaa56f86 100644 --- a/test/integration/production/test/dynamic.js +++ b/test/integration/production/test/dynamic.js @@ -16,6 +16,43 @@ export default (context, render) => { expect($('body').text()).toMatch(/Hello World 1/) }) + it('should render one dynamically imported component and load its css files', async () => { + const $ = await get$('/dynamic/css') + const cssFiles = $('link[rel=stylesheet]') + expect(cssFiles.length).toBe(1) + }) + + it('should render three dynamically imported components and load their css files', async () => { + const $ = await get$('/dynamic/many-dynamic-css') + const cssFiles = $('link[rel=stylesheet]') + expect(cssFiles.length).toBe(3) + }) + + it('should bundle two css modules for one dynamically imported component into one css file', async () => { + const $ = await get$('/dynamic/many-css-modules') + const cssFiles = $('link[rel=stylesheet]') + expect(cssFiles.length).toBe(1) + }) + + it('should bundle two css modules for nested components into one css file', async () => { + const $ = await get$('/dynamic/nested-css') + const cssFiles = $('link[rel=stylesheet]') + expect(cssFiles.length).toBe(1) + }) + + // It seem to be abnormal, dynamic CSS modules are completely self-sufficient, so shared styles are copied across files + it('should output two css files even in case of three css module files while one is shared across files', async () => { + const $ = await get$('/dynamic/shared-css-module') + const cssFiles = $('link[rel=stylesheet]') + expect(cssFiles.length).toBe(2) + }) + + it('should render one dynamically imported component without any css files', async () => { + const $ = await get$('/dynamic/no-css') + const cssFiles = $('link[rel=stylesheet]') + expect(cssFiles.length).toBe(0) + }) + it('should render even there are no physical chunk exists', async () => { let browser try {