diff --git a/.eslintrc.json b/.eslintrc.json index d00a011a84942..e9d42f59b9b3f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -61,6 +61,15 @@ "ignoreRestSiblings": true } ], + "no-unused-expressions": "off", + "@typescript-eslint/no-unused-expressions": [ + "error", + { + "allowShortCircuit": true, + "allowTernary": true, + "allowTaggedTemplates": true + } + ], "no-useless-constructor": "off", "@typescript-eslint/no-useless-constructor": "warn" } diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6f9f985a429a5..40e8b3f12e1e4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -62,5 +62,5 @@ jobs: displayName: 'Install dependencies' - script: | - node run-tests.js -g $(group) + node run-tests.js -g $(group) --timings displayName: 'Run tests' diff --git a/docs/advanced-features/custom-server.md b/docs/advanced-features/custom-server.md index 52d5efaf6b485..b7ee881c8e3fa 100644 --- a/docs/advanced-features/custom-server.md +++ b/docs/advanced-features/custom-server.md @@ -11,7 +11,7 @@ description: Start a Next.js app programmatically using a custom server.
  • Express integration
  • Hapi integration
  • Koa integration
  • -
  • SSR Catching
  • +
  • SSR Caching
  • diff --git a/docs/advanced-features/customizing-postcss-config.md b/docs/advanced-features/customizing-postcss-config.md new file mode 100644 index 0000000000000..de5597836deda --- /dev/null +++ b/docs/advanced-features/customizing-postcss-config.md @@ -0,0 +1,134 @@ +--- +description: Extend the PostCSS config and plugins added by Next.js with your own. +--- + +# Customizing PostCSS Config + +
    + Examples + +
    + +## Default Behavior + +Next.js compiles CSS for its [built-in CSS support](/docs/basic-features/built-in-css-support) using PostCSS. + +Out of the box, with no configuration, Next.js compiles CSS with the following transformations: + +1. [Autoprefixer](https://github.com/postcss/autoprefixer) automatically adds vendor prefixes to CSS rules (back to IE11). +1. [Cross-browser Flexbox bugs](https://github.com/philipwalton/flexbugs) are corrected to behave like [the spec](https://www.w3.org/TR/css-flexbox-1/). +1. New CSS features are automatically compiled for Internet Explorer 11 compatibility: + - [`all` Property](https://developer.mozilla.org/en-US/docs/Web/CSS/all) + - [Break Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/break-after) + - [`font-variant` Property](https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant) + - [Gap Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/gap) + - [Grid Layout](https://developer.mozilla.org/en-US/docs/Web/CSS/grid) + - [Media Query Ranges](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries#Syntax_improvements_in_Level_4) + +By default, [Custom Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/var) (CSS variables) are **not compiled** for IE11 support. + +CSS variables are not compiled because it is [not possible to safely do so](https://github.com/MadLittleMods/postcss-css-variables#caveats). +If you must use variables, consider using something like [Sass variables](https://sass-lang.com/documentation/variables) which are compiled away by [Sass](https://sass-lang.com/). + +## Customizing Target Browsers + +Next.js allows you to configure the target browsers (for [Autoprefixer](https://github.com/postcss/autoprefixer) and compiled css features) through [Browserslist](https://github.com/browserslist/browserslist). + +To customize browserslist, create a `browserslist` key in your `package.json` like so: + +```json +{ + "browserslist": [">0.3%", "not ie 11", "not dead", "not op_mini all"] +} +``` + +You can use the [browserl.ist](https://browserl.ist/?q=%3E0.3%25%2C+not+ie+11%2C+not+dead%2C+not+op_mini+all) tool to visualize what browsers you are targeting. + +## CSS Modules + +No configuration is needed to support CSS Modules. To enable CSS Modules for a file, rename the file to have the extension `.module.css`. + +You can learn more about [Next.js' CSS Module support here](/docs/basic-features/built-in-css-support). + +## Customizing Plugins + +> **Warning**: When you define a custom PostCSS configuration file, Next.js **completely disables** the defaults listed above. +> Be sure to manually configure all features you need compiled, including [Autoprefixer](https://github.com/postcss/autoprefixer). + +To customize the PostCSS configuration, create a `postcss.config.json` file in the root of your project. + +This is the default configuration used by Next.js: + +```json +{ + "plugins": [ + "postcss-flexbugs-fixes", + [ + "postcss-preset-env", + { + "autoprefixer": { + "flexbox": "no-2009" + }, + "stage": 3, + "features": { + "custom-properties": false + } + } + ] + ] +} +``` + +> **Note**: Next.js also allows the file to be named `.postcssrc.json`, or, to be read from the `postcss` key in `package.json`. + +It is also possible to configure PostCSS with a `postcss.config.js` file, which is useful when you want to conditionally include plugins based on environment: + +```js +module.exports = { + plugins: + process.env.NODE_ENV === 'production' + ? [ + 'postcss-flexbugs-fixes', + [ + 'postcss-preset-env', + { + autoprefixer: { + flexbox: 'no-2009', + }, + stage: 3, + features: { + 'custom-properties': false, + }, + }, + ], + ] + : [ + // No transformations in development + ], +} +``` + +Do **not use `require()`** to import the PostCSS Plugins. Plugins must be provided as strings. + +> **Note**: Next.js also allows the file to be named `.postcssrc.js`. + +> **Note**: If your `postcss.config.js` needs to support other non-Next.js tools in the same project, you must use the interoperable object-based format instead: +> +> ```js +> module.exports = { +> plugins: { +> 'postcss-flexbugs-fixes': {}, +> 'postcss-preset-env': { +> autoprefixer: { +> flexbox: 'no-2009', +> }, +> stage: 3, +> features: { +> 'custom-properties': false, +> }, +> }, +> }, +> } +> ``` diff --git a/docs/api-reference/next.config.js/runtime-configuration.md b/docs/api-reference/next.config.js/runtime-configuration.md index cdda97cdbd747..4a4a2a5ace6e1 100644 --- a/docs/api-reference/next.config.js/runtime-configuration.md +++ b/docs/api-reference/next.config.js/runtime-configuration.md @@ -28,7 +28,7 @@ Place any server-only runtime config under `serverRuntimeConfig`. Anything accessible to both client and server-side code should be under `publicRuntimeConfig`. -> A page that relies on `publicRuntimeConfig` **must** use `getInitialProps` to opt-out of [Automatic Static Optimization](/docs/advanced-features/automatic-static-optimization.md). +> A page that relies on `publicRuntimeConfig` **must** use `getInitialProps` to opt-out of [Automatic Static Optimization](/docs/advanced-features/automatic-static-optimization.md). Runtime configuration won't be available to any page (or component in a page) without `getInitialProps`. To get access to the runtime configs in your app use `next/config`, like so: diff --git a/docs/api-reference/next/link.md b/docs/api-reference/next/link.md index c9d6d34ae3aaf..52ed3f450b7a6 100644 --- a/docs/api-reference/next/link.md +++ b/docs/api-reference/next/link.md @@ -49,6 +49,8 @@ export default Home - [`replace`](#replace-the-url-instead-of-push) - Replace the current `history` state instead of adding a new url into the stack. Defaults to `false` - [`scroll`](#disable-scrolling-to-the-top-of-the-page) - Scroll to the top of the page after a navigation. Defaults to `true` +External URLs, and any links that don't require a route navigation using `/pages`, don't need to be handled with `Link`; use the anchor tag for such cases instead. + ## Dynamic routes A `Link` to a dynamic route is a combination of the `href` and `as` props. A link to the page `pages/post/[pid].js` will look like this: @@ -72,12 +74,38 @@ const pids = ['id1', 'id2', 'id3'] } ``` -## Example with `React.forwardRef` +## If the child is a custom component that wraps an `` tag + +If the child of `Link` is a custom component that wraps an `` tag, you must add `passHref` to `Link`. This is necessary if you’re using libraries like [styled-components](https://styled-components.com/). Without this, the `` tag will not have the `href` attribute, which might hurt your site’s SEO. + +```jsx +import Link from 'next/link' +import styled from 'styled-components' + +// This creates a custom component that wraps an tag +const RedLink = styled.a` + color: red; +` + +function NavLink({ href, name }) { + // Must add passHref to Link + return ( + + {name} + + ) +} + +export default NavLink +``` + +> **Note:** If you’re using [emotion](https://emotion.sh/)’s JSX pragma feature (`@jsx jsx`), you must use `passHref` even if you use an `` tag directly. + +## If the child is a function component -If the child component in `Link` is a function component, you'll need to wrap it in [`React.forwardRef`](https://reactjs.org/docs/react-api.html#reactforwardref) like in the following example: +If the child of `Link` is a function component, in addition to using `passHref`, you must wrap the component in [`React.forwardRef`](https://reactjs.org/docs/react-api.html#reactforwardref): ```jsx -import React from 'react' import Link from 'next/link' // `onClick`, `href`, and `ref` need to be passed to the DOM element @@ -92,7 +120,7 @@ const MyButton = React.forwardRef(({ onClick, href }, ref) => { function Home() { return ( - + ) @@ -145,29 +173,6 @@ The default behavior of the `Link` component is to `push` a new URL into the `hi The child of `Link` is `` instead of ``. `Link` will send the `onClick` property to `` but won't pass the `href` property. -## Forcing `Link` to expose `href` to its child - -If the child is an `` tag and doesn't have a `href` attribute we specify it so that the repetition is not needed by the user. However, sometimes, you’ll want to pass an `` tag inside of a wrapper and `Link` won’t recognize it as a _hyperlink_, and, consequently, won’t transfer its `href` to the child. - -In cases like that, you can add the `passHref` property to `Link`, forcing it to expose its `href` property to the child. Take a look at the following example: - -```jsx -import Link from 'next/link' -import Unexpected_A from 'third-library' - -function NavLink({ href, name }) { - return ( - - {name} - - ) -} - -export default NavLink -``` - -> **Please note**: using a tag other than `` and failing to pass `passHref` may result in links that appear to navigate correctly, but, when being crawled by search engines, will not be recognized as links (owing to the lack of `href` attribute). This may result in negative effects on your sites SEO. - ## Disable scrolling to the top of the page The default behavior of `Link` is to scroll to the top of the page. When there is a hash defined it will scroll to the specific id, just like a normal `` tag. To prevent scrolling to the top / hash `scroll={false}` can be added to `Link`: diff --git a/docs/api-routes/introduction.md b/docs/api-routes/introduction.md index 9537c9464edd7..e8411d08bf8a6 100644 --- a/docs/api-routes/introduction.md +++ b/docs/api-routes/introduction.md @@ -46,6 +46,8 @@ export default (req, res) => { } ``` +To fetch API endpoints, take a look into any of the examples at the start of this section. + > API Routes [do not specify CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS), meaning they are **same-origin only** by default. You can customize such behavior by wrapping the request handler with [micro-cors](/docs/api-routes/api-middlewares.md#micro-support). > API Routes do not increase your client-side bundle size. They are server-side only bundles. diff --git a/docs/basic-features/built-in-css-support.md b/docs/basic-features/built-in-css-support.md index 1bc6a1274659c..1fa7c5f48f360 100644 --- a/docs/basic-features/built-in-css-support.md +++ b/docs/basic-features/built-in-css-support.md @@ -15,7 +15,8 @@ For example, consider the following stylesheet named `styles.css`: ```css body { - font-family: 'SF Pro Text', 'SF Pro Icons', system-ui; + font-family: 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', 'Helvetica', + 'Arial', sans-serif; padding: 20px 20px 60px; max-width: 680px; margin: 0 auto; @@ -157,9 +158,9 @@ export default HelloWorld Please see the [styled-jsx documentation](https://github.com/zeit/styled-jsx) for more examples. -## Sass, Less, and Stylus Support +## Sass, Less and Stylus Support -To support importing `.scss`, `.less` or `.styl` files you can use the following plugins: +To support importing `.scss`, `.sass`, `.less`, or `.styl` files you can use the following plugins: - [@zeit/next-sass](https://github.com/zeit/next-plugins/tree/master/packages/next-sass) - [@zeit/next-less](https://github.com/zeit/next-plugins/tree/master/packages/next-less) diff --git a/docs/basic-features/typescript.md b/docs/basic-features/typescript.md index aa2366a9fc299..f0f9a6d4700c7 100644 --- a/docs/basic-features/typescript.md +++ b/docs/basic-features/typescript.md @@ -101,7 +101,7 @@ The following is an example of how to use the built-in types for API routes: import { NextApiRequest, NextApiResponse } from 'next' export default (req: NextApiRequest, res: NextApiResponse) => { - res.status(200).json({ name: 'Jhon Doe' }) + res.status(200).json({ name: 'John Doe' }) } ``` @@ -115,6 +115,6 @@ type Data = { } export default (req: NextApiRequest, res: NextApiResponse) => { - res.status(200).json({ name: 'Jhon Doe' }) + res.status(200).json({ name: 'John Doe' }) } ``` diff --git a/docs/getting-started.md b/docs/getting-started.md index 357e19d039b4a..64155e0391045 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -20,7 +20,7 @@ The interactive course with quizzes will guide you through everything you need t Install `next`, `react` and `react-dom` in your project: ```bash -npm install --save next react react-dom +npm install next react react-dom ``` Open `package.json` and add the following `scripts`: @@ -39,7 +39,7 @@ These scripts refer to the different stages of developing an application: - `build` - Runs `next build` which builds the application for production usage - `start` - Runs `next start` which starts a Next.js production server -Next.js is built around the concept of pages. A page is a [React Component](https://reactjs.org/docs/components-and-props.html) exported from a `.js`, `.ts`, or `.tsx` file in the `pages` directory. +Next.js is built around the concept of pages. A page is a [React Component](https://reactjs.org/docs/components-and-props.html) exported from a `.js`, `.jsx`, `.ts`, or `.tsx` file in the `pages` directory. Pages are associated with a route based on their file name. For example `pages/about.js` is mapped to `/about`. You can even add dynamic route parameters with the filename. diff --git a/docs/manifest.json b/docs/manifest.json index 72b0ca82a21c4..b3cccfe61edfd 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -121,6 +121,10 @@ "title": "Customizing Babel Config", "path": "/docs/advanced-features/customizing-babel-config.md" }, + { + "title": "Customizing PostCSS Config", + "path": "/docs/advanced-features/customizing-postcss-config.md" + }, { "title": "Custom Server", "path": "/docs/advanced-features/custom-server.md" diff --git a/errors/no-cache.md b/errors/no-cache.md index 4c572f51237d6..2056b7c88b56d 100644 --- a/errors/no-cache.md +++ b/errors/no-cache.md @@ -74,3 +74,34 @@ cache: - 'node_modules/**/*' # Cache `node_modules` for faster `yarn` or `npm i` - '.next/cache/**/*' # Cache Next.js for faster application rebuilds ``` + +**GitHub Actions** + +Using GitHub's [actions/cache](https://github.com/actions/cache), add the following step in your workflow file: + +```yaml +uses: actions/cache@v1 +with: + path: ${{ github.workspace }}/.next/cache + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }} +``` + +**Bitbucket Pipelines** + +Add or merge the following into your `bitbucket-pipelines.yml` at the top level (same level as `pipelines`): + +```yaml +definitions: + caches: + nextcache: .next/cache +``` + +Then reference it in the `caches` section of your pipeline's `step`: + +```yaml +- step: + name: your_step_name + caches: + - node + - nextcache +``` diff --git a/errors/postcss-function.md b/errors/postcss-function.md new file mode 100644 index 0000000000000..83b301eb9d082 --- /dev/null +++ b/errors/postcss-function.md @@ -0,0 +1,30 @@ +# PostCSS Configuration Is a Function + +#### Why This Error Occurred + +The project's custom PostCSS configuration exports a function instead of an object. + +#### Possible Ways to Fix It + +Adjust the custom PostCSS configuration to not export a function. +Instead, return a plain object—if you need environment information, read it from `process.env`. + +**Before** + +```js +module.exports = ({ env }) => ({ + plugins: { + 'postcss-plugin': env === 'production' ? {} : false, + }, +}) +``` + +**After** + +```js +module.exports = { + plugins: { + 'postcss-plugin': process.env.NODE_ENV === 'production' ? {} : false, + }, +} +``` diff --git a/errors/postcss-ignored-plugin.md b/errors/postcss-ignored-plugin.md new file mode 100644 index 0000000000000..658ad57f28575 --- /dev/null +++ b/errors/postcss-ignored-plugin.md @@ -0,0 +1,20 @@ +# Ignored PostCSS Plugin + +#### Why This Error Occurred + +The project's custom PostCSS configuration attempts to configure unnecessary plugins: + +- postcss-modules-values +- postcss-modules-scope +- postcss-modules-extract-imports +- postcss-modules-local-by-default +- postcss-modules + +#### Possible Ways to Fix It + +Remove the plugin specified in the error message from your custom PostCSS configuration. + +#### How do I configure CSS Modules? + +CSS Modules are supported in [Next.js' built-in CSS support](https://nextjs.org/docs/advanced-features/customizing-postcss-config). +You can [read more](https://nextjs.org/docs/advanced-features/customizing-postcss-config) about how to use them [here](https://nextjs.org/docs/advanced-features/customizing-postcss-config). diff --git a/errors/postcss-shape.md b/errors/postcss-shape.md new file mode 100644 index 0000000000000..889fe55bcbc68 --- /dev/null +++ b/errors/postcss-shape.md @@ -0,0 +1,129 @@ +# Invalid PostCSS Configuration + +#### Why This Error Occurred + +PostCSS configuration was provided in an unsupported shape. + +#### Possible Ways to Fix It + +PostCSS configuration must be defined in the following shape: + +```js +module.exports = { + plugins: [ + // A plugin that does not require configuration: + 'simple-plugin-example', + + // A plugin which needs a configuration object: + [ + 'plugin-with-configuration', + { + optionA: '...', + }, + ], + + // A plugin that is toggled on or off based on environment: + [ + 'plugin-toggled', + process.env.NODE_ENV === 'production' + ? { + optionA: '...', + } + : false, + ], + + // Boolean expressions are also valid. + // `true` enables the plugin, `false` disables the plugin: + ['plugin-toggled-2', true /* a === b, etc */], + ], +} +``` + +You can [read more](https://nextjs.org/docs/advanced-features/customizing-postcss-config) about configuring PostCSS in Next.js [here](https://nextjs.org/docs/advanced-features/customizing-postcss-config). + +#### Common Errors + +**Before: plugin is require()'d** + +```js +const pluginA = require('postcss-plugin-a') +module.exports = { + plugins: [require('postcss-plugin'), pluginA], +} +``` + +**After** + +```js +module.exports = { + plugins: ['postcss-plugin', 'postcss-plugin-a'], +} +``` + +--- + +**Before: plugin is instantiated with configuration** + +```js +module.exports = { + plugins: [ + require('postcss-plugin')({ + optionA: '...', + }), + ], +} +``` + +**After** + +```js +module.exports = { + plugins: [ + // Pay attention to this nested array. The first index is the plugin name, + // the second index is the configuration. + [ + 'postcss-plugin', + { + optionA: '...', + }, + ], + ], +} +``` + +--- + +**Before: plugin is missing configuration** + +```js +module.exports = { + plugins: [ + [ + 'postcss-plugin-1', + { + optionA: '...', + }, + ], + // This single-entry array is detected as misconfigured because it's + // missing the second element. To fix, unwrap the value. + ['postcss-plugin-2'], + ], +} +``` + +**After** + +```js +module.exports = { + plugins: [ + [ + 'postcss-plugin-1', + { + optionA: '...', + }, + ], + // Only string: + 'postcss-plugin-2', + ], +} +``` diff --git a/examples/api-routes-apollo-server-and-client-auth/README.md b/examples/api-routes-apollo-server-and-client-auth/README.md new file mode 100644 index 0000000000000..a449cf4c45ffb --- /dev/null +++ b/examples/api-routes-apollo-server-and-client-auth/README.md @@ -0,0 +1,55 @@ +# Apollo Server and Client Auth Example + +[Apollo](https://www.apollographql.com/client/) is a GraphQL client that allows you to easily query the exact data you need from a GraphQL server. In addition to fetching and mutating data, Apollo analyzes your queries and their results to construct a client-side cache of your data, which is kept up to date as further queries and mutations are run, fetching more results from the server. + +In this simple example, we integrate Apollo seamlessly with Next by wrapping our _pages/\_app.js_ inside a [higher-order component (HOC)](https://facebook.github.io/react/docs/higher-order-components.html). Using the HOC pattern we're able to pass down a central store of query result data created by Apollo into our React component hierarchy defined inside each page of our Next application. + +On initial page load, while on the server and inside `getInitialProps`, we invoke the Apollo method, [`getDataFromTree`](https://www.apollographql.com/docs/react/api/react-ssr/#getdatafromtree). This method returns a promise; at the point in which the promise resolves, our Apollo Client store is completely initialized. + +Note: Do not be alarmed that you see two renders being executed. Apollo recursively traverses the React render tree looking for Apollo query components. When it has done that, it fetches all these queries and then passes the result to a cache. This cache is then used to render the data on the server side (another React render). +https://www.apollographql.com/docs/react/api/react-ssr/#getdatafromtree + +## Deploy your own + +Deploy the example using [ZEIT Now](https://zeit.co/now): + +[![Deploy with ZEIT Now](https://zeit.co/button)](https://zeit.co/new/project?template=https://github.com/zeit/next.js/tree/canary/examples/api-routes-apollo-server-and-client-auth) + +## How to use + +### Using `create-next-app` + +Execute [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app) with [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) or [npx](https://github.com/zkat/npx#readme) to bootstrap the example: + +```bash +npx create-next-app --example api-routes-apollo-server-and-client-auth api-routes-apollo-server-and-client-auth-app +# or +yarn create next-app --example api-routes-apollo-server-and-client-auth api-routes-apollo-server-and-client-auth-app +``` + +### Download manually + +Download the example: + +```bash +curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/api-routes-apollo-server-and-client-auth +cd api-routes-apollo-server-and-client-auth +``` + +Install it and run: + +```bash +npm install +npm run dev +# or +yarn +yarn dev +``` + +> If you have issues installing `bcrypt`, follow this instructions: https://github.com/kelektiv/node.bcrypt.js/wiki/Installation-Instructions + +Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)): + +```bash +now +``` diff --git a/examples/api-routes-apollo-server-and-client-auth/apollo/client.js b/examples/api-routes-apollo-server-and-client-auth/apollo/client.js new file mode 100644 index 0000000000000..55135ad4eb17c --- /dev/null +++ b/examples/api-routes-apollo-server-and-client-auth/apollo/client.js @@ -0,0 +1,152 @@ +import React from 'react' +import Head from 'next/head' +import { ApolloProvider } from '@apollo/react-hooks' +import { ApolloClient } from 'apollo-client' +import { InMemoryCache } from 'apollo-cache-inmemory' + +let globalApolloClient = null + +/** + * Creates and provides the apolloContext + * to a next.js PageTree. Use it by wrapping + * your PageComponent via HOC pattern. + * @param {Function|Class} PageComponent + * @param {Object} [config] + * @param {Boolean} [config.ssr=true] + */ +export function withApollo(PageComponent, { ssr = true } = {}) { + const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => { + const client = apolloClient || initApolloClient(undefined, apolloState) + return ( + + + + ) + } + + // Set the correct displayName in development + if (process.env.NODE_ENV !== 'production') { + const displayName = + PageComponent.displayName || PageComponent.name || 'Component' + + if (displayName === 'App') { + console.warn('This withApollo HOC only works with PageComponents.') + } + + WithApollo.displayName = `withApollo(${displayName})` + } + + if (ssr || PageComponent.getInitialProps) { + WithApollo.getInitialProps = async ctx => { + const { AppTree } = ctx + + // Initialize ApolloClient, add it to the ctx object so + // we can use it in `PageComponent.getInitialProp`. + const apolloClient = (ctx.apolloClient = initApolloClient({ + res: ctx.res, + req: ctx.req, + })) + + // Run wrapped getInitialProps methods + let pageProps = {} + if (PageComponent.getInitialProps) { + pageProps = await PageComponent.getInitialProps(ctx) + } + + // Only on the server: + if (typeof window === 'undefined') { + // When redirecting, the response is finished. + // No point in continuing to render + if (ctx.res && ctx.res.finished) { + return pageProps + } + + // Only if ssr is enabled + if (ssr) { + try { + // Run all GraphQL queries + const { getDataFromTree } = await import('@apollo/react-ssr') + await getDataFromTree( + + ) + } catch (error) { + // Prevent Apollo Client GraphQL errors from crashing SSR. + // Handle them in components via the data.error prop: + // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error + console.error('Error while running `getDataFromTree`', error) + } + + // getDataFromTree does not call componentWillUnmount + // head side effect therefore need to be cleared manually + Head.rewind() + } + } + + // Extract query data from the Apollo store + const apolloState = apolloClient.cache.extract() + + return { + ...pageProps, + apolloState, + } + } + } + + return WithApollo +} + +/** + * Always creates a new apollo client on the server + * Creates or reuses apollo client in the browser. + * @param {Object} initialState + */ +function initApolloClient(ctx, initialState) { + // Make sure to create a new client for every server-side request so that data + // isn't shared between connections (which would be bad) + if (typeof window === 'undefined') { + return createApolloClient(ctx, initialState) + } + + // Reuse client on the client-side + if (!globalApolloClient) { + globalApolloClient = createApolloClient(ctx, initialState) + } + + return globalApolloClient +} + +/** + * Creates and configures the ApolloClient + * @param {Object} [initialState={}] + */ +function createApolloClient(ctx = {}, initialState = {}) { + const ssrMode = typeof window === 'undefined' + const cache = new InMemoryCache().restore(initialState) + + // Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient + return new ApolloClient({ + ssrMode, + link: createIsomorphLink(ctx), + cache, + }) +} + +function createIsomorphLink(ctx) { + if (typeof window === 'undefined') { + const { SchemaLink } = require('apollo-link-schema') + const { schema } = require('./schema') + return new SchemaLink({ schema, context: ctx }) + } else { + const { HttpLink } = require('apollo-link-http') + + return new HttpLink({ + uri: '/api/graphql', + credentials: 'same-origin', + }) + } +} diff --git a/examples/api-routes-apollo-server-and-client-auth/apollo/resolvers.js b/examples/api-routes-apollo-server-and-client-auth/apollo/resolvers.js new file mode 100644 index 0000000000000..76e6d851cdba0 --- /dev/null +++ b/examples/api-routes-apollo-server-and-client-auth/apollo/resolvers.js @@ -0,0 +1,95 @@ +import { AuthenticationError, UserInputError } from 'apollo-server-micro' +import cookie from 'cookie' +import jwt from 'jsonwebtoken' +import getConfig from 'next/config' +import bcrypt from 'bcrypt' +import v4 from 'uuid/v4' + +const JWT_SECRET = getConfig().serverRuntimeConfig.JWT_SECRET + +const users = [] + +function createUser(data) { + const salt = bcrypt.genSaltSync() + + return { + id: v4(), + email: data.email, + hashedPassword: bcrypt.hashSync(data.password, salt), + } +} + +function validPassword(user, password) { + return bcrypt.compareSync(password, user.hashedPassword) +} + +export const resolvers = { + Query: { + async viewer(_parent, _args, context, _info) { + const { token } = cookie.parse(context.req.headers.cookie ?? '') + if (token) { + try { + const { id, email } = jwt.verify(token, JWT_SECRET) + + return users.find(user => user.id === id && user.email === email) + } catch { + throw new AuthenticationError( + 'Authentication token is invalid, please log in' + ) + } + } + }, + }, + Mutation: { + async signUp(_parent, args, _context, _info) { + const user = createUser(args.input) + + users.push(user) + + return { user } + }, + + async signIn(_parent, args, context, _info) { + const user = users.find(user => user.email === args.input.email) + + if (user && validPassword(user, args.input.password)) { + const token = jwt.sign( + { email: user.email, id: user.id, time: new Date() }, + JWT_SECRET, + { + expiresIn: '6h', + } + ) + + context.res.setHeader( + 'Set-Cookie', + cookie.serialize('token', token, { + httpOnly: true, + maxAge: 6 * 60 * 60, + path: '/', + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + }) + ) + + return { user } + } + + throw new UserInputError('Invalid email and password combination') + }, + async signOut(_parent, _args, context, _info) { + context.res.setHeader( + 'Set-Cookie', + cookie.serialize('token', '', { + httpOnly: true, + maxAge: -1, + path: '/', + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + }) + ) + + return true + }, + }, +} diff --git a/examples/api-routes-apollo-server-and-client-auth/apollo/schema.js b/examples/api-routes-apollo-server-and-client-auth/apollo/schema.js new file mode 100644 index 0000000000000..f6d70b7e86243 --- /dev/null +++ b/examples/api-routes-apollo-server-and-client-auth/apollo/schema.js @@ -0,0 +1,8 @@ +import { makeExecutableSchema } from 'graphql-tools' +import { typeDefs } from './type-defs' +import { resolvers } from './resolvers' + +export const schema = makeExecutableSchema({ + typeDefs, + resolvers, +}) diff --git a/examples/api-routes-apollo-server-and-client-auth/apollo/type-defs.js b/examples/api-routes-apollo-server-and-client-auth/apollo/type-defs.js new file mode 100644 index 0000000000000..cd77f1e5b26e7 --- /dev/null +++ b/examples/api-routes-apollo-server-and-client-auth/apollo/type-defs.js @@ -0,0 +1,38 @@ +import gql from 'graphql-tag' + +export const typeDefs = gql` + type User { + id: ID! + email: String! + } + + input SignUpInput { + email: String! + password: String! + } + + input SignInInput { + email: String! + password: String! + } + + type SignUpPayload { + user: User! + } + + type SignInPayload { + user: User! + } + + type Query { + user(id: ID!): User! + users: [User]! + viewer: User + } + + type Mutation { + signUp(input: SignUpInput!): SignUpPayload! + signIn(input: SignInInput!): SignInPayload! + signOut: Boolean! + } +` diff --git a/examples/api-routes-apollo-server-and-client-auth/components/field.js b/examples/api-routes-apollo-server-and-client-auth/components/field.js new file mode 100644 index 0000000000000..5ad6fa2e3548e --- /dev/null +++ b/examples/api-routes-apollo-server-and-client-auth/components/field.js @@ -0,0 +1,21 @@ +export default function Field(props) { + return ( +
    + +
    + +
    + ) +} diff --git a/examples/api-routes-apollo-server-and-client-auth/lib/form.js b/examples/api-routes-apollo-server-and-client-auth/lib/form.js new file mode 100644 index 0000000000000..7fef9d257db75 --- /dev/null +++ b/examples/api-routes-apollo-server-and-client-auth/lib/form.js @@ -0,0 +1,13 @@ +export function getErrorMessage(error) { + if (error.graphQLErrors) { + for (const graphQLError of error.graphQLErrors) { + if ( + graphQLError.extensions && + graphQLError.extensions.code === 'BAD_USER_INPUT' + ) { + return graphQLError.message + } + } + } + return error.message +} diff --git a/examples/api-routes-apollo-server-and-client-auth/next.config.js b/examples/api-routes-apollo-server-and-client-auth/next.config.js new file mode 100644 index 0000000000000..35db685d842d2 --- /dev/null +++ b/examples/api-routes-apollo-server-and-client-auth/next.config.js @@ -0,0 +1,5 @@ +module.exports = { + serverRuntimeConfig: { + JWT_SECRET: 'changeme', + }, +} diff --git a/examples/api-routes-apollo-server-and-client-auth/package.json b/examples/api-routes-apollo-server-and-client-auth/package.json new file mode 100644 index 0000000000000..cc147e35e06fe --- /dev/null +++ b/examples/api-routes-apollo-server-and-client-auth/package.json @@ -0,0 +1,32 @@ +{ + "name": "with-apollo", + "version": "2.0.0", + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@apollo/react-common": "3.1.3", + "@apollo/react-hooks": "3.1.3", + "@apollo/react-ssr": "3.1.3", + "apollo-cache-inmemory": "1.6.5", + "apollo-client": "2.6.8", + "apollo-link-http": "1.5.16", + "apollo-link-schema": "1.2.4", + "apollo-server-micro": "2.9.16", + "apollo-utilities": "^1.3.2", + "bcrypt": "3.0.7", + "cookie": "0.4.0", + "graphql": "^14.0.2", + "graphql-tag": "2.10.1", + "jsonwebtoken": "8.5.1", + "next": "latest", + "prop-types": "^15.6.2", + "react": "^16.7.0", + "react-dom": "^16.7.0", + "uuid": "3.4.0" + }, + "author": "", + "license": "ISC" +} diff --git a/examples/api-routes-apollo-server-and-client-auth/pages/about.js b/examples/api-routes-apollo-server-and-client-auth/pages/about.js new file mode 100644 index 0000000000000..37a11a9e09651 --- /dev/null +++ b/examples/api-routes-apollo-server-and-client-auth/pages/about.js @@ -0,0 +1,11 @@ +import Link from 'next/link' + +export default () => ( +
    + This is a static page goto{' '} + + dynamic + {' '} + page. +
    +) diff --git a/examples/api-routes-apollo-server-and-client-auth/pages/api/graphql.js b/examples/api-routes-apollo-server-and-client-auth/pages/api/graphql.js new file mode 100644 index 0000000000000..1e85d8c07dde5 --- /dev/null +++ b/examples/api-routes-apollo-server-and-client-auth/pages/api/graphql.js @@ -0,0 +1,17 @@ +import { ApolloServer } from 'apollo-server-micro' +import { schema } from '../../apollo/schema' + +const apolloServer = new ApolloServer({ + schema, + context(ctx) { + return ctx + }, +}) + +export const config = { + api: { + bodyParser: false, + }, +} + +export default apolloServer.createHandler({ path: '/api/graphql' }) diff --git a/examples/api-routes-apollo-server-and-client-auth/pages/index.js b/examples/api-routes-apollo-server-and-client-auth/pages/index.js new file mode 100644 index 0000000000000..2ea42bf1f70f5 --- /dev/null +++ b/examples/api-routes-apollo-server-and-client-auth/pages/index.js @@ -0,0 +1,46 @@ +import { withApollo } from '../apollo/client' +import gql from 'graphql-tag' +import Link from 'next/link' +import { useQuery } from '@apollo/react-hooks' +import { useRouter } from 'next/router' + +const ViewerQuery = gql` + query ViewerQuery { + viewer { + id + email + } + } +` + +const Index = () => { + const router = useRouter() + const { data, loading } = useQuery(ViewerQuery) + + if ( + loading === false && + data.viewer === null && + typeof window !== 'undefined' + ) { + router.push('/signin') + } + + if (data && data.viewer) { + return ( +
    + You're signed in as {data.viewer.email} goto{' '} + + static + {' '} + page. or{' '} + + signout + +
    + ) + } + + return

    Loading...

    +} + +export default withApollo(Index) 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 new file mode 100644 index 0000000000000..04e7d140c4976 --- /dev/null +++ b/examples/api-routes-apollo-server-and-client-auth/pages/signin.js @@ -0,0 +1,75 @@ +import React from 'react' +import Link from 'next/link' +import { withApollo } from '../apollo/client' +import gql from 'graphql-tag' +import { useMutation } from '@apollo/react-hooks' +import Field from '../components/field' +import { getErrorMessage } from '../lib/form' +import { useRouter } from 'next/router' + +const SignInMutation = gql` + mutation SignInMutation($email: String!, $password: String!) { + signIn(input: { email: $email, password: $password }) { + user { + id + email + } + } + } +` + +function SignIn() { + const [signIn] = useMutation(SignInMutation) + const [errorMsg, setErrorMsg] = React.useState() + const router = useRouter() + + async function handleSubmit(event) { + event.preventDefault() + + const emailElement = event.currentTarget.elements.email + const passwordElement = event.currentTarget.elements.password + + try { + const { data } = await signIn({ + variables: { + email: emailElement.value, + password: passwordElement.value, + }, + }) + if (data.signIn.user) { + router.push('/') + } + } catch (error) { + setErrorMsg(getErrorMessage(error)) + } + } + + return ( + <> +

    Sign In

    +
    + {errorMsg &&

    {errorMsg}

    } + + + or{' '} + + Sign up + + + + ) +} + +export default withApollo(SignIn) diff --git a/examples/api-routes-apollo-server-and-client-auth/pages/signout.js b/examples/api-routes-apollo-server-and-client-auth/pages/signout.js new file mode 100644 index 0000000000000..7a058862257e3 --- /dev/null +++ b/examples/api-routes-apollo-server-and-client-auth/pages/signout.js @@ -0,0 +1,29 @@ +import React from 'react' +import { useMutation } from '@apollo/react-hooks' + +import gql from 'graphql-tag' +import { useRouter } from 'next/router' +import { withApollo } from '../apollo/client' + +const SignOutMutation = gql` + mutation SignOutMutation { + signOut + } +` + +function SignOut() { + const router = useRouter() + const [signOut] = useMutation(SignOutMutation) + + React.useEffect(() => { + if (typeof window !== 'undefined') { + signOut().then(() => { + router.push('/signin') + }) + } + }, [signOut, router]) + + return

    Signing out...

    +} + +export default withApollo(SignOut) 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 new file mode 100644 index 0000000000000..77146b7759ba2 --- /dev/null +++ b/examples/api-routes-apollo-server-and-client-auth/pages/signup.js @@ -0,0 +1,73 @@ +import React from 'react' +import Link from 'next/link' +import { withApollo } from '../apollo/client' +import gql from 'graphql-tag' +import { useMutation } from '@apollo/react-hooks' +import Field from '../components/field' +import { getErrorMessage } from '../lib/form' +import { useRouter } from 'next/router' + +const SignUpMutation = gql` + mutation SignUpMutation($email: String!, $password: String!) { + signUp(input: { email: $email, password: $password }) { + user { + id + email + } + } + } +` + +function SignUp() { + const [signUp] = useMutation(SignUpMutation) + const [errorMsg, setErrorMsg] = React.useState() + const router = useRouter() + + async function handleSubmit(event) { + event.preventDefault() + const emailElement = event.currentTarget.elements.email + const passwordElement = event.currentTarget.elements.password + + try { + await signUp({ + variables: { + email: emailElement.value, + password: passwordElement.value, + }, + }) + + router.push('/signin') + } catch (error) { + setErrorMsg(getErrorMessage(error)) + } + } + + return ( + <> +

    Sign Up

    +
    + {errorMsg &&

    {errorMsg}

    } + + + or{' '} + + Sign in + + + + ) +} + +export default withApollo(SignUp) diff --git a/examples/form-handler/README.md b/examples/form-handler/README.md index b75b9c10c9822..f67d64f21caab 100644 --- a/examples/form-handler/README.md +++ b/examples/form-handler/README.md @@ -1,6 +1,6 @@ # Form Handler -Sometimes handle multiple forms can be tricky, the idea is to have a global reducer with the name of each form and the inputs of it; making accessible everywhere. +Sometimes handling multiple forms can be tricky. The idea in this example is to have a global reducer with the name of each form and the inputs of it; making accessible everywhere. ## How to use diff --git a/examples/gh-pages/next.config.js b/examples/gh-pages/next.config.js index a682026bcc651..b7dc370739d71 100644 --- a/examples/gh-pages/next.config.js +++ b/examples/gh-pages/next.config.js @@ -5,11 +5,5 @@ const debug = process.env.NODE_ENV !== 'production' module.exports = { - exportPathMap: function() { - return { - '/': { page: '/' }, - '/about': { page: '/about' }, - } - }, assetPrefix: !debug ? '/Next-gh-page-example/' : '', } diff --git a/examples/with-dynamic-import/next.config.js b/examples/with-dynamic-import/next.config.js deleted file mode 100644 index 9d92b6dab1a8d..0000000000000 --- a/examples/with-dynamic-import/next.config.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - exportPathMap: function() { - return { - '/': { page: '/', query: { showMore: false } }, - '/about': { page: '/about' }, - } - }, -} diff --git a/examples/with-graphql-faunadb/README.md b/examples/with-graphql-faunadb/README.md index 68815c44ce86d..42fa330ebde36 100644 --- a/examples/with-graphql-faunadb/README.md +++ b/examples/with-graphql-faunadb/README.md @@ -16,7 +16,21 @@ By importing a `.gql` or `.graphql` schema into FaunaDB ([see our sample schema You can start with this template [using `create-next-app`](#using-create-next-app) or by [downloading the repository manually](#download-manually). -To use a live FaunaDB database, create one and import this example's `schema.gql` file using the FaunaDB console. Create a client secret, then paste it into `next.config.js`. +To use a live FaunaDB database, create a database at [dashboard.fauna.com](https://dashboard.fauna.com/) and generate an admin token by going to the **Security** tab on the left and then click **New Key**. Give the new key a name and select the 'Admin' Role. Copy the token since the setup script will ask for it. Do not use it in the frontend, it has superpowers which you don't want to give to your users. + +The database can then be set up with the delivered setup by running: + +``` +yarn setup +``` + +This script will ask for the admin token. Once you provide it with a valid token, this is what the script automatically does for you: + +- **Import the GraphQL schema**, by importing a GraphQL schema in FaunaDB, FaunaDB automatically sets up collections and indexes to support your queries. This is now done for you with this script but can also be done from the [dashboard.fauna.com](https://dashboard.fauna.com/) UI by going to the GraphQL tab +- **Create a role suitable for the Client**, FaunaDB has a security system that allows you to define which resources can be accessed for a specific token. That's how we limit our clients powers, feel free to look at the scripts/setup.js script to see how we make roles and tokens. +- **Create a token for that role** which is printed, this is the token to be used in the frontend. + +At the end, the newly generated client token will be printed and should be used to replace the '< GRAPHQL_SECRET >' placeholder in the next.config.js config. ### Using `create-next-app` diff --git a/examples/with-graphql-faunadb/next.config.js b/examples/with-graphql-faunadb/next.config.js index 53ded0ee42327..b80a98e8de9b9 100644 --- a/examples/with-graphql-faunadb/next.config.js +++ b/examples/with-graphql-faunadb/next.config.js @@ -8,7 +8,7 @@ module.exports = { | https://docs.fauna.com/fauna/current/security/ |-------------------------------------------------- */ - faunaDbSecret: 'fnADcnWRUcACE_6uDSw05MspruDdWKk88ZSmsm2a', + faunaDbSecret: '< GRAPHQL_SECRET >', faunaDbGraphQlEndpoint: 'https://graphql.fauna.com/graphql', }, } diff --git a/examples/with-graphql-faunadb/package.json b/examples/with-graphql-faunadb/package.json index 021dea5656291..7e5b79e34468e 100644 --- a/examples/with-graphql-faunadb/package.json +++ b/examples/with-graphql-faunadb/package.json @@ -5,11 +5,17 @@ "scripts": { "dev": "next", "build": "next build", - "start": "next start" + "start": "next start", + "setup": "node ./scripts/setup.js" }, "dependencies": { "next": "latest", "react": "^16.10.2", "react-dom": "^16.10.2" + }, + "devDependencies": { + "faunadb": "2.11.1", + "request": "2.88.0", + "stream-to-promise": "2.2.0" } } diff --git a/examples/with-graphql-faunadb/scripts/setup.js b/examples/with-graphql-faunadb/scripts/setup.js new file mode 100644 index 0000000000000..ca701141a7064 --- /dev/null +++ b/examples/with-graphql-faunadb/scripts/setup.js @@ -0,0 +1,103 @@ +// This script sets up the database to be used for this example application. +// Look at the code to see what is behind the magic +const faunadb = require('faunadb') +const q = faunadb.query +const request = require('request') +const fs = require('fs') +const streamToPromise = require('stream-to-promise') + +const readline = require('readline').createInterface({ + input: process.stdin, + output: process.stdout, +}) + +// In order to set up a database, we need an admin key, so let's ask the user for a key. +readline.question(`Please provide the FaunaDB admin key\n`, adminKey => { + // A graphql schema can be imported in override or merge mode: 'https://docs.fauna.com/fauna/current/api/graphql/endpoints#import' + const options = { + model: 'merge', + uri: 'https://graphql.fauna.com/import', + headers: { Authorization: `Bearer ${adminKey}` }, + } + const stream = fs.createReadStream('./schema.gql').pipe(request.post(options)) + + streamToPromise(stream) + .then(res => { + const readableResult = res.toString() + if (readableResult.startsWith('Invalid authorization header')) { + console.error('You need to provide a secret, closing. Try again') + return readline.close() + } else if (readableResult.startsWith('Invalid database secret')) { + console.error( + 'The secret you have provided is not valid, closing. Try again' + ) + return readline.close() + } else if (readableResult.includes('success')) { + console.log('1. Successfully imported schema') + return readline.close() + } + }) + .catch(err => { + console.error(err) + console.error(`Could not import schema, closing`) + }) + .then(res => { + // The GraphQL schema is important, this means that we now have a GuestbookEntry Colleciton and an entries index. + // Then we create a token that can only read and write to that index and collection + var client = new faunadb.Client({ secret: adminKey }) + return client + .query( + q.CreateRole({ + name: 'GuestbookRole', + privileges: [ + { + resource: q.Collection('GuestbookEntry'), + actions: { read: true, write: true }, + }, + { + resource: q.Index('entries'), + actions: { read: true }, + }, + ], + }) + ) + .then(res => { + console.log( + '2. Successfully created role to read and write guestbook entries' + ) + }) + .catch(err => { + if (err.toString().includes('instance already exists')) { + console.log('2. Role already exists.') + } else { + throw err + } + }) + }) + .catch(err => { + console.error(err) + console.error(`Failed to create role, closing`) + }) + .then(res => { + // The GraphQL schema is important, this means that we now have a GuestbookEntry Colleciton and an entries index. + // Then we create a token that can only read and write to that index and collection + var client = new faunadb.Client({ secret: adminKey }) + return client + .query( + q.CreateKey({ + role: q.Role('GuestbookRole'), + }) + ) + .then(res => { + console.log('3. Created key to use in client') + console.log( + 'Replace the < GRAPHQL_SECRET > placehold in next.config.js with:' + ) + console.log(res.secret) + }) + }) + .catch(err => { + console.error(err) + console.error(`Failed to create key, closing`) + }) +}) diff --git a/examples/with-stencil/packages/test-component/readme.md b/examples/with-stencil/packages/test-component/readme.md index 438a2241ee2a7..db8c4cca0c49f 100644 --- a/examples/with-stencil/packages/test-component/readme.md +++ b/examples/with-stencil/packages/test-component/readme.md @@ -61,12 +61,12 @@ Instead, use a prefix that fits your company or any name for a group of related ### Node Modules -- Run `npm install my-component --save` +- Run `npm install my-component` - Put a script tag similar to this `` in the head of your index.html - Then you can use the element anywhere in your template, JSX, html etc ### In a stencil-starter app -- Run `npm install my-component --save` +- Run `npm install my-component` - Add an import to the npm packages `import my-component;` - Then you can use the element anywhere in your template, JSX, html etc diff --git a/examples/with-typescript-graphql/README.md b/examples/with-typescript-graphql/README.md index bf299a32ec938..cd4ae272cd9c2 100644 --- a/examples/with-typescript-graphql/README.md +++ b/examples/with-typescript-graphql/README.md @@ -1,11 +1,11 @@ -# GraphQL and TypeScript Example +# TypeScript and GraphQL Example One of the strengths of GraphQL is [enforcing data types on runtime](https://graphql.github.io/graphql-spec/June2018/#sec-Value-Completion). Further, TypeScript and [GraphQL Code Generator](https://graphql-code-generator.com/) (graphql-codegen) make it safer by typing data statically, so you can write truly type-protected code with rich IDE assists. This template extends [Apollo Server and Client Example](https://github.com/zeit/next.js/tree/canary/examples/api-routes-apollo-server-and-client#readme) by rewriting in TypeScript and integrating [graphql-let](https://github.com/piglovesyou/graphql-let#readme), which runs [TypeScript React Apollo](https://graphql-code-generator.com/docs/plugins/typescript-react-apollo) in [graphql-codegen](https://github.com/dotansimha/graphql-code-generator#readme) under the hood. It enhances the typed GraphQL use as below. ```typescript jsx -import { useNewsQuery } from './news.grpahql' +import { useNewsQuery } from './news.graphql' const News: React.FC = () => { // Typed already️⚡️ @@ -59,7 +59,7 @@ now ## Notes -By default `**/*.graphqls` is recognized as GraphQL schema and `**/*.graphql` as GraphQL documents. If you prefer the other extensions, make sure the settings of the webpack loader in `next.config.js` and `.graphql-let.yml` point to the same files. +By default `**/*.graphqls` is recognized as GraphQL schema and `**/*.graphql` as GraphQL documents. If you prefer the other extensions, make sure the settings of the webpack loader in `next.config.js` and `.graphql-let.yml` are consistent. Note: Do not be alarmed that you see two renders being executed. Apollo recursively traverses the React render tree looking for Apollo query components. When it has done that, it fetches all these queries and then passes the result to a cache. This cache is then used to render the data on the server side (another React render). https://www.apollographql.com/docs/react/api/react-ssr/#getdatafromtree diff --git a/examples/with-typescript-graphql/lib/resolvers.ts b/examples/with-typescript-graphql/lib/resolvers.ts index b408e6353a9ab..1bd9511549d68 100644 --- a/examples/with-typescript-graphql/lib/resolvers.ts +++ b/examples/with-typescript-graphql/lib/resolvers.ts @@ -1,4 +1,6 @@ -const resolvers = { +import { IResolvers } from 'apollo-server-micro' + +const resolvers: IResolvers = { Query: { viewer(_parent, _args, _context, _info) { return { id: 1, name: 'John Smith', status: 'cached' } diff --git a/examples/with-xstate/README.md b/examples/with-xstate/README.md new file mode 100644 index 0000000000000..38367fc0ecfcf --- /dev/null +++ b/examples/with-xstate/README.md @@ -0,0 +1,46 @@ +# XState example + +This example shows how to integrate XState in Next.js. For more info about XState you can visit [here](https://xstate.js.org/). + +## Deploy your own + +Deploy the example using [ZEIT Now](https://zeit.co/now): + +[![Deploy with ZEIT Now](https://zeit.co/button)](https://zeit.co/new/project?template=https://github.com/zeit/next.js/tree/canary/examples/with-xstate) + +## How to use + +### Using `create-next-app` + +Execute [`create-next-app`](https://github.com/zeit/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 +npm init next-app --example with-xstate with-xstate-app +# or +yarn create next-app --example with-xstate with-xstate-app +``` + +### Download manually + +Download the example: + +```bash +curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-xstate +cd with-xstate +``` + +Install it and run: + +```bash +npm install +npm run dev +# or +yarn +yarn dev +``` + +Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)): + +```bash +now +``` diff --git a/examples/with-xstate/components/Counter.js b/examples/with-xstate/components/Counter.js new file mode 100644 index 0000000000000..10e6b440295f1 --- /dev/null +++ b/examples/with-xstate/components/Counter.js @@ -0,0 +1,10 @@ +export default ({ counter = {} }) => ( +
    +

    + Count: {counter.count} +

    + + + +
    +) diff --git a/examples/with-xstate/components/Toggle.js b/examples/with-xstate/components/Toggle.js new file mode 100644 index 0000000000000..ce609b97a1108 --- /dev/null +++ b/examples/with-xstate/components/Toggle.js @@ -0,0 +1,14 @@ +import React from 'react' + +const Toggle = ({ onToggle, active }) => { + return ( +
    +

    + Toogle status: {active ? 'On' : 'Off'} +

    + +
    + ) +} + +export default Toggle diff --git a/examples/with-xstate/components/index.js b/examples/with-xstate/components/index.js new file mode 100644 index 0000000000000..dd86cb409f508 --- /dev/null +++ b/examples/with-xstate/components/index.js @@ -0,0 +1,4 @@ +import Counter from './Counter' +import Toggle from './Toggle' + +export { Counter, Toggle } diff --git a/examples/with-xstate/machines/counterMachine.js b/examples/with-xstate/machines/counterMachine.js new file mode 100644 index 0000000000000..dac19012736ab --- /dev/null +++ b/examples/with-xstate/machines/counterMachine.js @@ -0,0 +1,20 @@ +import { Machine, assign } from 'xstate' + +const increment = context => context.count + 1 +const decrement = context => context.count - 1 + +export const counterMachine = Machine({ + initial: 'active', + context: { + count: 0, + }, + states: { + active: { + on: { + INC: { actions: assign({ count: increment }) }, + DEC: { actions: assign({ count: decrement }) }, + RESET: { actions: assign({ count: 0 }) }, + }, + }, + }, +}) diff --git a/examples/with-xstate/machines/toggleMachine.js b/examples/with-xstate/machines/toggleMachine.js new file mode 100644 index 0000000000000..3c79f27b57523 --- /dev/null +++ b/examples/with-xstate/machines/toggleMachine.js @@ -0,0 +1,14 @@ +import { Machine } from 'xstate' + +export const toggleMachine = Machine({ + id: 'toggle', + initial: 'inactive', + states: { + inactive: { + on: { TOGGLE: 'active' }, + }, + active: { + on: { TOGGLE: 'inactive' }, + }, + }, +}) diff --git a/examples/with-xstate/package.json b/examples/with-xstate/package.json new file mode 100644 index 0000000000000..899d59e455a9d --- /dev/null +++ b/examples/with-xstate/package.json @@ -0,0 +1,17 @@ +{ + "name": "with-xstate", + "version": "1.0.0", + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@xstate/react": "0.8.1", + "next": "latest", + "react": "^16.9.0", + "react-dom": "^16.9.0", + "xstate": "4.7.6" + }, + "license": "ISC" +} diff --git a/examples/with-xstate/pages/index.js b/examples/with-xstate/pages/index.js new file mode 100644 index 0000000000000..3e32a3b2cbd13 --- /dev/null +++ b/examples/with-xstate/pages/index.js @@ -0,0 +1,36 @@ +import React from 'react' +import { useMachine } from '@xstate/react' +import { Counter, Toggle } from '../components' +import { toggleMachine } from '../machines/toggleMachine' +import { counterMachine } from '../machines/counterMachine' + +const IndexPage = ({ count }) => { + const [toggleCurrent, toggleSend] = useMachine(toggleMachine) + const [counterCurrent, counterSend] = useMachine(counterMachine, { + context: { count }, + }) + + return ( +
    + counterSend('INC'), + decrement: () => counterSend('DEC'), + reset: () => counterSend('RESET'), + }} + /> +
    + toggleSend('TOGGLE')} + active={toggleCurrent.matches('active')} + /> +
    + ) +} + +IndexPage.getInitialProps = async () => { + return { count: 999 } +} + +export default IndexPage diff --git a/lerna.json b/lerna.json index 7023350e483ff..4224ec9796277 100644 --- a/lerna.json +++ b/lerna.json @@ -12,5 +12,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "9.2.1-canary.7" + "version": "9.2.1" } diff --git a/package.json b/package.json index 8af963c4e6e3d..c1866b0dc694c 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,8 @@ "@mdx-js/loader": "0.18.0", "@types/jest": "24.0.13", "@types/string-hash": "1.1.1", - "@typescript-eslint/eslint-plugin": "2.6.1", - "@typescript-eslint/parser": "2.6.1", + "@typescript-eslint/eslint-plugin": "2.17.0", + "@typescript-eslint/parser": "2.17.0", "@zeit/next-css": "1.0.2-canary.2", "@zeit/next-sass": "1.0.2-canary.2", "@zeit/next-typescript": "1.1.2-canary.0", @@ -60,9 +60,9 @@ "cross-env": "6.0.3", "cross-spawn": "6.0.5", "escape-string-regexp": "2.0.0", - "eslint": "6.6.0", - "eslint-plugin-react": "7.16.0", - "eslint-plugin-react-hooks": "2.2.0", + "eslint": "6.8.0", + "eslint-plugin-react": "7.18.0", + "eslint-plugin-react-hooks": "2.3.0", "execa": "2.0.3", "express": "4.17.0", "faunadb": "2.6.1", diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index c53da15c8f2a4..aeb07f1ed8bda 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "9.2.1-canary.7", + "version": "9.2.1", "keywords": [ "react", "next", diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 2c7bfa7856cd4..e316dc8450374 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "9.2.1-canary.7", + "version": "9.2.1", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-bundle-analyzer/readme.md b/packages/next-bundle-analyzer/readme.md index 18aa9dedeb114..1d2ac2d2f04a9 100644 --- a/packages/next-bundle-analyzer/readme.md +++ b/packages/next-bundle-analyzer/readme.md @@ -5,7 +5,7 @@ Use `webpack-bundle-analyzer` in your Next.js project ## Installation ``` -npm install --save @next/bundle-analyzer +npm install @next/bundle-analyzer ``` or diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index a6e699a47c38c..022aaaec43d31 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "9.2.1-canary.7", + "version": "9.2.1", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-mdx/readme.md b/packages/next-mdx/readme.md index 2378f35272b41..a45f28cef0dee 100644 --- a/packages/next-mdx/readme.md +++ b/packages/next-mdx/readme.md @@ -5,7 +5,7 @@ Use [MDX](https://github.com/mdx-js/mdx) with [Next.js](https://github.com/zeit/ ## Installation ``` -npm install --save @next/mdx @mdx-js/loader +npm install @next/mdx @mdx-js/loader ``` or diff --git a/packages/next-plugin-google-analytics/package.json b/packages/next-plugin-google-analytics/package.json index 4e978b56fd5f9..3b8e6e70716db 100644 --- a/packages/next-plugin-google-analytics/package.json +++ b/packages/next-plugin-google-analytics/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-google-analytics", - "version": "9.2.1-canary.7", + "version": "9.2.1", "nextjs": { "name": "Google Analytics", "required-env": [ diff --git a/packages/next-plugin-material-ui/package.json b/packages/next-plugin-material-ui/package.json index 035b8275024ec..eab16d20f2fa2 100644 --- a/packages/next-plugin-material-ui/package.json +++ b/packages/next-plugin-material-ui/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-material-ui", - "version": "9.2.1-canary.7", + "version": "9.2.1", "nextjs": { "name": "Material UI", "required-env": [] diff --git a/packages/next-plugin-sentry/package.json b/packages/next-plugin-sentry/package.json index cbc0044d8bc9c..f9b2f5330bb13 100644 --- a/packages/next-plugin-sentry/package.json +++ b/packages/next-plugin-sentry/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-sentry", - "version": "9.2.1-canary.7", + "version": "9.2.1", "nextjs": { "name": "Sentry", "required-env": [ diff --git a/packages/next/bin/next.ts b/packages/next/bin/next.ts index acc1694ff2447..6c65b90eac861 100755 --- a/packages/next/bin/next.ts +++ b/packages/next/bin/next.ts @@ -7,7 +7,7 @@ import arg from 'next/dist/compiled/arg/index.js' } catch (err) { // tslint:disable-next-line console.warn( - `The module '${dependency}' was not found. Next.js requires that you include it in 'dependencies' of your 'package.json'. To add it, run 'npm install --save ${dependency}'` + `The module '${dependency}' was not found. Next.js requires that you include it in 'dependencies' of your 'package.json'. To add it, run 'npm install ${dependency}'` ) } }) @@ -94,7 +94,7 @@ const React = require('react') if (typeof React.Suspense === 'undefined') { throw new Error( - `The version of React you are using is lower than the minimum required version needed for Next.js. Please upgrade "react" and "react-dom": "npm install --save react react-dom" https://err.sh/zeit/next.js/invalid-react-version` + `The version of React you are using is lower than the minimum required version needed for Next.js. Please upgrade "react" and "react-dom": "npm install react react-dom" https://err.sh/zeit/next.js/invalid-react-version` ) } diff --git a/packages/next/build/babel/plugins/next-page-config.ts b/packages/next/build/babel/plugins/next-page-config.ts index a101ca91f463a..9fb733db40a16 100644 --- a/packages/next/build/babel/plugins/next-page-config.ts +++ b/packages/next/build/babel/plugins/next-page-config.ts @@ -1,6 +1,6 @@ import { NodePath, PluginObj } from '@babel/core' import * as BabelTypes from '@babel/types' -import { PageConfig } from '../../../types' +import { PageConfig } from 'next/types' const configKeys = new Set(['amp']) const STRING_LITERAL_DROP_BUNDLE = '__NEXT_DROP_CLIENT_FILE__' diff --git a/packages/next/build/babel/preset.ts b/packages/next/build/babel/preset.ts index ac886d86fa8a7..8e95a8c2c0dcc 100644 --- a/packages/next/build/babel/preset.ts +++ b/packages/next/build/babel/preset.ts @@ -156,7 +156,6 @@ module.exports = ( helpers: true, regenerator: true, useESModules: supportsESM && presetEnvConfig.modules !== 'commonjs', - version: require('@babel/runtime-corejs2/package.json').version, absoluteRuntime: (process.versions as any).pnp ? __dirname : undefined, @@ -178,6 +177,7 @@ module.exports = ( ], require('@babel/plugin-proposal-optional-chaining'), require('@babel/plugin-proposal-nullish-coalescing-operator'), + isServer && require('@babel/plugin-syntax-bigint'), ].filter(Boolean), } } diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 30585597ce51f..964082a2b8e1d 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -53,6 +53,7 @@ import { generateBuildId } from './generate-build-id' import { isWriteable } from './is-writeable' import createSpinner from './spinner' import { + isPageStatic, collectPages, getPageSizeInKb, hasCustomAppGetInitialProps, @@ -416,7 +417,8 @@ export default async function build(dir: string, conf = null): Promise { const staticCheckWorkers = new Worker(staticCheckWorker, { numWorkers: config.experimental.cpus, enableWorkerThreads: config.experimental.workerThreads, - }) + }) as Worker & { isPageStatic: typeof isPageStatic } + staticCheckWorkers.getStdout().pipe(process.stdout) staticCheckWorkers.getStderr().pipe(process.stderr) @@ -481,7 +483,7 @@ export default async function build(dir: string, conf = null): Promise { if (nonReservedPage) { try { - let result: any = await (staticCheckWorkers as any).isPageStatic( + let result = await staticCheckWorkers.isPageStatic( page, serverBundle, runtimeEnvConfig @@ -492,7 +494,7 @@ export default async function build(dir: string, conf = null): Promise { hybridAmpPages.add(page) } - if (result.prerender) { + if (result.hasStaticProps) { ssgPages.add(page) isSsg = true @@ -500,7 +502,7 @@ export default async function build(dir: string, conf = null): Promise { additionalSsgPaths.set(page, result.prerenderRoutes) ssgPageRoutes = result.prerenderRoutes } - } else if (result.static && customAppGetInitialProps === false) { + } else if (result.isStatic && customAppGetInitialProps === false) { staticPages.add(page) isStatic = true } diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 85c4f7ed728b6..ac126d9c77494 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -479,9 +479,9 @@ export async function isPageStatic( serverBundle: string, runtimeEnvConfig: any ): Promise<{ - static?: boolean - prerender?: boolean + isStatic?: boolean isHybridAmp?: boolean + hasStaticProps?: boolean prerenderRoutes?: string[] | undefined }> { try { @@ -593,10 +593,10 @@ export async function isPageStatic( const config = mod.config || {} return { - static: !hasStaticProps && !hasGetInitialProps, + isStatic: !hasStaticProps && !hasGetInitialProps, isHybridAmp: config.amp === 'hybrid', prerenderRoutes: prerenderPaths, - prerender: hasStaticProps, + hasStaticProps, } } catch (err) { if (err.code === 'MODULE_NOT_FOUND') return {} diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 2352a9985f562..b94f2fd6526c7 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -28,7 +28,7 @@ import { VALID_MIDDLEWARE, } from './plugins/collect-plugins' import { build as buildConfiguration } from './webpack/config' -import { __overrideCssConfiguration } from './webpack/config/blocks/css' +import { __overrideCssConfiguration } from './webpack/config/blocks/css/overrideCssConfiguration' // @ts-ignore: JS file import { pluginLoaderOptions } from './webpack/loaders/next-plugin-loader' import BuildManifestPlugin from './webpack/plugins/build-manifest-plugin' @@ -44,6 +44,9 @@ import { ProfilingPlugin } from './webpack/plugins/profiling-plugin' import { ReactLoadablePlugin } from './webpack/plugins/react-loadable-plugin' import { ServerlessPlugin } from './webpack/plugins/serverless-plugin' import { TerserPlugin } from './webpack/plugins/terser-webpack-plugin/src/index' +import WebpackConformancePlugin, { + MinificationConformanceCheck, +} from './webpack/plugins/webpack-conformance-plugin' type ExcludesFalse = (x: T | false) => x is T @@ -825,6 +828,11 @@ export default async function getBaseWebpackConfig( chunkFilename: (inputChunkName: string) => inputChunkName.replace(/\.js$/, '.module.js'), }), + config.experimental.conformance && + !dev && + new WebpackConformancePlugin({ + tests: [new MinificationConformanceCheck()], + }), ].filter((Boolean as any) as ExcludesFalse), } @@ -834,6 +842,7 @@ export default async function getBaseWebpackConfig( isDevelopment: dev, isServer, hasSupportCss: !!config.experimental.css, + hasSupportScss: !!config.experimental.scss, assetPrefix: config.assetPrefix || '', }) diff --git a/packages/next/build/webpack/config/blocks/css/index.ts b/packages/next/build/webpack/config/blocks/css/index.ts index ef8fb5a60185a..0ba6ec7051db3 100644 --- a/packages/next/build/webpack/config/blocks/css/index.ts +++ b/packages/next/build/webpack/config/blocks/css/index.ts @@ -1,10 +1,10 @@ import curry from 'lodash.curry' import path from 'path' -import webpack, { Configuration, RuleSetRule } from 'webpack' +import webpack, { Configuration } from 'webpack' import MiniCssExtractPlugin from '../../../plugins/mini-css-extract-plugin' import { loader, plugin } from '../../helpers' import { ConfigurationContext, ConfigurationFn, pipe } from '../../utils' -import { getCssModuleLocalIdent } from './getCssModuleLocalIdent' +import { getCssModuleLoader, getGlobalCssLoader } from './loaders' import { getCustomDocumentError, getGlobalImportError, @@ -13,96 +13,20 @@ import { } from './messages' import { getPostCssPlugins } from './plugins' -function getClientStyleLoader({ - isDevelopment, - assetPrefix, -}: { - isDevelopment: boolean - assetPrefix: string -}): webpack.RuleSetUseItem { - return isDevelopment - ? { - loader: require.resolve('style-loader'), - options: { - // By default, style-loader injects CSS into the bottom - // of . This causes ordering problems between dev - // and prod. To fix this, we render a