mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Expose app.js (#4129)
* Expose pages/_app.js * Add tests for _app and _document * Uncomment deprecation warnings * Add documentation for _app, improve documentation of _document * Update docs / test for _document * Add _document to client compiler in development * Add missing app.js to comment * Only warn once * Add url-deprecated error page * Combine tests * Yse same message for all methods of ‘props.url’ * Update docs around _app * Update documentation * Quotes * Update table of contents
This commit is contained in:
parent
15dde33794
commit
eca8e8f64b
|
@ -3,7 +3,6 @@ import ReactDOM from 'react-dom'
|
||||||
import HeadManager from './head-manager'
|
import HeadManager from './head-manager'
|
||||||
import { createRouter } from '../lib/router'
|
import { createRouter } from '../lib/router'
|
||||||
import EventEmitter from '../lib/EventEmitter'
|
import EventEmitter from '../lib/EventEmitter'
|
||||||
import App from '../lib/app'
|
|
||||||
import { loadGetInitialProps, getURL } from '../lib/utils'
|
import { loadGetInitialProps, getURL } from '../lib/utils'
|
||||||
import PageLoader from '../lib/page-loader'
|
import PageLoader from '../lib/page-loader'
|
||||||
import * as asset from '../lib/asset'
|
import * as asset from '../lib/asset'
|
||||||
|
@ -69,6 +68,7 @@ export let router
|
||||||
export let ErrorComponent
|
export let ErrorComponent
|
||||||
let ErrorDebugComponent
|
let ErrorDebugComponent
|
||||||
let Component
|
let Component
|
||||||
|
let App
|
||||||
let stripAnsi = (s) => s
|
let stripAnsi = (s) => s
|
||||||
|
|
||||||
export const emitter = new EventEmitter()
|
export const emitter = new EventEmitter()
|
||||||
|
@ -82,16 +82,23 @@ export default async ({ ErrorDebugComponent: passedDebugComponent, stripAnsi: pa
|
||||||
stripAnsi = passedStripAnsi || stripAnsi
|
stripAnsi = passedStripAnsi || stripAnsi
|
||||||
ErrorDebugComponent = passedDebugComponent
|
ErrorDebugComponent = passedDebugComponent
|
||||||
ErrorComponent = await pageLoader.loadPage('/_error')
|
ErrorComponent = await pageLoader.loadPage('/_error')
|
||||||
|
App = await pageLoader.loadPage('/_app')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Component = await pageLoader.loadPage(page)
|
Component = await pageLoader.loadPage(page)
|
||||||
|
|
||||||
|
if (typeof Component !== 'function') {
|
||||||
|
throw new Error(`The default export is not a React Component in page: "${pathname}"`)
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(stripAnsi(`${err.message}\n${err.stack}`))
|
console.error(stripAnsi(`${err.message}\n${err.stack}`))
|
||||||
Component = ErrorComponent
|
Component = ErrorComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
router = createRouter(pathname, query, asPath, {
|
router = createRouter(pathname, query, asPath, {
|
||||||
|
initialProps: props,
|
||||||
pageLoader,
|
pageLoader,
|
||||||
|
App,
|
||||||
Component,
|
Component,
|
||||||
ErrorComponent,
|
ErrorComponent,
|
||||||
err
|
err
|
||||||
|
@ -136,7 +143,7 @@ export async function renderError (error) {
|
||||||
console.error(stripAnsi(errorMessage))
|
console.error(stripAnsi(errorMessage))
|
||||||
|
|
||||||
if (prod) {
|
if (prod) {
|
||||||
const initProps = { err: error, pathname, query, asPath }
|
const initProps = {Component: ErrorComponent, router, ctx: {err: error, pathname, query, asPath}}
|
||||||
const props = await loadGetInitialProps(ErrorComponent, initProps)
|
const props = await loadGetInitialProps(ErrorComponent, initProps)
|
||||||
renderReactElement(createElement(ErrorComponent, props), errorContainer)
|
renderReactElement(createElement(ErrorComponent, props), errorContainer)
|
||||||
} else {
|
} else {
|
||||||
|
@ -145,18 +152,19 @@ export async function renderError (error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doRender ({ Component, props, hash, err, emitter: emitterProp = emitter }) {
|
async function doRender ({ Component, props, hash, err, emitter: emitterProp = emitter }) {
|
||||||
|
// Usual getInitialProps fetching is handled in next/router
|
||||||
|
// this is for when ErrorComponent gets replaced by Component by HMR
|
||||||
if (!props && Component &&
|
if (!props && Component &&
|
||||||
Component !== ErrorComponent &&
|
Component !== ErrorComponent &&
|
||||||
lastAppProps.Component === ErrorComponent) {
|
lastAppProps.Component === ErrorComponent) {
|
||||||
// fetch props if ErrorComponent was replaced with a page component by HMR
|
|
||||||
const { pathname, query, asPath } = router
|
const { pathname, query, asPath } = router
|
||||||
props = await loadGetInitialProps(Component, { err, pathname, query, asPath })
|
props = await loadGetInitialProps(App, {Component, router, ctx: {err, pathname, query, asPath}})
|
||||||
}
|
}
|
||||||
|
|
||||||
Component = Component || lastAppProps.Component
|
Component = Component || lastAppProps.Component
|
||||||
props = props || lastAppProps.props
|
props = props || lastAppProps.props
|
||||||
|
|
||||||
const appProps = { Component, props, hash, err, router, headManager }
|
const appProps = { Component, hash, err, router, headManager, ...props }
|
||||||
// lastAppProps has to be set before ReactDom.render to account for ReactDom throwing an error.
|
// lastAppProps has to be set before ReactDom.render to account for ReactDom throwing an error.
|
||||||
lastAppProps = appProps
|
lastAppProps = appProps
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,13 @@ export default () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the App component changes we have to reload the current route
|
||||||
|
if (route === '/_app') {
|
||||||
|
Router.reload(Router.route)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since _document is server only we need to reload the full page when it changes.
|
||||||
if (route === '/_document') {
|
if (route === '/_document') {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
return
|
return
|
||||||
|
@ -36,6 +43,13 @@ export default () => {
|
||||||
},
|
},
|
||||||
|
|
||||||
change (route) {
|
change (route) {
|
||||||
|
// If the App component changes we have to reload the current route
|
||||||
|
if (route === '/_app') {
|
||||||
|
Router.reload(Router.route)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since _document is server only we need to reload the full page when it changes.
|
||||||
if (route === '/_document') {
|
if (route === '/_document') {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
return
|
return
|
||||||
|
|
23
errors/url-deprecated.md
Normal file
23
errors/url-deprecated.md
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Url is deprecated
|
||||||
|
|
||||||
|
#### Why This Error Occurred
|
||||||
|
|
||||||
|
In version prior to 6.x `url` got magically injected into every page component, since this is confusing and can now be added by the user using a custom `_app.js` we have deprecated this feature. To be removed in Next.js 7.0
|
||||||
|
|
||||||
|
#### Possible Ways to Fix It
|
||||||
|
|
||||||
|
The easiest way to get the same values that `url` had is to use `withRouter`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { withRouter } from 'next/router'
|
||||||
|
|
||||||
|
class Page extends React.Component {
|
||||||
|
render() {
|
||||||
|
const {router} = this.props
|
||||||
|
console.log(router)
|
||||||
|
return <div>{router.pathname}</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(Page)
|
||||||
|
```
|
76
lib/app.js
76
lib/app.js
|
@ -1,7 +1,7 @@
|
||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import shallowEquals from './shallow-equals'
|
import shallowEquals from './shallow-equals'
|
||||||
import { warn } from './utils'
|
import { execOnce, warn, loadGetInitialProps } from './utils'
|
||||||
import { makePublicRouterInstance } from './router'
|
import { makePublicRouterInstance } from './router'
|
||||||
|
|
||||||
export default class App extends Component {
|
export default class App extends Component {
|
||||||
|
@ -9,16 +9,26 @@ export default class App extends Component {
|
||||||
hasError: null
|
hasError: null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static displayName = 'App'
|
||||||
|
|
||||||
|
static async getInitialProps ({ Component, router, ctx }) {
|
||||||
|
const pageProps = await loadGetInitialProps(Component, ctx)
|
||||||
|
return {pageProps}
|
||||||
|
}
|
||||||
|
|
||||||
static childContextTypes = {
|
static childContextTypes = {
|
||||||
|
_containerProps: PropTypes.any,
|
||||||
headManager: PropTypes.object,
|
headManager: PropTypes.object,
|
||||||
router: PropTypes.object
|
router: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
getChildContext () {
|
getChildContext () {
|
||||||
const { headManager } = this.props
|
const { headManager } = this.props
|
||||||
|
const {hasError} = this.state
|
||||||
return {
|
return {
|
||||||
headManager,
|
headManager,
|
||||||
router: makePublicRouterInstance(this.props.router)
|
router: makePublicRouterInstance(this.props.router),
|
||||||
|
_containerProps: {...this.props, hasError}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,22 +39,19 @@ export default class App extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
if (this.state.hasError) return null
|
const {router, Component, pageProps} = this.props
|
||||||
|
|
||||||
const { Component, props, hash, router } = this.props
|
|
||||||
const url = createUrl(router)
|
const url = createUrl(router)
|
||||||
// If there no component exported we can't proceed.
|
return <Container>
|
||||||
// We'll tackle that here.
|
<Component url={url} {...pageProps} />
|
||||||
if (typeof Component !== 'function') {
|
</Container>
|
||||||
throw new Error(`The default export is not a React Component in page: "${url.pathname}"`)
|
|
||||||
}
|
|
||||||
const containerProps = { Component, props, hash, router, url }
|
|
||||||
|
|
||||||
return <Container {...containerProps} />
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Container extends Component {
|
export class Container extends Component {
|
||||||
|
static contextTypes = {
|
||||||
|
_containerProps: PropTypes.any
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.scrollToHash()
|
this.scrollToHash()
|
||||||
}
|
}
|
||||||
|
@ -71,10 +78,16 @@ class Container extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { Component, props, url } = this.props
|
const { hasError } = this.context._containerProps
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const {children} = this.props
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
return (<Component {...props} url={url} />)
|
return <>{children}</>
|
||||||
} else {
|
} else {
|
||||||
const ErrorDebug = require('./error-debug').default
|
const ErrorDebug = require('./error-debug').default
|
||||||
const { AppContainer } = require('react-hot-loader')
|
const { AppContainer } = require('react-hot-loader')
|
||||||
|
@ -83,39 +96,50 @@ class Container extends Component {
|
||||||
// https://github.com/gaearon/react-hot-loader/issues/442
|
// https://github.com/gaearon/react-hot-loader/issues/442
|
||||||
return (
|
return (
|
||||||
<AppContainer warnings={false} errorReporter={ErrorDebug}>
|
<AppContainer warnings={false} errorReporter={ErrorDebug}>
|
||||||
<Component {...props} url={url} />
|
{children}
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createUrl (router) {
|
const warnUrl = execOnce(() => warn(`Warning: the 'url' property is deprecated. https://err.sh/next.js/url-deprecated`))
|
||||||
|
|
||||||
|
export function createUrl (router) {
|
||||||
return {
|
return {
|
||||||
query: router.query,
|
get query () {
|
||||||
pathname: router.pathname,
|
warnUrl()
|
||||||
asPath: router.asPath,
|
return router.query
|
||||||
|
},
|
||||||
|
get pathname () {
|
||||||
|
warnUrl()
|
||||||
|
return router.pathname
|
||||||
|
},
|
||||||
|
get asPath () {
|
||||||
|
warnUrl()
|
||||||
|
return router.asPath
|
||||||
|
},
|
||||||
back: () => {
|
back: () => {
|
||||||
warn(`Warning: 'url.back()' is deprecated. Use "window.history.back()"`)
|
warnUrl()
|
||||||
router.back()
|
router.back()
|
||||||
},
|
},
|
||||||
push: (url, as) => {
|
push: (url, as) => {
|
||||||
warn(`Warning: 'url.push()' is deprecated. Use "next/router" APIs.`)
|
warnUrl()
|
||||||
return router.push(url, as)
|
return router.push(url, as)
|
||||||
},
|
},
|
||||||
pushTo: (href, as) => {
|
pushTo: (href, as) => {
|
||||||
warn(`Warning: 'url.pushTo()' is deprecated. Use "next/router" APIs.`)
|
warnUrl()
|
||||||
const pushRoute = as ? href : null
|
const pushRoute = as ? href : null
|
||||||
const pushUrl = as || href
|
const pushUrl = as || href
|
||||||
|
|
||||||
return router.push(pushRoute, pushUrl)
|
return router.push(pushRoute, pushUrl)
|
||||||
},
|
},
|
||||||
replace: (url, as) => {
|
replace: (url, as) => {
|
||||||
warn(`Warning: 'url.replace()' is deprecated. Use "next/router" APIs.`)
|
warnUrl()
|
||||||
return router.replace(url, as)
|
return router.replace(url, as)
|
||||||
},
|
},
|
||||||
replaceTo: (href, as) => {
|
replaceTo: (href, as) => {
|
||||||
warn(`Warning: 'url.replaceTo()' is deprecated. Use "next/router" APIs.`)
|
warnUrl()
|
||||||
const replaceRoute = as ? href : null
|
const replaceRoute = as ? href : null
|
||||||
const replaceUrl = as || href
|
const replaceUrl = as || href
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ const historyMethodWarning = execOnce((method) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
export default class Router {
|
export default class Router {
|
||||||
constructor (pathname, query, as, { pageLoader, Component, ErrorComponent, err } = {}) {
|
constructor (pathname, query, as, { initialProps, pageLoader, App, Component, ErrorComponent, err } = {}) {
|
||||||
// represents the current component key
|
// represents the current component key
|
||||||
this.route = toRoute(pathname)
|
this.route = toRoute(pathname)
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ export default class Router {
|
||||||
// Otherwise, this cause issues when when going back and
|
// Otherwise, this cause issues when when going back and
|
||||||
// come again to the errored page.
|
// come again to the errored page.
|
||||||
if (Component !== ErrorComponent) {
|
if (Component !== ErrorComponent) {
|
||||||
this.components[this.route] = { Component, err }
|
this.components[this.route] = { Component, props: initialProps, err }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handling Router Events
|
// Handling Router Events
|
||||||
|
@ -33,6 +33,7 @@ export default class Router {
|
||||||
|
|
||||||
this.pageLoader = pageLoader
|
this.pageLoader = pageLoader
|
||||||
this.prefetchQueue = new PQueue({ concurrency: 2 })
|
this.prefetchQueue = new PQueue({ concurrency: 2 })
|
||||||
|
this.App = App
|
||||||
this.ErrorComponent = ErrorComponent
|
this.ErrorComponent = ErrorComponent
|
||||||
this.pathname = pathname
|
this.pathname = pathname
|
||||||
this.query = query
|
this.query = query
|
||||||
|
@ -350,7 +351,7 @@ export default class Router {
|
||||||
const cancel = () => { cancelled = true }
|
const cancel = () => { cancelled = true }
|
||||||
this.componentLoadCancel = cancel
|
this.componentLoadCancel = cancel
|
||||||
|
|
||||||
const props = await loadGetInitialProps(Component, ctx)
|
const props = await loadGetInitialProps(this.App, {Component, router: this, ctx})
|
||||||
|
|
||||||
if (cancel === this.componentLoadCancel) {
|
if (cancel === this.componentLoadCancel) {
|
||||||
this.componentLoadCancel = null
|
this.componentLoadCancel = null
|
||||||
|
|
|
@ -21,7 +21,8 @@
|
||||||
"asset.js",
|
"asset.js",
|
||||||
"error.js",
|
"error.js",
|
||||||
"constants.js",
|
"constants.js",
|
||||||
"config.js"
|
"config.js",
|
||||||
|
"app.js"
|
||||||
],
|
],
|
||||||
"bin": {
|
"bin": {
|
||||||
"next": "./dist/bin/next"
|
"next": "./dist/bin/next"
|
||||||
|
|
1
pages/_app.js
Normal file
1
pages/_app.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('next/app')
|
112
readme.md
112
readme.md
|
@ -22,12 +22,20 @@ Next.js is a minimalistic framework for server-rendered React applications.
|
||||||
- [CSS](#css)
|
- [CSS](#css)
|
||||||
- [Built-in CSS support](#built-in-css-support)
|
- [Built-in CSS support](#built-in-css-support)
|
||||||
- [CSS-in-JS](#css-in-js)
|
- [CSS-in-JS](#css-in-js)
|
||||||
|
- [Importing CSS / Sass / Less / Stylus files](#importing-css--sass--less--stylus-files)
|
||||||
- [Static file serving (e.g.: images)](#static-file-serving-eg-images)
|
- [Static file serving (e.g.: images)](#static-file-serving-eg-images)
|
||||||
- [Populating `<head>`](#populating-head)
|
- [Populating `<head>`](#populating-head)
|
||||||
- [Fetching data and component lifecycle](#fetching-data-and-component-lifecycle)
|
- [Fetching data and component lifecycle](#fetching-data-and-component-lifecycle)
|
||||||
- [Routing](#routing)
|
- [Routing](#routing)
|
||||||
- [With `<Link>`](#with-link)
|
- [With `<Link>`](#with-link)
|
||||||
|
- [With URL object](#with-url-object)
|
||||||
|
- [Replace instead of push url](#replace-instead-of-push-url)
|
||||||
|
- [Using a component that supports `onClick`](#using-a-component-that-supports-onclick)
|
||||||
|
- [Forcing the Link to expose `href` to its child](#forcing-the-link-to-expose-href-to-its-child)
|
||||||
|
- [Disabling the scroll changes to top on page](#disabling-the-scroll-changes-to-top-on-page)
|
||||||
- [Imperatively](#imperatively)
|
- [Imperatively](#imperatively)
|
||||||
|
- [Intercepting `popstate`](#intercepting-popstate)
|
||||||
|
- [With URL object](#with-url-object-1)
|
||||||
- [Router Events](#router-events)
|
- [Router Events](#router-events)
|
||||||
- [Shallow Routing](#shallow-routing)
|
- [Shallow Routing](#shallow-routing)
|
||||||
- [Using a Higher Order Component](#using-a-higher-order-component)
|
- [Using a Higher Order Component](#using-a-higher-order-component)
|
||||||
|
@ -35,16 +43,34 @@ Next.js is a minimalistic framework for server-rendered React applications.
|
||||||
- [With `<Link>`](#with-link-1)
|
- [With `<Link>`](#with-link-1)
|
||||||
- [Imperatively](#imperatively-1)
|
- [Imperatively](#imperatively-1)
|
||||||
- [Custom server and routing](#custom-server-and-routing)
|
- [Custom server and routing](#custom-server-and-routing)
|
||||||
|
- [Disabling file-system routing](#disabling-file-system-routing)
|
||||||
|
- [Dynamic assetPrefix](#dynamic-assetprefix)
|
||||||
- [Dynamic Import](#dynamic-import)
|
- [Dynamic Import](#dynamic-import)
|
||||||
|
- [1. Basic Usage (Also does SSR)](#1-basic-usage-also-does-ssr)
|
||||||
|
- [2. With Custom Loading Component](#2-with-custom-loading-component)
|
||||||
|
- [3. With No SSR](#3-with-no-ssr)
|
||||||
|
- [4. With Multiple Modules At Once](#4-with-multiple-modules-at-once)
|
||||||
|
- [Custom `<App>`](#custom-app)
|
||||||
- [Custom `<Document>`](#custom-document)
|
- [Custom `<Document>`](#custom-document)
|
||||||
- [Custom error handling](#custom-error-handling)
|
- [Custom error handling](#custom-error-handling)
|
||||||
|
- [Reusing the built-in error page](#reusing-the-built-in-error-page)
|
||||||
- [Custom configuration](#custom-configuration)
|
- [Custom configuration](#custom-configuration)
|
||||||
|
- [Setting a custom build directory](#setting-a-custom-build-directory)
|
||||||
|
- [Disabling etag generation](#disabling-etag-generation)
|
||||||
|
- [Configuring the onDemandEntries](#configuring-the-ondemandentries)
|
||||||
|
- [Configuring extensions looked for when resolving pages in `pages`](#configuring-extensions-looked-for-when-resolving-pages-in-pages)
|
||||||
|
- [Configuring the build ID](#configuring-the-build-id)
|
||||||
- [Customizing webpack config](#customizing-webpack-config)
|
- [Customizing webpack config](#customizing-webpack-config)
|
||||||
- [Customizing babel config](#customizing-babel-config)
|
- [Customizing babel config](#customizing-babel-config)
|
||||||
|
- [Exposing configuration to the server / client side](#exposing-configuration-to-the-server--client-side)
|
||||||
- [CDN support with Asset Prefix](#cdn-support-with-asset-prefix)
|
- [CDN support with Asset Prefix](#cdn-support-with-asset-prefix)
|
||||||
- [Production deployment](#production-deployment)
|
- [Production deployment](#production-deployment)
|
||||||
- [Static HTML export](#static-html-export)
|
- [Static HTML export](#static-html-export)
|
||||||
|
- [Usage](#usage)
|
||||||
|
- [Limitation](#limitation)
|
||||||
- [Multi Zones](#multi-zones)
|
- [Multi Zones](#multi-zones)
|
||||||
|
- [How to define a zone](#how-to-define-a-zone)
|
||||||
|
- [How to merge them](#how-to-merge-them)
|
||||||
- [Recipes](#recipes)
|
- [Recipes](#recipes)
|
||||||
- [FAQ](#faq)
|
- [FAQ](#faq)
|
||||||
- [Contributing](#contributing)
|
- [Contributing](#contributing)
|
||||||
|
@ -923,6 +949,77 @@ const HelloBundle = dynamic({
|
||||||
export default () => <HelloBundle title="Dynamic Bundle" />
|
export default () => <HelloBundle title="Dynamic Bundle" />
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Custom `<App>`
|
||||||
|
|
||||||
|
<p><details>
|
||||||
|
<summary><b>Examples</b></summary>
|
||||||
|
<ul><li><a href="./examples/layout-component">Using `_app.js` for layout</a></li></ul>
|
||||||
|
<ul><li><a href="./examples/componentdidcatch">Using `_app.js` to override `componentDidCatch`</a></li></ul>
|
||||||
|
</details></p>
|
||||||
|
|
||||||
|
Next.js uses the `App` component to initialize pages. You can override it and control the page initialization. Which allows you can do amazing things like:
|
||||||
|
|
||||||
|
- Persisting layout between page changes
|
||||||
|
- Keeping state when navigating pages
|
||||||
|
- Custom error handling using `componentDidCatch`
|
||||||
|
- Inject additional data into pages (for example by processing GraphQL queries)
|
||||||
|
|
||||||
|
To override, create the `./pages/_app.js` file and override the App class as shown below:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import App, {Container} from 'next/app'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default class MyApp extends App {
|
||||||
|
static async getInitialProps ({ Component, router, ctx }) {
|
||||||
|
let pageProps = {}
|
||||||
|
|
||||||
|
if (Component.getInitialProps) {
|
||||||
|
pageProps = await Component.getInitialProps(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {pageProps}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {Component, pageProps} = this.props
|
||||||
|
return <Container>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</Container>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When using state inside app the `hasError` property has to be defined:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import App, {Container} from 'next/app'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default class MyApp extends App {
|
||||||
|
static async getInitialProps ({ Component, router, ctx }) {
|
||||||
|
let pageProps = {}
|
||||||
|
|
||||||
|
if (Component.getInitialProps) {
|
||||||
|
pageProps = await Component.getInitialProps(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {pageProps}
|
||||||
|
}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
hasError: null
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {Component, pageProps} = this.props
|
||||||
|
return <Container>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</Container>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Custom `<Document>`
|
### Custom `<Document>`
|
||||||
|
|
||||||
<p><details>
|
<p><details>
|
||||||
|
@ -931,6 +1028,10 @@ export default () => <HelloBundle title="Dynamic Bundle" />
|
||||||
<ul><li><a href="./examples/with-amp">Google AMP</a></li></ul>
|
<ul><li><a href="./examples/with-amp">Google AMP</a></li></ul>
|
||||||
</details></p>
|
</details></p>
|
||||||
|
|
||||||
|
- Is rendered on the server side
|
||||||
|
- Is used to change the initial server side rendered document markup
|
||||||
|
- Commonly used to implement server side rendering for css-in-js libraries like [styled-components](./examples/with-styled-components), [glamorous](./examples/with-glamorous) or [emotion](with-emotion). [styled-jsx](https://github.com/zeit/styled-jsx) is included with Next.js by default.
|
||||||
|
|
||||||
Pages in `Next.js` skip the definition of the surrounding document's markup. For example, you never include `<html>`, `<body>`, etc. To override that default behavior, you must create a file at `./pages/_document.js`, where you can extend the `Document` class:
|
Pages in `Next.js` skip the definition of the surrounding document's markup. For example, you never include `<html>`, `<body>`, etc. To override that default behavior, you must create a file at `./pages/_document.js`, where you can extend the `Document` class:
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
|
@ -939,13 +1040,11 @@ Pages in `Next.js` skip the definition of the surrounding document's markup. For
|
||||||
|
|
||||||
// ./pages/_document.js
|
// ./pages/_document.js
|
||||||
import Document, { Head, Main, NextScript } from 'next/document'
|
import Document, { Head, Main, NextScript } from 'next/document'
|
||||||
import flush from 'styled-jsx/server'
|
|
||||||
|
|
||||||
export default class MyDocument extends Document {
|
export default class MyDocument extends Document {
|
||||||
static getInitialProps({ renderPage }) {
|
static async getInitialProps(ctx) {
|
||||||
const { html, head, errorHtml, chunks, buildManifest } = renderPage()
|
const initialProps = await Document.getInitialProps(ctx)
|
||||||
const styles = flush()
|
return { ...initialProps }
|
||||||
return { html, head, errorHtml, chunks, styles, buildManifest }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -955,7 +1054,6 @@ export default class MyDocument extends Document {
|
||||||
<style>{`body { margin: 0 } /* custom! */`}</style>
|
<style>{`body { margin: 0 } /* custom! */`}</style>
|
||||||
</Head>
|
</Head>
|
||||||
<body className="custom_class">
|
<body className="custom_class">
|
||||||
{this.props.customValue}
|
|
||||||
<Main />
|
<Main />
|
||||||
<NextScript />
|
<NextScript />
|
||||||
</body>
|
</body>
|
||||||
|
@ -969,7 +1067,7 @@ The `ctx` object is equivalent to the one received in all [`getInitialProps`](#f
|
||||||
|
|
||||||
- `renderPage` (`Function`) a callback that executes the actual React rendering logic (synchronously). It's useful to decorate this function in order to support server-rendering wrappers like Aphrodite's [`renderStatic`](https://github.com/Khan/aphrodite#server-side-rendering)
|
- `renderPage` (`Function`) a callback that executes the actual React rendering logic (synchronously). It's useful to decorate this function in order to support server-rendering wrappers like Aphrodite's [`renderStatic`](https://github.com/Khan/aphrodite#server-side-rendering)
|
||||||
|
|
||||||
__Note: React-components outside of `<Main />` will not be initialised by the browser. If you need shared components in all your pages (like a menu or a toolbar), do _not_ add application logic here, but take a look at [this example](https://github.com/zeit/next.js/tree/master/examples/layout-component).__
|
__Note: React-components outside of `<Main />` will not be initialised by the browser. Do _not_ add application logic here. If you need shared components in all your pages (like a menu or a toolbar), take a look at the `App` component instead.__
|
||||||
|
|
||||||
### Custom error handling
|
### Custom error handling
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,8 @@ const nextNodeModulesDir = path.join(nextDir, 'node_modules')
|
||||||
const nextPagesDir = path.join(nextDir, 'pages')
|
const nextPagesDir = path.join(nextDir, 'pages')
|
||||||
const defaultPages = [
|
const defaultPages = [
|
||||||
'_error.js',
|
'_error.js',
|
||||||
'_document.js'
|
'_document.js',
|
||||||
|
'_app.js'
|
||||||
]
|
]
|
||||||
const interpolateNames = new Map(defaultPages.map((p) => {
|
const interpolateNames = new Map(defaultPages.map((p) => {
|
||||||
return [path.join(nextPagesDir, p), `dist/bundles/pages/${p}`]
|
return [path.join(nextPagesDir, p), `dist/bundles/pages/${p}`]
|
||||||
|
@ -71,11 +72,12 @@ function externalsConfig (dir, isServer) {
|
||||||
return callback()
|
return callback()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Webpack itself has to be compiled because it doesn't always use module relative paths
|
// Default pages have to be transpiled
|
||||||
if (res.match(/node_modules[/\\]next[/\\]dist[/\\]pages/)) {
|
if (res.match(/node_modules[/\\]next[/\\]dist[/\\]pages/)) {
|
||||||
return callback()
|
return callback()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Webpack itself has to be compiled because it doesn't always use module relative paths
|
||||||
if (res.match(/node_modules[/\\]webpack/)) {
|
if (res.match(/node_modules[/\\]webpack/)) {
|
||||||
return callback()
|
return callback()
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,9 @@ export async function getPagePaths (dir, {dev, isServer, pageExtensions}) {
|
||||||
let pages
|
let pages
|
||||||
|
|
||||||
if (dev) {
|
if (dev) {
|
||||||
// In development we only compile _document.js and _error.js when starting, since they're always needed. All other pages are compiled with on demand entries
|
// In development we only compile _document.js, _error.js and _app.js when starting, since they're always needed. All other pages are compiled with on demand entries
|
||||||
pages = await glob(isServer ? `pages/+(_document|_error).+(${pageExtensions})` : `pages/_error.+(${pageExtensions})`, { cwd: dir })
|
// _document also has to be in the client compiler in development because we want to detect HMR changes and reload the client
|
||||||
|
pages = await glob(`pages/+(_document|_app|_error).+(${pageExtensions})`, { cwd: dir })
|
||||||
} else {
|
} else {
|
||||||
// In production get all pages from the pages directory
|
// In production get all pages from the pages directory
|
||||||
pages = await glob(isServer ? `pages/**/*.+(${pageExtensions})` : `pages/**/!(_document)*.+(${pageExtensions})`, { cwd: dir })
|
pages = await glob(isServer ? `pages/**/*.+(${pageExtensions})` : `pages/**/!(_document)*.+(${pageExtensions})`, { cwd: dir })
|
||||||
|
@ -57,6 +58,12 @@ export function getPageEntries (pagePaths, {isServer = false, pageExtensions} =
|
||||||
entries[entry.name] = entry.files
|
entries[entry.name] = entry.files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appPagePath = path.join(nextPagesDir, '_app.js')
|
||||||
|
const appPageEntry = createEntry(appPagePath, {name: 'pages/_app.js'}) // default app.js
|
||||||
|
if (!entries[appPageEntry.name]) {
|
||||||
|
entries[appPageEntry.name] = appPageEntry.files
|
||||||
|
}
|
||||||
|
|
||||||
const errorPagePath = path.join(nextPagesDir, '_error.js')
|
const errorPagePath = path.join(nextPagesDir, '_error.js')
|
||||||
const errorPageEntry = createEntry(errorPagePath, {name: 'pages/_error.js'}) // default error.js
|
const errorPageEntry = createEntry(errorPagePath, {name: 'pages/_error.js'}) // default error.js
|
||||||
if (!entries[errorPageEntry.name]) {
|
if (!entries[errorPageEntry.name]) {
|
||||||
|
|
|
@ -91,6 +91,7 @@ export class Head extends Component {
|
||||||
return <head {...this.props}>
|
return <head {...this.props}>
|
||||||
{(head || []).map((h, i) => React.cloneElement(h, { key: h.key || i }))}
|
{(head || []).map((h, i) => React.cloneElement(h, { key: h.key || i }))}
|
||||||
{page !== '/_error' && <link rel='preload' href={`${assetPrefix}/_next/${buildId}/page${pagePathname}`} as='script' />}
|
{page !== '/_error' && <link rel='preload' href={`${assetPrefix}/_next/${buildId}/page${pagePathname}`} as='script' />}
|
||||||
|
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page/_app.js`} as='script' />
|
||||||
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page/_error.js`} as='script' />
|
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page/_error.js`} as='script' />
|
||||||
{this.getPreloadDynamicChunks()}
|
{this.getPreloadDynamicChunks()}
|
||||||
{this.getPreloadMainLinks()}
|
{this.getPreloadMainLinks()}
|
||||||
|
@ -204,6 +205,7 @@ export class NextScript extends Component {
|
||||||
`
|
`
|
||||||
}} />}
|
}} />}
|
||||||
{page !== '/_error' && <script async id={`__NEXT_PAGE__${pathname}`} src={`${assetPrefix}/_next/${buildId}/page${pagePathname}`} />}
|
{page !== '/_error' && <script async id={`__NEXT_PAGE__${pathname}`} src={`${assetPrefix}/_next/${buildId}/page${pagePathname}`} />}
|
||||||
|
<script async id={`__NEXT_PAGE__/_app`} src={`${assetPrefix}/_next/${buildId}/page/_app.js`} />
|
||||||
<script async id={`__NEXT_PAGE__/_error`} src={`${assetPrefix}/_next/${buildId}/page/_error.js`} />
|
<script async id={`__NEXT_PAGE__/_error`} src={`${assetPrefix}/_next/${buildId}/page/_error.js`} />
|
||||||
{staticMarkup ? null : this.getDynamicChunks()}
|
{staticMarkup ? null : this.getDynamicChunks()}
|
||||||
{staticMarkup ? null : this.getScripts()}
|
{staticMarkup ? null : this.getScripts()}
|
||||||
|
|
|
@ -28,6 +28,7 @@ const access = promisify(fs.access)
|
||||||
|
|
||||||
const blockedPages = {
|
const blockedPages = {
|
||||||
'/_document': true,
|
'/_document': true,
|
||||||
|
'/_app': true,
|
||||||
'/_error': true
|
'/_error': true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,20 +179,6 @@ export default class Server {
|
||||||
await serveStatic(req, res, path)
|
await serveStatic(req, res, path)
|
||||||
},
|
},
|
||||||
|
|
||||||
// This is very similar to the following route.
|
|
||||||
// But for this one, the page already built when the Next.js process starts.
|
|
||||||
// There's no need to build it in on-demand manner and check for other things.
|
|
||||||
// So, it's clean to have a seperate route for this.
|
|
||||||
'/_next/:buildId/page/_error.js': async (req, res, params) => {
|
|
||||||
if (!this.handleBuildId(params.buildId, res)) {
|
|
||||||
const error = new Error('INVALID_BUILD_ID')
|
|
||||||
return await renderScriptError(req, res, '/_error', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
const p = join(this.dir, `${this.dist}/bundles/pages/_error.js`)
|
|
||||||
await this.serveStatic(req, res, p)
|
|
||||||
},
|
|
||||||
|
|
||||||
'/_next/:buildId/page/:path*.js': async (req, res, params) => {
|
'/_next/:buildId/page/:path*.js': async (req, res, params) => {
|
||||||
const paths = params.path || ['']
|
const paths = params.path || ['']
|
||||||
const page = `/${paths.join('/')}`
|
const page = `/${paths.join('/')}`
|
||||||
|
@ -201,7 +188,7 @@ export default class Server {
|
||||||
return await renderScriptError(req, res, page, error)
|
return await renderScriptError(req, res, page, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.dev) {
|
if (this.dev && page !== '/_error' && page !== '/_app') {
|
||||||
try {
|
try {
|
||||||
await this.hotReloader.ensurePage(page)
|
await this.hotReloader.ensurePage(page)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { Router } from '../lib/router'
|
||||||
import { loadGetInitialProps, isResSent } from '../lib/utils'
|
import { loadGetInitialProps, isResSent } from '../lib/utils'
|
||||||
import { getAvailableChunks } from './utils'
|
import { getAvailableChunks } from './utils'
|
||||||
import Head, { defaultHead } from '../lib/head'
|
import Head, { defaultHead } from '../lib/head'
|
||||||
import App from '../lib/app'
|
|
||||||
import ErrorDebug from '../lib/error-debug'
|
import ErrorDebug from '../lib/error-debug'
|
||||||
import { flushChunks } from '../lib/dynamic'
|
import { flushChunks } from '../lib/dynamic'
|
||||||
import { BUILD_MANIFEST } from '../lib/constants'
|
import { BUILD_MANIFEST } from '../lib/constants'
|
||||||
|
@ -55,17 +54,26 @@ async function doRender (req, res, pathname, query, {
|
||||||
}
|
}
|
||||||
|
|
||||||
const documentPath = join(dir, dist, 'dist', 'bundles', 'pages', '_document')
|
const documentPath = join(dir, dist, 'dist', 'bundles', 'pages', '_document')
|
||||||
|
const appPath = join(dir, dist, 'dist', 'bundles', 'pages', '_app')
|
||||||
const buildManifest = require(join(dir, dist, BUILD_MANIFEST))
|
const buildManifest = require(join(dir, dist, BUILD_MANIFEST))
|
||||||
|
let [Component, Document, App] = await Promise.all([
|
||||||
let [Component, Document] = await Promise.all([
|
|
||||||
requirePage(page, {dir, dist}),
|
requirePage(page, {dir, dist}),
|
||||||
require(documentPath)
|
require(documentPath),
|
||||||
|
require(appPath)
|
||||||
])
|
])
|
||||||
|
|
||||||
Component = Component.default || Component
|
Component = Component.default || Component
|
||||||
|
|
||||||
|
if (typeof Component !== 'function') {
|
||||||
|
throw new Error(`The default export is not a React Component in page: "${pathname}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
App = App.default || App
|
||||||
Document = Document.default || Document
|
Document = Document.default || Document
|
||||||
const asPath = req.url
|
const asPath = req.url
|
||||||
const ctx = { err, req, res, pathname, query, asPath }
|
const ctx = { err, req, res, pathname, query, asPath }
|
||||||
const props = await loadGetInitialProps(Component, ctx)
|
const router = new Router(pathname, query, asPath)
|
||||||
|
const props = await loadGetInitialProps(App, {Component, router, ctx})
|
||||||
|
|
||||||
// the response might be finshed on the getinitialprops call
|
// the response might be finshed on the getinitialprops call
|
||||||
if (isResSent(res)) return
|
if (isResSent(res)) return
|
||||||
|
@ -73,8 +81,8 @@ async function doRender (req, res, pathname, query, {
|
||||||
const renderPage = (enhancer = Page => Page) => {
|
const renderPage = (enhancer = Page => Page) => {
|
||||||
const app = createElement(App, {
|
const app = createElement(App, {
|
||||||
Component: enhancer(Component),
|
Component: enhancer(Component),
|
||||||
props,
|
router,
|
||||||
router: new Router(pathname, query, asPath)
|
...props
|
||||||
})
|
})
|
||||||
|
|
||||||
const render = staticMarkup ? renderToStaticMarkup : renderToString
|
const render = staticMarkup ? renderToStaticMarkup : renderToString
|
||||||
|
|
44
test/integration/app-document/pages/_app.js
Normal file
44
test/integration/app-document/pages/_app.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import App, {Container} from 'next/app'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
class Layout extends React.Component {
|
||||||
|
state = {
|
||||||
|
random: false
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.setState({random: Math.random()})
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {children} = this.props
|
||||||
|
const {random} = this.state
|
||||||
|
return <div>
|
||||||
|
<p id='hello-app'>Hello App</p>
|
||||||
|
<p id='hello-hmr'>Hello HMR</p>
|
||||||
|
<p id='random-number'>{random}</p>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class MyApp extends App {
|
||||||
|
static async getInitialProps ({ Component, router, ctx }) {
|
||||||
|
let pageProps = {}
|
||||||
|
|
||||||
|
if (Component.getInitialProps) {
|
||||||
|
pageProps = await Component.getInitialProps(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {pageProps}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {Component, pageProps} = this.props
|
||||||
|
return <Container>
|
||||||
|
<Layout>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</Layout>
|
||||||
|
</Container>
|
||||||
|
}
|
||||||
|
}
|
24
test/integration/app-document/pages/_document.js
Normal file
24
test/integration/app-document/pages/_document.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import Document, { Head, Main, NextScript } from 'next/document'
|
||||||
|
|
||||||
|
export default class MyDocument extends Document {
|
||||||
|
static async getInitialProps (ctx) {
|
||||||
|
const initialProps = await Document.getInitialProps(ctx)
|
||||||
|
return { ...initialProps, customProperty: 'Hello Document' }
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<Head>
|
||||||
|
<style>{`body { margin: 0 } /* custom! */`}</style>
|
||||||
|
</Head>
|
||||||
|
<body className='custom_class'>
|
||||||
|
<p id='custom-property'>{this.props.customProperty}</p>
|
||||||
|
<p id='document-hmr'>Hello Document HMR</p>
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
6
test/integration/app-document/pages/about.js
Normal file
6
test/integration/app-document/pages/about.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export default () => <div>
|
||||||
|
<div className='page-about'>about</div>
|
||||||
|
<Link href='/'><a id='home-link'>home</a></Link>
|
||||||
|
</div>
|
5
test/integration/app-document/pages/index.js
Normal file
5
test/integration/app-document/pages/index.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import Link from 'next/link'
|
||||||
|
export default () => <div>
|
||||||
|
<div className='page-index'>index</div>
|
||||||
|
<Link href='/about'><a id='about-link'>about</a></Link>
|
||||||
|
</div>
|
84
test/integration/app-document/test/client.js
Normal file
84
test/integration/app-document/test/client.js
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
/* global describe, it, expect */
|
||||||
|
|
||||||
|
import webdriver from 'next-webdriver'
|
||||||
|
import { readFileSync, writeFileSync } from 'fs'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { check } from 'next-test-utils'
|
||||||
|
|
||||||
|
export default (context, render) => {
|
||||||
|
describe('Client side', () => {
|
||||||
|
it('should detect the changes to pages/_app.js and display it', async () => {
|
||||||
|
const browser = await webdriver(context.appPort, '/')
|
||||||
|
const text = await browser
|
||||||
|
.elementByCss('#hello-hmr').text()
|
||||||
|
expect(text).toBe('Hello HMR')
|
||||||
|
|
||||||
|
const appPath = join(__dirname, '../', 'pages', '_app.js')
|
||||||
|
|
||||||
|
const originalContent = readFileSync(appPath, 'utf8')
|
||||||
|
const editedContent = originalContent.replace('Hello HMR', 'Hi HMR')
|
||||||
|
|
||||||
|
// change the content
|
||||||
|
writeFileSync(appPath, editedContent, 'utf8')
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/Hi HMR/
|
||||||
|
)
|
||||||
|
|
||||||
|
// add the original content
|
||||||
|
writeFileSync(appPath, originalContent, 'utf8')
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/Hello HMR/
|
||||||
|
)
|
||||||
|
|
||||||
|
browser.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should detect the changes to pages/_document.js and display it', async () => {
|
||||||
|
const browser = await webdriver(context.appPort, '/')
|
||||||
|
const text = await browser
|
||||||
|
.elementByCss('#hello-hmr').text()
|
||||||
|
expect(text).toBe('Hello HMR')
|
||||||
|
|
||||||
|
const appPath = join(__dirname, '../', 'pages', '_document.js')
|
||||||
|
|
||||||
|
const originalContent = readFileSync(appPath, 'utf8')
|
||||||
|
const editedContent = originalContent.replace('Hello Document HMR', 'Hi Document HMR')
|
||||||
|
|
||||||
|
// change the content
|
||||||
|
writeFileSync(appPath, editedContent, 'utf8')
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/Hi Document HMR/
|
||||||
|
)
|
||||||
|
|
||||||
|
// add the original content
|
||||||
|
writeFileSync(appPath, originalContent, 'utf8')
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/Hello Document HMR/
|
||||||
|
)
|
||||||
|
|
||||||
|
browser.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should keep state between page navigations', async () => {
|
||||||
|
const browser = await webdriver(context.appPort, '/')
|
||||||
|
|
||||||
|
const randomNumber = await browser.elementByCss('#random-number').text()
|
||||||
|
|
||||||
|
const switchedRandomNumer = await browser
|
||||||
|
.elementByCss('#about-link').click()
|
||||||
|
.waitForElementByCss('.page-about')
|
||||||
|
.elementByCss('#random-number').text()
|
||||||
|
|
||||||
|
expect(switchedRandomNumer).toBe(randomNumber)
|
||||||
|
browser.close()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
33
test/integration/app-document/test/index.test.js
Normal file
33
test/integration/app-document/test/index.test.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
/* global jasmine, describe, beforeAll, afterAll */
|
||||||
|
|
||||||
|
import { join } from 'path'
|
||||||
|
import {
|
||||||
|
renderViaHTTP,
|
||||||
|
fetchViaHTTP,
|
||||||
|
findPort,
|
||||||
|
launchApp,
|
||||||
|
killApp
|
||||||
|
} from 'next-test-utils'
|
||||||
|
|
||||||
|
// test suits
|
||||||
|
import rendering from './rendering'
|
||||||
|
import client from './client'
|
||||||
|
|
||||||
|
const context = {}
|
||||||
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5
|
||||||
|
|
||||||
|
describe('Document and App', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
context.appPort = await findPort()
|
||||||
|
context.server = await launchApp(join(__dirname, '../'), context.appPort, true)
|
||||||
|
|
||||||
|
// pre-build all pages at the start
|
||||||
|
await Promise.all([
|
||||||
|
renderViaHTTP(context.appPort, '/')
|
||||||
|
])
|
||||||
|
})
|
||||||
|
afterAll(() => killApp(context.server))
|
||||||
|
|
||||||
|
rendering(context, 'Rendering via HTTP', (p, q) => renderViaHTTP(context.appPort, p, q), (p, q) => fetchViaHTTP(context.appPort, p, q))
|
||||||
|
client(context, (p, q) => renderViaHTTP(context.appPort, p, q))
|
||||||
|
})
|
36
test/integration/app-document/test/rendering.js
Normal file
36
test/integration/app-document/test/rendering.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/* global describe, test, expect */
|
||||||
|
|
||||||
|
import cheerio from 'cheerio'
|
||||||
|
|
||||||
|
export default function ({ app }, suiteName, render, fetch) {
|
||||||
|
async function get$ (path, query) {
|
||||||
|
const html = await render(path, query)
|
||||||
|
return cheerio.load(html)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe(suiteName, () => {
|
||||||
|
describe('_document', () => {
|
||||||
|
test('It has a custom body class', async () => {
|
||||||
|
const $ = await get$('/')
|
||||||
|
expect($('body').hasClass('custom_class'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('It injects custom head tags', async () => {
|
||||||
|
const $ = await get$('/')
|
||||||
|
expect($('head').text().includes('body { margin: 0 }'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('It passes props from Document.getInitialProps to Document', async () => {
|
||||||
|
const $ = await get$('/')
|
||||||
|
expect($('#custom-property').text() === 'Hello Document')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_app', () => {
|
||||||
|
test('It shows a custom tag', async () => {
|
||||||
|
const $ = await get$('/')
|
||||||
|
expect($('hello-app').text() === 'Hello App')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in a new issue