1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00

custom document support (#405)

This commit is contained in:
Naoyuki Kanezawa 2016-12-17 03:42:40 +09:00 committed by Guillermo Rauch
parent 43b0e6f514
commit 8ddafaea5c
14 changed files with 249 additions and 74 deletions

View file

@ -30,7 +30,7 @@ domready(() => {
const container = document.getElementById('__next') const container = document.getElementById('__next')
const appProps = { Component, props, router, headManager } const appProps = { Component, props, router, headManager }
rehydrate(ids) if (ids) rehydrate(ids)
render(createElement(App, appProps), container) render(createElement(App, appProps), container)
}) })

View file

@ -23,6 +23,9 @@ const handlers = {
// reload to recover from runtime errors // reload to recover from runtime errors
next.router.reload(route) next.router.reload(route)
} }
},
hardReload () {
window.location.reload()
} }
} }

1
document.js Normal file
View file

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

View file

@ -1,52 +0,0 @@
import React from 'react'
import htmlescape from 'htmlescape'
import readPkgUp from 'read-pkg-up'
const { pkg } = readPkgUp.sync({
cwd: __dirname,
normalize: false
})
export default ({ head, css, html, data, dev, staticMarkup, cdn }) => {
return <html>
<head>
{(head || []).map((h, i) => React.cloneElement(h, { key: i }))}
<style dangerouslySetInnerHTML={{ __html: css }} />
</head>
<body>
<div id='__next' dangerouslySetInnerHTML={{ __html: html }} />
{staticMarkup ? null : <script dangerouslySetInnerHTML={{
__html: `__NEXT_DATA__ =${htmlescape(data)}; module={};`
}} />}
{staticMarkup ? null : createClientScript({ dev, cdn })}
<script type='text/javascript' src='/_next/commons.js' />
</body>
</html>
}
function createClientScript ({ dev, cdn }) {
if (dev) {
return <script type='text/javascript' src='/_next/next-dev.bundle.js' />
}
if (!cdn) {
return <script type='text/javascript' src='/_next/next.bundle.js' />
}
return <script dangerouslySetInnerHTML={{ __html: `
(function () {
load('https://cdn.zeit.co/next.js/${pkg.version}/next.min.js', function (err) {
if (err) load('/_next/next.bundle.js')
})
function load (src, fn) {
fn = fn || function () {}
var script = document.createElement('script')
script.src = src
script.onload = function () { fn(null) }
script.onerror = fn
script.crossorigin = 'anonymous'
document.body.appendChild(script)
}
})()
`}} />
}

View file

@ -42,6 +42,7 @@
"babel-preset-es2015": "6.18.0", "babel-preset-es2015": "6.18.0",
"babel-preset-react": "6.16.0", "babel-preset-react": "6.16.0",
"babel-runtime": "6.20.0", "babel-runtime": "6.20.0",
"chokidar": "1.6.1",
"cross-spawn": "5.0.1", "cross-spawn": "5.0.1",
"del": "2.2.2", "del": "2.2.2",
"domready": "1.0.8", "domready": "1.0.8",

1
pages/_document.js Normal file
View file

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

62
server/build/babel.js Normal file
View file

@ -0,0 +1,62 @@
import { resolve, join, dirname } from 'path'
import { readFile, writeFile } from 'mz/fs'
import { transform } from 'babel-core'
import chokidar from 'chokidar'
import mkdirp from 'mkdirp-then'
const babelRuntimePath = require.resolve('babel-runtime/package')
.replace(/[\\/]package\.json$/, '')
export default babel
async function babel (dir, { dev = false } = {}) {
dir = resolve(dir)
let src
try {
src = await readFile(join(dir, 'pages', '_document.js'), 'utf8')
} catch (err) {
if (err.code === 'ENOENT') {
src = await readFile(join(__dirname, '..', '..', 'pages', '_document.js'), 'utf8')
} else {
throw err
}
}
const { code } = transform(src, {
babelrc: false,
sourceMaps: dev ? 'inline' : false,
presets: ['es2015', 'react'],
plugins: [
require.resolve('babel-plugin-react-require'),
require.resolve('babel-plugin-transform-async-to-generator'),
require.resolve('babel-plugin-transform-object-rest-spread'),
require.resolve('babel-plugin-transform-class-properties'),
require.resolve('babel-plugin-transform-runtime'),
[
require.resolve('babel-plugin-module-resolver'),
{
alias: {
'babel-runtime': babelRuntimePath,
react: require.resolve('react'),
'next/link': require.resolve('../../lib/link'),
'next/css': require.resolve('../../lib/css'),
'next/head': require.resolve('../../lib/head'),
'next/document': require.resolve('../../server/document')
}
}
]
]
})
const file = join(dir, '.next', 'dist', 'pages', '_document.js')
await mkdirp(dirname(file))
await writeFile(file, code)
}
export function watch (dir) {
return chokidar.watch('pages/_document.js', {
cwd: dir,
ignoreInitial: true
})
}

View file

@ -1,4 +1,5 @@
import webpack from './webpack' import webpack from './webpack'
import babel from './babel'
import clean from './clean' import clean from './clean'
export default async function build (dir) { export default async function build (dir) {
@ -7,6 +8,13 @@ export default async function build (dir) {
clean(dir) clean(dir)
]) ])
await Promise.all([
runCompiler(compiler),
babel(dir)
])
}
function runCompiler (compiler) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
compiler.run((err, stats) => { compiler.run((err, stats) => {
if (err) return reject(err) if (err) return reject(err)

View file

@ -58,7 +58,9 @@ export default class WatchPagesPlugin {
} }
isPageFile (f) { isPageFile (f) {
return f.indexOf(this.dir) === 0 && extname(f) === '.js' return f.indexOf(this.dir) === 0 &&
relative(this.dir, f) !== '_document.js' &&
extname(f) === '.js'
} }
} }

View file

@ -13,7 +13,10 @@ import DetachPlugin from './plugins/detach-plugin'
export default async function createCompiler (dir, { hotReload = false, dev = false } = {}) { export default async function createCompiler (dir, { hotReload = false, dev = false } = {}) {
dir = resolve(dir) dir = resolve(dir)
const pages = await glob('pages/**/*.js', { cwd: dir }) const pages = await glob('pages/**/*.js', {
cwd: dir,
ignore: 'pages/_document.js'
})
const entry = {} const entry = {}
const defaultEntries = hotReload ? ['next/dist/client/webpack-hot-middleware-client'] : [] const defaultEntries = hotReload ? ['next/dist/client/webpack-hot-middleware-client'] : []
@ -146,7 +149,8 @@ export default async function createCompiler (dir, { hotReload = false, dev = fa
'next/link': require.resolve('../../lib/link'), 'next/link': require.resolve('../../lib/link'),
'next/prefetch': require.resolve('../../lib/prefetch'), 'next/prefetch': require.resolve('../../lib/prefetch'),
'next/css': require.resolve('../../lib/css'), 'next/css': require.resolve('../../lib/css'),
'next/head': require.resolve('../../lib/head') 'next/head': require.resolve('../../lib/head'),
'next/document': require.resolve('../../server/document')
} }
} }
] ]

118
server/document.js Normal file
View file

@ -0,0 +1,118 @@
import React, { Component, PropTypes } from 'react'
import htmlescape from 'htmlescape'
import { renderStatic } from 'glamor/server'
import readPkgUp from 'read-pkg-up'
const { pkg } = readPkgUp.sync({
cwd: __dirname,
normalize: false
})
export default class Document extends Component {
static getInitialProps ({ renderPage }) {
let head
const { html, css, ids } = renderStatic(() => {
const page = renderPage()
head = page.head
return page.html
})
const nextCSS = { css, ids }
return { html, head, nextCSS }
}
static childContextTypes = {
_documentProps: PropTypes.any
}
constructor (props) {
super(props)
const { __NEXT_DATA__, nextCSS } = props
if (nextCSS) __NEXT_DATA__.ids = nextCSS.ids
}
getChildContext () {
return { _documentProps: this.props }
}
render () {
return <html>
<Head />
<body>
<Main />
<NextScript />
</body>
</html>
}
}
export class Head extends Component {
static contextTypes = {
_documentProps: PropTypes.any
}
render () {
const { head, nextCSS } = this.context._documentProps
return <head>
{(head || []).map((h, i) => React.cloneElement(h, { key: i }))}
{nextCSS ? <style dangerouslySetInnerHTML={{ __html: nextCSS.css }} /> : null}
{this.props.children}
</head>
}
}
export class Main extends Component {
static contextTypes = {
_documentProps: PropTypes.any
}
render () {
const { html, __NEXT_DATA__, staticMarkup } = this.context._documentProps
return <div>
<div id='__next' dangerouslySetInnerHTML={{ __html: html }} />
{staticMarkup ? null : <script dangerouslySetInnerHTML={{
__html: `__NEXT_DATA__ = ${htmlescape(__NEXT_DATA__)}; module={};`
}} />}
</div>
}
}
export class NextScript extends Component {
static contextTypes = {
_documentProps: PropTypes.any
}
render () {
const { staticMarkup, dev, cdn } = this.context._documentProps
return <div>
{staticMarkup ? null : createClientScript({ dev, cdn })}
<script type='text/javascript' src='/_next/commons.js' />
</div>
}
}
function createClientScript ({ dev, cdn }) {
if (dev) {
return <script type='text/javascript' src='/_next/next-dev.bundle.js' />
}
if (!cdn) {
return <script type='text/javascript' src='/_next/next.bundle.js' />
}
return <script dangerouslySetInnerHTML={{ __html: `
(function () {
load('https://cdn.zeit.co/next.js/${pkg.version}/next.min.js', function (err) {
if (err) load('/_next/next.bundle.js')
})
function load (src, fn) {
fn = fn || function () {}
var script = document.createElement('script')
script.src = src
script.onload = function () { fn(null) }
script.onerror = fn
script.crossorigin = 'anonymous'
document.body.appendChild(script)
}
})()
`}} />
}

View file

@ -3,6 +3,7 @@ import webpackDevMiddleware from 'webpack-dev-middleware'
import webpackHotMiddleware from 'webpack-hot-middleware' import webpackHotMiddleware from 'webpack-hot-middleware'
import isWindowsBash from 'is-windows-bash' import isWindowsBash from 'is-windows-bash'
import webpack from './build/webpack' import webpack from './build/webpack'
import babel, { watch } from './build/babel'
import read from './read' import read from './read'
export default class HotReloader { export default class HotReloader {
@ -12,6 +13,7 @@ export default class HotReloader {
this.middlewares = [] this.middlewares = []
this.webpackDevMiddleware = null this.webpackDevMiddleware = null
this.webpackHotMiddleware = null this.webpackHotMiddleware = null
this.watcher = null
this.initialized = false this.initialized = false
this.stats = null this.stats = null
this.compilationErrors = null this.compilationErrors = null
@ -33,7 +35,11 @@ export default class HotReloader {
} }
async start () { async start () {
await this.prepareMiddlewares() this.watch()
await Promise.all([
this.prepareMiddlewares(),
babel(this.dir, { dev: true })
])
this.stats = await this.waitUntilValid() this.stats = await this.waitUntilValid()
} }
@ -163,6 +169,26 @@ export default class HotReloader {
send (action, ...args) { send (action, ...args) {
this.webpackHotMiddleware.publish({ action, data: args }) this.webpackHotMiddleware.publish({ action, data: args })
} }
watch () {
const onChange = (path) => {
babel(this.dir, { dev: true })
.then(() => {
const f = join(this.dir, '.next', 'dist', relative(this.dir, path))
delete require.cache[f]
this.send('hardReload')
})
}
this.watcher = watch(this.dir)
this.watcher
.on('add', onChange)
.on('change', onChange)
.on('unlink', onChange)
.on('error', (err) => {
console.error(err)
})
}
} }
function deleteCache (path) { function deleteCache (path) {

View file

@ -2,13 +2,11 @@ import { join } from 'path'
import { parse } from 'url' import { parse } from 'url'
import { createElement } from 'react' import { createElement } from 'react'
import { renderToString, renderToStaticMarkup } from 'react-dom/server' import { renderToString, renderToStaticMarkup } from 'react-dom/server'
import { renderStatic } from 'glamor/server'
import requireModule from './require' import requireModule from './require'
import read from './read' import read from './read'
import getConfig from './config' import getConfig from './config'
import Router from '../lib/router' import Router from '../lib/router'
import Document from '../lib/document' import Head, { defaultHead } from '../lib/head'
import Head, {defaultHead} from '../lib/head'
import App from '../lib/app' import App from '../lib/app'
export async function render (url, ctx = {}, { export async function render (url, ctx = {}, {
@ -17,8 +15,12 @@ export async function render (url, ctx = {}, {
staticMarkup = false staticMarkup = false
} = {}) { } = {}) {
const path = getPath(url) const path = getPath(url)
const mod = await requireModule(join(dir, '.next', 'dist', 'pages', path)) let [Component, Document] = await Promise.all([
const Component = mod.default || mod requireModule(join(dir, '.next', 'dist', 'pages', path)),
requireModule(join(dir, '.next', 'dist', 'pages', '_document'))
])
Component = Component.default || Component
Document = Document.default || Document
const [ const [
props, props,
@ -30,33 +32,32 @@ export async function render (url, ctx = {}, {
read(join(dir, '.next', 'bundles', 'pages', dev ? '_error-debug' : '_error')) read(join(dir, '.next', 'bundles', 'pages', dev ? '_error-debug' : '_error'))
]) ])
const { html, css, ids } = renderStatic(() => { const renderPage = () => {
const app = createElement(App, { const app = createElement(App, {
Component, Component,
props, props,
router: new Router(ctx.req ? ctx.req.url : url) router: new Router(ctx.req ? ctx.req.url : url)
}) })
const html = (staticMarkup ? renderToStaticMarkup : renderToString)(app)
return (staticMarkup ? renderToStaticMarkup : renderToString)(app)
})
const head = Head.rewind() || defaultHead() const head = Head.rewind() || defaultHead()
return { html, head }
}
const config = await getConfig(dir) const config = await getConfig(dir)
const docProps = await Document.getInitialProps({ ...ctx, renderPage })
const doc = createElement(Document, { const doc = createElement(Document, {
html, __NEXT_DATA__: {
head,
css,
data: {
component, component,
errorComponent, errorComponent,
props, props,
ids: ids,
err: (ctx.err && dev) ? errorToJSON(ctx.err) : null err: (ctx.err && dev) ? errorToJSON(ctx.err) : null
}, },
dev, dev,
staticMarkup, staticMarkup,
cdn: config.cdn cdn: config.cdn,
...docProps
}) })
return '<!DOCTYPE html>' + renderToStaticMarkup(doc) return '<!DOCTYPE html>' + renderToStaticMarkup(doc)

View file

@ -8,7 +8,7 @@ import { render as _render } from '../server/render'
const dir = join(__dirname, 'fixtures', 'basic') const dir = join(__dirname, 'fixtures', 'basic')
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000 jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000
describe('integration tests', () => { describe('integration tests', () => {
beforeAll(() => build(dir)) beforeAll(() => build(dir))