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 { 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
|
||||
|
||||
|
|
|
@ -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
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 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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
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)
|
||||
- [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
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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]) {
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
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