From 26e6193a97e1d4bb03f2dafad30e9506a08685fb Mon Sep 17 00:00:00 2001 From: nkzawa Date: Sat, 15 Oct 2016 00:05:08 +0900 Subject: [PATCH 1/8] hot-reload: initial --- bin/next | 2 +- bin/next-dev | 6 +- client/next-dev.js | 8 +- client/next.js | 5 +- gulpfile.js | 44 +++++++++- lib/app.js | 12 ++- package.json | 5 +- {lib/pages => pages}/_error.js | 0 server/build/index.js | 2 +- server/build/webpack.js | 105 +++++++++++++++++++++++ server/hot-reloader.js | 44 ++++++++++ server/index.js | 22 +++-- server/loaders/hot-self-accept-loader.js | 14 +++ server/read.js | 32 +++++-- server/render.js | 9 +- server/resolve.js | 5 +- 16 files changed, 285 insertions(+), 30 deletions(-) rename {lib/pages => pages}/_error.js (100%) create mode 100644 server/build/webpack.js create mode 100644 server/hot-reloader.js create mode 100644 server/loaders/hot-self-accept-loader.js diff --git a/bin/next b/bin/next index 81f7d96c..f6c77564 100755 --- a/bin/next +++ b/bin/next @@ -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) }) diff --git a/bin/next-dev b/bin/next-dev index 34b6c758..093bd99e 100755 --- a/bin/next-dev +++ b/bin/next-dev @@ -4,6 +4,8 @@ import { resolve } 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' const argv = parseArgs(process.argv.slice(2), { alias: { @@ -20,7 +22,9 @@ const dir = resolve(argv._[0] || '.') build(dir) .then(async () => { - const srv = new Server({ dir, dev: true }) + const compiler = await webpack(dir, { hotReload: true }) + 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); }) diff --git a/client/next-dev.js b/client/next-dev.js index b1516d2a..041e7a20 100644 --- a/client/next-dev.js +++ b/client/next-dev.js @@ -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 diff --git a/client/next.js b/client/next.js index 09adf75f..970fdf80 100644 --- a/client/next.js +++ b/client/next.js @@ -13,10 +13,11 @@ const { const App = app ? evalScript(app).default : DefaultApp const Component = evalScript(component).default -const router = new Router(location.href, { Component }) +export const router = new Router(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) diff --git a/gulpfile.js b/gulpfile.js index 3caee0bb..f955b675 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -65,6 +65,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')) @@ -83,7 +93,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')) @@ -102,7 +126,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')) @@ -157,6 +195,7 @@ gulp.task('clean-test', () => { gulp.task('default', [ 'compile', 'build', + 'copy', 'test', 'watch' ]) @@ -165,6 +204,7 @@ gulp.task('release', (cb) => { sequence('clean', [ 'compile', 'build-release', + 'copy', 'test' ], 'clean-test', cb) }) diff --git a/lib/app.js b/lib/app.js index 53b6c78d..dea3bebf 100644 --- a/lib/app.js +++ b/lib/app.js @@ -1,4 +1,5 @@ import React, { Component, PropTypes } from 'react' +import { AppContainer } from 'react-hot-loader' export default class App extends Component { static childContextTypes = { @@ -28,6 +29,7 @@ export default class App extends Component { const props = data.props || this.state.props const state = propsToState({ ...data, + props, router }) @@ -50,7 +52,15 @@ export default class App extends Component { render () { const { Component, props } = this.state - return React.createElement(Component, { ...props }) + + if ('undefined' === typeof window) { + // workaround for https://github.com/gaearon/react-hot-loader/issues/283 + return + } + + return + + } } diff --git a/package.json b/package.json index 3defb291..1f0e148d 100644 --- a/package.json +++ b/package.json @@ -36,13 +36,16 @@ "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" }, "devDependencies": { + "babel-plugin-transform-remove-strict-mode": "0.0.2", "del": "2.2.2", "gulp": "3.9.1", "gulp-ava": "0.14.1", diff --git a/lib/pages/_error.js b/pages/_error.js similarity index 100% rename from lib/pages/_error.js rename to pages/_error.js diff --git a/server/build/index.js b/server/build/index.js index 569fe639..7e94e35a 100644 --- a/server/build/index.js +++ b/server/build/index.js @@ -5,7 +5,7 @@ import bundle from './bundle' export default async function build (dir) { const dstDir = resolve(dir, '.next') - const templateDir = resolve(__dirname, '..', '..', 'lib', 'pages') + const templateDir = resolve(__dirname, '..', '..', 'pages') // create `.next/pages/_error.js` // which may be overwriten by the user sciprt, `pages/_error.js` diff --git a/server/build/webpack.js b/server/build/webpack.js new file mode 100644 index 00000000..1b6fd190 --- /dev/null +++ b/server/build/webpack.js @@ -0,0 +1,105 @@ +import { resolve } from 'path' +import webpack from 'webpack' +import glob from 'glob-promise' + +export default async function createCompiler(dir, { hotReload = false } = {}) { + const pages = await glob('**/*.js', { cwd: resolve(dir, 'pages') }) + + const entry = {} + const defaultEntries = hotReload ? ['webpack/hot/only-dev-server'] : [] + for (const p of pages) { + entry[p] = defaultEntries.concat(['./pages/' + p]) + } + + if (!entry['_error.js']) { + entry._error = resolve(__dirname, '..', '..', 'pages', '_error.js') + } + + const nodeModulesDir = resolve(__dirname, '..', '..', '..', 'node_modules') + + const plugins = hotReload + ? [new webpack.HotModuleReplacementPlugin()] + : [ + new webpack.optimize.UglifyJsPlugin({ + compress: { warnings: false }, + sourceMap: false + }) + ] + + const babelRuntimePath = require.resolve('babel-runtime/package') + .replace(/[\\\/]package\.json$/, ''); + + const loaders = [{ + test: /\.js$/, + loader: 'babel', + include: [ + resolve(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', '_bundles', 'pages'), + 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 + } + }) +} diff --git a/server/hot-reloader.js b/server/hot-reloader.js new file mode 100644 index 00000000..d4a01189 --- /dev/null +++ b/server/hot-reloader.js @@ -0,0 +1,44 @@ +import WebpackDevServer from 'webpack-dev-server' + +export default class HotReloader { + constructor (compiler) { + this.server = new WebpackDevServer(compiler, { + publicPath: '/', + hot: true, + noInfo: true, + clientLogLevel: 'warning' + }) + } + + 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 + } +} diff --git a/server/index.js b/server/index.js index b9d90dfc..2ed86aea 100644 --- a/server/index.js +++ b/server/index.js @@ -3,11 +3,13 @@ import { resolve } from 'path' import send from 'send' import Router from './router' import { render, renderJSON } from './render' +import HotReloader from './hot-reloader' 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 +41,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) @@ -57,10 +63,11 @@ export default class Server { } async render (req, res) { - const { dir, dev } = this + const { dir, dev, hotReloader } = this + const mfs = hotReloader ? hotReloader.fileSystem : null let html try { - html = await render(req.url, { req, res }, { dir, dev }) + html = await render(req.url, { req, res }, { dir, dev, mfs }) } catch (err) { if ('ENOENT' === err.code) { res.statusCode = 404 @@ -68,17 +75,18 @@ export default class Server { console.error(err) res.statusCode = 500 } - html = await render('/_error', { req, res, err }, { dir, dev }) + html = await render('/_error', { req, res, err }, { dir, dev, mfs }) } sendHTML(res, html) } async renderJSON (req, res) { - const { dir } = this + const { dir, hotReloader } = this + const mfs = hotReloader ? hotReloader.fileSystem : null let json try { - json = await renderJSON(req.url, { dir }) + json = await renderJSON(req.url, { dir, mfs }) } catch (err) { if ('ENOENT' === err.code) { res.statusCode = 404 @@ -86,7 +94,7 @@ export default class Server { console.error(err) res.statusCode = 500 } - json = await renderJSON('/_error.json', { dir }) + json = await renderJSON('/_error.json', { dir, mfs }) } const data = JSON.stringify(json) diff --git a/server/loaders/hot-self-accept-loader.js b/server/loaders/hot-self-accept-loader.js new file mode 100644 index 00000000..d7c63e91 --- /dev/null +++ b/server/loaders/hot-self-accept-loader.js @@ -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 }) + } + } + ` +} diff --git a/server/read.js b/server/read.js index 747207cc..3aa595d9 100644 --- a/server/read.js +++ b/server/read.js @@ -8,13 +8,33 @@ const cache = {} * and read and cache the file content */ -async function read (path) { - const f = await resolve(path) - let promise = cache[f] - if (!promise) { - promise = cache[f] = fs.readFile(f, 'utf8') +async function read (path, { mfs }) { + const f = await (mfs ? resolveFromMFS(path, mfs) : resolve(path)) + if (mfs) { + return mfs.readFileSync(f, 'utf8') + } else { + let promise = cache[f] + if (!promise) { + promise = cache[f] = fs.readFile(f, 'utf8') + } + return promise } - return promise +} + +function resolveFromMFS (path, mfs) { + const isFile = (file, cb) => { + if (!mfs.existsSync(file)) return cb(null, false) + + let stat + try { + stat = mfs.statSync(file) + } catch (err) { + return cb(err) + } + cb(null, stat.isFile() || stat.isFIFO()) + } + const readFile = mfs.readFile.bind(mfs) + return resolve(path, { isFile, readFile }) } module.exports = read diff --git a/server/render.js b/server/render.js index 670fc550..453cb3bd 100644 --- a/server/render.js +++ b/server/render.js @@ -14,7 +14,8 @@ import { StyleSheetServer } from '../lib/css' export async function render (url, ctx = {}, { dir = process.cwd(), dev = false, - staticMarkup = false + staticMarkup = false, + mfs } = {}) { const path = getPath(url) const p = await requireResolve(resolve(dir, '.next', 'pages', path)) @@ -22,7 +23,7 @@ export async function render (url, ctx = {}, { const Component = mod.default || mod const props = await (Component.getInitialProps ? Component.getInitialProps(ctx) : {}) - const component = await read(resolve(dir, '.next', '_bundles', 'pages', path)) + const component = await read(resolve(dir, '.next', '_bundles', 'pages', path), { mfs }) const { html, css } = StyleSheetServer.renderStatic(() => { const app = createElement(App, { @@ -53,9 +54,9 @@ export async function render (url, ctx = {}, { return '' + renderToStaticMarkup(doc) } -export async function renderJSON (url, { dir = process.cwd() } = {}) { +export async function renderJSON (url, { dir = process.cwd(), mfs } = {}) { const path = getPath(url) - const component = await read(resolve(dir, '.next', '_bundles', 'pages', path)) + const component = await read(resolve(dir, '.next', '_bundles', 'pages', path), { mfs }) return { component } } diff --git a/server/resolve.js b/server/resolve.js index 6b56fed0..03ad5d7d 100644 --- a/server/resolve.js +++ b/server/resolve.js @@ -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) }) From e7ffb2c17db7f37feacbf06517d9fdbf44615fab Mon Sep 17 00:00:00 2001 From: nkzawa Date: Sun, 16 Oct 2016 01:17:27 +0900 Subject: [PATCH 2/8] emit modules as file --- package.json | 1 + server/build/webpack.js | 43 ++++++++++++++++++------------ server/loaders/emit-file-loader.js | 16 +++++++++++ 3 files changed, 43 insertions(+), 17 deletions(-) create mode 100644 server/loaders/emit-file-loader.js diff --git a/package.json b/package.json index 1f0e148d..0f11cc39 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "cross-spawn": "4.0.2", "glob-promise": "1.0.6", "htmlescape": "1.1.1", + "loader-utils": "0.2.16", "minimist": "1.2.0", "mkdirp-then": "1.2.0", "mz": "2.4.0", diff --git a/server/build/webpack.js b/server/build/webpack.js index 1b6fd190..00fb86c1 100644 --- a/server/build/webpack.js +++ b/server/build/webpack.js @@ -1,39 +1,41 @@ -import { resolve } from 'path' +import { resolve, join } from 'path' import webpack from 'webpack' import glob from 'glob-promise' export default async function createCompiler(dir, { hotReload = false } = {}) { - const pages = await glob('**/*.js', { cwd: resolve(dir, 'pages') }) + 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[p] = defaultEntries.concat(['./pages/' + p]) + entry[join('_bundles', p)] = defaultEntries.concat(['./' + p]) } - if (!entry['_error.js']) { - entry._error = resolve(__dirname, '..', '..', 'pages', '_error.js') + const errEntry = join('_bundles', 'pages', '_error.js') + if (!entry[errEntry]) { + entry[errEntry] = resolve(__dirname, '..', '..', 'pages', '_error.js') } const nodeModulesDir = resolve(__dirname, '..', '..', '..', 'node_modules') - const plugins = hotReload - ? [new webpack.HotModuleReplacementPlugin()] - : [ - new webpack.optimize.UglifyJsPlugin({ - compress: { warnings: false }, - sourceMap: false - }) - ] - const babelRuntimePath = require.resolve('babel-runtime/package') .replace(/[\\\/]package\.json$/, ''); const loaders = [{ + test: /\.js$/, + loader: 'emit-file-loader', + include: dir, + exclude: /node_modules/, + query: { + name: '[path][name].[ext]' + } + }, { test: /\.js$/, loader: 'babel', include: [ - resolve(dir), + dir, resolve(__dirname, '..', '..', 'pages') ], exclude: /node_modules/, @@ -67,7 +69,7 @@ export default async function createCompiler(dir, { hotReload = false } = {}) { context: dir, entry, output: { - path: resolve(dir, '.next', '_bundles', 'pages'), + path: resolve(dir, '.next'), filename: '[name]', libraryTarget: 'commonjs2', publicPath: hotReload ? 'http://localhost:3030/' : null @@ -94,7 +96,14 @@ export default async function createCompiler(dir, { hotReload = false } = {}) { resolve(__dirname, '..', 'loaders') ] }, - plugins, + plugins: [ + hotReload + ? new webpack.HotModuleReplacementPlugin() + : new webpack.optimize.UglifyJsPlugin({ + compress: { warnings: false }, + sourceMap: false + }) + ], module: { preLoaders: [ { test: /\.json$/, loader: 'json-loader' } diff --git a/server/loaders/emit-file-loader.js b/server/loaders/emit-file-loader.js new file mode 100644 index 00000000..01ccf39d --- /dev/null +++ b/server/loaders/emit-file-loader.js @@ -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 +} From 3032ade283f4a2819ba5a125f70db818f7b5867e Mon Sep 17 00:00:00 2001 From: nkzawa Date: Sun, 16 Oct 2016 04:49:42 +0900 Subject: [PATCH 3/8] write assets to disk --- bin/next-dev | 6 ++---- package.json | 3 ++- server/build/webpack.js | 20 ++++++++++++-------- server/hot-reloader.js | 12 ++++++++++++ server/index.js | 15 ++++++--------- server/read.js | 41 +++++++++++------------------------------ server/render.js | 15 ++++++--------- server/require.js | 6 ++++++ 8 files changed, 57 insertions(+), 61 deletions(-) create mode 100644 server/require.js diff --git a/bin/next-dev b/bin/next-dev index 093bd99e..351e21ba 100755 --- a/bin/next-dev +++ b/bin/next-dev @@ -3,7 +3,6 @@ import { resolve } 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' @@ -20,9 +19,8 @@ const argv = parseArgs(process.argv.slice(2), { const dir = resolve(argv._[0] || '.') -build(dir) -.then(async () => { - const compiler = await webpack(dir, { hotReload: 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) diff --git a/package.json b/package.json index 0f11cc39..c2ff45ca 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "send": "0.14.1", "url": "0.11.0", "webpack": "1.13.2", - "webpack-dev-server": "1.16.2" + "webpack-dev-server": "1.16.2", + "write-file-webpack-plugin": "3.3.0" }, "devDependencies": { "babel-plugin-transform-remove-strict-mode": "0.0.2", diff --git a/server/build/webpack.js b/server/build/webpack.js index 00fb86c1..0b600014 100644 --- a/server/build/webpack.js +++ b/server/build/webpack.js @@ -1,6 +1,7 @@ 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) @@ -20,6 +21,16 @@ export default async function createCompiler(dir, { hotReload = false } = {}) { const nodeModulesDir = resolve(__dirname, '..', '..', '..', 'node_modules') + const plugins = hotReload ? [ + new webpack.HotModuleReplacementPlugin(), + new WriteFilePlugin({ log: false }) + ] : [ + new webpack.optimize.UglifyJsPlugin({ + compress: { warnings: false }, + sourceMap: false + }) + ] + const babelRuntimePath = require.resolve('babel-runtime/package') .replace(/[\\\/]package\.json$/, ''); @@ -96,14 +107,7 @@ export default async function createCompiler(dir, { hotReload = false } = {}) { resolve(__dirname, '..', 'loaders') ] }, - plugins: [ - hotReload - ? new webpack.HotModuleReplacementPlugin() - : new webpack.optimize.UglifyJsPlugin({ - compress: { warnings: false }, - sourceMap: false - }) - ], + plugins, module: { preLoaders: [ { test: /\.json$/, loader: 'json-loader' } diff --git a/server/hot-reloader.js b/server/hot-reloader.js index d4a01189..a697e0dd 100644 --- a/server/hot-reloader.js +++ b/server/hot-reloader.js @@ -1,4 +1,5 @@ import WebpackDevServer from 'webpack-dev-server' +import read from './read' export default class HotReloader { constructor (compiler) { @@ -8,6 +9,17 @@ export default class HotReloader { 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 () { diff --git a/server/index.js b/server/index.js index 2ed86aea..8fd826b0 100644 --- a/server/index.js +++ b/server/index.js @@ -3,7 +3,6 @@ import { resolve } from 'path' import send from 'send' import Router from './router' import { render, renderJSON } from './render' -import HotReloader from './hot-reloader' export default class Server { constructor ({ dir = '.', dev = false, hotReloader }) { @@ -63,11 +62,10 @@ export default class Server { } async render (req, res) { - const { dir, dev, hotReloader } = this - const mfs = hotReloader ? hotReloader.fileSystem : null + const { dir, dev } = this let html try { - html = await render(req.url, { req, res }, { dir, dev, mfs }) + html = await render(req.url, { req, res }, { dir, dev }) } catch (err) { if ('ENOENT' === err.code) { res.statusCode = 404 @@ -75,18 +73,17 @@ export default class Server { console.error(err) res.statusCode = 500 } - html = await render('/_error', { req, res, err }, { dir, dev, mfs }) + html = await render('/_error', { req, res, err }, { dir, dev }) } sendHTML(res, html) } async renderJSON (req, res) { - const { dir, hotReloader } = this - const mfs = hotReloader ? hotReloader.fileSystem : null + const { dir } = this let json try { - json = await renderJSON(req.url, { dir, mfs }) + json = await renderJSON(req.url, { dir }) } catch (err) { if ('ENOENT' === err.code) { res.statusCode = 404 @@ -94,7 +91,7 @@ export default class Server { console.error(err) res.statusCode = 500 } - json = await renderJSON('/_error.json', { dir, mfs }) + json = await renderJSON('/_error.json', { dir }) } const data = JSON.stringify(json) diff --git a/server/read.js b/server/read.js index 3aa595d9..4afc24aa 100644 --- a/server/read.js +++ b/server/read.js @@ -1,42 +1,23 @@ import fs from 'mz/fs' import resolve from './resolve' -const cache = {} - /** * resolve a file like `require.resolve`, * and read and cache the file content */ -async function read (path, { mfs }) { - const f = await (mfs ? resolveFromMFS(path, mfs) : resolve(path)) - if (mfs) { - return mfs.readFileSync(f, 'utf8') - } else { - let promise = cache[f] - if (!promise) { - promise = cache[f] = fs.readFile(f, 'utf8') - } - return promise +async function read (path, opts = {}) { + const f = await resolve(path) + if (cache.hasOwnProperty(f)) { + return cache[f] } + + const data = fs.readFile(f, 'utf8') + cache[f] = data + return data } -function resolveFromMFS (path, mfs) { - const isFile = (file, cb) => { - if (!mfs.existsSync(file)) return cb(null, false) +export default read +export const cache = {} - let stat - try { - stat = mfs.statSync(file) - } catch (err) { - return cb(err) - } - cb(null, stat.isFile() || stat.isFIFO()) - } - const readFile = mfs.readFile.bind(mfs) - return resolve(path, { isFile, readFile }) -} - -module.exports = read - -exports.cache = cache +read.cache = cache diff --git a/server/render.js b/server/render.js index 453cb3bd..a93c6992 100644 --- a/server/render.js +++ b/server/render.js @@ -2,8 +2,7 @@ import { relative, resolve } from 'path' import { parse } from 'url' import { createElement } from 'react' import { renderToString, renderToStaticMarkup } from 'react-dom/server' -import fs from 'mz/fs' -import requireResolve from './resolve' +import requireModule from './require' import read from './read' import Router from '../lib/router' import Document from '../lib/document' @@ -14,16 +13,14 @@ import { StyleSheetServer } from '../lib/css' export async function render (url, ctx = {}, { dir = process.cwd(), dev = false, - staticMarkup = false, - mfs + 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) : {}) - const component = await read(resolve(dir, '.next', '_bundles', 'pages', path), { mfs }) + const component = await read(resolve(dir, '.next', '_bundles', 'pages', path)) const { html, css } = StyleSheetServer.renderStatic(() => { const app = createElement(App, { @@ -54,9 +51,9 @@ export async function render (url, ctx = {}, { return '' + renderToStaticMarkup(doc) } -export async function renderJSON (url, { dir = process.cwd(), mfs } = {}) { +export async function renderJSON (url, { dir = process.cwd() } = {}) { const path = getPath(url) - const component = await read(resolve(dir, '.next', '_bundles', 'pages', path), { mfs }) + const component = await read(resolve(dir, '.next', '_bundles', 'pages', path)) return { component } } diff --git a/server/require.js b/server/require.js new file mode 100644 index 00000000..3ca13f32 --- /dev/null +++ b/server/require.js @@ -0,0 +1,6 @@ +import resolve from './resolve' + +export default async function requireModule (path) { + const f = await resolve(path) + return require(f) +} From 3bc8fdecb3541727b04e31d084a6e634196693f0 Mon Sep 17 00:00:00 2001 From: nkzawa Date: Sun, 16 Oct 2016 11:49:09 +0900 Subject: [PATCH 4/8] always use write-file-webpack-plugin --- server/build/webpack.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/build/webpack.js b/server/build/webpack.js index 0b600014..7d3e70b2 100644 --- a/server/build/webpack.js +++ b/server/build/webpack.js @@ -21,14 +21,14 @@ export default async function createCompiler(dir, { hotReload = false } = {}) { const nodeModulesDir = resolve(__dirname, '..', '..', '..', 'node_modules') - const plugins = hotReload ? [ - new webpack.HotModuleReplacementPlugin(), - new WriteFilePlugin({ log: false }) - ] : [ - new webpack.optimize.UglifyJsPlugin({ + 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') From 82f23e2aca3b0cb36edaed3fd3b2ec1b5f577df8 Mon Sep 17 00:00:00 2001 From: nkzawa Date: Sun, 16 Oct 2016 13:01:17 +0900 Subject: [PATCH 5/8] fix generating _error.js --- server/build/webpack.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/server/build/webpack.js b/server/build/webpack.js index 7d3e70b2..3d688177 100644 --- a/server/build/webpack.js +++ b/server/build/webpack.js @@ -15,9 +15,8 @@ export default async function createCompiler(dir, { hotReload = false } = {}) { } const errEntry = join('_bundles', 'pages', '_error.js') - if (!entry[errEntry]) { - entry[errEntry] = resolve(__dirname, '..', '..', 'pages', '_error.js') - } + const defaultErrorPath = resolve(__dirname, '..', '..', 'pages', '_error.js') + if (!entry[errEntry]) entry[errEntry] = defaultErrorPath const nodeModulesDir = resolve(__dirname, '..', '..', '..', 'node_modules') @@ -37,7 +36,10 @@ export default async function createCompiler(dir, { hotReload = false } = {}) { const loaders = [{ test: /\.js$/, loader: 'emit-file-loader', - include: dir, + include: [ + dir, + resolve(__dirname, '..', '..', 'pages') + ], exclude: /node_modules/, query: { name: '[path][name].[ext]' @@ -113,6 +115,12 @@ export default async function createCompiler(dir, { hotReload = false } = {}) { { test: /\.json$/, loader: 'json-loader' } ], loaders + }, + customInterpolateName: function (url, name, opts) { + if (defaultErrorPath === this.resourcePath) { + return 'pages/_error.js' + } + return url } }) } From 577e2c27a76712de0ef585d3965d53a6731a9fb3 Mon Sep 17 00:00:00 2001 From: nkzawa Date: Mon, 17 Oct 2016 10:54:36 +0900 Subject: [PATCH 6/8] use webpack for transpiling too --- server/build/bundle.js | 53 --------------------------------------- server/build/index.js | 35 +++++++++++--------------- server/build/transpile.js | 51 ------------------------------------- 3 files changed, 15 insertions(+), 124 deletions(-) delete mode 100644 server/build/bundle.js delete mode 100644 server/build/transpile.js diff --git a/server/build/bundle.js b/server/build/bundle.js deleted file mode 100644 index b8816f37..00000000 --- a/server/build/bundle.js +++ /dev/null @@ -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() - }) - }) -} diff --git a/server/build/index.js b/server/build/index.js index 7e94e35a..f8aff5a5 100644 --- a/server/build/index.js +++ b/server/build/index.js @@ -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, '..', '..', 'pages') + const compiler = await webpack(dir) - // create `.next/pages/_error.js` - // which may be overwriten by the user sciprt, `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() + }) + }) } diff --git a/server/build/transpile.js b/server/build/transpile.js deleted file mode 100644 index a7d464cd..00000000 --- a/server/build/transpile.js +++ /dev/null @@ -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+' }) -} From 9b585e91d8692f2961d4dde698e28084ceffa1ce Mon Sep 17 00:00:00 2001 From: nkzawa Date: Mon, 17 Oct 2016 10:57:51 +0900 Subject: [PATCH 7/8] move loaders --- server/{ => build}/loaders/emit-file-loader.js | 0 server/{ => build}/loaders/hot-self-accept-loader.js | 0 server/build/webpack.js | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename server/{ => build}/loaders/emit-file-loader.js (100%) rename server/{ => build}/loaders/hot-self-accept-loader.js (100%) diff --git a/server/loaders/emit-file-loader.js b/server/build/loaders/emit-file-loader.js similarity index 100% rename from server/loaders/emit-file-loader.js rename to server/build/loaders/emit-file-loader.js diff --git a/server/loaders/hot-self-accept-loader.js b/server/build/loaders/hot-self-accept-loader.js similarity index 100% rename from server/loaders/hot-self-accept-loader.js rename to server/build/loaders/hot-self-accept-loader.js diff --git a/server/build/webpack.js b/server/build/webpack.js index 3d688177..0cfce0b0 100644 --- a/server/build/webpack.js +++ b/server/build/webpack.js @@ -106,7 +106,7 @@ export default async function createCompiler(dir, { hotReload = false } = {}) { resolveLoader: { root: [ nodeModulesDir, - resolve(__dirname, '..', 'loaders') + resolve(__dirname, 'loaders') ] }, plugins, From c697618e1141e40bcd61fb82e38490a274664c62 Mon Sep 17 00:00:00 2001 From: nkzawa Date: Mon, 17 Oct 2016 11:19:42 +0900 Subject: [PATCH 8/8] server/read: remove unused argument --- server/read.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/read.js b/server/read.js index 4afc24aa..d39d1fdd 100644 --- a/server/read.js +++ b/server/read.js @@ -6,7 +6,7 @@ import resolve from './resolve' * and read and cache the file content */ -async function read (path, opts = {}) { +async function read (path) { const f = await resolve(path) if (cache.hasOwnProperty(f)) { return cache[f]