Skip to content

Commit 8ddafae

Browse files
nkzawarauchg
authored andcommitted
custom document support (vercel#405)
1 parent 43b0e6f commit 8ddafae

14 files changed

+249
-74
lines changed

client/next.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ domready(() => {
3030
const container = document.getElementById('__next')
3131
const appProps = { Component, props, router, headManager }
3232

33-
rehydrate(ids)
33+
if (ids) rehydrate(ids)
3434
render(createElement(App, appProps), container)
3535
})
3636

client/webpack-hot-middleware-client.js

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ const handlers = {
2323
// reload to recover from runtime errors
2424
next.router.reload(route)
2525
}
26+
},
27+
hardReload () {
28+
window.location.reload()
2629
}
2730
}
2831

document.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('./dist/server/document')

lib/document.js

-52
This file was deleted.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"babel-preset-es2015": "6.18.0",
4343
"babel-preset-react": "6.16.0",
4444
"babel-runtime": "6.20.0",
45+
"chokidar": "1.6.1",
4546
"cross-spawn": "5.0.1",
4647
"del": "2.2.2",
4748
"domready": "1.0.8",

pages/_document.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('next/document')

server/build/babel.js

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { resolve, join, dirname } from 'path'
2+
import { readFile, writeFile } from 'mz/fs'
3+
import { transform } from 'babel-core'
4+
import chokidar from 'chokidar'
5+
import mkdirp from 'mkdirp-then'
6+
7+
const babelRuntimePath = require.resolve('babel-runtime/package')
8+
.replace(/[\\/]package\.json$/, '')
9+
10+
export default babel
11+
12+
async function babel (dir, { dev = false } = {}) {
13+
dir = resolve(dir)
14+
15+
let src
16+
try {
17+
src = await readFile(join(dir, 'pages', '_document.js'), 'utf8')
18+
} catch (err) {
19+
if (err.code === 'ENOENT') {
20+
src = await readFile(join(__dirname, '..', '..', 'pages', '_document.js'), 'utf8')
21+
} else {
22+
throw err
23+
}
24+
}
25+
26+
const { code } = transform(src, {
27+
babelrc: false,
28+
sourceMaps: dev ? 'inline' : false,
29+
presets: ['es2015', 'react'],
30+
plugins: [
31+
require.resolve('babel-plugin-react-require'),
32+
require.resolve('babel-plugin-transform-async-to-generator'),
33+
require.resolve('babel-plugin-transform-object-rest-spread'),
34+
require.resolve('babel-plugin-transform-class-properties'),
35+
require.resolve('babel-plugin-transform-runtime'),
36+
[
37+
require.resolve('babel-plugin-module-resolver'),
38+
{
39+
alias: {
40+
'babel-runtime': babelRuntimePath,
41+
react: require.resolve('react'),
42+
'next/link': require.resolve('../../lib/link'),
43+
'next/css': require.resolve('../../lib/css'),
44+
'next/head': require.resolve('../../lib/head'),
45+
'next/document': require.resolve('../../server/document')
46+
}
47+
}
48+
]
49+
]
50+
})
51+
52+
const file = join(dir, '.next', 'dist', 'pages', '_document.js')
53+
await mkdirp(dirname(file))
54+
await writeFile(file, code)
55+
}
56+
57+
export function watch (dir) {
58+
return chokidar.watch('pages/_document.js', {
59+
cwd: dir,
60+
ignoreInitial: true
61+
})
62+
}

server/build/index.js

+8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import webpack from './webpack'
2+
import babel from './babel'
23
import clean from './clean'
34

45
export default async function build (dir) {
@@ -7,6 +8,13 @@ export default async function build (dir) {
78
clean(dir)
89
])
910

11+
await Promise.all([
12+
runCompiler(compiler),
13+
babel(dir)
14+
])
15+
}
16+
17+
function runCompiler (compiler) {
1018
return new Promise((resolve, reject) => {
1119
compiler.run((err, stats) => {
1220
if (err) return reject(err)

server/build/plugins/watch-pages-plugin.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ export default class WatchPagesPlugin {
5858
}
5959

6060
isPageFile (f) {
61-
return f.indexOf(this.dir) === 0 && extname(f) === '.js'
61+
return f.indexOf(this.dir) === 0 &&
62+
relative(this.dir, f) !== '_document.js' &&
63+
extname(f) === '.js'
6264
}
6365
}
6466

server/build/webpack.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import DetachPlugin from './plugins/detach-plugin'
1313
export default async function createCompiler (dir, { hotReload = false, dev = false } = {}) {
1414
dir = resolve(dir)
1515

16-
const pages = await glob('pages/**/*.js', { cwd: dir })
16+
const pages = await glob('pages/**/*.js', {
17+
cwd: dir,
18+
ignore: 'pages/_document.js'
19+
})
1720

1821
const entry = {}
1922
const defaultEntries = hotReload ? ['next/dist/client/webpack-hot-middleware-client'] : []
@@ -146,7 +149,8 @@ export default async function createCompiler (dir, { hotReload = false, dev = fa
146149
'next/link': require.resolve('../../lib/link'),
147150
'next/prefetch': require.resolve('../../lib/prefetch'),
148151
'next/css': require.resolve('../../lib/css'),
149-
'next/head': require.resolve('../../lib/head')
152+
'next/head': require.resolve('../../lib/head'),
153+
'next/document': require.resolve('../../server/document')
150154
}
151155
}
152156
]

server/document.js

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import React, { Component, PropTypes } from 'react'
2+
import htmlescape from 'htmlescape'
3+
import { renderStatic } from 'glamor/server'
4+
import readPkgUp from 'read-pkg-up'
5+
6+
const { pkg } = readPkgUp.sync({
7+
cwd: __dirname,
8+
normalize: false
9+
})
10+
11+
export default class Document extends Component {
12+
static getInitialProps ({ renderPage }) {
13+
let head
14+
const { html, css, ids } = renderStatic(() => {
15+
const page = renderPage()
16+
head = page.head
17+
return page.html
18+
})
19+
const nextCSS = { css, ids }
20+
return { html, head, nextCSS }
21+
}
22+
23+
static childContextTypes = {
24+
_documentProps: PropTypes.any
25+
}
26+
27+
constructor (props) {
28+
super(props)
29+
const { __NEXT_DATA__, nextCSS } = props
30+
if (nextCSS) __NEXT_DATA__.ids = nextCSS.ids
31+
}
32+
33+
getChildContext () {
34+
return { _documentProps: this.props }
35+
}
36+
37+
render () {
38+
return <html>
39+
<Head />
40+
<body>
41+
<Main />
42+
<NextScript />
43+
</body>
44+
</html>
45+
}
46+
}
47+
48+
export class Head extends Component {
49+
static contextTypes = {
50+
_documentProps: PropTypes.any
51+
}
52+
53+
render () {
54+
const { head, nextCSS } = this.context._documentProps
55+
return <head>
56+
{(head || []).map((h, i) => React.cloneElement(h, { key: i }))}
57+
{nextCSS ? <style dangerouslySetInnerHTML={{ __html: nextCSS.css }} /> : null}
58+
{this.props.children}
59+
</head>
60+
}
61+
}
62+
63+
export class Main extends Component {
64+
static contextTypes = {
65+
_documentProps: PropTypes.any
66+
}
67+
68+
render () {
69+
const { html, __NEXT_DATA__, staticMarkup } = this.context._documentProps
70+
return <div>
71+
<div id='__next' dangerouslySetInnerHTML={{ __html: html }} />
72+
{staticMarkup ? null : <script dangerouslySetInnerHTML={{
73+
__html: `__NEXT_DATA__ = ${htmlescape(__NEXT_DATA__)}; module={};`
74+
}} />}
75+
</div>
76+
}
77+
}
78+
79+
export class NextScript extends Component {
80+
static contextTypes = {
81+
_documentProps: PropTypes.any
82+
}
83+
84+
render () {
85+
const { staticMarkup, dev, cdn } = this.context._documentProps
86+
return <div>
87+
{staticMarkup ? null : createClientScript({ dev, cdn })}
88+
<script type='text/javascript' src='/_next/commons.js' />
89+
</div>
90+
}
91+
}
92+
93+
function createClientScript ({ dev, cdn }) {
94+
if (dev) {
95+
return <script type='text/javascript' src='/_next/next-dev.bundle.js' />
96+
}
97+
98+
if (!cdn) {
99+
return <script type='text/javascript' src='/_next/next.bundle.js' />
100+
}
101+
102+
return <script dangerouslySetInnerHTML={{ __html: `
103+
(function () {
104+
load('https://cdn.zeit.co/next.js/${pkg.version}/next.min.js', function (err) {
105+
if (err) load('/_next/next.bundle.js')
106+
})
107+
function load (src, fn) {
108+
fn = fn || function () {}
109+
var script = document.createElement('script')
110+
script.src = src
111+
script.onload = function () { fn(null) }
112+
script.onerror = fn
113+
script.crossorigin = 'anonymous'
114+
document.body.appendChild(script)
115+
}
116+
})()
117+
`}} />
118+
}

server/hot-reloader.js

+27-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import webpackDevMiddleware from 'webpack-dev-middleware'
33
import webpackHotMiddleware from 'webpack-hot-middleware'
44
import isWindowsBash from 'is-windows-bash'
55
import webpack from './build/webpack'
6+
import babel, { watch } from './build/babel'
67
import read from './read'
78

89
export default class HotReloader {
@@ -12,6 +13,7 @@ export default class HotReloader {
1213
this.middlewares = []
1314
this.webpackDevMiddleware = null
1415
this.webpackHotMiddleware = null
16+
this.watcher = null
1517
this.initialized = false
1618
this.stats = null
1719
this.compilationErrors = null
@@ -33,7 +35,11 @@ export default class HotReloader {
3335
}
3436

3537
async start () {
36-
await this.prepareMiddlewares()
38+
this.watch()
39+
await Promise.all([
40+
this.prepareMiddlewares(),
41+
babel(this.dir, { dev: true })
42+
])
3743
this.stats = await this.waitUntilValid()
3844
}
3945

@@ -163,6 +169,26 @@ export default class HotReloader {
163169
send (action, ...args) {
164170
this.webpackHotMiddleware.publish({ action, data: args })
165171
}
172+
173+
watch () {
174+
const onChange = (path) => {
175+
babel(this.dir, { dev: true })
176+
.then(() => {
177+
const f = join(this.dir, '.next', 'dist', relative(this.dir, path))
178+
delete require.cache[f]
179+
this.send('hardReload')
180+
})
181+
}
182+
183+
this.watcher = watch(this.dir)
184+
this.watcher
185+
.on('add', onChange)
186+
.on('change', onChange)
187+
.on('unlink', onChange)
188+
.on('error', (err) => {
189+
console.error(err)
190+
})
191+
}
166192
}
167193

168194
function deleteCache (path) {

0 commit comments

Comments
 (0)