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] })
proc.on('close', (code) => process.exit(code))
proc.on('error', (err) => {
console.log(err)
console.error(err)
process.exit(1)
})

View file

@ -3,7 +3,8 @@ import { exec } from 'child_process'
import { resolve, join } from 'path'
import parseArgs from 'minimist'
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'
const argv = parseArgs(process.argv.slice(2), {
@ -25,9 +26,10 @@ const open = url => {
const dir = resolve(argv._[0] || '.')
build(dir)
.then(async () => {
const srv = new Server({ dir, dev: true })
webpack(dir, { hotReload: true })
.then(async (compiler) => {
const hotReloader = new HotReloader(compiler)
const srv = new Server({ dir, dev: true, hotReloader })
await srv.start(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 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 container = document.getElementById('__next')
const appProps = { Component, props, router, headManager }
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'))
})
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', () => {
return gulp.src('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')
.pipe(webpack({
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(notify('Built dev client'))
@ -117,7 +141,21 @@ gulp.task('build-release-client', ['compile-lib', 'compile-client'], () => {
}
}),
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(notify('Built release client'))
@ -179,6 +217,7 @@ gulp.task('clean-test', () => {
gulp.task('default', [
'compile',
'build',
'copy',
'test',
'watch'
])
@ -187,6 +226,7 @@ gulp.task('release', (cb) => {
sequence('clean', [
'compile',
'build-release',
'copy',
'test'
], 'clean-test', cb)
})

View file

@ -1,4 +1,5 @@
import React, { Component, PropTypes } from 'react'
import { AppContainer } from 'react-hot-loader'
export default class App extends Component {
static childContextTypes = {
@ -51,7 +52,15 @@ export default class App extends Component {
render () {
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",
"gulp-benchmark": "^1.1.1",
"htmlescape": "1.1.1",
"loader-utils": "0.2.16",
"minimist": "1.2.0",
"mkdirp-then": "1.2.0",
"mz": "2.4.0",
"path-match": "1.2.4",
"react": "15.3.2",
"react-dom": "15.3.2",
"react-hot-loader": "3.0.0-beta.6",
"resolve": "1.1.7",
"run-sequence": "1.2.2",
"send": "0.14.1",
"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": {
"babel-eslint": "^7.0.0",
"babel-plugin-transform-remove-strict-mode": "0.0.2",
"del": "2.2.2",
"gulp": "3.9.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 glob from 'glob-promise'
import transpile from './transpile'
import bundle from './bundle'
import webpack from './webpack'
export default async function build (dir) {
const dstDir = resolve(dir, '.next')
const templateDir = resolve(__dirname, '..', '..', 'lib', 'pages')
const compiler = await webpack(dir)
// create `.next/pages/_error.js`
// which may be overwriten by the user script, `pages/_error.js`
const templatPaths = await glob('**/*.js', { cwd: templateDir })
await Promise.all(templatPaths.map(async (p) => {
await transpile(resolve(templateDir, p), resolve(dstDir, 'pages', p))
}))
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err) return reject(err)
const paths = await glob('**/*.js', { cwd: dir, ignore: 'node_modules/**' })
await Promise.all(paths.map(async (p) => {
await transpile(resolve(dir, p), resolve(dstDir, p))
}))
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)
}
const pagePaths = await glob('pages/**/*.js', { cwd: dstDir })
await Promise.all(pagePaths.map(async (p) => {
await bundle(resolve(dstDir, p), resolve(dstDir, '_bundles', p))
}))
resolve()
})
})
}

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'
export default class Server {
constructor ({ dir = '.', dev = false }) {
constructor ({ dir = '.', dev = false, hotReloader }) {
this.dir = resolve(dir)
this.dev = dev
this.hotReloader = hotReloader
this.router = new Router()
this.http = http.createServer((req, res) => {
@ -39,6 +40,10 @@ export default class Server {
await this.render(req, res)
})
if (this.hotReloader) {
await this.hotReloader.start()
}
await new Promise((resolve, reject) => {
this.http.listen(port, (err) => {
if (err) return reject(err)

View file

@ -1,8 +1,6 @@
import fs from 'mz/fs'
import resolve from './resolve'
const cache = {}
/**
* resolve a file like `require.resolve`,
* and read and cache the file content
@ -10,13 +8,16 @@ const cache = {}
async function read (path) {
const f = await resolve(path)
let promise = cache[f]
if (!promise) {
promise = cache[f] = fs.readFile(f, 'utf8')
if (cache.hasOwnProperty(f)) {
return cache[f]
}
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 { createElement } from 'react'
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
import requireResolve from './resolve'
import requireModule from './require'
import read from './read'
import Router from '../lib/router'
import Document from '../lib/document'
@ -16,8 +16,7 @@ export async function render (url, ctx = {}, {
staticMarkup = false
} = {}) {
const path = getPath(url)
const p = await requireResolve(resolve(dir, '.next', 'pages', path))
const mod = require(p)
const mod = await requireModule(resolve(dir, '.next', 'pages', path))
const Component = mod.default || mod
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) => {
_resolve(id, opts, (err, path) => {
if (err) {
const e = new Error(err)
e.code = 'ENOENT'
return reject(e)
err.code = 'ENOENT'
return reject(err)
}
resolve(path)
})