diff --git a/.eslintignore b/.eslintignore index 84c23fbd68700..b080c941c55f3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -17,6 +17,7 @@ packages/next-codemod/transforms/__tests__/**/* packages/next-codemod/**/*.js packages/next-codemod/**/*.d.ts packages/next-env/**/*.d.ts +packages/create-next-app/templates/** test/integration/async-modules/** test/integration/eslint/** test-timings.json diff --git a/docs/api-reference/next.config.js/basepath.md b/docs/api-reference/next.config.js/basepath.md index beb1217044523..0df919b35a057 100644 --- a/docs/api-reference/next.config.js/basepath.md +++ b/docs/api-reference/next.config.js/basepath.md @@ -4,7 +4,14 @@ description: Learn more about setting a base path in Next.js # Base Path -> This feature was introduced in [Next.js 9.5](https://nextjs.org/blog/next-9-5) and up. If you’re using older versions of Next.js, please upgrade before trying it out. +
+ Version History + +| Version | Changes | +| -------- | ---------------- | +| `v9.5.0` | Base Path added. | + +
To deploy a Next.js application under a sub-path of a domain you can use the `basePath` config option. diff --git a/docs/api-reference/next.config.js/custom-webpack-config.md b/docs/api-reference/next.config.js/custom-webpack-config.md index d339d1ae14548..ed8faf1b61a02 100644 --- a/docs/api-reference/next.config.js/custom-webpack-config.md +++ b/docs/api-reference/next.config.js/custom-webpack-config.md @@ -15,7 +15,6 @@ Before continuing to add custom webpack configuration to your application make s Some commonly asked for features are available as plugins: -- [@zeit/next-less](https://github.com/vercel/next-plugins/tree/master/packages/next-less) - [@next/mdx](https://github.com/vercel/next.js/tree/canary/packages/next-mdx) - [@next/bundle-analyzer](https://github.com/vercel/next.js/tree/canary/packages/next-bundle-analyzer) diff --git a/docs/api-reference/next.config.js/headers.md b/docs/api-reference/next.config.js/headers.md index 392c80f1ff84a..69bc627a84aef 100644 --- a/docs/api-reference/next.config.js/headers.md +++ b/docs/api-reference/next.config.js/headers.md @@ -4,8 +4,6 @@ description: Add custom HTTP headers to your Next.js app. # Headers -> This feature was introduced in [Next.js 9.5](https://nextjs.org/blog/next-9-5) and up. If you’re using older versions of Next.js, please upgrade before trying it out. -
Examples
+
+ Version History + +| Version | Changes | +| --------- | -------------- | +| `v10.2.0` | `has` added. | +| `v9.5.0` | Headers added. | + +
+ Headers allow you to set custom HTTP headers for an incoming request path. To set custom HTTP headers you can use the `headers` key in `next.config.js`: diff --git a/docs/api-reference/next.config.js/introduction.md b/docs/api-reference/next.config.js/introduction.md index b01cb75745366..474257638f4bc 100644 --- a/docs/api-reference/next.config.js/introduction.md +++ b/docs/api-reference/next.config.js/introduction.md @@ -44,7 +44,7 @@ module.exports = (phase, { defaultConfig }) => { } ``` -The commented lines are the place where you can put the configs allowed by `next.config.js`, which are defined [here](https://github.com/vercel/next.js/blob/canary/packages/next/next-server/server/config-shared.ts#L33). +The commented lines are the place where you can put the configs allowed by `next.config.js`, which are defined [here](https://github.com/vercel/next.js/blob/canary/packages/next/next-server/server/config-shared.ts#L68). However, none of the configs are required, and it's not necessary to understand what each config does. Instead, search for the features you need to enable or modify in this section and they will show you what to do. diff --git a/docs/api-reference/next.config.js/redirects.md b/docs/api-reference/next.config.js/redirects.md index ab6cdb04cd7f4..8471627ff580f 100644 --- a/docs/api-reference/next.config.js/redirects.md +++ b/docs/api-reference/next.config.js/redirects.md @@ -4,8 +4,6 @@ description: Add redirects to your Next.js app. # Redirects -> This feature was introduced in [Next.js 9.5](https://nextjs.org/blog/next-9-5) and up. If you’re using older versions of Next.js, please upgrade before trying it out. -
Examples
+
+ Version History + +| Version | Changes | +| --------- | ---------------- | +| `v10.2.0` | `has` added. | +| `v9.5.0` | Redirects added. | + +
+ Redirects allow you to redirect an incoming request path to a different destination path. Redirects are only available on the Node.js environment and do not affect client-side routing. diff --git a/docs/api-reference/next.config.js/rewrites.md b/docs/api-reference/next.config.js/rewrites.md index 96caf3d30b05e..1d5e0c5592fb5 100644 --- a/docs/api-reference/next.config.js/rewrites.md +++ b/docs/api-reference/next.config.js/rewrites.md @@ -4,8 +4,6 @@ description: Add rewrites to your Next.js app. # Rewrites -> This feature was introduced in [Next.js 9.5](https://nextjs.org/blog/next-9-5) and up. If you’re using older versions of Next.js, please upgrade before trying it out. -
Examples
+
+ Version History + +| Version | Changes | +| --------- | --------------- | +| `v10.2.0` | `has` added. | +| `v9.5.0` | Rewrites added. | + +
+ Rewrites allow you to map an incoming request path to a different destination path. +Rewrites act as a URL proxy and mask the destination path, making it appear the user hasn't changed their location on the site. In contrast, [redirects](/docs/api-reference/next.config.js/redirects.md) will reroute to a new page a show the URL changes. + Rewrites are only available on the Node.js environment and do not affect client-side routing. To use rewrites you can use the `rewrites` key in `next.config.js`: @@ -317,7 +327,7 @@ module.exports = { } ``` -See additional information on incremental adoption [in the docs here](https://nextjs.org/docs/migrating/incremental-adoption). +See additional information on incremental adoption [in the docs here](/docs/migrating/incremental-adoption.md). ### Rewrites with basePath support diff --git a/docs/api-reference/next.config.js/trailing-slash.md b/docs/api-reference/next.config.js/trailing-slash.md index 4fad79dfcd24a..ee2757f872ccc 100644 --- a/docs/api-reference/next.config.js/trailing-slash.md +++ b/docs/api-reference/next.config.js/trailing-slash.md @@ -4,7 +4,14 @@ description: Configure Next.js pages to resolve with or without a trailing slash # Trailing Slash -> This feature was introduced in [Next.js 9.5](https://nextjs.org/blog/next-9-5) and up. If you’re using older versions of Next.js, please upgrade before trying it out. +
+ Version History + +| Version | Changes | +| -------- | --------------------- | +| `v9.5.0` | Trailing Slash added. | + +
By default Next.js will redirect urls with trailing slashes to their counterpart without a trailing slash. For example `/about/` will redirect to `/about`. You can configure this behavior to act the opposite way, where urls without trailing slashes are redirected to their counterparts with trailing slashes. diff --git a/docs/api-reference/next/router.md b/docs/api-reference/next/router.md index 4922e9196d632..5e095a553099f 100644 --- a/docs/api-reference/next/router.md +++ b/docs/api-reference/next/router.md @@ -74,6 +74,7 @@ router.push(url, as, options) - `options` - Optional object with the following configuration options: - `scroll` - Optional boolean, controls scrolling to the top of the page after navigation. Defaults to `true` - [`shallow`](/docs/routing/shallow-routing.md): Update the path of the current page without rerunning [`getStaticProps`](/docs/basic-features/data-fetching.md#getstaticprops-static-generation), [`getServerSideProps`](/docs/basic-features/data-fetching.md#getserversideprops-server-side-rendering) or [`getInitialProps`](/docs/api-reference/data-fetching/getInitialProps.md). Defaults to `false` + - `locale` - Optional string, indicates locale of the new page > You don't need to use `router.push` for external URLs. [window.location](https://developer.mozilla.org/en-US/docs/Web/API/Window/location) is better suited for those cases. diff --git a/docs/api-routes/response-helpers.md b/docs/api-routes/response-helpers.md index 61baff4d5f1e3..0418b2362e446 100644 --- a/docs/api-routes/response-helpers.md +++ b/docs/api-routes/response-helpers.md @@ -23,7 +23,7 @@ export default function handler(req, res) { The included helpers are: - `res.status(code)` - A function to set the status code. `code` must be a valid [HTTP status code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) -- `res.json(json)` - Sends a JSON response. `json` must be a valid JSON object +- `res.json(body)` - Sends a JSON response. `body` must be a [serialiazable object](https://developer.mozilla.org/en-US/docs/Glossary/Serialization) - `res.send(body)` - Sends the HTTP response. `body` can be a `string`, an `object` or a `Buffer` - `res.redirect([status,] path)` - Redirects to a specified path or URL. `status` must be a valid [HTTP status code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes). If not specified, `status` defaults to "307" "Temporary redirect". diff --git a/docs/basic-features/built-in-css-support.md b/docs/basic-features/built-in-css-support.md index 14be187874a1c..b160a2340484c 100644 --- a/docs/basic-features/built-in-css-support.md +++ b/docs/basic-features/built-in-css-support.md @@ -181,19 +181,6 @@ module.exports = { } ``` -## Less and Stylus Support - -To support importing `.less` or `.styl` files you can use the following plugins: - -- [@zeit/next-less](https://github.com/vercel/next-plugins/tree/master/packages/next-less) -- [@zeit/next-stylus](https://github.com/vercel/next-plugins/tree/master/packages/next-stylus) - -If using the less plugin, don't forget to add a dependency on less as well, otherwise you'll see an error like: - -```bash -Error: Cannot find module 'less' -``` - ## CSS-in-JS
diff --git a/docs/basic-features/data-fetching.md b/docs/basic-features/data-fetching.md index 99271082cea90..cf0dbe880712e 100644 --- a/docs/basic-features/data-fetching.md +++ b/docs/basic-features/data-fetching.md @@ -222,20 +222,27 @@ export default Blog ### Incremental Static Regeneration -> This feature was introduced in [Next.js 9.5](https://nextjs.org/blog/next-9-5#stable-incremental-static-regeneration) and up. If you’re using older versions of Next.js, please upgrade before trying Incremental Static Regeneration. -
Examples
-With [`getStaticProps`](#getstaticprops-static-generation) you don't have to stop relying on dynamic content, as **static content can also be dynamic**. Incremental Static Regeneration allows you to update _existing_ pages by re-rendering them in the background as traffic comes in. +
+ Version History + +| Version | Changes | +| -------- | ---------------- | +| `v9.5.0` | Base Path added. | -Inspired by [stale-while-revalidate](https://tools.ietf.org/html/rfc5861), background regeneration ensures traffic is served uninterruptedly, always from static storage, and the newly built page is pushed only after it's done generating. +
-Consider our previous [`getStaticProps` example](#simple-example), but now with regeneration enabled: +Next.js allows you to create or update static pages _after_ you’ve built your site. Incremental Static Regeneration (ISR) enables you to use static-generation on a per-page basis, **without needing to rebuild the entire site**. With ISR, you can retain the benefits of static while scaling to millions of pages. + +Consider our previous [`getStaticProps` example](#simple-example), but now with Incremental Static Regeneration enabled through the `revalidate` property: ```jsx function Blog({ posts }) { @@ -261,25 +268,42 @@ export async function getStaticProps() { }, // Next.js will attempt to re-generate the page: // - When a request comes in - // - At most once every second - revalidate: 1, // In seconds + // - At most once every 10 seconds + revalidate: 10, // In seconds } } +// This function gets called at build time on server-side. +// It may be called again, on a serverless function, if +// the path has not been generated. +export async function getStaticPaths() { + const res = await fetch('https://.../posts') + const posts = await res.json() + + // Get the paths we want to pre-render based on posts + const paths = posts.map((post) => ({ + params: { id: post.id }, + })) + + // We'll pre-render only these paths at build time. + // { fallback: blocking } will server-render pages + // on-demand if the path doesn't exist. + return { paths, fallback: 'blocking' } +} + export default Blog ``` -Now the list of blog posts will be revalidated once per second; if you add a new blog post it will be available almost immediately, without having to re-build your app or make a new deployment. - -This works perfectly with [`fallback: true`](#fallback-true). Because now you can have a list of posts that's always up to date with the latest posts, and have a [blog post page](#fallback-pages) that generates blog posts on-demand, no matter how many posts you add or update. +When a request is made to a page that was pre-rendered at build time, it will initially show the cached page. -#### Static content at scale +- Any requests to the page after the initial request and before 10 seconds are also cached and instantaneous. +- After the 10-second window, the next request will still show the cached (stale) page +- Next.js triggers a regeneration of the page in the background. +- Once the page has been successfully generated, Next.js will invalidate the cache and show the updated product page. If the background regeneration fails, the old page remains unaltered. -Unlike traditional SSR, [Incremental Static Regeneration](#incremental-static-regeneration) ensures you retain the benefits of static: +When a request is made to a path that hasn’t been generated, Next.js will server-render the page on the first request. Future requests will serve the static file from the cache. -- No spikes in latency. Pages are served consistently fast -- Pages never go offline. If the background page re-generation fails, the old page remains unaltered -- Low database and backend load. Pages are re-computed at most once concurrently +To learn how to persist the cache globally and handle rollbacks, learn more about [Incremental Static Regeneration](https://vercel.com/docs/next.js/incremental-static-regeneration). ### Reading files: Use `process.cwd()` diff --git a/docs/basic-features/font-optimization.md b/docs/basic-features/font-optimization.md index aebf952d77376..1c63fda0332ca 100644 --- a/docs/basic-features/font-optimization.md +++ b/docs/basic-features/font-optimization.md @@ -74,7 +74,7 @@ class MyDocument extends Document { export default MyDocument ``` -Automatic Webfont Optimization currently supports Google Fonts, with support for other font providers coming soon. We're also planning to add control over [loading strategies](https://github.com/vercel/next.js/issues/21555) and `font-display` values. +Automatic Webfont Optimization currently supports Google Fonts and Typekit with support for other font providers coming soon. We're also planning to add control over [loading strategies](https://github.com/vercel/next.js/issues/21555) and `font-display` values. ## Disabling Optimization diff --git a/docs/getting-started.md b/docs/getting-started.md index 6de8da2f92a16..9c7d752b89959 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -27,6 +27,14 @@ npx create-next-app yarn create next-app ``` +If you want to start with a TypeScript project you can use the `--typescript` flag: + +```bash +npx create-next-app --typescript +# or +yarn create next-app --typescript +``` + After the installation is complete, follow the instructions to start the development server. Try editing `pages/index.js` and see the result on your browser. For more information on how to use `create-next-app`, you can review the [`create-next-app` documentation](/docs/api-reference/create-next-app.md) diff --git a/errors/manifest.json b/errors/manifest.json index 431783539f372..70862c14293a0 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -267,6 +267,10 @@ "title": "no-sync-scripts", "path": "/errors/no-sync-scripts.md" }, + { + "title": "no-title-in-document-head", + "path": "/errors/no-title-in-document-head.md" + }, { "title": "no-unwanted-polyfillio", "path": "/errors/no-unwanted-polyfillio.md" diff --git a/errors/no-title-in-document-head.md b/errors/no-title-in-document-head.md new file mode 100644 index 0000000000000..771e99674d5c4 --- /dev/null +++ b/errors/no-title-in-document-head.md @@ -0,0 +1,30 @@ +# No Title in Document Head + +### Why This Error Occurred + +A `` element was defined within the `Head` component imported from `next/document`, which should only be used for any `<head>` code that is common for all pages. Title tags should be defined at the page-level using `next/head`. + +### Possible Ways to Fix It + +Within a page or component, import and use `next/head` to define a page title: + +```jsx +import Head from 'next/head' + +export class Home { + render() { + return ( + <div> + <Head> + <title>My page title + + + ) + } +} +``` + +### Useful links + +- [next/head](https://nextjs.org/docs/api-reference/next/head) +- [Custom Document](https://nextjs.org/docs/advanced-features/custom-document) diff --git a/errors/prerender-error.md b/errors/prerender-error.md index 77e564fd683f6..87e301c459db6 100644 --- a/errors/prerender-error.md +++ b/errors/prerender-error.md @@ -10,3 +10,4 @@ While prerendering a page an error occurred. This can occur for many reasons fro - Check for any code that assumes a prop is available even when it might not be. e.g., have default data for all dynamic pages' props. - Check for any out of date modules that you might be relying on - Make sure your component handles `fallback` if it is enabled in `getStaticPaths`. [Fallback docs](https://nextjs.org/docs/basic-features/data-fetching#the-fallback-key-required) +- Make sure you are not trying to export (`next export`) pages that have server-side rendering enabled [(getServerSideProps)](https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering) diff --git a/examples/api-routes-apollo-server-and-client-auth/pages/signin.js b/examples/api-routes-apollo-server-and-client-auth/pages/signin.js index 0e652164cffef..59349fb0e57dc 100644 --- a/examples/api-routes-apollo-server-and-client-auth/pages/signin.js +++ b/examples/api-routes-apollo-server-and-client-auth/pages/signin.js @@ -65,7 +65,7 @@ function SignIn() { label="Password" /> or{' '} - + Sign up diff --git a/examples/api-routes-apollo-server-and-client-auth/pages/signup.js b/examples/api-routes-apollo-server-and-client-auth/pages/signup.js index ce16f159e4c94..bcbf81f2410c3 100644 --- a/examples/api-routes-apollo-server-and-client-auth/pages/signup.js +++ b/examples/api-routes-apollo-server-and-client-auth/pages/signup.js @@ -60,7 +60,7 @@ function SignUp() { label="Password" /> or{' '} - + Sign in diff --git a/examples/with-react-hook-form/.gitignore b/examples/with-react-hook-form/.gitignore new file mode 100644 index 0000000000000..e3b3fe7726885 --- /dev/null +++ b/examples/with-react-hook-form/.gitignore @@ -0,0 +1,34 @@ +# 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 \ No newline at end of file diff --git a/examples/with-react-hook-form/README.md b/examples/with-react-hook-form/README.md new file mode 100644 index 0000000000000..20ef12175a311 --- /dev/null +++ b/examples/with-react-hook-form/README.md @@ -0,0 +1,23 @@ +# with react-hook-form + +This example shows how to integrate react-hook-form in Next.js + +Form handling doesn't have to be painful. React Hook Form will help you write less code while achieving better performance. For more information, see [react-hook-form](https://react-hook-form.com) + +## Deploy your own + +Deploy the example using [Vercel](https://vercel.com/now): + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-react-hook-form&project-name=with-react-hook-form&repository-name=with-react-hook-form) + +## How to use + +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 +npx create-next-app --example with-react-hook-form with-react-hook-form-app +# or +yarn create next-app --example with-react-hook-form with-react-hook-form-app +``` + +Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). diff --git a/examples/with-react-hook-form/package.json b/examples/with-react-hook-form/package.json new file mode 100644 index 0000000000000..60fa4a2191e6d --- /dev/null +++ b/examples/with-react-hook-form/package.json @@ -0,0 +1,16 @@ +{ + "name": "with-react-hook-form", + "version": "1.0.0", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "latest", + "react": "17.0.1", + "react-dom": "17.0.1", + "react-hook-form": "7.4.0" + }, + "license": "MIT" +} diff --git a/examples/with-react-hook-form/pages/_app.js b/examples/with-react-hook-form/pages/_app.js new file mode 100644 index 0000000000000..0bd950249faeb --- /dev/null +++ b/examples/with-react-hook-form/pages/_app.js @@ -0,0 +1,5 @@ +import '../styles/global.css' + +export default function MyApp({ Component, pageProps }) { + return +} diff --git a/examples/with-react-hook-form/pages/index.js b/examples/with-react-hook-form/pages/index.js new file mode 100644 index 0000000000000..b063590ee6044 --- /dev/null +++ b/examples/with-react-hook-form/pages/index.js @@ -0,0 +1,75 @@ +import { useState } from 'react' +import { useForm } from 'react-hook-form' + +const IndexPage = () => { + const [user, setUser] = useState() + const { + register, + formState: { errors }, + handleSubmit, + } = useForm() + const onSubmit = ({ username, password, remember }) => { + // You should handle login logic with username, password and remember form data + setUser({ name: username }) + } + + return ( +
+ {user ? ( + Hello, {user.name}! + ) : ( +
+
+

LOGIN

+
+
+ + {errors.username && ( + {errors.username.message} + )} +
+
+ + {errors.password && ( + {errors.password.message} + )} +
+
+ + +
+
+ +
+
+ )} +
+ ) +} + +export default IndexPage diff --git a/examples/with-react-hook-form/styles/global.css b/examples/with-react-hook-form/styles/global.css new file mode 100644 index 0000000000000..9cfcfe629d991 --- /dev/null +++ b/examples/with-react-hook-form/styles/global.css @@ -0,0 +1,97 @@ +@import url('https://fonts.googleapis.com/css2?family=Poppins&display=swap'); + +:root { + --white: #fff; + --light-border: #ccc; + --danger: #ff0000; + --primary-color: #a4a4f7; + --primary-color-light: #7373f9; + --secondary-color: #f0f8ff; + --shadow-color: #ddd; + font-family: 'Poppins', sans-serif; + font-size: 12px; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +.container { + display: flex; + height: 100vh; + justify-content: center; + align-items: center; +} + +.row { + margin: 10px 20px; +} + +.row-remember { + display: flex; + align-items: center; + border-bottom: 1px solid var(--light-border); + padding: 0 0 15px 0; +} + +.container form { + background-color: var(--secondary-color); + padding: 2rem; + border-radius: 5px; + box-shadow: 12px 12px 10px var(--shadow-color); + flex: 1; +} + +@media screen and (min-width: 768px) { + .container form { + flex: 0.5; + max-width: 600px; + } +} + +.form-header { + text-align: center; + font-size: 1.5rem; +} + +.form-field { + width: 100%; + padding: 10px; + outline: none; + border: 1px solid var(--light-border); + border-radius: 5px; +} + +.form-field.has-error { + border: 1px solid var(--danger); +} + +.error-label { + color: var(--danger); +} + +.remember-label { + margin: 0 10px; +} + +.btn { + width: 100%; + height: 3rem; + border: 1px solid var(--light-border); + border-radius: 5px; +} + +.btn:hover { + background-color: var(--primary-color-light); +} + +.login-btn { + background-color: var(--primary-color); + color: var(--white); +} + +.hello-user { + font-size: 2rem; +} diff --git a/examples/with-sentry/.env.local.example b/examples/with-sentry/.env.local.example deleted file mode 100644 index 24d834ca25973..0000000000000 --- a/examples/with-sentry/.env.local.example +++ /dev/null @@ -1,25 +0,0 @@ -NEXT_PUBLIC_SENTRY_DSN= - -# Only required to upload sourcemaps -# SENTRY_ORG= -# SENTRY_PROJECT= -# SENTRY_AUTH_TOKEN= -# -# Only required if sentry for organization -# Ex: https://sentry.ORG.com/ -# SENTRY_URL= -# -# For sourcemaps to work with server-side exceptions, the file path of the -# uploaded .map file needs to match the file paths in Error.stack. In Node.js, -# Error.stack file paths are absolute. Since the .map files we upload to Sentry -# have relative paths (~/_next), Error.stack needs to be rewritten to also use -# relative paths, which is handled in Sentry.init via Sentry's RewriteFrames -# integration. -# -# Normally, the root directory could be detected with __dirname, but __dirname -# isn't yet supported in Vercel serverless functions: -# https://github.com/vercel/next.js/issues/8251 -# -# To work around this issue, provide the root directory containing Next.js's -# build output here. In the Vercel environment, this is /var/task/. -# NEXT_PUBLIC_SENTRY_SERVER_ROOT_DIR= diff --git a/examples/with-sentry/README.md b/examples/with-sentry/README.md index f38bd64b1e3cb..a08bb22be75c0 100644 --- a/examples/with-sentry/README.md +++ b/examples/with-sentry/README.md @@ -1,18 +1,20 @@ # Sentry -This is an example showing how to use [Sentry](https://sentry.io) to catch & report errors on both client + server side. +This is an example showing how to use [Sentry](https://sentry.io) to catch & report errors on both client + server side, using the [official Sentry SDK for Next.js](https://docs.sentry.io/platforms/javascript/guides/nextjs/). -- `_app.js` renders on both the server and client. It initializes Sentry to catch any unhandled exceptions +- `_app.js` renders on both the server and client - `_error.js` is rendered by Next.js while handling certain types of exceptions for you. It is overridden so those exceptions can be passed along to Sentry -- Each API route also initializes Sentry, so it can work independently in the "serverless" build config -- `next.config.js` enables source maps in production, uploads them to a new Sentry release, and swaps out `@sentry/node` for `@sentry/browser` when building the client bundle +- Each API route is handled with `withSentry` +- `next.config.js` automatically configures the app to use Sentry through `withSentry` ## Deploy your own -Once you have access to your [Sentry DSN](#step-1-enable-error-tracking), deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): +Once you have access to your [Sentry DSN](https://docs.sentry.io/product/sentry-basics/dsn-explainer/#where-to-find-your-dsn), deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-sentry&project-name=with-sentry&repository-name=with-sentry&env=NEXT_PUBLIC_SENTRY_DSN&envDescription=DSN%20Key%20required%20by%20Sentry&envLink=https://github.com/vercel/next.js/tree/canary/examples/with-sentry%23step-1-enable-error-tracking) +Check out [Sentry’s Vercel Integration](#sentry-integration). + ## How To Use 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: @@ -25,61 +27,8 @@ yarn create next-app --example with-sentry with-sentry-app ## Configuration -### Step 1. Enable error tracking - -Copy the `.env.local.example` file in this directory to `.env.local` (which will be ignored by Git): - -```bash -cp .env.local.example .env.local -``` - -Next, Copy your Sentry DSN. You can get it from the settings of your project in **Client Keys (DSN)**. Then, copy the string labeled **DSN** and set it as the value for `NEXT_PUBLIC_SENTRY_DSN` inside `.env.local` - -> **Note:** Error tracking is disabled in development mode using the `NODE_ENV` environment variable. To change this behavior, remove the `enabled` property from the `Sentry.init()` call inside your `utils/sentry.js` file. - -### Step 2. Run Next.js in development mode - -```bash -npm install -npm run dev - -# or - -yarn install -yarn dev -``` - -Your app should be up and running on [http://localhost:3000](http://localhost:3000)! If it doesn't work, post on [GitHub discussions](https://github.com/vercel/next.js/discussions). - -### Step 3. Automatic sourcemap upload (optional) - -#### Using Vercel - -You will need to install and configure the [Sentry Vercel integration](https://docs.sentry.io/workflow/integrations/vercel). After you've completed the project linking step, all the needed environment variables will be set in your Vercel project, with the exception of `NEXT_PUBLIC_SENTRY_SERVER_ROOT_DIR`, which should be set to `/var/task/`. - -> **Note:** A Vercel project connected to a [Git integration](https://vercel.com/docs/platform/deployments#git) is required before adding the Sentry integration. - -#### Without Using Vercel - -1. Set up the `NEXT_PUBLIC_SENTRY_DSN` environment variable as described above. -2. Save your Sentry organization slug as the `SENTRY_ORG` environment variable and your project slug as the `SENTRY_PROJECT` environment variable in `.env.local`. -3. Save your git provider's commit SHA as `VERCEL_GIT_COMMIT_SHA` environment variable in `.env.local`. -4. Create an auth token in Sentry. The recommended way to do this is by creating a new internal integration for your organization. To do so, go into **Settings > Developer Settings > New internal integration**. After the integration is created, copy the Token. -5. Save the token inside the `SENTRY_AUTH_TOKEN` environment variable in `.env.local`. -6. Set `NEXT_PUBLIC_SENTRY_SERVER_ROOT_DIR` to the absolute path of the folder the Next.js app is running from - -> **Note:** Sourcemap upload is disabled in development mode using the `NODE_ENV` environment variable. To change this behavior, remove the `NODE_ENV === 'production'` check from your `next.config.js` file. - -## Other configuration options - -More configurations are available for the [Sentry webpack plugin](https://github.com/getsentry/sentry-webpack-plugin) using [Sentry Configuration variables](https://docs.sentry.io/cli/configuration/) for defining the releases/verbosity/etc. - -## Notes - -- By default, neither sourcemaps nor error tracking are enabled in development mode (see Configuration). -- When enabled in development mode, error handling [works differently than in production](https://nextjs.org/docs/advanced-features/custom-error-page#customizing-the-error-page) as `_error.js` is never actually called. -- The build output will contain warning about unhandled Promise rejections. This is caused by the test pages, and is expected. When deploying to Vercel, "Client Error 1" will actually be sent to Sentry during the build, while that test page is being statically rendered. -- By default, source maps are uploaded to [sentry.io](https://sentry.io). If you're self-hosting Sentry, add `SENTRY_URL` to `.env` or `.env.locale` and set it to the base domain of your installation, which by default is `https://sentry.io/`. +You can [configure your app automatically](https://docs.sentry.io/platforms/javascript/guides/nextjs/#configure) or do the [manual setup](https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/). +Both ways lead to having your custom config files (`next.config.js`, `sentry.client.config.js`, `sentry.server.config.js`, and `sentry.properties`); so you can delete them from the example, they are here to illustrate how an example app looks like. ## Deploy on Vercel @@ -96,3 +45,7 @@ To deploy your local project to Vercel, push it to GitHub/GitLab/Bitbucket and [ Alternatively, you can deploy using our template by clicking on the Deploy button below. [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-sentry&project-name=with-sentry&repository-name=with-sentry&env=NEXT_PUBLIC_SENTRY_DSN&envDescription=DSN%20Key%20required%20by%20Sentry&envLink=https://github.com/vercel/next.js/tree/canary/examples/with-sentry%23step-1-enable-error-tracking) + +## Sentry Integration + +Sentry’s Vercel Integration connects your Sentry and Vercel projects to automatically upload source maps and notify Sentry of release deployment. Learn more about this integration in [Sentry’s full documentation](https://docs.sentry.io/product/integrations/vercel/). diff --git a/examples/with-sentry/next.config.js b/examples/with-sentry/next.config.js index f23bbaa222670..a5aae5a754546 100644 --- a/examples/with-sentry/next.config.js +++ b/examples/with-sentry/next.config.js @@ -1,78 +1,24 @@ -// Use the SentryWebpack plugin to upload the source maps during build step -const SentryWebpackPlugin = require('@sentry/webpack-plugin') -const { - NEXT_PUBLIC_SENTRY_DSN: SENTRY_DSN, - SENTRY_ORG, - SENTRY_PROJECT, - SENTRY_AUTH_TOKEN, - NODE_ENV, - VERCEL_GIT_COMMIT_SHA, -} = process.env +// This file sets a custom webpack configuration to use your Next.js app +// with Sentry. +// https://nextjs.org/docs/api-reference/next.config.js/introduction +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ -process.env.SENTRY_DSN = SENTRY_DSN -const basePath = '' +const { withSentryConfig } = require('@sentry/nextjs') -module.exports = { - productionBrowserSourceMaps: true, - env: { - // Make the COMMIT_SHA available to the client so that Sentry events can be - // marked for the release they belong to. It may be undefined if running - // outside of Vercel - NEXT_PUBLIC_COMMIT_SHA: VERCEL_GIT_COMMIT_SHA, - }, - webpack: (config, options) => { - // In `pages/_app.js`, Sentry is imported from @sentry/browser. While - // @sentry/node will run in a Node.js environment. @sentry/node will use - // Node.js-only APIs to catch even more unhandled exceptions. - // - // This works well when Next.js is SSRing your page on a server with - // Node.js, but it is not what we want when your client-side bundle is being - // executed by a browser. - // - // Luckily, Next.js will call this webpack function twice, once for the - // server and once for the client. Read more: - // https://nextjs.org/docs/api-reference/next.config.js/custom-webpack-config - // - // So ask Webpack to replace @sentry/node imports with @sentry/browser when - // building the browser's bundle - if (!options.isServer) { - config.resolve.alias['@sentry/node'] = '@sentry/browser' - } - - // Define an environment variable so source code can check whether or not - // it's running on the server so we can correctly initialize Sentry - config.plugins.push( - new options.webpack.DefinePlugin({ - 'process.env.NEXT_IS_SERVER': JSON.stringify( - options.isServer.toString() - ), - }) - ) +const moduleExports = { + // Your existing module.exports +} - // When all the Sentry configuration env variables are available/configured - // The Sentry webpack plugin gets pushed to the webpack plugins to build - // and upload the source maps to sentry. - // This is an alternative to manually uploading the source maps - // Note: This is disabled in development mode. - if ( - SENTRY_DSN && - SENTRY_ORG && - SENTRY_PROJECT && - SENTRY_AUTH_TOKEN && - VERCEL_GIT_COMMIT_SHA && - NODE_ENV === 'production' - ) { - config.plugins.push( - new SentryWebpackPlugin({ - include: '.next', - ignore: ['node_modules'], - stripPrefix: ['webpack://_N_E/'], - urlPrefix: `~${basePath}/_next`, - release: VERCEL_GIT_COMMIT_SHA, - }) - ) - } - return config - }, - basePath, +const SentryWebpackPluginOptions = { + // Additional config options for the Sentry Webpack plugin. Keep in mind that + // the following options are set automatically, and overriding them is not + // recommended: + // release, url, org, project, authToken, configFile, stripPrefix, + // urlPrefix, include, ignore + // For all available options, see: + // https://github.com/getsentry/sentry-webpack-plugin#options. } + +// Make sure adding Sentry options is the last code to run before exporting, to +// ensure that your source maps include changes from all other Webpack plugins +module.exports = withSentryConfig(moduleExports, SentryWebpackPluginOptions) diff --git a/examples/with-sentry/package.json b/examples/with-sentry/package.json index ceeab004b5515..1a3d3f5796afc 100644 --- a/examples/with-sentry/package.json +++ b/examples/with-sentry/package.json @@ -8,10 +8,7 @@ "start": "next start" }, "dependencies": { - "@sentry/browser": "^5.21.3", - "@sentry/integrations": "^5.21.3", - "@sentry/node": "^5.21.3", - "@sentry/webpack-plugin": "^1.12.1", + "@sentry/nextjs": "^6.3.5", "next": "latest", "react": "^16.8.6", "react-dom": "^16.8.6" diff --git a/examples/with-sentry/pages/_app.js b/examples/with-sentry/pages/_app.js index 98d5042c2e26b..2ff89fd9b6fab 100644 --- a/examples/with-sentry/pages/_app.js +++ b/examples/with-sentry/pages/_app.js @@ -1,7 +1,3 @@ -import { init } from '../utils/sentry' - -init() - export default function App({ Component, pageProps, err }) { // Workaround for https://github.com/vercel/next.js/issues/8592 return diff --git a/examples/with-sentry/pages/_error.js b/examples/with-sentry/pages/_error.js index f34ff9cea2e60..003b2c873a255 100644 --- a/examples/with-sentry/pages/_error.js +++ b/examples/with-sentry/pages/_error.js @@ -1,5 +1,6 @@ import NextErrorComponent from 'next/error' -import * as Sentry from '@sentry/node' + +import * as Sentry from '@sentry/nextjs' const MyError = ({ statusCode, hasGetInitialPropsRun, err }) => { if (!hasGetInitialPropsRun && err) { diff --git a/examples/with-sentry/pages/api/test1.js b/examples/with-sentry/pages/api/test1.js index 546d3215d0714..461c2353bbb52 100644 --- a/examples/with-sentry/pages/api/test1.js +++ b/examples/with-sentry/pages/api/test1.js @@ -1,10 +1,10 @@ -import { init } from '../../utils/sentry' - -init() +import { withSentry } from '@sentry/nextjs' const doAsyncWork = () => Promise.reject(new Error('API Test 1')) doAsyncWork() -export default async function handler(req, res) { +async function handler(req, res) { res.status(200).json({ name: 'John Doe' }) } + +export default withSentry(handler) diff --git a/examples/with-sentry/pages/api/test2.js b/examples/with-sentry/pages/api/test2.js index 3df4ab6372896..87001cffcf9fc 100644 --- a/examples/with-sentry/pages/api/test2.js +++ b/examples/with-sentry/pages/api/test2.js @@ -1,6 +1,4 @@ -import { init } from '../../utils/sentry' - -init() +import { withSentry } from '@sentry/nextjs' function work() { throw new Error('API Test 2') @@ -8,6 +6,8 @@ function work() { work() -export default async function handler(req, res) { +async function handler(req, res) { res.status(200).json({ name: 'John Doe' }) } + +export default withSentry(handler) diff --git a/examples/with-sentry/pages/api/test3.js b/examples/with-sentry/pages/api/test3.js index 152c058a33df2..13cf097741580 100644 --- a/examples/with-sentry/pages/api/test3.js +++ b/examples/with-sentry/pages/api/test3.js @@ -1,13 +1,13 @@ -import { init } from '../../utils/sentry' - -init() +import { withSentry } from '@sentry/nextjs' function work() { throw new Error('API Test 3') } -export default async function handler(req, res) { +async function handler(req, res) { work() res.status(200).json({ name: 'John Doe' }) } + +export default withSentry(handler) diff --git a/examples/with-sentry/pages/api/test4.js b/examples/with-sentry/pages/api/test4.js index 699569c781b38..f4724110c0e10 100644 --- a/examples/with-sentry/pages/api/test4.js +++ b/examples/with-sentry/pages/api/test4.js @@ -1,10 +1,6 @@ -import * as Sentry from '@sentry/node' +import * as Sentry from '@sentry/nextjs' -import { init } from '../../utils/sentry' - -init() - -export default async function handler(req, res) { +async function handler(req, res) { try { throw new Error('API Test 4') } catch (error) { @@ -16,3 +12,5 @@ export default async function handler(req, res) { await Sentry.flush(2000) res.status(200).json({ name: 'John Doe' }) } + +export default Sentry.withSentry(handler) diff --git a/examples/with-sentry/pages/ssr/test4.js b/examples/with-sentry/pages/ssr/test4.js index e828894825c61..737339f85dd3d 100644 --- a/examples/with-sentry/pages/ssr/test4.js +++ b/examples/with-sentry/pages/ssr/test4.js @@ -1,4 +1,4 @@ -import * as Sentry from '@sentry/node' +import * as Sentry from '@sentry/nextjs' const Test4 = () =>

SSR Test 4

diff --git a/examples/with-sentry/sentry.client.config.js b/examples/with-sentry/sentry.client.config.js new file mode 100644 index 0000000000000..eca167eb76fdf --- /dev/null +++ b/examples/with-sentry/sentry.client.config.js @@ -0,0 +1,14 @@ +// This file configures the intialization of Sentry on the browser. +// The config you add here will be used whenever a page is visited. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from '@sentry/nextjs' + +const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN + +Sentry.init({ + dsn: SENTRY_DSN, + // Note: if you want to override the automatic release value, do not set a + // `release` value here - use the environment variable `SENTRY_RELEASE`, so + // that it will also get attached to your source maps +}) diff --git a/examples/with-sentry/sentry.properties b/examples/with-sentry/sentry.properties new file mode 100644 index 0000000000000..3837f095ce042 --- /dev/null +++ b/examples/with-sentry/sentry.properties @@ -0,0 +1,5 @@ +defaults.url=# your base url +defaults.org=# your org +defaults.project=# your project +auth.token=# your auth token +cli.executable=# [optional] path to the cli executable diff --git a/examples/with-sentry/sentry.server.config.js b/examples/with-sentry/sentry.server.config.js new file mode 100644 index 0000000000000..f1ff06b8e5eed --- /dev/null +++ b/examples/with-sentry/sentry.server.config.js @@ -0,0 +1,14 @@ +// This file configures the initialization of Sentry on the server. +// The config you add here will be used whenever the server handles a request. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from '@sentry/nextjs' + +const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN + +Sentry.init({ + dsn: SENTRY_DSN, + // Note: if you want to override the automatic release value, do not set a + // `release` value here - use the environment variable `SENTRY_RELEASE`, so + // that it will also get attached to your source maps +}) diff --git a/examples/with-sentry/utils/sentry.js b/examples/with-sentry/utils/sentry.js deleted file mode 100644 index 22fe73507c192..0000000000000 --- a/examples/with-sentry/utils/sentry.js +++ /dev/null @@ -1,35 +0,0 @@ -import * as Sentry from '@sentry/node' -import { RewriteFrames } from '@sentry/integrations' - -export const init = () => { - if (process.env.NEXT_PUBLIC_SENTRY_DSN) { - const integrations = [] - if ( - process.env.NEXT_IS_SERVER === 'true' && - process.env.NEXT_PUBLIC_SENTRY_SERVER_ROOT_DIR - ) { - // For Node.js, rewrite Error.stack to use relative paths, so that source - // maps starting with ~/_next map to files in Error.stack with path - // app:///_next - integrations.push( - new RewriteFrames({ - iteratee: (frame) => { - frame.filename = frame.filename.replace( - process.env.NEXT_PUBLIC_SENTRY_SERVER_ROOT_DIR, - 'app:///' - ) - frame.filename = frame.filename.replace('.next', '_next') - return frame - }, - }) - ) - } - - Sentry.init({ - enabled: process.env.NODE_ENV === 'production', - integrations, - dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, - release: process.env.NEXT_PUBLIC_COMMIT_SHA, - }) - } -} diff --git a/examples/with-storybook/.gitignore b/examples/with-storybook/.gitignore index a423604f97788..5f31e17fdd42c 100644 --- a/examples/with-storybook/.gitignore +++ b/examples/with-storybook/.gitignore @@ -34,4 +34,4 @@ yarn-error.log* .vercel # Storybook -/storybook-static \ No newline at end of file +/storybook-static diff --git a/examples/with-storybook/package.json b/examples/with-storybook/package.json index 3b151db4e3b42..2e6727d545bc0 100644 --- a/examples/with-storybook/package.json +++ b/examples/with-storybook/package.json @@ -7,7 +7,8 @@ "build": "next build", "start": "next start", "storybook": "start-storybook -p 6006", - "build-storybook": "build-storybook" + "build-storybook": "build-storybook -s public/", + "serve-storybook": "serve storybook-static" }, "dependencies": { "next": "latest", @@ -19,6 +20,7 @@ "@storybook/addon-essentials": "6.0.26", "@storybook/addon-links": "6.0.26", "@storybook/react": "6.0.26", - "babel-loader": "^8.0.5" + "babel-loader": "^8.0.5", + "serve": "11.3.2" } } diff --git a/examples/with-storybook/public/serve.json b/examples/with-storybook/public/serve.json new file mode 100644 index 0000000000000..0967ef424bce6 --- /dev/null +++ b/examples/with-storybook/public/serve.json @@ -0,0 +1 @@ +{} diff --git a/examples/with-supertokens/README.md b/examples/with-supertokens/README.md index f8c8232b192f4..e9e856ae58c63 100644 --- a/examples/with-supertokens/README.md +++ b/examples/with-supertokens/README.md @@ -10,7 +10,7 @@ Deploy the example using [Vercel](https://vercel.com): ## How to use -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: +- 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 npx create-next-app --example with-supertokens with-supertokens-app @@ -18,8 +18,14 @@ npx create-next-app --example with-supertokens with-supertokens-app yarn create next-app --example with-supertokens with-supertokens-app ``` +- Run `yarn install` + +- Run `npm run dev` to start the application on `http://localhost:3000`. + ## Configuration +> Until you do this, social login will not work. But you can still try out email password sign up / in. + - Create a `.env.local` file and copy the content of `.env.local.example` into it: ```bash diff --git a/examples/with-supertokens/config/supertokensConfig.js b/examples/with-supertokens/config/supertokensConfig.js index 720cc8d247049..b6509c888f13e 100644 --- a/examples/with-supertokens/config/supertokensConfig.js +++ b/examples/with-supertokens/config/supertokensConfig.js @@ -49,7 +49,6 @@ export let backendConfig = () => { export let frontendConfig = () => { return { - useReactRouterDom: false, appInfo, recipeList: [ ThirdPartyEmailPasswordReact.init({ diff --git a/examples/with-supertokens/package.json b/examples/with-supertokens/package.json index b529f3023465a..ac40d1262d1a4 100644 --- a/examples/with-supertokens/package.json +++ b/examples/with-supertokens/package.json @@ -11,8 +11,8 @@ "next": "latest", "react": "17.0.1", "react-dom": "17.0.1", - "supertokens-auth-react": "^0.9.0", - "supertokens-node": "^4.3.0" + "supertokens-auth-react": "^0.12.0", + "supertokens-node": "^5.0.0" }, "license": "MIT" } diff --git a/examples/with-zustand/README.md b/examples/with-zustand/README.md index fcfa14abbf05a..83aff9f87c174 100644 --- a/examples/with-zustand/README.md +++ b/examples/with-zustand/README.md @@ -10,7 +10,7 @@ To illustrate SSG and SSR, go to `/ssg` and `/ssr`, those pages are using Next.j The trick here for supporting universal Zustand is to separate the cases for the client and the server. When we are on the server we want to create a new store every time, otherwise different users data will be mixed up. If we are in the client we want to use always the same store. That's what we accomplish on `store.js`. -All components have access to the Zustand store using `useStore`. +All components have access to the Zustand store using `useStore()` returned from zustand's `createContext()` function. On the server side every request initializes a new store, because otherwise different user data can be mixed up. On the client side the same store is used, even between page changes. diff --git a/examples/with-zustand/components/clock.js b/examples/with-zustand/components/clock.js index 4e44981951cc5..04f53bd18a1ae 100644 --- a/examples/with-zustand/components/clock.js +++ b/examples/with-zustand/components/clock.js @@ -1,4 +1,4 @@ -import { useStore } from '../lib/zustandProvider' +import { useStore } from '../lib/store' import shallow from 'zustand/shallow' const useClock = () => { diff --git a/examples/with-zustand/components/counter.js b/examples/with-zustand/components/counter.js index 7514be0e30277..338da5563cd93 100644 --- a/examples/with-zustand/components/counter.js +++ b/examples/with-zustand/components/counter.js @@ -1,4 +1,4 @@ -import { useStore } from '../lib/zustandProvider' +import { useStore } from '../lib/store' import shallow from 'zustand/shallow' const useCounter = () => { const { count, increment, decrement, reset } = useStore( diff --git a/examples/with-zustand/components/page.js b/examples/with-zustand/components/page.js index 9bfec37916441..1f669f1d50b43 100644 --- a/examples/with-zustand/components/page.js +++ b/examples/with-zustand/components/page.js @@ -2,7 +2,7 @@ import useInterval from '../lib/useInterval' import Clock from './clock' import Counter from './counter' import Nav from './nav' -import { useStore } from '../lib/zustandProvider' +import { useStore } from '../lib/store' export default function Page() { const { tick } = useStore() diff --git a/examples/with-zustand/lib/store.js b/examples/with-zustand/lib/store.js index 3a064fcabefef..49bc825adc535 100644 --- a/examples/with-zustand/lib/store.js +++ b/examples/with-zustand/lib/store.js @@ -1,5 +1,6 @@ -import { useMemo } from 'react' +import { useLayoutEffect } from 'react' import create from 'zustand' +import createContext from 'zustand/context' let store @@ -9,7 +10,9 @@ const initialState = { count: 0, } -function initStore(preloadedState = initialState) { +export const { Provider, useStore } = createContext(initialState) + +export const initializeStore = (preloadedState = {}) => { return create((set, get) => ({ ...initialState, ...preloadedState, @@ -37,31 +40,30 @@ function initStore(preloadedState = initialState) { })) } -export const initializeStore = (preloadedState) => { - let _store = store ?? initStore(preloadedState) +export function useHydrate(initialState) { + let _store = store ?? initializeStore(initialState) + + // For SSR & SSG, always use a new store. + if (typeof window !== 'undefined') { + // For CSR, always re-use same store. + if (!store) { + store = _store + } - // After navigating to a page with an initial Zustand state, merge that state - // with the current state in the store, and create a new store - if (preloadedState && store) { - _store = initStore({ - ...store.getState(), - ...preloadedState, - }) - // Reset the current store - store = undefined + // And if initialState changes, then merge states in the next render cycle. + // + // eslint complaining "React Hooks must be called in the exact same order in every component render" + // is ignorable as this code runs in the same order in a given environment (CSR/SSR/SSG) + // eslint-disable-next-line react-hooks/rules-of-hooks + useLayoutEffect(() => { + if (initialState && store) { + store.setState({ + ...store.getState(), + ...initialState, + }) + } + }, [initialState]) } - // For SSG and SSR always create a new store - if (typeof window === 'undefined') return _store - // Create the store once in the client - if (!store) store = _store - return _store } - -export function useHydrate(initialState) { - const state = - typeof initialState === 'string' ? JSON.parse(initialState) : initialState - const store = useMemo(() => initializeStore(state), [state]) - return store -} diff --git a/examples/with-zustand/lib/zustandProvider.js b/examples/with-zustand/lib/zustandProvider.js deleted file mode 100644 index d9fa9de31f133..0000000000000 --- a/examples/with-zustand/lib/zustandProvider.js +++ /dev/null @@ -1,14 +0,0 @@ -import { createContext, useContext } from 'react' - -export const StoreContext = createContext(null) - -export const StoreProvider = ({ children, store }) => { - return {children} -} - -export const useStore = (selector, eqFn) => { - const store = useContext(StoreContext) - const values = store(selector, eqFn) - - return values -} diff --git a/examples/with-zustand/package.json b/examples/with-zustand/package.json index 43ed3b892251b..1440d8d38a974 100644 --- a/examples/with-zustand/package.json +++ b/examples/with-zustand/package.json @@ -10,7 +10,7 @@ "next": "latest", "react": "^16.9.0", "react-dom": "^16.9.0", - "zustand": "3.1.3" + "zustand": "3.5.0" }, "license": "MIT" } diff --git a/examples/with-zustand/pages/_app.js b/examples/with-zustand/pages/_app.js index fef3255d9c39f..082837c7aac24 100644 --- a/examples/with-zustand/pages/_app.js +++ b/examples/with-zustand/pages/_app.js @@ -1,12 +1,10 @@ -import { StoreProvider } from '../lib/zustandProvider' -import { useHydrate } from '../lib/store' +import { useHydrate, Provider } from '../lib/store' export default function App({ Component, pageProps }) { const store = useHydrate(pageProps.initialZustandState) - return ( - + - + ) } diff --git a/examples/with-zustand/pages/ssr.js b/examples/with-zustand/pages/ssr.js index fd1c9190d9ba7..c7ba357021700 100644 --- a/examples/with-zustand/pages/ssr.js +++ b/examples/with-zustand/pages/ssr.js @@ -14,6 +14,10 @@ export function getServerSideProps() { zustandStore.getState().tick(Date.now(), false) return { - props: { initialZustandState: JSON.stringify(zustandStore.getState()) }, + props: { + // the "stringify and then parse again" piece is required as next.js + // isn't able to serialize it to JSON properly + initialZustandState: JSON.parse(JSON.stringify(zustandStore.getState())), + }, } } diff --git a/packages/create-next-app/README.md b/packages/create-next-app/README.md index 35ff93340914f..deeb80d655671 100644 --- a/packages/create-next-app/README.md +++ b/packages/create-next-app/README.md @@ -16,6 +16,7 @@ npx create-next-app blog-app `create-next-app` comes with the following options: +- **--ts, --typescript** - Initialize as a TypeScript project. - **-e, --example [name]|[github-url]** - An example to bootstrap the app with. You can use an example name from the [Next.js repo](https://github.com/vercel/next.js/tree/master/examples) or a GitHub URL. The URL can use any branch and/or subdirectory. - **--example-path <path-to-example>** - In a rare case, your GitHub URL might contain a branch name with a slash (e.g. bug/fix-1) and the path to the example (e.g. foo/bar). In this case, you must specify the path to the example separately: `--example-path foo/bar` - **--use-npm** - Explicitly tell the CLI to bootstrap the app using npm. Yarn will be used by default if it's installed diff --git a/packages/create-next-app/create-app.ts b/packages/create-next-app/create-app.ts index 2db10c89c0756..577ed31e1fa39 100644 --- a/packages/create-next-app/create-app.ts +++ b/packages/create-next-app/create-app.ts @@ -28,13 +28,16 @@ export async function createApp({ useNpm, example, examplePath, + typescript, }: { appPath: string useNpm: boolean example?: string examplePath?: string + typescript?: boolean }): Promise { let repoInfo: RepoInfo | undefined + const template = typescript ? 'typescript' : 'default' if (example) { let repoUrl: URL | undefined @@ -124,6 +127,9 @@ export async function createApp({ process.chdir(root) if (example) { + /** + * If an example repository is provided, clone it. + */ try { if (repoInfo) { const repoInfo2 = repoInfo @@ -154,7 +160,7 @@ export async function createApp({ const ignorePath = path.join(root, '.gitignore') if (!fs.existsSync(ignorePath)) { fs.copyFileSync( - path.join(__dirname, 'templates', 'default', 'gitignore'), + path.join(__dirname, 'templates', template, 'gitignore'), ignorePath ) } @@ -165,30 +171,83 @@ export async function createApp({ await install(root, null, { useYarn, isOnline }) console.log() } else { + /** + * Otherwise, if an example repository is not provided for cloning, proceed + * by installing from a template. + */ + console.log(chalk.bold(`Using ${displayedCommand}.`)) + /** + * Create a package.json for the new project. + */ const packageJson = { name: appName, version: '0.1.0', private: true, - scripts: { dev: 'next dev', build: 'next build', start: 'next start' }, + scripts: { + dev: 'next dev', + build: 'next build', + start: 'next start', + }, } + /** + * Write it to disk. + */ fs.writeFileSync( path.join(root, 'package.json'), JSON.stringify(packageJson, null, 2) + os.EOL ) + /** + * These flags will be passed to `install()`. + */ + const installFlags = { useYarn, isOnline } + /** + * Default dependencies. + */ + const dependencies = ['react', 'react-dom', 'next'] + /** + * Default devDependencies. + */ + const devDependencies = [] + /** + * TypeScript projects will have type definitions and other devDependencies. + */ + if (typescript) { + devDependencies.push('typescript', '@types/react', '@types/next') + } + /** + * Install package.json dependencies if they exist. + */ + if (dependencies.length) { + console.log() + console.log('Installing dependencies:') + for (const dependency of dependencies) { + console.log(`- ${chalk.cyan(dependency)}`) + } + console.log() - console.log( - `Installing ${chalk.cyan('react')}, ${chalk.cyan( - 'react-dom' - )}, and ${chalk.cyan('next')} using ${displayedCommand}...` - ) - console.log() + await install(root, dependencies, installFlags) + } + /** + * Install package.json devDependencies if they exist. + */ + if (devDependencies.length) { + console.log() + console.log('Installing devDependencies:') + for (const devDependency of devDependencies) { + console.log(`- ${chalk.cyan(devDependency)}`) + } + console.log() - await install(root, ['react', 'react-dom', 'next'], { useYarn, isOnline }) + const devInstallFlags = { devDependencies: true, ...installFlags } + await install(root, devDependencies, devInstallFlags) + } console.log() - + /** + * Copy the template files to the target directory. + */ await cpy('**', root, { parents: true, - cwd: path.join(__dirname, 'templates', 'default'), + cwd: path.join(__dirname, 'templates', template), rename: (name) => { switch (name) { case 'gitignore': { diff --git a/packages/create-next-app/helpers/install.ts b/packages/create-next-app/helpers/install.ts index 872c8e0be2976..f1c814f58d92a 100644 --- a/packages/create-next-app/helpers/install.ts +++ b/packages/create-next-app/helpers/install.ts @@ -2,41 +2,98 @@ import chalk from 'chalk' import spawn from 'cross-spawn' +interface InstallArgs { + /** + * Indicate whether to install packages using Yarn. + */ + useYarn: boolean + /** + * Indicate whether there is an active Internet connection. + */ + isOnline: boolean + /** + * Indicate whether the given dependencies are devDependencies. + */ + devDependencies?: boolean +} + +/** + * Spawn a package manager installation with either Yarn or NPM. + * + * @returns A Promise that resolves once the installation is finished. + */ export function install( root: string, dependencies: string[] | null, - { useYarn, isOnline }: { useYarn: boolean; isOnline: boolean } + { useYarn, isOnline, devDependencies }: InstallArgs ): Promise { + /** + * NPM-specific command-line flags. + */ + const npmFlags: string[] = ['--logLevel', 'error'] + /** + * Yarn-specific command-line flags. + */ + const yarnFlags: string[] = [] + /** + * Return a Promise that resolves once the installation is finished. + */ return new Promise((resolve, reject) => { - let command: string let args: string[] - if (useYarn) { - command = 'yarnpkg' - args = dependencies ? ['add', '--exact'] : ['install'] - if (!isOnline) { - args.push('--offline') - } - if (dependencies) { + let command: string = useYarn ? 'yarnpkg' : 'npm' + + if (dependencies && dependencies.length) { + /** + * If there are dependencies, run a variation of `{displayCommand} add`. + */ + if (useYarn) { + /** + * Call `yarn add --exact (--offline)? (-D)? ...`. + */ + args = ['add', '--exact'] + if (!isOnline) args.push('--offline') + args.push('--cwd', root) + if (devDependencies) args.push('--dev') + args.push(...dependencies) + } else { + /** + * Call `npm install [--save|--save-dev] ...`. + */ + args = ['install', '--save-exact'] + args.push(devDependencies ? '--save-dev' : '--save') args.push(...dependencies) } - args.push('--cwd', root) - - if (!isOnline) { - console.log(chalk.yellow('You appear to be offline.')) - console.log(chalk.yellow('Falling back to the local Yarn cache.')) - console.log() + } else { + /** + * If there are no dependencies, run a variation of `{displayCommand} + * install`. + */ + args = ['install'] + if (useYarn) { + if (!isOnline) { + console.log(chalk.yellow('You appear to be offline.')) + console.log(chalk.yellow('Falling back to the local Yarn cache.')) + console.log() + args.push('--offline') + } + } else { + if (!isOnline) { + console.log(chalk.yellow('You appear to be offline.')) + console.log() + } } + } + /** + * Add any package manager-specific flags. + */ + if (useYarn) { + args.push(...yarnFlags) } else { - command = 'npm' - args = ([ - 'install', - dependencies && '--save', - dependencies && '--save-exact', - '--loglevel', - 'error', - ].filter(Boolean) as string[]).concat(dependencies || []) + args.push(...npmFlags) } - + /** + * Spawn the installation process. + */ const child = spawn(command, args, { stdio: 'inherit', env: { ...process.env, ADBLOCK: '1', DISABLE_OPENCOLLECTIVE: '1' }, diff --git a/packages/create-next-app/index.ts b/packages/create-next-app/index.ts index ab7431fc3b266..a94752171140d 100644 --- a/packages/create-next-app/index.ts +++ b/packages/create-next-app/index.ts @@ -19,7 +19,20 @@ const program = new Commander.Command(packageJson.name) .action((name) => { projectPath = name }) - .option('--use-npm', 'Explicitly tell the CLI to bootstrap the app using npm') + .option( + '--ts, --typescript', + ` + + Initialize as a TypeScript project. +` + ) + .option( + '--use-npm', + ` + + Explicitly tell the CLI to bootstrap the app using npm +` + ) .option( '-e, --example [name]|[github-url]', ` @@ -113,6 +126,7 @@ async function run(): Promise { useNpm: !!program.useNpm, example: example && example !== 'default' ? example : undefined, examplePath: program.examplePath, + typescript: program.typescript, }) } catch (reason) { if (!(reason instanceof DownloadError)) { @@ -131,7 +145,11 @@ async function run(): Promise { throw reason } - await createApp({ appPath: resolvedProjectPath, useNpm: !!program.useNpm }) + await createApp({ + appPath: resolvedProjectPath, + useNpm: !!program.useNpm, + typescript: program.typescript, + }) } } diff --git a/packages/create-next-app/templates/typescript/README-template.md b/packages/create-next-app/templates/typescript/README-template.md new file mode 100644 index 0000000000000..b12f3e33e7d5b --- /dev/null +++ b/packages/create-next-app/templates/typescript/README-template.md @@ -0,0 +1,34 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. + +[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. + +The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/packages/create-next-app/templates/typescript/gitignore b/packages/create-next-app/templates/typescript/gitignore new file mode 100644 index 0000000000000..1437c53f70bc2 --- /dev/null +++ b/packages/create-next-app/templates/typescript/gitignore @@ -0,0 +1,34 @@ +# 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/packages/create-next-app/templates/typescript/next-env.d.ts b/packages/create-next-app/templates/typescript/next-env.d.ts new file mode 100644 index 0000000000000..7b7aa2c7727d8 --- /dev/null +++ b/packages/create-next-app/templates/typescript/next-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/packages/create-next-app/templates/typescript/pages/_app.tsx b/packages/create-next-app/templates/typescript/pages/_app.tsx new file mode 100644 index 0000000000000..945e892613976 --- /dev/null +++ b/packages/create-next-app/templates/typescript/pages/_app.tsx @@ -0,0 +1,7 @@ +import '../styles/globals.css' +import type { AppProps } from 'next/app' + +function MyApp({ Component, pageProps }: AppProps) { + return +} +export default MyApp diff --git a/packages/create-next-app/templates/typescript/pages/api/hello.ts b/packages/create-next-app/templates/typescript/pages/api/hello.ts new file mode 100644 index 0000000000000..3d66af99d6010 --- /dev/null +++ b/packages/create-next-app/templates/typescript/pages/api/hello.ts @@ -0,0 +1,10 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next' + +type Data = { + name: string +} + +export default (req: NextApiRequest, res: NextApiResponse) => { + res.status(200).json({ name: 'John Doe' }) +} diff --git a/packages/create-next-app/templates/typescript/pages/index.tsx b/packages/create-next-app/templates/typescript/pages/index.tsx new file mode 100644 index 0000000000000..08145bba9a578 --- /dev/null +++ b/packages/create-next-app/templates/typescript/pages/index.tsx @@ -0,0 +1,69 @@ +import Head from 'next/head' +import Image from 'next/image' +import styles from '../styles/Home.module.css' + +export default function Home() { + return ( + + ) +} diff --git a/packages/create-next-app/templates/typescript/public/favicon.ico b/packages/create-next-app/templates/typescript/public/favicon.ico new file mode 100644 index 0000000000000..4965832f2c9b0 Binary files /dev/null and b/packages/create-next-app/templates/typescript/public/favicon.ico differ diff --git a/packages/create-next-app/templates/typescript/public/vercel.svg b/packages/create-next-app/templates/typescript/public/vercel.svg new file mode 100644 index 0000000000000..fbf0e25a651c2 --- /dev/null +++ b/packages/create-next-app/templates/typescript/public/vercel.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/packages/create-next-app/templates/typescript/styles/Home.module.css b/packages/create-next-app/templates/typescript/styles/Home.module.css new file mode 100644 index 0000000000000..35454bb748190 --- /dev/null +++ b/packages/create-next-app/templates/typescript/styles/Home.module.css @@ -0,0 +1,121 @@ +.container { + min-height: 100vh; + padding: 0 0.5rem; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100vh; +} + +.main { + padding: 5rem 0; + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.footer { + width: 100%; + height: 100px; + border-top: 1px solid #eaeaea; + display: flex; + justify-content: center; + align-items: center; +} + +.footer a { + display: flex; + justify-content: center; + align-items: center; + flex-grow: 1; +} + +.title a { + color: #0070f3; + text-decoration: none; +} + +.title a:hover, +.title a:focus, +.title a:active { + text-decoration: underline; +} + +.title { + margin: 0; + line-height: 1.15; + font-size: 4rem; +} + +.title, +.description { + text-align: center; +} + +.description { + line-height: 1.5; + font-size: 1.5rem; +} + +.code { + background: #fafafa; + border-radius: 5px; + padding: 0.75rem; + font-size: 1.1rem; + font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, + Bitstream Vera Sans Mono, Courier New, monospace; +} + +.grid { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + max-width: 800px; + margin-top: 3rem; +} + +.card { + margin: 1rem; + padding: 1.5rem; + text-align: left; + color: inherit; + text-decoration: none; + border: 1px solid #eaeaea; + border-radius: 10px; + transition: color 0.15s ease, border-color 0.15s ease; + width: 45%; +} + +.card:hover, +.card:focus, +.card:active { + color: #0070f3; + border-color: #0070f3; +} + +.card h2 { + margin: 0 0 1rem 0; + font-size: 1.5rem; +} + +.card p { + margin: 0; + font-size: 1.25rem; + line-height: 1.5; +} + +.logo { + height: 1em; + margin-left: 0.5rem; +} + +@media (max-width: 600px) { + .grid { + width: 100%; + flex-direction: column; + } +} diff --git a/packages/create-next-app/templates/typescript/styles/globals.css b/packages/create-next-app/templates/typescript/styles/globals.css new file mode 100644 index 0000000000000..e5e2dcc23baf1 --- /dev/null +++ b/packages/create-next-app/templates/typescript/styles/globals.css @@ -0,0 +1,16 @@ +html, +body { + padding: 0; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, + Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; +} + +a { + color: inherit; + text-decoration: none; +} + +* { + box-sizing: border-box; +} diff --git a/packages/create-next-app/templates/typescript/tsconfig.json b/packages/create-next-app/templates/typescript/tsconfig.json new file mode 100644 index 0000000000000..4fa631c261428 --- /dev/null +++ b/packages/create-next-app/templates/typescript/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve" + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/packages/create-next-app/tsconfig.json b/packages/create-next-app/tsconfig.json index d0e9e223e6a05..aad5b046673d5 100644 --- a/packages/create-next-app/tsconfig.json +++ b/packages/create-next-app/tsconfig.json @@ -6,5 +6,6 @@ "resolveJsonModule": true, "esModuleInterop": true, "skipLibCheck": false - } + }, + "exclude": ["templates", "dist"] } diff --git a/packages/eslint-plugin-next/lib/index.js b/packages/eslint-plugin-next/lib/index.js index 876aa2e91630e..4565a833fe11d 100644 --- a/packages/eslint-plugin-next/lib/index.js +++ b/packages/eslint-plugin-next/lib/index.js @@ -4,6 +4,7 @@ module.exports = { 'no-sync-scripts': require('./rules/no-sync-scripts'), 'no-html-link-for-pages': require('./rules/no-html-link-for-pages'), 'no-unwanted-polyfillio': require('./rules/no-unwanted-polyfillio'), + 'no-title-in-document-head': require('./rules/no-title-in-document-head'), }, configs: { recommended: { @@ -13,6 +14,7 @@ module.exports = { '@next/next/no-sync-scripts': 1, '@next/next/no-html-link-for-pages': 1, '@next/next/no-unwanted-polyfillio': 1, + '@next/next/no-title-in-document-head': 1, }, }, }, diff --git a/packages/eslint-plugin-next/lib/rules/no-title-in-document-head.js b/packages/eslint-plugin-next/lib/rules/no-title-in-document-head.js new file mode 100644 index 0000000000000..a945566d7c8ee --- /dev/null +++ b/packages/eslint-plugin-next/lib/rules/no-title-in-document-head.js @@ -0,0 +1,48 @@ +module.exports = { + meta: { + docs: { + description: 'Disallow using with Head from next/document', + }, + }, + create: function (context) { + let headFromNextDocument = false + return { + ImportDeclaration(node) { + if (node.source.value === 'next/document') { + if (node.specifiers.some(({ local }) => local.name === 'Head')) { + headFromNextDocument = true + } + } + }, + JSXElement(node) { + if (!headFromNextDocument) { + return + } + + if ( + node.openingElement && + node.openingElement.name && + node.openingElement.name.name !== 'Head' + ) { + return + } + + const titleTag = node.children.find( + (child) => + child.openingElement && + child.openingElement.name && + child.openingElement.name.type === 'JSXIdentifier' && + child.openingElement.name.name === 'title' + ) + + if (titleTag) { + context.report({ + node: titleTag, + message: + 'Titles should be defined at the page-level using next/head. See https://nextjs.org/docs/messages/no-title-in-document-head.', + }) + } + }, + } + }, +} diff --git a/packages/next-plugin-storybook/preset.js b/packages/next-plugin-storybook/preset.js index 372fa9b341bec..9f737d3a29ce9 100644 --- a/packages/next-plugin-storybook/preset.js +++ b/packages/next-plugin-storybook/preset.js @@ -15,7 +15,7 @@ async function webpackFinal(config) { target: 'server', config: nextConfig, buildId: 'storybook', - rewrites: [], + rewrites: { beforeFiles: [], afterFiles: [], fallback: [] }, }) config.plugins = [...config.plugins, ...nextWebpackConfig.plugins] diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 1c9d1e8f19b20..668a9b0098ecb 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -183,7 +183,11 @@ export default async function build( const ignoreTypeScriptErrors = Boolean(config.typescript?.ignoreBuildErrors) const typeCheckStart = process.hrtime() const typeCheckingSpinner = createSpinner({ - prefixText: `${Log.prefixes.info} Checking validity of types`, + prefixText: `${Log.prefixes.info} ${ + ignoreTypeScriptErrors + ? 'Skipping validation of types' + : 'Checking validity of types' + }`, }) const verifyResult = await nextBuildSpan @@ -194,15 +198,17 @@ export default async function build( const typeCheckEnd = process.hrtime(typeCheckStart) - telemetry.record( - eventTypeCheckCompleted({ - durationInSeconds: typeCheckEnd[0], - typescriptVersion: verifyResult.version, - inputFilesCount: verifyResult.result?.inputFilesCount, - totalFilesCount: verifyResult.result?.totalFilesCount, - incremental: verifyResult.result?.incremental, - }) - ) + if (!ignoreTypeScriptErrors) { + telemetry.record( + eventTypeCheckCompleted({ + durationInSeconds: typeCheckEnd[0], + typescriptVersion: verifyResult.version, + inputFilesCount: verifyResult.result?.inputFilesCount, + totalFilesCount: verifyResult.result?.totalFilesCount, + incremental: verifyResult.result?.incremental, + }) + ) + } if (typeCheckingSpinner) { typeCheckingSpinner.stopAndPersist() diff --git a/packages/next/next-server/lib/constants.ts b/packages/next/next-server/lib/constants.ts index c07b817533ce2..eed0b3877ba92 100644 --- a/packages/next/next-server/lib/constants.ts +++ b/packages/next/next-server/lib/constants.ts @@ -37,5 +37,8 @@ export const TEMPORARY_REDIRECT_STATUS = 307 export const PERMANENT_REDIRECT_STATUS = 308 export const STATIC_PROPS_ID = '__N_SSG' export const SERVER_PROPS_ID = '__N_SSP' -export const OPTIMIZED_FONT_PROVIDERS = ['https://fonts.googleapis.com/css'] +export const OPTIMIZED_FONT_PROVIDERS = [ + 'https://fonts.googleapis.com/css', + 'https://use.typekit.net/', +] export const STATIC_STATUS_PAGES = ['/500'] diff --git a/packages/next/next-server/lib/head.tsx b/packages/next/next-server/lib/head.tsx index 2a6fa5a40e13b..f035ef5797485 100644 --- a/packages/next/next-server/lib/head.tsx +++ b/packages/next/next-server/lib/head.tsx @@ -147,9 +147,10 @@ function reduceComponents( c.type === 'link' && c.props['href'] && // TODO(prateekbh@): Replace this with const from `constants` when the tree shaking works. - ['https://fonts.googleapis.com/css'].some((url) => - c.props['href'].startsWith(url) - ) + [ + 'https://fonts.googleapis.com/css', + 'https://use.typekit.net/', + ].some((url) => c.props['href'].startsWith(url)) ) { const newProps = { ...(c.props || {}) } newProps['data-href'] = newProps['href'] diff --git a/packages/next/next-server/server/api-utils.ts b/packages/next/next-server/server/api-utils.ts index da33727430bb4..350a87dac212f 100644 --- a/packages/next/next-server/server/api-utils.ts +++ b/packages/next/next-server/server/api-utils.ts @@ -114,7 +114,12 @@ export async function parseBody( req: NextApiRequest, limit: string | number ): Promise<any> { - const contentType = parse(req.headers['content-type'] || 'text/plain') + let contentType + try { + contentType = parse(req.headers['content-type'] || 'text/plain') + } catch { + contentType = parse('text/plain') + } const { type, parameters } = contentType const encoding = parameters.charset || 'utf-8' diff --git a/test/eslint-plugin-next/no-title-in-document-head.unit.test.js b/test/eslint-plugin-next/no-title-in-document-head.unit.test.js new file mode 100644 index 0000000000000..b20f5fa358ab6 --- /dev/null +++ b/test/eslint-plugin-next/no-title-in-document-head.unit.test.js @@ -0,0 +1,70 @@ +const rule = require('@next/eslint-plugin-next/lib/rules/no-title-in-document-head') +const RuleTester = require('eslint').RuleTester + +RuleTester.setDefaultConfig({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + modules: true, + jsx: true, + }, + }, +}) + +var ruleTester = new RuleTester() +ruleTester.run('no-title-in-document-head', rule, { + valid: [ + `import Head from "next/head"; + + class Test { + render() { + return ( + <Head> + <title>My page title + + ); + } + }`, + + `import Document, { Html, Head } from "next/document"; + + class MyDocument extends Document { + render() { + return ( + + + + + ); + } + } + + export default MyDocument; + `, + ], + + invalid: [ + { + code: ` + import { Head } from "next/document"; + + class Test { + render() { + return ( + + My page title + + ); + } + }`, + errors: [ + { + message: + 'Titles should be defined at the page-level using next/head. See https://nextjs.org/docs/messages/no-title-in-document-head.', + type: 'JSXElement', + }, + ], + }, + ], +}) diff --git a/test/integration/api-body-parser/test/index.test.js b/test/integration/api-body-parser/test/index.test.js index ea90f43bdbcf8..c9a9b7200d310 100644 --- a/test/integration/api-body-parser/test/index.test.js +++ b/test/integration/api-body-parser/test/index.test.js @@ -36,6 +36,13 @@ function runTests() { expect(data).toEqual([{ title: 'Nextjs' }]) killApp(server) }) + + it("should not throw if request's content-type is invalid", async () => { + await startServer() + const status = await makeRequestWithInvalidContentType() + expect(status).toBe(200) + killApp(server) + }) } async function makeRequest() { @@ -50,6 +57,18 @@ async function makeRequest() { return data } +async function makeRequestWithInvalidContentType() { + const status = await fetchViaHTTP(appPort, '/api', null, { + method: 'POST', + headers: { + 'Content-Type': 'application/json;', + }, + body: JSON.stringify([{ title: 'Nextjs' }]), + }).then((res) => res.status) + + return status +} + const startServer = async (optEnv = {}, opts) => { const scriptPath = join(appDir, 'server.js') context.appPort = appPort = await getPort() diff --git a/test/integration/create-next-app/index.test.js b/test/integration/create-next-app/index.test.js index 6f35a294b61b5..3e3c868076177 100644 --- a/test/integration/create-next-app/index.test.js +++ b/test/integration/create-next-app/index.test.js @@ -91,6 +91,37 @@ describe('create next app', () => { }) }) + it('should support typescript flag', async () => { + await usingTempDir(async (cwd) => { + const projectName = 'typescript' + const res = await run([projectName, '--typescript'], { cwd }) + expect(res.exitCode).toBe(0) + + expect( + fs.existsSync(path.join(cwd, projectName, 'package.json')) + ).toBeTruthy() + expect( + fs.existsSync(path.join(cwd, projectName, 'pages/index.tsx')) + ).toBeTruthy() + expect( + fs.existsSync(path.join(cwd, projectName, 'pages/_app.tsx')) + ).toBeTruthy() + expect( + fs.existsSync(path.join(cwd, projectName, 'pages/api/hello.ts')) + ).toBeTruthy() + expect( + fs.existsSync(path.join(cwd, projectName, 'tsconfig.json')) + ).toBeTruthy() + expect( + fs.existsSync(path.join(cwd, projectName, 'next-env.d.ts')) + ).toBeTruthy() + // check we copied default `.gitignore` + expect( + fs.existsSync(path.join(cwd, projectName, '.gitignore')) + ).toBeTruthy() + }) + }) + it('should allow example with GitHub URL', async () => { await usingTempDir(async (cwd) => { const projectName = 'github-app' diff --git a/test/integration/font-optimization/fixtures/with-google/manifest-snapshot.json b/test/integration/font-optimization/fixtures/with-google/manifest-snapshot.json new file mode 100644 index 0000000000000..766f70f7ae879 --- /dev/null +++ b/test/integration/font-optimization/fixtures/with-google/manifest-snapshot.json @@ -0,0 +1,22 @@ +[ + { + "url": "https://fonts.googleapis.com/css?family=Voces", + "content": "@font-face{font-family:'Voces';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/voces/v12/-F6_fjJyLyU8d7PGDmk.woff) format('woff')}@font-face{font-family:'Voces';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/voces/v12/-F6_fjJyLyU8d7PIDm_6pClI_ik.woff2) format('woff2');unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:'Voces';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/voces/v12/-F6_fjJyLyU8d7PGDm_6pClI.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}" + }, + { + "url": "https://fonts.googleapis.com/css2?family=Modak", + "content": "@font-face{font-family:'Modak';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/modak/v8/EJRYQgs1XtIEsnME.woff) format('woff')}@font-face{font-family:'Modak';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/modak/v8/EJRYQgs1XtIEskMB-hR77LKVTy8.woff2) format('woff2');unicode-range:U+0900-097F,U+1CD0-1CF6,U+1CF8-1CF9,U+200C-200D,U+20A8,U+20B9,U+25CC,U+A830-A839,U+A8E0-A8FB}@font-face{font-family:'Modak';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/modak/v8/EJRYQgs1XtIEskMO-hR77LKVTy8.woff2) format('woff2');unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:'Modak';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/modak/v8/EJRYQgs1XtIEskMA-hR77LKV.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}" + }, + { + "url": "https://fonts.googleapis.com/css2?family=Modak", + "content": "@font-face{font-family:'Modak';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/modak/v8/EJRYQgs1XtIEsnME.woff) format('woff')}@font-face{font-family:'Modak';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/modak/v8/EJRYQgs1XtIEskMB-hR77LKVTy8.woff2) format('woff2');unicode-range:U+0900-097F,U+1CD0-1CF6,U+1CF8-1CF9,U+200C-200D,U+20A8,U+20B9,U+25CC,U+A830-A839,U+A8E0-A8FB}@font-face{font-family:'Modak';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/modak/v8/EJRYQgs1XtIEskMO-hR77LKVTy8.woff2) format('woff2');unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:'Modak';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/modak/v8/EJRYQgs1XtIEskMA-hR77LKV.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}" + }, + { + "url": "https://fonts.googleapis.com/css2?family=Roboto:wght@700", + "content": "@font-face{font-family:'Roboto';font-style:normal;font-weight:700;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmWUlvAA.woff) format('woff')}@font-face{font-family:'Roboto';font-style:normal;font-weight:700;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmWUlfCRc4AMP6lbBP.woff2) format('woff2');unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:'Roboto';font-style:normal;font-weight:700;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmWUlfABc4AMP6lbBP.woff2) format('woff2');unicode-range:U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:'Roboto';font-style:normal;font-weight:700;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmWUlfCBc4AMP6lbBP.woff2) format('woff2');unicode-range:U+1F00-1FFF}@font-face{font-family:'Roboto';font-style:normal;font-weight:700;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmWUlfBxc4AMP6lbBP.woff2) format('woff2');unicode-range:U+0370-03FF}@font-face{font-family:'Roboto';font-style:normal;font-weight:700;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmWUlfCxc4AMP6lbBP.woff2) format('woff2');unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:'Roboto';font-style:normal;font-weight:700;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmWUlfChc4AMP6lbBP.woff2) format('woff2');unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:'Roboto';font-style:normal;font-weight:700;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmWUlfBBc4AMP6lQ.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}" + }, + { + "url": "https://fonts.googleapis.com/css2?family=Roboto:wght@400;700;900&display=swap", + "content": "@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Me5g.woff) format('woff')}@font-face{font-family:'Roboto';font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmWUlvAA.woff) format('woff')}@font-face{font-family:'Roboto';font-style:normal;font-weight:900;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmYUtvAA.woff) format('woff')}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2) format('woff2');unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Mu5mxKKTU1Kvnz.woff2) format('woff2');unicode-range:U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Mu7mxKKTU1Kvnz.woff2) format('woff2');unicode-range:U+1F00-1FFF}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Mu4WxKKTU1Kvnz.woff2) format('woff2');unicode-range:U+0370-03FF}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Mu7WxKKTU1Kvnz.woff2) format('woff2');unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Mu7GxKKTU1Kvnz.woff2) format('woff2');unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:'Roboto';font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmWUlfCRc4AMP6lbBP.woff2) format('woff2');unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:'Roboto';font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmWUlfABc4AMP6lbBP.woff2) format('woff2');unicode-range:U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:'Roboto';font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmWUlfCBc4AMP6lbBP.woff2) format('woff2');unicode-range:U+1F00-1FFF}@font-face{font-family:'Roboto';font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmWUlfBxc4AMP6lbBP.woff2) format('woff2');unicode-range:U+0370-03FF}@font-face{font-family:'Roboto';font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmWUlfCxc4AMP6lbBP.woff2) format('woff2');unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:'Roboto';font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmWUlfChc4AMP6lbBP.woff2) format('woff2');unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:'Roboto';font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmWUlfBBc4AMP6lQ.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:'Roboto';font-style:normal;font-weight:900;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmYUtfCRc4AMP6lbBP.woff2) format('woff2');unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:'Roboto';font-style:normal;font-weight:900;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmYUtfABc4AMP6lbBP.woff2) format('woff2');unicode-range:U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:'Roboto';font-style:normal;font-weight:900;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmYUtfCBc4AMP6lbBP.woff2) format('woff2');unicode-range:U+1F00-1FFF}@font-face{font-family:'Roboto';font-style:normal;font-weight:900;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmYUtfBxc4AMP6lbBP.woff2) format('woff2');unicode-range:U+0370-03FF}@font-face{font-family:'Roboto';font-style:normal;font-weight:900;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmYUtfCxc4AMP6lbBP.woff2) format('woff2');unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:'Roboto';font-style:normal;font-weight:900;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmYUtfChc4AMP6lbBP.woff2) format('woff2');unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:'Roboto';font-style:normal;font-weight:900;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmYUtfBBc4AMP6lQ.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}" + } +] diff --git a/test/integration/font-optimization/fixtures/with-google/next.config.js b/test/integration/font-optimization/fixtures/with-google/next.config.js new file mode 100644 index 0000000000000..4ba52ba2c8df6 --- /dev/null +++ b/test/integration/font-optimization/fixtures/with-google/next.config.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/test/integration/font-optimization/pages/_document.js b/test/integration/font-optimization/fixtures/with-google/pages/_document.js similarity index 100% rename from test/integration/font-optimization/pages/_document.js rename to test/integration/font-optimization/fixtures/with-google/pages/_document.js diff --git a/test/integration/font-optimization/pages/amp.js b/test/integration/font-optimization/fixtures/with-google/pages/amp.js similarity index 100% rename from test/integration/font-optimization/pages/amp.js rename to test/integration/font-optimization/fixtures/with-google/pages/amp.js diff --git a/test/integration/font-optimization/pages/index.js b/test/integration/font-optimization/fixtures/with-google/pages/index.js similarity index 100% rename from test/integration/font-optimization/pages/index.js rename to test/integration/font-optimization/fixtures/with-google/pages/index.js diff --git a/test/integration/font-optimization/pages/nonce.js b/test/integration/font-optimization/fixtures/with-google/pages/nonce.js similarity index 100% rename from test/integration/font-optimization/pages/nonce.js rename to test/integration/font-optimization/fixtures/with-google/pages/nonce.js diff --git a/test/integration/font-optimization/pages/stars.js b/test/integration/font-optimization/fixtures/with-google/pages/stars.js similarity index 100% rename from test/integration/font-optimization/pages/stars.js rename to test/integration/font-optimization/fixtures/with-google/pages/stars.js diff --git a/test/integration/font-optimization/pages/static-head.js b/test/integration/font-optimization/fixtures/with-google/pages/static-head.js similarity index 100% rename from test/integration/font-optimization/pages/static-head.js rename to test/integration/font-optimization/fixtures/with-google/pages/static-head.js diff --git a/test/integration/font-optimization/pages/with-font.js b/test/integration/font-optimization/fixtures/with-google/pages/with-font.js similarity index 100% rename from test/integration/font-optimization/pages/with-font.js rename to test/integration/font-optimization/fixtures/with-google/pages/with-font.js diff --git a/test/integration/font-optimization/pages/without-font.js b/test/integration/font-optimization/fixtures/with-google/pages/without-font.js similarity index 100% rename from test/integration/font-optimization/pages/without-font.js rename to test/integration/font-optimization/fixtures/with-google/pages/without-font.js diff --git a/test/integration/font-optimization/server.js b/test/integration/font-optimization/fixtures/with-google/server.js similarity index 100% rename from test/integration/font-optimization/server.js rename to test/integration/font-optimization/fixtures/with-google/server.js diff --git a/test/integration/font-optimization/fixtures/with-typekit/manifest-snapshot.json b/test/integration/font-optimization/fixtures/with-typekit/manifest-snapshot.json new file mode 100644 index 0000000000000..42681af1533b5 --- /dev/null +++ b/test/integration/font-optimization/fixtures/with-typekit/manifest-snapshot.json @@ -0,0 +1,14 @@ +[ + { + "url": "https://use.typekit.net/plm1izr.css", + "content": "@import url(\"https://p.typekit.net/p.css?s=1&k=plm1izr&ht=tk&f=32266&a=23152309&app=typekit&e=css\");@font-face{font-family:\"birra-2\";src:url(\"https://use.typekit.net/af/23e0ad/00000000000000003b9b410c/27/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n7&v=3\") format(\"woff2\"),url(\"https://use.typekit.net/af/23e0ad/00000000000000003b9b410c/27/d?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n7&v=3\") format(\"woff\"),url(\"https://use.typekit.net/af/23e0ad/00000000000000003b9b410c/27/a?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n7&v=3\") format(\"opentype\");font-display:auto;font-style:normal;font-weight:700}.tk-birra-2{font-family:\"birra-2\",serif}@import url(\"https://p.typekit.net/p.css?s=1&k=plm1izr&ht=tk&f=32266&a=23152309&app=typekit&e=css\");@font-face{font-family:\"birra-2\";src:url(\"https://use.typekit.net/af/23e0ad/00000000000000003b9b410c/27/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n7&v=3\") format(\"woff2\"),url(\"https://use.typekit.net/af/23e0ad/00000000000000003b9b410c/27/d?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n7&v=3\") format(\"woff\"),url(\"https://use.typekit.net/af/23e0ad/00000000000000003b9b410c/27/a?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n7&v=3\") format(\"opentype\");font-display:auto;font-style:normal;font-weight:700}.tk-birra-2{font-family:\"birra-2\",serif}" + }, + { + "url": "https://use.typekit.net/ucs7mcf.css", + "content": "@import url(\"https://p.typekit.net/p.css?s=1&k=ucs7mcf&ht=tk&f=43886&a=23152309&app=typekit&e=css\");@font-face{font-family:\"flegrei\";src:url(\"https://use.typekit.net/af/74a5d1/00000000000000003b9b3d6e/27/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3\") format(\"woff2\"),url(\"https://use.typekit.net/af/74a5d1/00000000000000003b9b3d6e/27/d?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3\") format(\"woff\"),url(\"https://use.typekit.net/af/74a5d1/00000000000000003b9b3d6e/27/a?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3\") format(\"opentype\");font-display:auto;font-style:normal;font-weight:400}.tk-flegrei{font-family:\"flegrei\",sans-serif}@import url(\"https://p.typekit.net/p.css?s=1&k=ucs7mcf&ht=tk&f=43886&a=23152309&app=typekit&e=css\");@font-face{font-family:\"flegrei\";src:url(\"https://use.typekit.net/af/74a5d1/00000000000000003b9b3d6e/27/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3\") format(\"woff2\"),url(\"https://use.typekit.net/af/74a5d1/00000000000000003b9b3d6e/27/d?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3\") format(\"woff\"),url(\"https://use.typekit.net/af/74a5d1/00000000000000003b9b3d6e/27/a?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3\") format(\"opentype\");font-display:auto;font-style:normal;font-weight:400}.tk-flegrei{font-family:\"flegrei\",sans-serif}" + }, + { + "url": "https://use.typekit.net/erd0sed.css", + "content": "@import url(\"https://p.typekit.net/p.css?s=1&k=erd0sed&ht=tk&f=43885&a=23152309&app=typekit&e=css\");@font-face{font-family:\"pantelleria\";src:url(\"https://use.typekit.net/af/1f141c/00000000000000003b9b3d6f/27/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3\") format(\"woff2\"),url(\"https://use.typekit.net/af/1f141c/00000000000000003b9b3d6f/27/d?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3\") format(\"woff\"),url(\"https://use.typekit.net/af/1f141c/00000000000000003b9b3d6f/27/a?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3\") format(\"opentype\");font-display:auto;font-style:normal;font-weight:400}.tk-pantelleria{font-family:\"pantelleria\",sans-serif}@import url(\"https://p.typekit.net/p.css?s=1&k=erd0sed&ht=tk&f=43885&a=23152309&app=typekit&e=css\");@font-face{font-family:\"pantelleria\";src:url(\"https://use.typekit.net/af/1f141c/00000000000000003b9b3d6f/27/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3\") format(\"woff2\"),url(\"https://use.typekit.net/af/1f141c/00000000000000003b9b3d6f/27/d?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3\") format(\"woff\"),url(\"https://use.typekit.net/af/1f141c/00000000000000003b9b3d6f/27/a?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3\") format(\"opentype\");font-display:auto;font-style:normal;font-weight:400}.tk-pantelleria{font-family:\"pantelleria\",sans-serif}" + } +] diff --git a/test/integration/font-optimization/fixtures/with-typekit/next.config.js b/test/integration/font-optimization/fixtures/with-typekit/next.config.js new file mode 100644 index 0000000000000..4ba52ba2c8df6 --- /dev/null +++ b/test/integration/font-optimization/fixtures/with-typekit/next.config.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/test/integration/font-optimization/fixtures/with-typekit/pages/_document.js b/test/integration/font-optimization/fixtures/with-typekit/pages/_document.js new file mode 100644 index 0000000000000..33cd61eee7ef8 --- /dev/null +++ b/test/integration/font-optimization/fixtures/with-typekit/pages/_document.js @@ -0,0 +1,27 @@ +import * as React from 'react' +/// @ts-ignore +import Document, { Main, NextScript, Head } from 'next/document' + +export default class MyDocument extends Document { + constructor(props) { + super(props) + const { __NEXT_DATA__, ids } = props + if (ids) { + __NEXT_DATA__.ids = ids + } + } + + render() { + return ( + + + + + +
+ + + + ) + } +} diff --git a/test/integration/font-optimization/fixtures/with-typekit/pages/amp.js b/test/integration/font-optimization/fixtures/with-typekit/pages/amp.js new file mode 100644 index 0000000000000..53ac86b915b16 --- /dev/null +++ b/test/integration/font-optimization/fixtures/with-typekit/pages/amp.js @@ -0,0 +1,9 @@ +import React from 'react' + +const Page = () => { + return
Hi!
+} + +export const config = { amp: true } + +export default Page diff --git a/test/integration/font-optimization/fixtures/with-typekit/pages/index.js b/test/integration/font-optimization/fixtures/with-typekit/pages/index.js new file mode 100644 index 0000000000000..39cf2d1f7b376 --- /dev/null +++ b/test/integration/font-optimization/fixtures/with-typekit/pages/index.js @@ -0,0 +1,7 @@ +import React from 'react' + +const Page = () => { + return
Hi!
+} + +export default Page diff --git a/test/integration/font-optimization/fixtures/with-typekit/pages/nonce.js b/test/integration/font-optimization/fixtures/with-typekit/pages/nonce.js new file mode 100644 index 0000000000000..2c6b7e91af930 --- /dev/null +++ b/test/integration/font-optimization/fixtures/with-typekit/pages/nonce.js @@ -0,0 +1,19 @@ +import React from 'react' +import Head from 'next/head' + +const Page = () => { + return ( + <> + + + +
Hi!
+ + ) +} + +export default Page diff --git a/test/integration/font-optimization/fixtures/with-typekit/pages/stars.js b/test/integration/font-optimization/fixtures/with-typekit/pages/stars.js new file mode 100644 index 0000000000000..c8ebb251db270 --- /dev/null +++ b/test/integration/font-optimization/fixtures/with-typekit/pages/stars.js @@ -0,0 +1,26 @@ +import Head from 'next/head' + +function Home({ stars }) { + return ( +
+ + Create Next App + + + + +
+
Next stars: {stars}
+
+
+ ) +} + +Home.getInitialProps = async () => { + return { stars: Math.random() * 1000 } +} + +export default Home diff --git a/test/integration/font-optimization/fixtures/with-typekit/pages/static-head.js b/test/integration/font-optimization/fixtures/with-typekit/pages/static-head.js new file mode 100644 index 0000000000000..9b0ba8af28a32 --- /dev/null +++ b/test/integration/font-optimization/fixtures/with-typekit/pages/static-head.js @@ -0,0 +1,15 @@ +import React from 'react' +import Head from 'next/head' + +const Page = () => { + return ( + <> + + + +
Hi!
+ + ) +} + +export default Page diff --git a/test/integration/font-optimization/fixtures/with-typekit/pages/with-font.js b/test/integration/font-optimization/fixtures/with-typekit/pages/with-font.js new file mode 100644 index 0000000000000..b1a1fa4011d49 --- /dev/null +++ b/test/integration/font-optimization/fixtures/with-typekit/pages/with-font.js @@ -0,0 +1,27 @@ +import Head from 'next/head' +import Link from 'next/link' + +const WithFont = () => { + return ( + <> + + + + +
+ Page with custom fonts +
+
+ Without font +
+ + ) +} + +export const getServerSideProps = async () => { + return { + props: {}, + } +} + +export default WithFont diff --git a/test/integration/font-optimization/fixtures/with-typekit/pages/without-font.js b/test/integration/font-optimization/fixtures/with-typekit/pages/without-font.js new file mode 100644 index 0000000000000..2185cb252e07e --- /dev/null +++ b/test/integration/font-optimization/fixtures/with-typekit/pages/without-font.js @@ -0,0 +1,26 @@ +import Head from 'next/head' +import Link from 'next/link' + +const WithoutFont = () => { + return ( + <> + + + +
+ Page without custom fonts +
+
+ With font +
+ + ) +} + +export const getServerSideProps = async () => { + return { + props: {}, + } +} + +export default WithoutFont diff --git a/test/integration/font-optimization/fixtures/with-typekit/server.js b/test/integration/font-optimization/fixtures/with-typekit/server.js new file mode 100644 index 0000000000000..6a98fa3d30806 --- /dev/null +++ b/test/integration/font-optimization/fixtures/with-typekit/server.js @@ -0,0 +1,111 @@ +const http = require('http') +const url = require('url') +const fs = require('fs') +const path = require('path') +const server = http.createServer(async (req, res) => { + let { pathname } = url.parse(req.url) + pathname = pathname.replace(/\/$/, '') + let isDataReq = false + if (pathname.startsWith('/_next/data')) { + isDataReq = true + pathname = pathname + .replace(`/_next/data/${process.env.BUILD_ID}/`, '/') + .replace(/\.json$/, '') + } + console.log('serving', pathname) + + if (pathname === '/favicon.ico') { + res.statusCode = 404 + return res.end() + } + + if (pathname.startsWith('/_next/static/')) { + res.write( + fs.readFileSync( + path.join( + __dirname, + './.next/static/', + decodeURI(pathname.slice('/_next/static/'.length)) + ), + 'utf8' + ) + ) + return res.end() + } else { + const ext = isDataReq ? 'json' : 'html' + if ( + fs.existsSync( + path.join(__dirname, `./.next/serverless/pages${pathname}.${ext}`) + ) + ) { + res.write( + fs.readFileSync( + path.join(__dirname, `./.next/serverless/pages${pathname}.${ext}`), + 'utf8' + ) + ) + return res.end() + } + + let re + try { + re = require(`./.next/serverless/pages${pathname}`) + } catch { + const d = decodeURI(pathname) + if ( + fs.existsSync( + path.join(__dirname, `./.next/serverless/pages${d}.${ext}`) + ) + ) { + res.write( + fs.readFileSync( + path.join(__dirname, `./.next/serverless/pages${d}.${ext}`), + 'utf8' + ) + ) + return res.end() + } + + const routesManifest = require('./.next/routes-manifest.json') + const { dynamicRoutes } = routesManifest + dynamicRoutes.some(({ page, regex }) => { + if (new RegExp(regex).test(pathname)) { + if ( + fs.existsSync( + path.join(__dirname, `./.next/serverless/pages${page}.${ext}`) + ) + ) { + res.write( + fs.readFileSync( + path.join(__dirname, `./.next/serverless/pages${page}.${ext}`), + 'utf8' + ) + ) + res.end() + return true + } + + re = require(`./.next/serverless/pages${page}`) + return true + } + return false + }) + } + if (!res.finished) { + try { + return await (typeof re.render === 'function' + ? re.render(req, res) + : re.default(req, res)) + } catch (e) { + console.log('FAIL_FUNCTION', e) + res.statusCode = 500 + res.write('FAIL_FUNCTION') + res.end() + } + } + } +}) + +server.listen(process.env.PORT, () => { + console.log('ready on', process.env.PORT) +}) diff --git a/test/integration/font-optimization/test/index.test.js b/test/integration/font-optimization/test/index.test.js index 317a3477d39a8..76b5d4b5d9c4a 100644 --- a/test/integration/font-optimization/test/index.test.js +++ b/test/integration/font-optimization/test/index.test.js @@ -1,6 +1,8 @@ /* eslint-env jest */ +import cheerio from 'cheerio' import { join } from 'path' +import fs from 'fs-extra' import { killApp, findPort, @@ -9,17 +11,10 @@ import { renderViaHTTP, initNextServerScript, } from 'next-test-utils' -import fs from 'fs-extra' -import cheerio from 'cheerio' jest.setTimeout(1000 * 60 * 2) -const appDir = join(__dirname, '../') -const nextConfig = join(appDir, 'next.config.js') -let builtServerPagesDir -let builtPage -let appPort -let app +const fixturesDir = join(__dirname, '..', 'fixtures') const fsExists = (file) => fs @@ -27,7 +22,7 @@ const fsExists = (file) => .then(() => true) .catch(() => false) -async function getBuildId() { +async function getBuildId(appDir) { return fs.readFile(join(appDir, '.next', 'BUILD_ID'), 'utf8') } @@ -36,196 +31,240 @@ const startServerlessEmulator = async (dir, port) => { const env = Object.assign( {}, { ...process.env }, - { PORT: port, BUILD_ID: await getBuildId() } + { PORT: port, BUILD_ID: await getBuildId(dir) } ) return initNextServerScript(scriptPath, /ready on/i, env) } -function runTests() { - it('should inline the google fonts for static pages', async () => { - const html = await renderViaHTTP(appPort, '/index') - expect(await fsExists(builtPage('font-manifest.json'))).toBe(true) - expect(html).toContain( - '' - ) - expect(html).toMatch( - /