decouple builder from renderer + improvements

This commit is contained in:
Pooya Parsa 2017-06-14 20:43:43 +04:30
parent b61694ca21
commit 42bf9bb41d
13 changed files with 473 additions and 396 deletions

View File

@ -64,10 +64,10 @@ if (typeof options.rootDir !== 'string') {
// Force development mode: add hot reloading and watching changes
options.dev = true
var nuxt = module.exports = new Nuxt(options)
var nuxt = new Nuxt(options)
var port = argv.port || process.env.PORT || process.env.npm_package_config_nuxt_port
var host = argv.hostname || process.env.HOST || process.env.npm_package_config_nuxt_host
var server = nuxt.server = new nuxt.Server(nuxt).listen(port, host)
var server = new Nuxt.Server(nuxt).listen(port, host)
listenOnConfigChanges(nuxt, server)
@ -102,3 +102,5 @@ function listenOnConfigChanges(nuxt, server) {
chokidar.watch(nuxtConfigFile, Object.assign({}, nuxt.options.watchers.chokidar, { ignoreInitial: true }))
.on('all', build)
}
module.exports = nuxt

View File

@ -55,7 +55,9 @@ if (typeof options.rootDir !== 'string') {
}
options.dev = false // Force production mode (no webpack middleware called)
var nuxt = module.exports = new Nuxt(options)
var nuxt = new Nuxt(options)
var port = argv.port || process.env.PORT || process.env.npm_package_config_nuxt_port
var host = argv.hostname || process.env.HOST || process.env.npm_package_config_nuxt_host
module.exports = nuxt.server = new nuxt.Server(nuxt).listen(port, host)
new Nuxt.Server(nuxt).listen(port, host)
module.exports = nuxt

View File

@ -4,15 +4,16 @@ import fs from 'fs-extra'
import hash from 'hash-sum'
import pify from 'pify'
import webpack from 'webpack'
import PostCompilePlugin from 'post-compile-webpack-plugin'
import serialize from 'serialize-javascript'
import { createBundleRenderer } from 'vue-server-renderer'
import webpackDevMiddleware from 'webpack-dev-middleware'
import webpackHotMiddleware from 'webpack-hot-middleware'
import { join, resolve, basename, dirname } from 'path'
import Tapable from 'tappable'
import { isUrl, r, wp } from './utils'
import { isUrl, r, wp, createRoutes } from './utils'
import clientWebpackConfig from './webpack/client.config.js'
import serverWebpackConfig from './webpack/server.config.js'
import defaults from './defaults'
import MFS from 'memory-fs'
const debug = require('debug')('nuxt:build')
debug.color = 2 // Force green color
@ -24,12 +25,39 @@ 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'
debug('loaded')
export default class Builder extends Tapable {
constructor (nuxt) {
super()
this.nuxt = nuxt
this.options = nuxt.options
this._buildStatus = STATUS.INITIAL
this.initialized = false
// Fields that set on build
this.compiler = null
this.webpackDevMiddleware = null
this.webpackHotMiddleware = null
if (nuxt.initialized) {
// If nuxt already initialized
this._init = this.init().catch(this.nuxt.errorHandler)
} else {
// Wait for hook
this.nuxt.plugin('init', this.init.bind(this))
}
}
async init () {
if (this._init) {
return this._init
}
// Add extra loaders only if they are not already provided
let extraDefaults = {}
if (this.options.build && !Array.isArray(this.options.build.loaders)) {
@ -38,37 +66,25 @@ export default class Builder extends Tapable {
if (this.options.build && !Array.isArray(this.options.build.postcss)) {
extraDefaults.postcss = defaultsPostcss
}
this.options.build = _.defaultsDeep(this.options.build, extraDefaults)
_.defaultsDeep(this.options.build, extraDefaults)
/* istanbul ignore if */
if (this.options.dev && isUrl(this.options.build.publicPath)) {
this.options.build.publicPath = defaults.build.publicPath
}
// Stats
this.webpackStats = {
// If store defined, update store options to true unless explicitly disabled
if (this.options.store !== false && fs.existsSync(join(this.options.srcDir, 'store'))) {
this.options.store = true
}
// Mute stats on dev
this.webpackStats = this.options.dev ? '' : {
chunks: false,
children: false,
modules: false,
colors: true
}
// Register lifecycle hooks
if (this.nuxt.options.dev) {
// Don't await for build on dev (faster startup)
this.nuxt.plugin('afterInit', () => {
this.build().catch(this.nuxt.errorHandler)
})
} else {
this.nuxt.plugin('init', () => {
// Guess it is build or production
// If build is not called it may be nuxt.start
if (this._buildStatus === STATUS.INITIAL) {
return this.production()
}
})
}
this._buildStatus = STATUS.INITIAL
}
async build () {
@ -76,7 +92,6 @@ export default class Builder extends Tapable {
if (this._buildStatus === STATUS.BUILD_DONE) {
return this
}
// If building
if (this._buildStatus === STATUS.BUILDING) {
return new Promise((resolve) => {
@ -92,7 +107,6 @@ export default class Builder extends Tapable {
// Check if pages dir exists and warn if not
this._nuxtPages = typeof this.options.build.createRoutes !== 'function'
if (this._nuxtPages) {
if (!fs.existsSync(join(this.options.srcDir, 'pages'))) {
let dir = this.options.srcDir
@ -113,39 +127,19 @@ export default class Builder extends Tapable {
if (!this.options.dev) {
await mkdirp(r(this.options.buildDir, 'dist'))
}
// Generate routes and interpret the template files
await this.generateRoutesAndFiles()
// Generate .nuxt/dist/ files
await this.buildFiles()
// Start webpack build
await this.webpackBuild()
// Flag to set that building is done
this._buildStatus = STATUS.BUILD_DONE
return this
}
async production () {
// Production, create server-renderer
const serverConfig = this.getWebpackServerConfig()
const bundlePath = join(serverConfig.output.path, 'server-bundle.json')
const manifestPath = join(serverConfig.output.path, 'client-manifest.json')
if (!fs.existsSync(bundlePath) || !fs.existsSync(manifestPath)) {
throw new Error(`No build files found in ${serverConfig.output.path}, please run \`nuxt build\` before launching \`nuxt start\``)
}
const bundle = fs.readFileSync(bundlePath, 'utf8')
const manifest = fs.readFileSync(manifestPath, 'utf8')
this.createRenderer(JSON.parse(bundle), JSON.parse(manifest))
this.addAppTemplate()
return this
}
addAppTemplate () {
let templatePath = resolve(this.options.buildDir, 'dist', 'index.html')
if (fs.existsSync(templatePath)) {
this.appTemplate = _.template(fs.readFileSync(templatePath, 'utf8'), {
interpolate: /{{([\s\S]+?)}}/g
})
}
}
async generateRoutesAndFiles () {
debug('Generating files...')
// -- Templates --
@ -171,7 +165,7 @@ export default class Builder extends Tapable {
env: this.options.env,
head: this.options.head,
middleware: fs.existsSync(join(this.options.srcDir, 'middleware')),
store: this.options.store || fs.existsSync(join(this.options.srcDir, 'store')),
store: this.options.store,
css: this.options.css,
plugins: this.options.plugins.map((p, i) => {
if (typeof p === 'string') p = { src: p }
@ -180,7 +174,7 @@ export default class Builder extends Tapable {
}),
appPath: './App.vue',
layouts: Object.assign({}, this.options.layouts),
loading: (typeof this.options.loading === 'string' ? r(this.options.srcDir, this.options.loading) : this.options.loading),
loading: typeof this.options.loading === 'string' ? r(this.options.srcDir, this.options.loading) : this.options.loading,
transition: this.options.transition,
components: {
ErrorPage: this.options.ErrorPage ? r(this.options.ErrorPage) : null
@ -212,7 +206,7 @@ export default class Builder extends Tapable {
if (this._nuxtPages) {
// Use nuxt.js createRoutes bases on pages/
const files = await glob('pages/**/*.vue', { cwd: this.options.srcDir })
templateVars.router.routes = this.createRoutes(files, this.options.srcDir)
templateVars.router.routes = createRoutes(files, this.options.srcDir)
} else {
templateVars.router.routes = this.options.build.createRoutes(this.options.srcDir)
}
@ -221,8 +215,6 @@ export default class Builder extends Tapable {
// let the user extend the routes
this.options.router.extendRoutes(templateVars.router.routes, r)
}
// Routes for generate command
this.routes = this.flatRoutes(templateVars.router.routes || [])
// -- Store --
// Add store if needed
@ -290,258 +282,100 @@ export default class Builder extends Tapable {
}))
}
async buildFiles () {
if (this.options.dev) {
debug('Adding webpack middleware...')
this.createWebpackMiddleware()
this.webpackWatchAndUpdate()
this.watchFiles()
} else {
webpackBuild () {
debug('Building files...')
await this.webpackRunClient()
await this.webpackRunServer()
this.addAppTemplate()
}
let compilersOptions = []
// Client
let clientConfig = clientWebpackConfig.call(this)
clientConfig.name = '$client'
compilersOptions.push(clientConfig)
// Server
if (this.options.ssr !== false) {
let serverConfig = serverWebpackConfig.call(this)
serverConfig.name = '$server'
compilersOptions.push(serverConfig)
}
createRoutes (files, srcDir) {
let routes = []
files.forEach((file) => {
let keys = file.replace(/^pages/, '').replace(/\.vue$/, '').replace(/\/{2,}/g, '/').split('/').slice(1)
let route = { name: '', path: '', component: r(srcDir, file) }
let parent = routes
keys.forEach((key, i) => {
route.name = route.name ? route.name + '-' + key.replace('_', '') : key.replace('_', '')
route.name += (key === '_') ? 'all' : ''
let child = _.find(parent, { name: route.name })
if (child) {
if (!child.children) {
child.children = []
}
parent = child.children
route.path = ''
} else {
if (key === 'index' && (i + 1) === keys.length) {
route.path += (i > 0 ? '' : '/')
} else {
route.path += '/' + (key === '_' ? '*' : key.replace('_', ':'))
if (key !== '_' && key.indexOf('_') !== -1) {
route.path += '?'
}
}
// Leverage webpack multi-compiler for faster builds
this.compiler = webpack(compilersOptions)
// Access to compilers with name
this.compiler.compilers.forEach(compiler => {
if (compiler.name) {
this.compiler[compiler.name] = compiler
}
})
// Order Routes path
parent.push(route)
parent.sort((a, b) => {
if (!a.path.length || a.path === '/') {
return -1
}
if (!b.path.length || b.path === '/') {
return 1
}
let res = 0
let _a = a.path.split('/')
let _b = b.path.split('/')
for (let i = 0; i < _a.length; i++) {
if (res !== 0) {
break
}
let y = (_a[i].indexOf('*') > -1) ? 2 : (_a[i].indexOf(':') > -1 ? 1 : 0)
let z = (_b[i].indexOf('*') > -1) ? 2 : (_b[i].indexOf(':') > -1 ? 1 : 0)
res = y - z
if (i === _b.length - 1 && res === 0) {
res = 1
}
}
return res === 0 ? -1 : res
})
})
return this.cleanChildrenRoutes(routes)
// Add middleware for dev
if (this.options.dev) {
this.webpackDev()
}
cleanChildrenRoutes (routes, isChild = false) {
let start = -1
let routesIndex = []
routes.forEach((route) => {
if (/-index$/.test(route.name) || route.name === 'index') {
// Save indexOf 'index' key in name
let res = route.name.split('-')
let s = res.indexOf('index')
start = (start === -1 || s < start) ? s : start
routesIndex.push(res)
// Start build
return new Promise((resolve, reject) => {
this.compiler.run((err, multiStats) => {
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()) {
return reject(new Error('Webpack build exited with errors'))
}
}
resolve()
})
routes.forEach((route) => {
route.path = (isChild) ? route.path.replace('/', '') : route.path
if (route.path.indexOf('?') > -1) {
let names = route.name.split('-')
let paths = route.path.split('/')
if (!isChild) {
paths.shift()
} // clean first / for parents
routesIndex.forEach((r) => {
let i = r.indexOf('index') - start // children names
if (i < paths.length) {
for (let a = 0; a <= i; a++) {
if (a === i) {
paths[a] = paths[a].replace('?', '')
}
if (a < i && names[a] !== r[a]) {
break
}
}
}
})
route.path = (isChild ? '' : '/') + paths.join('/')
}
route.name = route.name.replace(/-index$/, '')
if (route.children) {
if (route.children.find((child) => child.path === '')) {
delete route.name
}
route.children = this.cleanChildrenRoutes(route.children, true)
}
webpackDev () {
debug('Adding webpack middleware...')
// Use MFS for faster builds
let mfs = new MFS()
this.compiler.compilers.forEach(compiler => {
compiler.outputFileSystem = mfs
})
return routes
}
flatRoutes (router, path = '', routes = []) {
router.forEach((r) => {
if (!r.path.includes(':') && !r.path.includes('*')) {
if (r.children) {
this.flatRoutes(r.children, path + r.path + '/', routes)
} else {
routes.push((r.path === '' && path[path.length - 1] === '/' ? path.slice(0, -1) : path) + r.path)
}
}
})
return routes
}
let clientConfig = this.compiler.$client.options
getWebpackClientConfig () {
return clientWebpackConfig.call(this)
}
getWebpackServerConfig () {
return serverWebpackConfig.call(this)
}
createWebpackMiddleware () {
const clientConfig = this.getWebpackClientConfig()
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'
// setup on the fly compilation + hot-reload
// Setup on the fly compilation + hot-reload
clientConfig.entry.app = _.flatten(['webpack-hot-middleware/client?reload=true', clientConfig.entry.app])
clientConfig.plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new PostCompilePlugin(stats => {
if (!stats.hasErrors() && !stats.hasWarnings()) {
// We don't use os.host() here because browsers have special behaviour with localhost
// For example chrome allows Geolocation api only to https or localhost origins
let _host = host === '0.0.0.0' ? 'localhost' : host
console.log(`> Open http://${_host}:${port}\n`) // eslint-disable-line no-console
}
})
new webpack.NoEmitOnErrorsPlugin()
)
const clientCompiler = webpack(clientConfig)
this.clientCompiler = clientCompiler
// Add the middleware to the instance context
this.webpackDevMiddleware = pify(require('webpack-dev-middleware')(clientCompiler, {
// Create webpack dev middleware
this.webpackDevMiddleware = pify(webpackDevMiddleware(this.compiler.$client, {
publicPath: clientConfig.output.publicPath,
stats: this.webpackStats,
quiet: true,
noInfo: true,
watchOptions: this.options.watchers.webpack
}))
this.webpackHotMiddleware = pify(require('webpack-hot-middleware')(clientCompiler, {
log: () => {
}
this.webpackHotMiddleware = pify(webpackHotMiddleware(this.compiler.$client, {
log: false,
heartbeat: 2500
}))
clientCompiler.plugin('done', () => {
const fs = this.webpackDevMiddleware.fileSystem
const filePath = join(clientConfig.output.path, 'index.html')
if (fs.existsSync(filePath)) {
const template = fs.readFileSync(filePath, 'utf-8')
this.appTemplate = _.template(template, {
interpolate: /{{([\s\S]+?)}}/g
})
}
this.watchHandler()
})
}
webpackWatchAndUpdate () {
const MFS = require('memory-fs') // <- dependencies of webpack
const serverFS = new MFS()
const clientFS = this.clientCompiler.outputFileSystem
const serverConfig = this.getWebpackServerConfig()
const serverCompiler = webpack(serverConfig)
const bundlePath = join(serverConfig.output.path, 'server-bundle.json')
const manifestPath = join(serverConfig.output.path, 'client-manifest.json')
serverCompiler.outputFileSystem = serverFS
const watchHandler = (err) => {
if (err) throw err
const bundleExists = serverFS.existsSync(bundlePath)
const manifestExists = clientFS.existsSync(manifestPath)
if (bundleExists && manifestExists) {
const bundle = serverFS.readFileSync(bundlePath, 'utf8')
const manifest = clientFS.readFileSync(manifestPath, 'utf8')
this.createRenderer(JSON.parse(bundle), JSON.parse(manifest))
// Run after compilation is done
this.compiler.plugin('done', async stats => {
// Reload renderer if available
if (this.nuxt.renderer) {
await this.nuxt.renderer.loadResources(mfs)
}
// Show open URL
if (!stats.hasErrors() && !stats.hasWarnings()) {
let _host = host === '0.0.0.0' ? 'localhost' : host
console.log(`> Open http://${_host}:${port}\n`) // eslint-disable-line no-console
}
this.watchHandler = watchHandler
this.webpackServerWatcher = serverCompiler.watch(this.options.watchers.webpack, watchHandler)
}
})
webpackRunClient () {
return new Promise((resolve, reject) => {
const clientConfig = this.getWebpackClientConfig()
const clientCompiler = webpack(clientConfig)
clientCompiler.run((err, stats) => {
if (err) return reject(err)
console.log('[nuxt:build:client]\n', stats.toString(this.webpackStats)) // eslint-disable-line no-console
if (stats.hasErrors()) {
return reject(new Error('Webpack build exited with errors'))
}
resolve()
})
})
}
webpackRunServer () {
return new Promise((resolve, reject) => {
const serverConfig = this.getWebpackServerConfig()
const serverCompiler = webpack(serverConfig)
serverCompiler.run((err, stats) => {
if (err) return reject(err)
console.log('[nuxt:build:server]\n', stats.toString(this.webpackStats)) // eslint-disable-line no-console
if (stats.hasErrors()) return reject(new Error('Webpack build exited with errors'))
const bundlePath = join(serverConfig.output.path, 'server-bundle.json')
const manifestPath = join(serverConfig.output.path, 'client-manifest.json')
readFile(bundlePath, 'utf8')
.then(bundle => {
readFile(manifestPath, 'utf8')
.then(manifest => {
this.createRenderer(JSON.parse(bundle), JSON.parse(manifest))
resolve()
})
})
})
})
}
createRenderer (bundle, manifest) {
// Create bundle renderer to give a fresh context for every request
this.renderer = createBundleRenderer(bundle, Object.assign({
clientManifest: manifest,
runInNewContext: false,
basedir: this.options.rootDir
}, this.options.build.ssr))
this.renderToString = pify(this.renderer.renderToString)
this.renderToStream = this.renderer.renderToStream
this.watchFiles()
}
watchFiles () {

View File

@ -1,4 +1,43 @@
export default {
import _ from 'lodash'
import { join, resolve } from 'path'
import { existsSync } from 'fs'
export default function defaults (_options) {
// Clone options to prevent unwanted side-effects
const options = Object.assign({}, _options)
// Normalize options
if (options.loading === true) {
delete options.loading
}
if (options.router && typeof options.router.middleware === 'string') {
options.router.middleware = [options.router.middleware]
}
if (options.router && typeof options.router.base === 'string') {
options._routerBaseSpecified = true
}
if (typeof options.transition === 'string') {
options.transition = { name: options.transition }
}
// Apply defaults
_.defaultsDeep(options, defaultOptions)
// Resolve dirs
options.rootDir = (typeof options.rootDir === 'string' && options.rootDir ? options.rootDir : process.cwd())
options.srcDir = (typeof options.srcDir === 'string' && options.srcDir ? resolve(options.rootDir, options.srcDir) : options.rootDir)
options.buildDir = join(options.rootDir, options.buildDir)
// If app.html is defined, set the template path to the user template
options.appTemplatePath = resolve(__dirname, 'views/app.template.html')
if (existsSync(join(options.srcDir, 'app.html'))) {
options.appTemplatePath = join(options.srcDir, 'app.html')
}
return options
}
const defaultOptions = {
dev: (process.env.NODE_ENV !== 'production'),
buildDir: '.nuxt',
build: {
@ -78,6 +117,7 @@ export default {
scrollBehavior: null
},
render: {
ssr: {},
http2: {
push: false
},

View File

@ -4,7 +4,7 @@ import _ from 'lodash'
import { resolve, join, dirname, sep } from 'path'
import { minify } from 'html-minifier'
import Tapable from 'tappable'
import { isUrl, promisifyRoute, waitFor } from './utils'
import { isUrl, promisifyRoute, waitFor, flatRoutes } from './utils'
const debug = require('debug')('nuxt:generate')
const copy = pify(fs.copy)
@ -12,6 +12,8 @@ const remove = pify(fs.remove)
const writeFile = pify(fs.writeFile)
const mkdirp = pify(fs.mkdirp)
debug('loaded')
export default class Generator extends Tapable {
constructor (nuxt) {
super()
@ -31,7 +33,7 @@ export default class Generator extends Tapable {
let distNuxtPath = join(distPath, (isUrl(this.options.build.publicPath) ? '' : this.options.build.publicPath))
// Launch build process
await this.nuxt.builder.build()
await this.nuxt.build()
// Clean destination folder
await remove(distPath)
@ -78,7 +80,7 @@ export default class Generator extends Tapable {
}
// Generate only index.html for router.mode = 'hash'
let routes = (this.options.router.mode === 'hash') ? ['/'] : this.nuxt.builder.routes
let routes = (this.options.router.mode === 'hash') ? ['/'] : flatRoutes(this.options.router.routes)
routes = decorateWithPayloads(routes)
while (routes.length) {

View File

@ -1,77 +1,29 @@
import _ from 'lodash'
import fs from 'fs-extra'
import { resolve, join } from 'path'
import Tapable from 'tappable'
import * as Utils from './utils'
import Builder from './builder'
import Renderer from './renderer'
import Generator from './generator'
import ModuleContainer from './module-container'
import Server from './server'
import Defaults from './defaults'
import defaults from './defaults'
export default class Nuxt extends Tapable {
constructor (_options = {}) {
super()
// Clone options to prevent unwanted side-effects
const options = Object.assign({}, _options)
this.options = defaults(_options)
// Normalize options
if (options.loading === true) {
delete options.loading
}
if (options.router && typeof options.router.middleware === 'string') {
options.router.middleware = [options.router.middleware]
}
if (options.router && typeof options.router.base === 'string') {
this._routerBaseSpecified = true
}
if (typeof options.transition === 'string') {
options.transition = { name: options.transition }
}
// Apply defaults
this.options = _.defaultsDeep(options, Nuxt.Defaults)
// Resolve dirs
this.options.rootDir = (typeof options.rootDir === 'string' && options.rootDir ? options.rootDir : process.cwd())
this.options.srcDir = (typeof options.srcDir === 'string' && options.srcDir ? resolve(options.rootDir, options.srcDir) : this.options.rootDir)
this.options.buildDir = join(this.options.rootDir, options.buildDir)
// If store defined, update store options to true
if (fs.existsSync(join(this.options.srcDir, 'store'))) {
this.options.store = true
}
// If app.html is defined, set the template path to the user template
this.options.appTemplatePath = resolve(__dirname, 'views/app.template.html')
if (fs.existsSync(join(this.options.srcDir, 'app.html'))) {
this.options.appTemplatePath = join(this.options.srcDir, 'app.html')
}
this.initialized = false
this.errorHandler = this.errorHandler.bind(this)
// Create instance of core components
this.moduleContainer = new Nuxt.ModuleContainer(this)
this.builder = new Nuxt.Builder(this)
this.renderer = new Nuxt.Renderer(this)
this.generator = new Nuxt.Generator(this)
// Backward compatibility
this.render = this.renderer.render.bind(this.renderer)
this.renderRoute = this.renderer.renderRoute.bind(this.renderer)
this.renderAndGetWindow = this.renderer.renderAndGetWindow.bind(this.renderer)
this.build = this.builder.build.bind(this.builder)
this.generate = this.generator.generate.bind(this.generator)
this.dir = options.rootDir
this.srcDir = options.srcDir
this.buildDir = options.buildDir
this.dev = options.dev
this.Server = Nuxt.Server
this.Utils = Nuxt.Utils
this.errorHandler = this.errorHandler.bind(this)
this._init = this.init().catch(this.errorHandler)
this.initialized = false
}
async init () {
@ -79,29 +31,56 @@ export default class Nuxt extends Tapable {
return this._init
}
// Call to build on dev
if (this.options.dev) {
this.builder.build().catch(this.errorHandler)
}
// Wait for all components to be ready
// Including modules
await this.applyPluginsAsync('beforeInit')
// Including Build
await this.applyPluginsAsync('init')
// Extra jobs
this.initialized = true
this.applyPluginsAsync('afterInit').catch(this.errorHandler)
this.initialized = true
return this
}
get builder () {
if (this._builder) {
return this._builder
}
const Builder = require('./builder').default
this._builder = new Builder(this)
return this._builder
}
get generator () {
if (this._generator) {
return this._generator
}
const Generator = require('./generator').default
this._generator = new Generator(this)
return this._generator
}
build () {
return this.builder.build.apply(this.builder, arguments)
}
generate () {
return this.generator.generate.apply(this.generator, arguments)
}
errorHandler () {
// Global error handler
// Silent
if (this.options.errorHandler === false) {
return
}
// Custom eventHandler
// Custom errorHandler
if (typeof this.options.errorHandler === 'function') {
return this.options.errorHandler.apply(this, arguments)
}
// Default
// Default handler
// eslint-disable-next-line no-console
console.error.apply(this, arguments)
process.exit(1)
@ -131,6 +110,9 @@ export default class Nuxt extends Tapable {
if (this.customFilesWatcher) {
this.customFilesWatcher.close()
}
promises.push(this.applyPluginsAsync('close'))
return Promise.all(promises).then(() => {
if (typeof callback === 'function') callback()
})
@ -138,10 +120,7 @@ export default class Nuxt extends Tapable {
}
// Add core components to Nuxt class
Nuxt.Defaults = Defaults
Nuxt.Utils = Utils
Nuxt.Renderer = Renderer
Nuxt.Builder = Builder
Nuxt.ModuleContainer = ModuleContainer
Nuxt.Server = Server
Nuxt.Generator = Generator

View File

@ -7,8 +7,9 @@ import pify from 'pify'
import serveStatic from 'serve-static'
import compression from 'compression'
import _ from 'lodash'
import { resolve } from 'path'
import {readFileSync} from 'fs'
import { resolve, join } from 'path'
import fs from 'fs-extra'
import { createBundleRenderer } from 'vue-server-renderer'
import { getContext, setAnsiColors, encodeHtml } from './utils'
const debug = require('debug')('nuxt:render')
@ -17,13 +18,43 @@ setAnsiColors(ansiHTML)
let jsdom = null
const parseTemplate = templateStr => _.template(templateStr, {
interpolate: /{{([\s\S]+?)}}/g
})
export default class Renderer extends Tapable {
constructor (nuxt) {
super()
this.nuxt = nuxt
this.options = nuxt.options
this.nuxt.plugin('init', () => {
// Will be loaded by createRenderer
this.bundleRenderer = null
this.renderToStream = null
this.renderToString = null
if (nuxt.initialized) {
// If nuxt already initialized
this._init = this.init().catch(this.nuxt.errorHandler)
} else {
// Wait for hook
this.nuxt.plugin('init', this.init.bind(this))
}
}
async init () {
if (this._init) {
return this._init
}
// Renderer runtime resources
this.resources = {
clientManifest: null,
serverBundle: null,
appTemplate: null,
errorTemplate: parseTemplate(fs.readFileSync(resolve(__dirname, 'views', 'error.html'), 'utf8'))
}
// For serving static/ files to /
this.serveStatic = pify(serveStatic(resolve(this.options.srcDir, 'static'), this.options.render.static))
@ -37,41 +68,99 @@ export default class Renderer extends Tapable {
this.gzipMiddleware = pify(compression(this.options.render.gzip))
}
// Error template
this.errorTemplate = _.template(readFileSync(resolve(__dirname, 'views', 'error.html'), 'utf8'), {
interpolate: /{{([\s\S]+?)}}/g
})
// Try to load resources from fs
return this.loadResources()
}
async loadResources (_fs = fs, distPath) {
distPath = distPath || resolve(this.options.buildDir, 'dist')
const resourceMap = {
clientManifest: {
path: join(distPath, 'client-manifest.json'),
transform: JSON.parse
},
serverBundle: {
path: join(distPath, 'server-bundle.json'),
transform: JSON.parse
},
appTemplate: {
path: join(distPath, 'index.html'),
transform: parseTemplate
}
}
Object.keys(resourceMap).forEach(resourceKey => {
let { path, transform } = resourceMap[resourceKey]
let data
if (_fs.existsSync(path)) {
data = _fs.readFileSync(path, 'utf8')
if (typeof transform === 'function') {
data = transform(data)
}
}
if (data) {
this.resources[resourceKey] = data
}
})
this.createRenderer()
}
createRenderer () {
// If resources are not yet provided
if (!this.resources.serverBundle || !this.resources.clientManifest) {
return
}
// Create bundle renderer for SSR
this.bundleRenderer = createBundleRenderer(this.resources.serverBundle, Object.assign({
clientManifest: this.resources.clientManifest,
runInNewContext: false,
basedir: this.options.rootDir
}, this.options.render.ssr))
// Promisify renderToString
this.bundleRenderer.renderToString = pify(this.bundleRenderer.renderToString)
debug('ready')
}
async render (req, res) {
/* istanbul ignore if */
if (!this.nuxt.builder.renderer || !this.nuxt.builder.appTemplate) {
if (!this.bundleRenderer || !this.resources.appTemplate) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(this.render(req, res))
}, 1000)
})
}
// Get context
const context = getContext(req, res)
res.statusCode = 200
try {
if (this.options.dev) {
// Call webpack middleware only in development
if (this.options.dev && this.nuxt.builder && this.nuxt.builder.webpackDevMiddleware) {
await this.nuxt.builder.webpackDevMiddleware(req, res)
await this.nuxt.builder.webpackHotMiddleware(req, res)
}
if (!this.options.dev && this.options.render.gzip) {
// Gzip middleware for production
if (this.gzipMiddleware) {
await this.gzipMiddleware(req, res)
}
// If base in req.url, remove it for the middleware and vue-router
if (this.options.router.base !== '/' && req.url.indexOf(this.options.router.base) === 0) {
// Compatibility with base url for dev server
req.url = req.url.replace(this.options.router.base, '/')
}
// Serve static/ files
await this.serveStatic(req, res)
// Serve .nuxt/dist/ files (only for production)
if (!this.options.dev && req.url.indexOf(this.options.build.publicPath) === 0) {
const url = req.url
@ -80,17 +169,22 @@ export default class Renderer extends Tapable {
/* istanbul ignore next */
req.url = url
}
if (this.options.dev && req.url.indexOf(this.options.build.publicPath) === 0 && req.url.includes('.hot-update.json')) {
res.statusCode = 404
return res.end()
}
const { html, error, redirected, resourceHints } = await this.renderRoute(req.url, context)
if (redirected) {
return html
}
if (error) {
res.statusCode = context.nuxt.error.statusCode || 500
}
// ETag header
if (!error && this.options.render.etag) {
const etag = generateETag(html, this.options.render.etag)
@ -101,6 +195,7 @@ export default class Renderer extends Tapable {
}
res.setHeader('ETag', etag)
}
// HTTP2 push headers
if (!error && this.options.render.http2.push) {
// Parse resourceHints to extract HTTP.2 prefetch/push headers
@ -118,6 +213,8 @@ export default class Renderer extends Tapable {
// https://blog.cloudflare.com/http-2-server-push-with-multiple-assets-per-link-header
res.setHeader('Link', pushAssets.join(','))
}
// Send response
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.setHeader('Content-Length', Buffer.byteLength(html))
res.end(html, 'utf8')
@ -127,11 +224,13 @@ export default class Renderer extends Tapable {
console.error(err) // eslint-disable-line no-console
return err
}
const html = this.errorTemplate({
// Render error template
const html = this.resources.errorTemplate({
/* istanbul ignore if */
error: err,
stack: ansiHTML(encodeHtml(err.stack))
})
// Send response
res.statusCode = 500
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.setHeader('Content-Length', Buffer.byteLength(html))
@ -143,29 +242,34 @@ export default class Renderer extends Tapable {
async renderRoute (url, context = {}) {
// Log rendered url
debug(`Rendering url ${url}`)
// Add url and isSever to the context
context.url = url
context.isServer = true
// Call renderToString from the bundleRenderer and generate the HTML (will update the context as well)
let APP = await this.nuxt.builder.renderToString(context)
let APP = await this.bundleRenderer.renderToString(context)
if (!context.nuxt.serverRendered) {
APP = '<div id="__nuxt"></div>'
}
const m = context.meta.inject()
let HEAD = m.meta.text() + m.title.text() + m.link.text() + m.style.text() + m.script.text() + m.noscript.text()
if (this._routerBaseSpecified) {
if (this.options._routerBaseSpecified) {
HEAD += `<base href="${this.options.router.base}">`
}
const resourceHints = context.renderResourceHints()
HEAD += resourceHints + context.renderStyles()
APP += `<script type="text/javascript">window.__NUXT__=${serialize(context.nuxt, { isJSON: true })}</script>`
APP += context.renderScripts()
const html = this.nuxt.builder.appTemplate({
const html = this.resources.appTemplate({
HTML_ATTRS: 'data-n-head-ssr ' + m.htmlAttrs.text(),
BODY_ATTRS: m.bodyAttrs.text(),
HEAD,
APP
})
return {
html,
resourceHints,

View File

@ -97,3 +97,118 @@ export function r () {
args = args.map(normalize)
return wp(resolve.apply(null, args))
}
export function flatRoutes (router, path = '', routes = []) {
router.forEach((r) => {
if (!r.path.includes(':') && !r.path.includes('*')) {
if (r.children) {
flatRoutes(r.children, path + r.path + '/', routes)
} else {
routes.push((r.path === '' && path[path.length - 1] === '/' ? path.slice(0, -1) : path) + r.path)
}
}
})
return routes
}
export function cleanChildrenRoutes (routes, isChild = false) {
let start = -1
let routesIndex = []
routes.forEach((route) => {
if (/-index$/.test(route.name) || route.name === 'index') {
// Save indexOf 'index' key in name
let res = route.name.split('-')
let s = res.indexOf('index')
start = (start === -1 || s < start) ? s : start
routesIndex.push(res)
}
})
routes.forEach((route) => {
route.path = (isChild) ? route.path.replace('/', '') : route.path
if (route.path.indexOf('?') > -1) {
let names = route.name.split('-')
let paths = route.path.split('/')
if (!isChild) {
paths.shift()
} // clean first / for parents
routesIndex.forEach((r) => {
let i = r.indexOf('index') - start // children names
if (i < paths.length) {
for (let a = 0; a <= i; a++) {
if (a === i) {
paths[a] = paths[a].replace('?', '')
}
if (a < i && names[a] !== r[a]) {
break
}
}
}
})
route.path = (isChild ? '' : '/') + paths.join('/')
}
route.name = route.name.replace(/-index$/, '')
if (route.children) {
if (route.children.find((child) => child.path === '')) {
delete route.name
}
route.children = cleanChildrenRoutes(route.children, true)
}
})
return routes
}
export function createRoutes (files, srcDir) {
let routes = []
files.forEach((file) => {
let keys = file.replace(/^pages/, '').replace(/\.vue$/, '').replace(/\/{2,}/g, '/').split('/').slice(1)
let route = { name: '', path: '', component: r(srcDir, file) }
let parent = routes
keys.forEach((key, i) => {
route.name = route.name ? route.name + '-' + key.replace('_', '') : key.replace('_', '')
route.name += (key === '_') ? 'all' : ''
let child = _.find(parent, { name: route.name })
if (child) {
if (!child.children) {
child.children = []
}
parent = child.children
route.path = ''
} else {
if (key === 'index' && (i + 1) === keys.length) {
route.path += (i > 0 ? '' : '/')
} else {
route.path += '/' + (key === '_' ? '*' : key.replace('_', ':'))
if (key !== '_' && key.indexOf('_') !== -1) {
route.path += '?'
}
}
}
})
// Order Routes path
parent.push(route)
parent.sort((a, b) => {
if (!a.path.length || a.path === '/') {
return -1
}
if (!b.path.length || b.path === '/') {
return 1
}
let res = 0
let _a = a.path.split('/')
let _b = b.path.split('/')
for (let i = 0; i < _a.length; i++) {
if (res !== 0) {
break
}
let y = (_a[i].indexOf('*') > -1) ? 2 : (_a[i].indexOf(':') > -1 ? 1 : 0)
let z = (_b[i].indexOf('*') > -1) ? 2 : (_b[i].indexOf(':') > -1 ? 1 : 0)
res = y - z
if (i === _b.length - 1 && res === 0) {
res = 1
}
}
return res === 0 ? -1 : res
})
})
return cleanChildrenRoutes(routes)
}

View File

@ -84,7 +84,6 @@
"offline-plugin": "^4.8.1",
"opencollective": "^1.0.3",
"pify": "^3.0.0",
"post-compile-webpack-plugin": "^0.1.1",
"preload-webpack-plugin": "^1.2.2",
"progress-bar-webpack-plugin": "^1.9.3",
"script-ext-html-webpack-plugin": "^1.8.1",

View File

@ -18,7 +18,7 @@ test.before('Init Nuxt.js', async t => {
}
nuxt = new Nuxt(options)
await nuxt.build()
server = new nuxt.Server(nuxt)
server = new Nuxt.Server(nuxt)
server.listen(port, 'localhost')
})

View File

@ -15,7 +15,7 @@ test.before('Init Nuxt.js', async t => {
}
nuxt = new Nuxt(options)
await nuxt.build()
server = new nuxt.Server(nuxt)
server = new Nuxt.Server(nuxt)
server.listen(port, 'localhost')
})

View File

@ -15,7 +15,7 @@ test.before('Init Nuxt.js', async t => {
}
nuxt = new Nuxt(options)
await nuxt.build()
server = new nuxt.Server(nuxt)
server = new Nuxt.Server(nuxt)
server.listen(port, 'localhost')
})

View File

@ -17,7 +17,7 @@ test.before('Init Nuxt.js', async t => {
config.dev = false
nuxt = new Nuxt(config)
await nuxt.build()
server = new nuxt.Server(nuxt)
server = new Nuxt.Server(nuxt)
server.listen(port, 'localhost')
})