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:
parent
43b0e6f514
commit
8ddafaea5c
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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
1
document.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('./dist/server/document')
|
|
@ -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)
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
`}} />
|
|
||||||
}
|
|
|
@ -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
1
pages/_document.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('next/document')
|
62
server/build/babel.js
Normal file
62
server/build/babel.js
Normal 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
|
||||||
|
})
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
118
server/document.js
Normal 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)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
`}} />
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
Loading…
Reference in a new issue