many improvements

This commit is contained in:
Pooya Parsa 2017-06-16 02:49:53 +04:30
parent d68b4f0c00
commit 5722a92c4c
10 changed files with 268 additions and 182 deletions

View File

@ -87,12 +87,10 @@ function listenOnConfigChanges(nuxt, server) {
options.rootDir = rootDir
nuxt.close()
.then(() => {
nuxt.renderer = null
debug('Rebuilding the app...')
return new Nuxt(options).build()
})
.then((nuxt) => {
var nuxt = new Nuxt(options)
server.nuxt = nuxt
return nuxt.ready()
})
.catch((error) => {
console.error('Error while rebuild the app:', error) // eslint-disable-line no-console

View File

@ -7,11 +7,11 @@ import webpack from 'webpack'
import serialize from 'serialize-javascript'
import { join, resolve, basename, dirname } from 'path'
import Tapable from 'tappable'
import chalk from 'chalk'
import MFS from 'memory-fs'
import { sequence, parallel } from "./utils"
import { r, wp, createRoutes } from './utils'
import clientWebpackConfig from './webpack/client.config.js'
import serverWebpackConfig from './webpack/server.config.js'
import MFS from 'memory-fs'
const debug = require('debug')('nuxt:build')
debug.color = 2 // Force green color
@ -23,9 +23,6 @@ const writeFile = pify(fs.writeFile)
const mkdirp = pify(fs.mkdirp)
const glob = pify(require('glob'))
const host = process.env.HOST || process.env.npm_package_config_nuxt_host || 'localhost'
const port = process.env.PORT || process.env.npm_package_config_nuxt_port || '3000'
export default class Builder extends Tapable {
constructor (nuxt) {
super()
@ -50,7 +47,7 @@ export default class Builder extends Tapable {
_.defaultsDeep(this.options.build, extraDefaults)
// Mute stats on dev
this.webpackStats = this.options.dev ? '' : {
this.webpackStats = this.options.dev ? false : {
chunks: false,
children: false,
modules: false,
@ -220,7 +217,7 @@ export default class Builder extends Tapable {
}))
// Interpret and move template files to .nuxt/
return Promise.all(templatesFiles.map(async ({ src, dst, options, custom }) => {
await Promise.all(templatesFiles.map(async ({ src, dst, options, custom }) => {
// Add template to watchers
this.options.build.watch.push(src)
// Render template to dst
@ -245,7 +242,7 @@ export default class Builder extends Tapable {
// Write file
await writeFile(path, content, 'utf8')
// Fix webpack loop (https://github.com/webpack/watchpack/issues/25#issuecomment-287789288)
const dateFS = Date.now() / 1000 - 30
const dateFS = Date.now() / 1000 - 1000
return utimes(path, dateFS, dateFS)
}))
}
@ -256,16 +253,23 @@ export default class Builder extends Tapable {
// Client
let clientConfig = clientWebpackConfig.call(this)
clientConfig.name = '$client'
compilersOptions.push(clientConfig)
// Server
let serverConfig = serverWebpackConfig.call(this)
serverConfig.name = '$server'
compilersOptions.push(serverConfig)
// Leverage webpack multi-compiler for faster builds
this.compiler = webpack(compilersOptions)
// Simulate webpack multi compiler interface
// Separate compilers are simpler, safer and faster
this.compiler = { cache: {}, compilers: [] }
compilersOptions.forEach(compilersOption => {
this.compiler.compilers.push(webpack(compilersOption))
})
this.compiler.plugin = (...args) => {
this.compiler.compilers.forEach(compiler => {
compiler.plugin(...args)
})
}
// Access to compilers with name
this.compiler.compilers.forEach(compiler => {
@ -274,71 +278,84 @@ export default class Builder extends Tapable {
}
})
// Add middleware for dev
// Add dev Stuff
if (this.options.dev) {
this.webpackDev()
}
// Start build
return new Promise((resolve, reject) => {
const handler = (err, multiStats) => {
// Start Builds
return parallel(this.compiler.compilers, compiler => new Promise((resolve, reject) => {
let _resolved = false
const handler = (err, stats) => {
if (_resolved) return
_resolved = true
if (err) {
return reject(err)
}
for (let _stats of multiStats.stats) {
console.log(_stats.toString(this.webpackStats)) // eslint-disable-line no-console
if (_stats.hasErrors()) {
if (!this.options.dev) {
// Show build stats for production
console.log(stats.toString(this.webpackStats)) // eslint-disable-line no-console
if (stats.hasErrors()) {
return reject(new Error('Webpack build exited with errors'))
}
}
// Use watch handler instead of compiler.apply('done') to prevent duplicate emits
this.applyPlugins('reload', multiStats)
resolve()
}
if (this.options.dev) {
this.compiler.watch(this.options.watchers.webpack, handler)
if (compiler.options.name === 'client') {
// Client watch is started by dev-middleware
resolve()
} else {
// Build and watch for changes
compiler.watch(this.options.watchers.webpack, handler)
}
} else {
this.compiler.run(handler)
// Production build
compiler.run(handler)
}
})
}))
}
webpackDev () {
debug('Adding webpack middleware...')
// Use MFS for faster builds
// Use shared MFS + Cache for faster builds
let mfs = new MFS()
this.compiler.compilers.forEach(compiler => {
compiler.outputFileSystem = mfs
compiler.cache = this.compiler.cache
})
// Watch
this.plugin('reload', () => {
// Show open URL
let _host = host === '0.0.0.0' ? 'localhost' : host
// eslint-disable-next-line no-console
console.log(chalk.bold(chalk.bgCyan.black(' OPEN ') + chalk.cyan(` http://${_host}:${port}\n`)))
// Run after each compile
this.compiler.plugin('done', stats => {
console.log(stats.toString(this.webpackStats)) // eslint-disable-line no-console
// Reload renderer if available
if (this.nuxt.renderer) {
this.nuxt.renderer.loadResources(mfs)
}
})
// Create webpack Dev/Hot middleware
this.webpackDevMiddleware = pify(require('webpack-dev-middleware')(this.compiler.$client, {
// Add dev Middleware
debug('Adding webpack middleware...')
// Create webpack dev middleware
this.webpackDevMiddleware = pify(require('webpack-dev-middleware')(this.compiler.client, {
publicPath: this.options.build.publicPath,
stats: this.webpackStats,
quiet: true,
noInfo: true,
quiet: true,
watchOptions: this.options.watchers.webpack
}))
this.webpackHotMiddleware = pify(require('webpack-hot-middleware')(this.compiler.$client, {
this.webpackHotMiddleware = pify(require('webpack-hot-middleware')(this.compiler.client, {
log: false,
heartbeat: 2500
}))
// Stop webpack middleware on nuxt.close()
this.nuxt.plugin('close', () => new Promise(resolve => {
this.webpackDevMiddleware.close(() => resolve())
}))
// Start watching files
this.watchFiles()
}
@ -360,13 +377,21 @@ export default class Builder extends Tapable {
})
/* istanbul ignore next */
const refreshFiles = _.debounce(this.generateRoutesAndFiles, 200)
// Watch for internals
this.filesWatcher = chokidar.watch(patterns, options)
// Watch for src Files
let filesWatcher = chokidar.watch(patterns, options)
.on('add', refreshFiles)
.on('unlink', refreshFiles)
// Watch for custom provided files
this.customFilesWatcher = chokidar.watch(_.uniq(this.options.build.watch), options)
let customFilesWatcher = chokidar.watch(_.uniq(this.options.build.watch), options)
.on('change', refreshFiles)
// Stop watching on nuxt.close()
this.nuxt.plugin('close', () => {
filesWatcher.close()
customFilesWatcher.close()
})
}
}

View File

@ -1,11 +1,14 @@
import Tapable from 'tappable'
import Builder from './builder'
import chalk from 'chalk'
import * as Utils from './utils'
import Renderer from './renderer'
import ModuleContainer from './module-container'
import Server from './server'
import defaults from './defaults'
const defaultHost = process.env.HOST || process.env.npm_package_config_nuxt_host || 'localhost'
const defaultPort = process.env.PORT || process.env.npm_package_config_nuxt_port || '3000'
export default class Nuxt extends Tapable {
constructor (_options = {}) {
super()
@ -57,7 +60,7 @@ export default class Nuxt extends Tapable {
if (this._builder) {
return this._builder
}
// const Builder = require('./builder').default
const Builder = require('./builder').default
this._builder = new Builder(this)
return this._builder
}
@ -90,36 +93,28 @@ export default class Nuxt extends Tapable {
process.exit(1)
}
serverReady ({ host = defaultHost, port = defaultPort } = {}) {
let _host = host === '0.0.0.0' ? 'localhost' : host
// eslint-disable-next-line no-console
console.log('\n' + chalk.bold(chalk.bgBlue.black(' OPEN ') + chalk.blue(` http://${_host}:${port}\n`)))
return this.applyPluginsAsync('serverReady').catch(this.errorHandler)
}
async close (callback) {
let promises = []
/* istanbul ignore if */
if (this.webpackDevMiddleware) {
const p = new Promise((resolve, reject) => {
this.webpackDevMiddleware.close(() => resolve())
})
promises.push(p)
}
/* istanbul ignore if */
if (this.webpackServerWatcher) {
const p = new Promise((resolve, reject) => {
this.webpackServerWatcher.close(() => resolve())
})
promises.push(p)
}
/* istanbul ignore if */
if (this.filesWatcher) {
this.filesWatcher.close()
}
/* istanbul ignore if */
if (this.customFilesWatcher) {
this.customFilesWatcher.close()
}
// Call for close
await this.applyPluginsAsync('close')
promises.push(this.applyPluginsAsync('close'))
// Remove all references
delete this._generator
delete this._builder
return Promise.all(promises).then(() => {
if (typeof callback === 'function') callback()
})
this.initialized = false
if (typeof callback === 'function') {
callback()
}
}
}

View File

@ -10,6 +10,7 @@ import _ from 'lodash'
import { resolve, join } from 'path'
import fs from 'fs-extra'
import { createBundleRenderer } from 'vue-server-renderer'
import chalk from 'chalk'
import { getContext, setAnsiColors, encodeHtml } from './utils'
const debug = require('debug')('nuxt:render')
@ -39,6 +40,7 @@ export default class Renderer extends Tapable {
errorTemplate: parseTemplate(fs.readFileSync(resolve(__dirname, 'views', 'error.html'), 'utf8'))
}
// Initialize
if (nuxt.initialized) {
// If nuxt already initialized
this._ready = this.ready().catch(this.nuxt.errorHandler)
@ -71,16 +73,18 @@ export default class Renderer extends Tapable {
// Load resources from fs
if (!this.options.dev) {
return this.loadResources()
await this.loadResources()
}
return this
}
async loadResources (_fs = fs, distPath) {
distPath = distPath || resolve(this.options.buildDir, 'dist')
async loadResources (_fs = fs, isServer) {
let distPath = resolve(this.options.buildDir, 'dist')
const resourceMap = {
clientManifest: {
path: join(distPath, 'client-manifest.json'),
path: join(distPath, 'vue-ssr-client-manifest.json'),
transform: JSON.parse
},
serverBundle: {
@ -116,7 +120,7 @@ export default class Renderer extends Tapable {
})
if (updated.length > 0) {
// debug('Updated', updated.join(', '))
// debug('Updated', updated.join(', '), isServer)
this.createRenderer()
}
}
@ -136,6 +140,8 @@ export default class Renderer extends Tapable {
// Promisify renderToString
this.bundleRenderer.renderToString = pify(this.bundleRenderer.renderToString)
this.nuxt.serverReady()
}
async render (req, res) {

View File

@ -8,17 +8,33 @@ class Server {
this.options = nuxt.options
// Initialize
if (nuxt.initialized) {
// If nuxt already initialized
this._ready = this.ready().catch(this.nuxt.errorHandler)
} else {
// Wait for hook
this.nuxt.plugin('afterInit', () => {
this._ready = this.ready()
return this._ready
})
}
}
async ready() {
if (this._ready) {
return this._ready
}
this.app = connect()
this.server = http.createServer(this.app)
this.nuxt.ready()
.then(() => {
// Add Middleware
this.options.serverMiddleware.forEach(m => {
this.useMiddleware(m)
})
// Add default render middleware
this.useMiddleware(this.render.bind(this))
})
// Add Middleware
this.options.serverMiddleware.forEach(m => {
this.useMiddleware(m)
})
// Add default render middleware
this.useMiddleware(this.render.bind(this))
return this
}
@ -49,12 +65,13 @@ class Server {
host = host || 'localhost'
port = port || 3000
this.nuxt.ready()
.then(() => {
this.server.listen(port, host, () => {
let _host = host === '0.0.0.0' ? 'localhost' : host
console.log('Ready on http://%s:%s', _host, port) // eslint-disable-line no-console
.then(() => {
this.server.listen(port, host, () => {
// Renderer calls showURL when server is really ready
// this.nuxt.showURL(host, port)
})
})
})
.catch(this.nuxt.errorHandler)
return this
}

View File

@ -63,6 +63,10 @@ export function sequence (tasks, fn) {
return tasks.reduce((promise, task) => promise.then(() => fn(task)), Promise.resolve())
}
export function parallel (tasks, fn) {
return Promise.all(tasks.map(task => fn(task)))
}
export function chainFn (base, fn) {
/* istanbul ignore if */
if (!(fn instanceof Function)) {

View File

@ -1,6 +1,8 @@
import ExtractTextPlugin from 'extract-text-webpack-plugin'
import { defaults } from 'lodash'
import { join } from 'path'
import { join, resolve, } from 'path'
import webpack from 'webpack'
import { cloneDeep } from 'lodash'
import { isUrl, urlJoin } from '../utils'
import vueLoaderConfig from './vue-loader.config'
import { styleLoader, extractStyles } from './helpers'
@ -15,12 +17,15 @@ import { styleLoader, extractStyles } from './helpers'
*/
export default function webpackBaseConfig ({ isClient, isServer }) {
const nodeModulesDir = join(__dirname, '..', 'node_modules')
let config = {
devtool: (this.options.dev ? 'cheap-module-source-map' : false),
const config = {
devtool: this.options.dev ? 'cheap-module-source-map' : false,
entry: {
vendor: ['vue', 'vue-router', 'vue-meta']
},
output: {
path: resolve(this.options.buildDir, 'dist'),
filename: this.options.build.filenames.app,
publicPath: (isUrl(this.options.build.publicPath)
? this.options.build.publicPath
: urlJoin(this.options.router.base, this.options.build.publicPath))
@ -28,7 +33,7 @@ export default function webpackBaseConfig ({ isClient, isServer }) {
performance: {
maxEntrypointSize: 300000,
maxAssetSize: 300000,
hints: (this.options.dev ? false : 'warning')
hints: this.options.dev ? false : 'warning'
},
resolve: {
extensions: ['.js', '.json', '.vue', '.ts'],
@ -57,6 +62,7 @@ export default function webpackBaseConfig ({ isClient, isServer }) {
]
},
module: {
noParse: /es6-promise\.js$/, // avoid webpack shimming process
rules: [
{
test: /\.vue$/,
@ -98,12 +104,33 @@ export default function webpackBaseConfig ({ isClient, isServer }) {
},
plugins: this.options.build.plugins
}
// CSS extraction
if (extractStyles.call(this)) {
config.plugins.push(
new ExtractTextPlugin({ filename: this.options.build.filenames.css })
)
}
// Return config
return config
// --------------------------------------
// Dev specific config
// --------------------------------------
if (this.options.dev) {
//
}
// --------------------------------------
// Production specific config
// --------------------------------------
if (!this.options.dev) {
// This is needed in webpack 2 for minify CSS
config.plugins.push(
new webpack.LoaderOptionsPlugin({
minimize: true
})
)
}
// Clone deep avoid leaking config between Client and Server
return cloneDeep(config)
}

View File

@ -22,89 +22,108 @@ import base from './base.config.js'
export default function webpackClientConfig () {
let config = base.call(this, { isClient: true })
config.name = 'client'
// Entry
config.entry.app = resolve(this.options.buildDir, 'client.js')
// Add vendors
if (this.options.store) {
config.entry.vendor.push('vuex')
if (!this.options.dev) {
if (this.options.store) {
config.entry.vendor.push('vuex')
}
config.entry.vendor = config.entry.vendor.concat(this.options.build.vendor)
// Extract vendor chunks for better caching
config.plugins.push(
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: this.options.build.filenames.vendor,
minChunks (module) {
// A module is extracted into the vendor chunk when...
return (
// If it's inside node_modules
/node_modules/.test(module.context) &&
// Do not externalize if the request is a CSS file
!/\.(css|less|scss|sass|styl|stylus)$/.test(module.request)
)
}
})
)
}
config.entry.vendor = config.entry.vendor.concat(this.options.build.vendor)
// Output
config.output.path = resolve(this.options.buildDir, 'dist')
config.output.filename = this.options.build.filenames.app
// env object defined in nuxt.config.js
// Env object defined in nuxt.config.js
let env = {}
each(this.options.env, (value, key) => {
env['process.env.' + key] = (typeof value === 'string' ? JSON.stringify(value) : value)
})
// Webpack plugins
config.plugins = (config.plugins || []).concat([
// Strip comments in Vue code
new webpack.DefinePlugin(Object.assign(env, {
'process.env.NODE_ENV': JSON.stringify(env.NODE_ENV || (this.options.dev ? 'development' : 'production')),
'process.BROWSER_BUILD': true,
'process.SERVER_BUILD': false,
'process.browser': true,
'process.server': true
})),
// Extract vendor chunks for better caching
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: this.options.build.filenames.vendor,
minChunks (module) {
// A module is extracted into the vendor chunk when...
return (
// If it's inside node_modules
/node_modules/.test(module.context) &&
// Do not externalize if the request is a CSS file
!/\.(css|less|scss|sass|styl|stylus)$/.test(module.request)
)
}
}),
// Extract webpack runtime & manifest
// Webpack common plugins
if (!Array.isArray(config.plugins)) {
config.plugins = []
}
// Generate output HTML
config.plugins.push(
new HTMLPlugin({
template: this.options.appTemplatePath,
inject: false
})
)
// Generate vue-ssr-client-manifest
config.plugins.push(
new VueSSRClientPlugin({
filename: 'vue-ssr-client-manifest.json'
})
)
// Extract webpack runtime & manifest
config.plugins.push(
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity,
filename: this.options.build.filenames.manifest
}),
// Generate output HTML
new HTMLPlugin({
template: this.options.appTemplatePath,
inject: false // <- Resources will be injected using vue server renderer
}),
// Generate client manifest json
new VueSSRClientPlugin({
filename: 'client-manifest.json'
})
])
// client bundle progress bar
)
// Define Env
config.plugins.push(
new webpack.DefinePlugin(Object.assign(env, {
'process.env.NODE_ENV': JSON.stringify(env.NODE_ENV || (this.options.dev ? 'development' : 'production')),
'process.env.VUE_ENV': JSON.stringify('client'),
'process.browser': true,
'process.server': false
}))
)
// Build progress bar
config.plugins.push(
new ProgressBarPlugin()
)
// Add friendly error plugin
// --------------------------------------
// Dev specific config
// --------------------------------------
if (this.options.dev) {
// Add friendly error plugin
config.plugins.push(new FriendlyErrorsWebpackPlugin())
}
// Dev client build
if (this.options.dev) {
// Add HMR support
config.entry.app = flatten(['webpack-hot-middleware/client?name=$client&reload=true', config.entry.app])
config.entry.app = ['webpack-hot-middleware/client?name=$client&reload=true', config.entry.app]
config.output.filename = '[name].js'
config.plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
)
}
// Production client build
// --------------------------------------
// Production specific config
// --------------------------------------
if (!this.options.dev) {
// Minify JS
config.plugins.push(
// This is needed in webpack 2 for minifying CSS
new webpack.LoaderOptionsPlugin({
minimize: true
}),
// Minify JS
new webpack.optimize.UglifyJsPlugin({
sourceMap: true,
compress: {
@ -112,7 +131,15 @@ export default function webpackClientConfig () {
}
})
)
// Webpack Bundle Analyzer
if (this.options.build.analyze) {
config.plugins.push(
new BundleAnalyzerPlugin(Object.assign({}, this.options.build.analyze))
)
}
}
// Extend config
if (typeof this.options.build.extend === 'function') {
this.options.build.extend.call(this, config, {
@ -120,22 +147,6 @@ export default function webpackClientConfig () {
isClient: true
})
}
// Offline-plugin integration
if (!this.options.dev && this.options.offline) {
const offlineOpts = typeof this.options.offline === 'object' ? this.options.offline : {}
config.plugins.push(
new OfflinePlugin(defaults(offlineOpts, {}))
)
}
// Webpack Bundle Analyzer
if (!this.options.dev && this.options.build.analyze) {
let options = {}
if (typeof this.options.build.analyze === 'object') {
options = this.options.build.analyze
}
config.plugins.push(
new BundleAnalyzerPlugin(options)
)
}
return config
}

View File

@ -13,6 +13,8 @@ import base from './base.config.js'
export default function webpackServerConfig () {
let config = base.call(this, { isServer: true })
config.name = 'server'
// env object defined in nuxt.config.js
let env = {}
each(this.options.env, (value, key) => {
@ -24,7 +26,6 @@ export default function webpackServerConfig () {
devtool: (this.options.dev ? 'source-map' : false),
entry: resolve(this.options.buildDir, 'server.js'),
output: Object.assign({}, config.output, {
path: resolve(this.options.buildDir, 'dist'),
filename: 'server-bundle.js',
libraryTarget: 'commonjs2'
}),
@ -32,6 +33,8 @@ export default function webpackServerConfig () {
hints: false
},
externals: [
// https://webpack.js.org/configuration/externals/#externals
// https://github.com/liady/webpack-node-externals
nodeExternals({
// load non-javascript files with extensions, presumably via loaders
whitelist: [/\.(?!(?:js|json)$).{1,5}$/i]
@ -42,21 +45,19 @@ export default function webpackServerConfig () {
filename: 'server-bundle.json'
}),
new webpack.DefinePlugin(Object.assign(env, {
'process.env.NODE_ENV': JSON.stringify(this.options.dev ? 'development' : 'production'),
'process.BROWSER_BUILD': false, // deprecated
'process.SERVER_BUILD': true, // deprecated
'process.env.NODE_ENV': JSON.stringify(env.NODE_ENV || (this.options.dev ? 'development' : 'production')),
'process.env.VUE_ENV': JSON.stringify('server'),
'process.browser': false,
'process.server': true
}))
])
})
// This is needed in webpack 2 for minifying CSS
// --------------------------------------
// Production specific config
// --------------------------------------
if (!this.options.dev) {
config.plugins.push(
new webpack.LoaderOptionsPlugin({
minimize: true
})
)
}
// Extend config
@ -66,5 +67,6 @@ export default function webpackServerConfig () {
isServer: true
})
}
return config
}

View File

@ -9,7 +9,7 @@ export default function ({ isClient }) {
}))
// https://github.com/vuejs/vue-loader/blob/master/docs/en/configurations
let config = {
const config = {
postcss: this.options.build.postcss,
loaders: {
'js': 'babel-loader?' + babelOptions,
@ -23,6 +23,7 @@ export default function ({ isClient }) {
preserveWhitespace: false,
extractCSS: extractStyles.call(this)
}
// Return the config
return config
}