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

Merge pull request #31 from zeit/add/hot-reload

Hot code reloading
This commit is contained in:
Naoyuki Kanezawa 2016-10-17 11:23:05 +09:00 committed by GitHub
commit 3c88a3d7a8
20 changed files with 327 additions and 151 deletions

View file

@ -26,6 +26,6 @@ const bin = resolve(__dirname, 'next-' + cmd)
const proc = spawn(bin, args, { stdio: 'inherit', customFds: [0, 1, 2] }) const proc = spawn(bin, args, { stdio: 'inherit', customFds: [0, 1, 2] })
proc.on('close', (code) => process.exit(code)) proc.on('close', (code) => process.exit(code))
proc.on('error', (err) => { proc.on('error', (err) => {
console.log(err) console.error(err)
process.exit(1) process.exit(1)
}) })

View file

@ -3,7 +3,8 @@ import { exec } from 'child_process'
import { resolve, join } from 'path' import { resolve, join } from 'path'
import parseArgs from 'minimist' import parseArgs from 'minimist'
import Server from '../server' import Server from '../server'
import build from '../server/build' import HotReloader from '../server/hot-reloader'
import webpack from '../server/build/webpack'
import { exists } from 'mz/fs' import { exists } from 'mz/fs'
const argv = parseArgs(process.argv.slice(2), { const argv = parseArgs(process.argv.slice(2), {
@ -25,9 +26,10 @@ const open = url => {
const dir = resolve(argv._[0] || '.') const dir = resolve(argv._[0] || '.')
build(dir) webpack(dir, { hotReload: true })
.then(async () => { .then(async (compiler) => {
const srv = new Server({ dir, dev: true }) const hotReloader = new HotReloader(compiler)
const srv = new Server({ dir, dev: true, hotReloader })
await srv.start(argv.port) await srv.start(argv.port)
console.log('> Ready on http://localhost:%d', argv.port) console.log('> Ready on http://localhost:%d', argv.port)

View file

@ -1 +1,7 @@
import './next' import 'react-hot-loader/patch'
import 'webpack-dev-server/client?http://localhost:3030'
import * as next from './next'
module.exports = next
window.next = next

View file

@ -13,10 +13,11 @@ const {
const App = app ? evalScript(app).default : DefaultApp const App = app ? evalScript(app).default : DefaultApp
const Component = evalScript(component).default const Component = evalScript(component).default
const router = new Router(window.location.href, { Component }) export const router = new Router(window.location.href, { Component })
const headManager = new HeadManager() const headManager = new HeadManager()
const container = document.getElementById('__next') const container = document.getElementById('__next')
const appProps = { Component, props, router, headManager } const appProps = { Component, props, router, headManager }
StyleSheet.rehydrate(classNames) StyleSheet.rehydrate(classNames)
render(createElement(App, { ...appProps }), container) render(createElement(App, appProps), container)

View file

@ -67,6 +67,16 @@ gulp.task('compile-test', () => {
.pipe(notify('Compiled test files')) .pipe(notify('Compiled test files'))
}) })
gulp.task('copy', [
'copy-pages',
'copy-test-fixtures'
])
gulp.task('copy-pages', () => {
return gulp.src('pages/**/*.js')
.pipe(gulp.dest('dist/pages'))
})
gulp.task('copy-test-fixtures', () => { gulp.task('copy-test-fixtures', () => {
return gulp.src('test/fixtures/**/*') return gulp.src('test/fixtures/**/*')
.pipe(gulp.dest('dist/test/fixtures')) .pipe(gulp.dest('dist/test/fixtures'))
@ -98,7 +108,21 @@ gulp.task('build-dev-client', ['compile-lib', 'compile-client'], () => {
.src('dist/client/next-dev.js') .src('dist/client/next-dev.js')
.pipe(webpack({ .pipe(webpack({
quiet: true, quiet: true,
output: { filename: 'next-dev.bundle.js' } output: { filename: 'next-dev.bundle.js' },
module: {
loaders: [
{
test: /eval-script\.js$/,
exclude: /node_modules/,
loader: 'babel',
query: {
plugins: [
'babel-plugin-transform-remove-strict-mode'
]
}
}
]
}
})) }))
.pipe(gulp.dest('dist/client')) .pipe(gulp.dest('dist/client'))
.pipe(notify('Built dev client')) .pipe(notify('Built dev client'))
@ -117,7 +141,21 @@ gulp.task('build-release-client', ['compile-lib', 'compile-client'], () => {
} }
}), }),
new webpack.webpack.optimize.UglifyJsPlugin() new webpack.webpack.optimize.UglifyJsPlugin()
] ],
module: {
loaders: [
{
test: /eval-script\.js$/,
exclude: /node_modules/,
loader: 'babel',
query: {
plugins: [
'babel-plugin-transform-remove-strict-mode'
]
}
}
]
}
})) }))
.pipe(gulp.dest('dist/client')) .pipe(gulp.dest('dist/client'))
.pipe(notify('Built release client')) .pipe(notify('Built release client'))
@ -179,6 +217,7 @@ gulp.task('clean-test', () => {
gulp.task('default', [ gulp.task('default', [
'compile', 'compile',
'build', 'build',
'copy',
'test', 'test',
'watch' 'watch'
]) ])
@ -187,6 +226,7 @@ gulp.task('release', (cb) => {
sequence('clean', [ sequence('clean', [
'compile', 'compile',
'build-release', 'build-release',
'copy',
'test' 'test'
], 'clean-test', cb) ], 'clean-test', cb)
}) })

View file

@ -1,4 +1,5 @@
import React, { Component, PropTypes } from 'react' import React, { Component, PropTypes } from 'react'
import { AppContainer } from 'react-hot-loader'
export default class App extends Component { export default class App extends Component {
static childContextTypes = { static childContextTypes = {
@ -51,7 +52,15 @@ export default class App extends Component {
render () { render () {
const { Component, props } = this.state const { Component, props } = this.state
return React.createElement(Component, { ...props })
if (typeof window === 'undefined') {
// workaround for https://github.com/gaearon/react-hot-loader/issues/283
return <Component {...props} />
}
return <AppContainer>
<Component {...props} />
</AppContainer>
} }
} }

View file

@ -33,20 +33,25 @@
"glob-promise": "1.0.6", "glob-promise": "1.0.6",
"gulp-benchmark": "^1.1.1", "gulp-benchmark": "^1.1.1",
"htmlescape": "1.1.1", "htmlescape": "1.1.1",
"loader-utils": "0.2.16",
"minimist": "1.2.0", "minimist": "1.2.0",
"mkdirp-then": "1.2.0", "mkdirp-then": "1.2.0",
"mz": "2.4.0", "mz": "2.4.0",
"path-match": "1.2.4", "path-match": "1.2.4",
"react": "15.3.2", "react": "15.3.2",
"react-dom": "15.3.2", "react-dom": "15.3.2",
"react-hot-loader": "3.0.0-beta.6",
"resolve": "1.1.7", "resolve": "1.1.7",
"run-sequence": "1.2.2", "run-sequence": "1.2.2",
"send": "0.14.1", "send": "0.14.1",
"url": "0.11.0", "url": "0.11.0",
"webpack": "1.13.2" "webpack": "1.13.2",
"webpack-dev-server": "1.16.2",
"write-file-webpack-plugin": "3.3.0"
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^7.0.0", "babel-eslint": "^7.0.0",
"babel-plugin-transform-remove-strict-mode": "0.0.2",
"del": "2.2.2", "del": "2.2.2",
"gulp": "3.9.1", "gulp": "3.9.1",
"gulp-ava": "0.14.1", "gulp-ava": "0.14.1",

View file

@ -1,53 +0,0 @@
import { resolve, dirname, basename } from 'path'
import webpack from 'webpack'
export default function bundle (src, dst) {
const compiler = webpack({
entry: src,
output: {
path: dirname(dst),
filename: basename(dst),
libraryTarget: 'commonjs2'
},
externals: [
'react',
'react-dom',
{
[require.resolve('react')]: 'react',
[require.resolve('../../lib/link')]: 'next/link',
[require.resolve('../../lib/css')]: 'next/css',
[require.resolve('../../lib/head')]: 'next/head'
}
],
resolveLoader: {
root: resolve(__dirname, '..', '..', 'node_modules')
},
plugins: [
new webpack.optimize.UglifyJsPlugin({
compress: { warnings: false },
sourceMap: false
})
],
module: {
preLoaders: [
{ test: /\.json$/, loader: 'json-loader' }
]
}
})
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err) return reject(err)
const jsonStats = stats.toJson()
if (jsonStats.errors.length > 0) {
const error = new Error(jsonStats.errors[0])
error.errors = jsonStats.errors
error.warnings = jsonStats.warnings
return reject(error)
}
resolve()
})
})
}

View file

@ -1,26 +1,21 @@
import { resolve } from 'path' import webpack from './webpack'
import glob from 'glob-promise'
import transpile from './transpile'
import bundle from './bundle'
export default async function build (dir) { export default async function build (dir) {
const dstDir = resolve(dir, '.next') const compiler = await webpack(dir)
const templateDir = resolve(__dirname, '..', '..', 'lib', 'pages')
// create `.next/pages/_error.js` return new Promise((resolve, reject) => {
// which may be overwriten by the user script, `pages/_error.js` compiler.run((err, stats) => {
const templatPaths = await glob('**/*.js', { cwd: templateDir }) if (err) return reject(err)
await Promise.all(templatPaths.map(async (p) => {
await transpile(resolve(templateDir, p), resolve(dstDir, 'pages', p))
}))
const paths = await glob('**/*.js', { cwd: dir, ignore: 'node_modules/**' }) const jsonStats = stats.toJson()
await Promise.all(paths.map(async (p) => { if (jsonStats.errors.length > 0) {
await transpile(resolve(dir, p), resolve(dstDir, p)) const error = new Error(jsonStats.errors[0])
})) error.errors = jsonStats.errors
error.warnings = jsonStats.warnings
return reject(error)
}
const pagePaths = await glob('pages/**/*.js', { cwd: dstDir }) resolve()
await Promise.all(pagePaths.map(async (p) => { })
await bundle(resolve(dstDir, p), resolve(dstDir, '_bundles', p)) })
}))
} }

View file

@ -0,0 +1,16 @@
import loaderUrils from 'loader-utils'
module.exports = function (content) {
this.cacheable()
const query = loaderUrils.parseQuery(this.query)
const name = query.name || '[hash].[ext]'
const context = query.context || this.options.context
const regExp = query.regExp
const opts = { context, content, regExp }
const interpolatedName = loaderUrils.interpolateName(this, name, opts)
this.emitFile(interpolatedName, content)
return content
}

View file

@ -0,0 +1,14 @@
module.exports = function (content) {
this.cacheable()
return content + `
if (module.hot) {
module.hot.accept()
if ('idle' !== module.hot.status()) {
const Component = module.exports.default || module.exports
next.router.notify({ Component })
}
}
`
}

View file

@ -1,51 +0,0 @@
import { dirname } from 'path'
import fs from 'mz/fs'
import mkdirp from 'mkdirp-then'
import { transformFile } from 'babel-core'
import preset2015 from 'babel-preset-es2015'
import presetReact from 'babel-preset-react'
import transformAsyncToGenerator from 'babel-plugin-transform-async-to-generator'
import transformClassProperties from 'babel-plugin-transform-class-properties'
import transformObjectRestSpread from 'babel-plugin-transform-object-rest-spread'
import transformRuntime from 'babel-plugin-transform-runtime'
import moduleAlias from 'babel-plugin-module-alias'
const babelRuntimePath = require.resolve('babel-runtime/package')
.replace(/[\\\/]package\.json$/, '')
const babelOptions = {
presets: [preset2015, presetReact],
plugins: [
transformAsyncToGenerator,
transformClassProperties,
transformObjectRestSpread,
transformRuntime,
[
moduleAlias,
[
{ src: `npm:${babelRuntimePath}`, expose: 'babel-runtime' },
{ src: `npm:${require.resolve('react')}`, expose: 'react' },
{ src: `npm:${require.resolve('../../lib/link')}`, expose: 'next/link' },
{ src: `npm:${require.resolve('../../lib/css')}`, expose: 'next/css' },
{ src: `npm:${require.resolve('../../lib/head')}`, expose: 'next/head' }
]
]
],
ast: false
}
export default async function transpile (src, dst) {
const code = await new Promise((resolve, reject) => {
transformFile(src, babelOptions, (err, result) => {
if (err) return reject(err)
resolve(result.code)
})
})
await writeFile(dst, code)
}
async function writeFile (path, data) {
await mkdirp(dirname(path))
await fs.writeFile(path, data, { encoding: 'utf8', flag: 'w+' })
}

126
server/build/webpack.js Normal file
View file

@ -0,0 +1,126 @@
import { resolve, join } from 'path'
import webpack from 'webpack'
import glob from 'glob-promise'
import WriteFilePlugin from 'write-file-webpack-plugin'
export default async function createCompiler (dir, { hotReload = false } = {}) {
dir = resolve(dir)
const pages = await glob('pages/**/*.js', { cwd: dir })
const entry = {}
const defaultEntries = hotReload ? ['webpack/hot/only-dev-server'] : []
for (const p of pages) {
entry[join('_bundles', p)] = defaultEntries.concat(['./' + p])
}
const errEntry = join('_bundles', 'pages', '_error.js')
const defaultErrorPath = resolve(__dirname, '..', '..', 'pages', '_error.js')
if (!entry[errEntry]) entry[errEntry] = defaultErrorPath
const nodeModulesDir = resolve(__dirname, '..', '..', '..', 'node_modules')
const plugins = [
hotReload
? new webpack.HotModuleReplacementPlugin()
: new webpack.optimize.UglifyJsPlugin({
compress: { warnings: false },
sourceMap: false
}),
new WriteFilePlugin({ log: false })
]
const babelRuntimePath = require.resolve('babel-runtime/package')
.replace(/[\\\/]package\.json$/, '')
const loaders = [{
test: /\.js$/,
loader: 'emit-file-loader',
include: [
dir,
resolve(__dirname, '..', '..', 'pages')
],
exclude: /node_modules/,
query: {
name: '[path][name].[ext]'
}
}, {
test: /\.js$/,
loader: 'babel',
include: [
dir,
resolve(__dirname, '..', '..', 'pages')
],
exclude: /node_modules/,
query: {
presets: ['es2015', 'react'],
plugins: [
'transform-async-to-generator',
'transform-object-rest-spread',
'transform-class-properties',
'transform-runtime',
[
'module-alias',
[
{ src: `npm:${babelRuntimePath}`, expose: 'babel-runtime' },
{ src: `npm:${require.resolve('react')}`, expose: 'react' },
{ src: `npm:${require.resolve('../../lib/link')}`, expose: 'next/link' },
{ src: `npm:${require.resolve('../../lib/css')}`, expose: 'next/css' },
{ src: `npm:${require.resolve('../../lib/head')}`, expose: 'next/head' }
]
]
]
}
}]
.concat(hotReload ? [{
test: /\.js$/,
loader: 'hot-self-accept-loader',
include: resolve(dir, 'pages')
}] : [])
return webpack({
context: dir,
entry,
output: {
path: resolve(dir, '.next'),
filename: '[name]',
libraryTarget: 'commonjs2',
publicPath: hotReload ? 'http://localhost:3030/' : null
},
externals: [
'react',
'react-dom',
{
[require.resolve('react')]: 'react',
[require.resolve('../../lib/link')]: 'next/link',
[require.resolve('../../lib/css')]: 'next/css',
[require.resolve('../../lib/head')]: 'next/head'
}
],
resolve: {
root: [
nodeModulesDir,
resolve(dir, 'node_modules')
]
},
resolveLoader: {
root: [
nodeModulesDir,
resolve(__dirname, 'loaders')
]
},
plugins,
module: {
preLoaders: [
{ test: /\.json$/, loader: 'json-loader' }
],
loaders
},
customInterpolateName: function (url, name, opts) {
if (defaultErrorPath === this.resourcePath) {
return 'pages/_error.js'
}
return url
}
})
}

56
server/hot-reloader.js Normal file
View file

@ -0,0 +1,56 @@
import WebpackDevServer from 'webpack-dev-server'
import read from './read'
export default class HotReloader {
constructor (compiler) {
this.server = new WebpackDevServer(compiler, {
publicPath: '/',
hot: true,
noInfo: true,
clientLogLevel: 'warning'
})
compiler.plugin('after-emit', (compilation, callback) => {
const { assets } = compilation
for (const f of Object.keys(assets)) {
const source = assets[f]
// delete updated file caches
delete require.cache[source.existsAt]
delete read.cache[source.existsAt]
}
callback()
})
}
async start () {
await this.waitBuild()
await this.listen()
}
async waitBuild () {
const stats = await new Promise((resolve) => {
this.server.middleware.waitUntilValid(resolve)
})
const jsonStats = stats.toJson()
if (jsonStats.errors.length > 0) {
const err = new Error(jsonStats.errors[0])
err.errors = jsonStats.errors
err.warnings = jsonStats.warnings
throw err
}
}
listen () {
return new Promise((resolve, reject) => {
this.server.listen(3030, (err) => {
if (err) return reject(err)
resolve()
})
})
}
get fileSystem () {
return this.server.middleware.fileSystem
}
}

View file

@ -5,9 +5,10 @@ import Router from './router'
import { render, renderJSON } from './render' import { render, renderJSON } from './render'
export default class Server { export default class Server {
constructor ({ dir = '.', dev = false }) { constructor ({ dir = '.', dev = false, hotReloader }) {
this.dir = resolve(dir) this.dir = resolve(dir)
this.dev = dev this.dev = dev
this.hotReloader = hotReloader
this.router = new Router() this.router = new Router()
this.http = http.createServer((req, res) => { this.http = http.createServer((req, res) => {
@ -39,6 +40,10 @@ export default class Server {
await this.render(req, res) await this.render(req, res)
}) })
if (this.hotReloader) {
await this.hotReloader.start()
}
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
this.http.listen(port, (err) => { this.http.listen(port, (err) => {
if (err) return reject(err) if (err) return reject(err)

View file

@ -1,8 +1,6 @@
import fs from 'mz/fs' import fs from 'mz/fs'
import resolve from './resolve' import resolve from './resolve'
const cache = {}
/** /**
* resolve a file like `require.resolve`, * resolve a file like `require.resolve`,
* and read and cache the file content * and read and cache the file content
@ -10,13 +8,16 @@ const cache = {}
async function read (path) { async function read (path) {
const f = await resolve(path) const f = await resolve(path)
let promise = cache[f] if (cache.hasOwnProperty(f)) {
if (!promise) { return cache[f]
promise = cache[f] = fs.readFile(f, 'utf8')
} }
return promise
const data = fs.readFile(f, 'utf8')
cache[f] = data
return data
} }
module.exports = read export default read
export const cache = {}
exports.cache = cache read.cache = cache

View file

@ -2,7 +2,7 @@ import { resolve } 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 requireResolve from './resolve' import requireModule from './require'
import read from './read' import read from './read'
import Router from '../lib/router' import Router from '../lib/router'
import Document from '../lib/document' import Document from '../lib/document'
@ -16,8 +16,7 @@ export async function render (url, ctx = {}, {
staticMarkup = false staticMarkup = false
} = {}) { } = {}) {
const path = getPath(url) const path = getPath(url)
const p = await requireResolve(resolve(dir, '.next', 'pages', path)) const mod = await requireModule(resolve(dir, '.next', 'pages', path))
const mod = require(p)
const Component = mod.default || mod const Component = mod.default || mod
const props = await (Component.getInitialProps ? Component.getInitialProps(ctx) : {}) const props = await (Component.getInitialProps ? Component.getInitialProps(ctx) : {})

6
server/require.js Normal file
View file

@ -0,0 +1,6 @@
import resolve from './resolve'
export default async function requireModule (path) {
const f = await resolve(path)
return require(f)
}

View file

@ -4,9 +4,8 @@ export default function resolve (id, opts) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
_resolve(id, opts, (err, path) => { _resolve(id, opts, (err, path) => {
if (err) { if (err) {
const e = new Error(err) err.code = 'ENOENT'
e.code = 'ENOENT' return reject(err)
return reject(e)
} }
resolve(path) resolve(path)
}) })