1
0
Fork 0
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:
Tim Neutkens 2018-04-12 10:33:22 +02:00 committed by GitHub
parent 15dde33794
commit eca8e8f64b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 477 additions and 68 deletions

1
app.js Normal file
View file

@ -0,0 +1 @@
module.exports = require('./dist/lib/app')

View file

@ -3,7 +3,6 @@ import ReactDOM from 'react-dom'
import HeadManager from './head-manager'
import { createRouter } from '../lib/router'
import EventEmitter from '../lib/EventEmitter'
import App from '../lib/app'
import { loadGetInitialProps, getURL } from '../lib/utils'
import PageLoader from '../lib/page-loader'
import * as asset from '../lib/asset'
@ -69,6 +68,7 @@ export let router
export let ErrorComponent
let ErrorDebugComponent
let Component
let App
let stripAnsi = (s) => s
export const emitter = new EventEmitter()
@ -82,16 +82,23 @@ export default async ({ ErrorDebugComponent: passedDebugComponent, stripAnsi: pa
stripAnsi = passedStripAnsi || stripAnsi
ErrorDebugComponent = passedDebugComponent
ErrorComponent = await pageLoader.loadPage('/_error')
App = await pageLoader.loadPage('/_app')
try {
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) {
console.error(stripAnsi(`${err.message}\n${err.stack}`))
Component = ErrorComponent
}
router = createRouter(pathname, query, asPath, {
initialProps: props,
pageLoader,
App,
Component,
ErrorComponent,
err
@ -136,7 +143,7 @@ export async function renderError (error) {
console.error(stripAnsi(errorMessage))
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)
renderReactElement(createElement(ErrorComponent, props), errorContainer)
} else {
@ -145,18 +152,19 @@ export async function renderError (error) {
}
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 &&
Component !== ErrorComponent &&
lastAppProps.Component === ErrorComponent) {
// fetch props if ErrorComponent was replaced with a page component by HMR
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
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 = appProps

View file

@ -27,6 +27,13 @@ export default () => {
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') {
window.location.reload()
return
@ -36,6 +43,13 @@ export default () => {
},
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') {
window.location.reload()
return

23
errors/url-deprecated.md Normal file
View 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)
```

View file

@ -1,7 +1,7 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import shallowEquals from './shallow-equals'
import { warn } from './utils'
import { execOnce, warn, loadGetInitialProps } from './utils'
import { makePublicRouterInstance } from './router'
export default class App extends Component {
@ -9,16 +9,26 @@ export default class App extends Component {
hasError: null
}
static displayName = 'App'
static async getInitialProps ({ Component, router, ctx }) {
const pageProps = await loadGetInitialProps(Component, ctx)
return {pageProps}
}
static childContextTypes = {
_containerProps: PropTypes.any,
headManager: PropTypes.object,
router: PropTypes.object
}
getChildContext () {
const { headManager } = this.props
const {hasError} = this.state
return {
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 () {
if (this.state.hasError) return null
const { Component, props, hash, router } = this.props
const {router, Component, pageProps} = this.props
const url = createUrl(router)
// If there no component exported we can't proceed.
// We'll tackle that here.
if (typeof Component !== 'function') {
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} />
return <Container>
<Component url={url} {...pageProps} />
</Container>
}
}
class Container extends Component {
export class Container extends Component {
static contextTypes = {
_containerProps: PropTypes.any
}
componentDidMount () {
this.scrollToHash()
}
@ -71,10 +78,16 @@ class Container extends Component {
}
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') {
return (<Component {...props} url={url} />)
return <>{children}</>
} else {
const ErrorDebug = require('./error-debug').default
const { AppContainer } = require('react-hot-loader')
@ -83,39 +96,50 @@ class Container extends Component {
// https://github.com/gaearon/react-hot-loader/issues/442
return (
<AppContainer warnings={false} errorReporter={ErrorDebug}>
<Component {...props} url={url} />
{children}
</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 {
query: router.query,
pathname: router.pathname,
asPath: router.asPath,
get query () {
warnUrl()
return router.query
},
get pathname () {
warnUrl()
return router.pathname
},
get asPath () {
warnUrl()
return router.asPath
},
back: () => {
warn(`Warning: 'url.back()' is deprecated. Use "window.history.back()"`)
warnUrl()
router.back()
},
push: (url, as) => {
warn(`Warning: 'url.push()' is deprecated. Use "next/router" APIs.`)
warnUrl()
return router.push(url, as)
},
pushTo: (href, as) => {
warn(`Warning: 'url.pushTo()' is deprecated. Use "next/router" APIs.`)
warnUrl()
const pushRoute = as ? href : null
const pushUrl = as || href
return router.push(pushRoute, pushUrl)
},
replace: (url, as) => {
warn(`Warning: 'url.replace()' is deprecated. Use "next/router" APIs.`)
warnUrl()
return router.replace(url, as)
},
replaceTo: (href, as) => {
warn(`Warning: 'url.replaceTo()' is deprecated. Use "next/router" APIs.`)
warnUrl()
const replaceRoute = as ? href : null
const replaceUrl = as || href

View file

@ -15,7 +15,7 @@ const historyMethodWarning = execOnce((method) => {
})
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
this.route = toRoute(pathname)
@ -25,7 +25,7 @@ export default class Router {
// Otherwise, this cause issues when when going back and
// come again to the errored page.
if (Component !== ErrorComponent) {
this.components[this.route] = { Component, err }
this.components[this.route] = { Component, props: initialProps, err }
}
// Handling Router Events
@ -33,6 +33,7 @@ export default class Router {
this.pageLoader = pageLoader
this.prefetchQueue = new PQueue({ concurrency: 2 })
this.App = App
this.ErrorComponent = ErrorComponent
this.pathname = pathname
this.query = query
@ -350,7 +351,7 @@ export default class Router {
const cancel = () => { cancelled = true }
this.componentLoadCancel = cancel
const props = await loadGetInitialProps(Component, ctx)
const props = await loadGetInitialProps(this.App, {Component, router: this, ctx})
if (cancel === this.componentLoadCancel) {
this.componentLoadCancel = null

View file

@ -21,7 +21,8 @@
"asset.js",
"error.js",
"constants.js",
"config.js"
"config.js",
"app.js"
],
"bin": {
"next": "./dist/bin/next"

1
pages/_app.js Normal file
View file

@ -0,0 +1 @@
module.exports = require('next/app')

112
readme.md
View file

@ -22,12 +22,20 @@ Next.js is a minimalistic framework for server-rendered React applications.
- [CSS](#css)
- [Built-in CSS support](#built-in-css-support)
- [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)
- [Populating `<head>`](#populating-head)
- [Fetching data and component lifecycle](#fetching-data-and-component-lifecycle)
- [Routing](#routing)
- [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)
- [Intercepting `popstate`](#intercepting-popstate)
- [With URL object](#with-url-object-1)
- [Router Events](#router-events)
- [Shallow Routing](#shallow-routing)
- [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)
- [Imperatively](#imperatively-1)
- [Custom server and routing](#custom-server-and-routing)
- [Disabling file-system routing](#disabling-file-system-routing)
- [Dynamic assetPrefix](#dynamic-assetprefix)
- [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 error handling](#custom-error-handling)
- [Reusing the built-in error page](#reusing-the-built-in-error-page)
- [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 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)
- [Production deployment](#production-deployment)
- [Static HTML export](#static-html-export)
- [Usage](#usage)
- [Limitation](#limitation)
- [Multi Zones](#multi-zones)
- [How to define a zone](#how-to-define-a-zone)
- [How to merge them](#how-to-merge-them)
- [Recipes](#recipes)
- [FAQ](#faq)
- [Contributing](#contributing)
@ -923,6 +949,77 @@ const HelloBundle = dynamic({
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>`
<p><details>
@ -931,6 +1028,10 @@ export default () => <HelloBundle title="Dynamic Bundle" />
<ul><li><a href="./examples/with-amp">Google AMP</a></li></ul>
</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:
```jsx
@ -939,13 +1040,11 @@ Pages in `Next.js` skip the definition of the surrounding document's markup. For
// ./pages/_document.js
import Document, { Head, Main, NextScript } from 'next/document'
import flush from 'styled-jsx/server'
export default class MyDocument extends Document {
static getInitialProps({ renderPage }) {
const { html, head, errorHtml, chunks, buildManifest } = renderPage()
const styles = flush()
return { html, head, errorHtml, chunks, styles, buildManifest }
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps }
}
render() {
@ -955,7 +1054,6 @@ export default class MyDocument extends Document {
<style>{`body { margin: 0 } /* custom! */`}</style>
</Head>
<body className="custom_class">
{this.props.customValue}
<Main />
<NextScript />
</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)
__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

View file

@ -22,7 +22,8 @@ const nextNodeModulesDir = path.join(nextDir, 'node_modules')
const nextPagesDir = path.join(nextDir, 'pages')
const defaultPages = [
'_error.js',
'_document.js'
'_document.js',
'_app.js'
]
const interpolateNames = new Map(defaultPages.map((p) => {
return [path.join(nextPagesDir, p), `dist/bundles/pages/${p}`]
@ -71,11 +72,12 @@ function externalsConfig (dir, isServer) {
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/)) {
return callback()
}
// Webpack itself has to be compiled because it doesn't always use module relative paths
if (res.match(/node_modules[/\\]webpack/)) {
return callback()
}

View file

@ -16,8 +16,9 @@ export async function getPagePaths (dir, {dev, isServer, pageExtensions}) {
let pages
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
pages = await glob(isServer ? `pages/+(_document|_error).+(${pageExtensions})` : `pages/_error.+(${pageExtensions})`, { cwd: dir })
// 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
// _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 {
// In production get all pages from the pages directory
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
}
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 errorPageEntry = createEntry(errorPagePath, {name: 'pages/_error.js'}) // default error.js
if (!entries[errorPageEntry.name]) {

View file

@ -91,6 +91,7 @@ export class Head extends Component {
return <head {...this.props}>
{(head || []).map((h, i) => React.cloneElement(h, { key: h.key || i }))}
{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' />
{this.getPreloadDynamicChunks()}
{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}`} />}
<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`} />
{staticMarkup ? null : this.getDynamicChunks()}
{staticMarkup ? null : this.getScripts()}

View file

@ -28,6 +28,7 @@ const access = promisify(fs.access)
const blockedPages = {
'/_document': true,
'/_app': true,
'/_error': true
}
@ -178,20 +179,6 @@ export default class Server {
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) => {
const paths = params.path || ['']
const page = `/${paths.join('/')}`
@ -201,7 +188,7 @@ export default class Server {
return await renderScriptError(req, res, page, error)
}
if (this.dev) {
if (this.dev && page !== '/_error' && page !== '/_app') {
try {
await this.hotReloader.ensurePage(page)
} catch (error) {

View file

@ -9,7 +9,6 @@ import { Router } from '../lib/router'
import { loadGetInitialProps, isResSent } from '../lib/utils'
import { getAvailableChunks } from './utils'
import Head, { defaultHead } from '../lib/head'
import App from '../lib/app'
import ErrorDebug from '../lib/error-debug'
import { flushChunks } from '../lib/dynamic'
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 appPath = join(dir, dist, 'dist', 'bundles', 'pages', '_app')
const buildManifest = require(join(dir, dist, BUILD_MANIFEST))
let [Component, Document] = await Promise.all([
let [Component, Document, App] = await Promise.all([
requirePage(page, {dir, dist}),
require(documentPath)
require(documentPath),
require(appPath)
])
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
const asPath = req.url
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
if (isResSent(res)) return
@ -73,8 +81,8 @@ async function doRender (req, res, pathname, query, {
const renderPage = (enhancer = Page => Page) => {
const app = createElement(App, {
Component: enhancer(Component),
props,
router: new Router(pathname, query, asPath)
router,
...props
})
const render = staticMarkup ? renderToStaticMarkup : renderToString

View 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>
}
}

View 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>
)
}
}

View 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>

View 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>

View 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()
})
})
}

View 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))
})

View 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')
})
})
})
}