mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-23 14:15:13 +00:00
decouple builder from renderer + improvements
This commit is contained in:
parent
b61694ca21
commit
42bf9bb41d
@ -64,10 +64,10 @@ if (typeof options.rootDir !== 'string') {
|
|||||||
// Force development mode: add hot reloading and watching changes
|
// Force development mode: add hot reloading and watching changes
|
||||||
options.dev = true
|
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 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 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)
|
listenOnConfigChanges(nuxt, server)
|
||||||
|
|
||||||
@ -102,3 +102,5 @@ function listenOnConfigChanges(nuxt, server) {
|
|||||||
chokidar.watch(nuxtConfigFile, Object.assign({}, nuxt.options.watchers.chokidar, { ignoreInitial: true }))
|
chokidar.watch(nuxtConfigFile, Object.assign({}, nuxt.options.watchers.chokidar, { ignoreInitial: true }))
|
||||||
.on('all', build)
|
.on('all', build)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = nuxt
|
||||||
|
@ -55,7 +55,9 @@ if (typeof options.rootDir !== 'string') {
|
|||||||
}
|
}
|
||||||
options.dev = false // Force production mode (no webpack middleware called)
|
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 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 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
|
||||||
|
428
lib/builder.js
428
lib/builder.js
@ -4,15 +4,16 @@ import fs from 'fs-extra'
|
|||||||
import hash from 'hash-sum'
|
import hash from 'hash-sum'
|
||||||
import pify from 'pify'
|
import pify from 'pify'
|
||||||
import webpack from 'webpack'
|
import webpack from 'webpack'
|
||||||
import PostCompilePlugin from 'post-compile-webpack-plugin'
|
|
||||||
import serialize from 'serialize-javascript'
|
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 { join, resolve, basename, dirname } from 'path'
|
||||||
import Tapable from 'tappable'
|
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 clientWebpackConfig from './webpack/client.config.js'
|
||||||
import serverWebpackConfig from './webpack/server.config.js'
|
import serverWebpackConfig from './webpack/server.config.js'
|
||||||
import defaults from './defaults'
|
import defaults from './defaults'
|
||||||
|
import MFS from 'memory-fs'
|
||||||
|
|
||||||
const debug = require('debug')('nuxt:build')
|
const debug = require('debug')('nuxt:build')
|
||||||
debug.color = 2 // Force green color
|
debug.color = 2 // Force green color
|
||||||
@ -24,12 +25,39 @@ const writeFile = pify(fs.writeFile)
|
|||||||
const mkdirp = pify(fs.mkdirp)
|
const mkdirp = pify(fs.mkdirp)
|
||||||
const glob = pify(require('glob'))
|
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 {
|
export default class Builder extends Tapable {
|
||||||
constructor (nuxt) {
|
constructor (nuxt) {
|
||||||
super()
|
super()
|
||||||
this.nuxt = nuxt
|
this.nuxt = nuxt
|
||||||
this.options = nuxt.options
|
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
|
// Add extra loaders only if they are not already provided
|
||||||
let extraDefaults = {}
|
let extraDefaults = {}
|
||||||
if (this.options.build && !Array.isArray(this.options.build.loaders)) {
|
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)) {
|
if (this.options.build && !Array.isArray(this.options.build.postcss)) {
|
||||||
extraDefaults.postcss = defaultsPostcss
|
extraDefaults.postcss = defaultsPostcss
|
||||||
}
|
}
|
||||||
this.options.build = _.defaultsDeep(this.options.build, extraDefaults)
|
_.defaultsDeep(this.options.build, extraDefaults)
|
||||||
|
|
||||||
/* istanbul ignore if */
|
/* istanbul ignore if */
|
||||||
if (this.options.dev && isUrl(this.options.build.publicPath)) {
|
if (this.options.dev && isUrl(this.options.build.publicPath)) {
|
||||||
this.options.build.publicPath = defaults.build.publicPath
|
this.options.build.publicPath = defaults.build.publicPath
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats
|
// If store defined, update store options to true unless explicitly disabled
|
||||||
this.webpackStats = {
|
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,
|
chunks: false,
|
||||||
children: false,
|
children: false,
|
||||||
modules: false,
|
modules: false,
|
||||||
colors: true
|
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 () {
|
async build () {
|
||||||
@ -76,7 +92,6 @@ export default class Builder extends Tapable {
|
|||||||
if (this._buildStatus === STATUS.BUILD_DONE) {
|
if (this._buildStatus === STATUS.BUILD_DONE) {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
// If building
|
// If building
|
||||||
if (this._buildStatus === STATUS.BUILDING) {
|
if (this._buildStatus === STATUS.BUILDING) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@ -92,7 +107,6 @@ export default class Builder extends Tapable {
|
|||||||
|
|
||||||
// Check if pages dir exists and warn if not
|
// Check if pages dir exists and warn if not
|
||||||
this._nuxtPages = typeof this.options.build.createRoutes !== 'function'
|
this._nuxtPages = typeof this.options.build.createRoutes !== 'function'
|
||||||
|
|
||||||
if (this._nuxtPages) {
|
if (this._nuxtPages) {
|
||||||
if (!fs.existsSync(join(this.options.srcDir, 'pages'))) {
|
if (!fs.existsSync(join(this.options.srcDir, 'pages'))) {
|
||||||
let dir = this.options.srcDir
|
let dir = this.options.srcDir
|
||||||
@ -113,39 +127,19 @@ export default class Builder extends Tapable {
|
|||||||
if (!this.options.dev) {
|
if (!this.options.dev) {
|
||||||
await mkdirp(r(this.options.buildDir, 'dist'))
|
await mkdirp(r(this.options.buildDir, 'dist'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate routes and interpret the template files
|
// Generate routes and interpret the template files
|
||||||
await this.generateRoutesAndFiles()
|
await this.generateRoutesAndFiles()
|
||||||
// Generate .nuxt/dist/ files
|
|
||||||
await this.buildFiles()
|
// Start webpack build
|
||||||
|
await this.webpackBuild()
|
||||||
|
|
||||||
// Flag to set that building is done
|
// Flag to set that building is done
|
||||||
this._buildStatus = STATUS.BUILD_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
|
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 () {
|
async generateRoutesAndFiles () {
|
||||||
debug('Generating files...')
|
debug('Generating files...')
|
||||||
// -- Templates --
|
// -- Templates --
|
||||||
@ -171,7 +165,7 @@ export default class Builder extends Tapable {
|
|||||||
env: this.options.env,
|
env: this.options.env,
|
||||||
head: this.options.head,
|
head: this.options.head,
|
||||||
middleware: fs.existsSync(join(this.options.srcDir, 'middleware')),
|
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,
|
css: this.options.css,
|
||||||
plugins: this.options.plugins.map((p, i) => {
|
plugins: this.options.plugins.map((p, i) => {
|
||||||
if (typeof p === 'string') p = { src: p }
|
if (typeof p === 'string') p = { src: p }
|
||||||
@ -180,7 +174,7 @@ export default class Builder extends Tapable {
|
|||||||
}),
|
}),
|
||||||
appPath: './App.vue',
|
appPath: './App.vue',
|
||||||
layouts: Object.assign({}, this.options.layouts),
|
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,
|
transition: this.options.transition,
|
||||||
components: {
|
components: {
|
||||||
ErrorPage: this.options.ErrorPage ? r(this.options.ErrorPage) : null
|
ErrorPage: this.options.ErrorPage ? r(this.options.ErrorPage) : null
|
||||||
@ -212,7 +206,7 @@ export default class Builder extends Tapable {
|
|||||||
if (this._nuxtPages) {
|
if (this._nuxtPages) {
|
||||||
// Use nuxt.js createRoutes bases on pages/
|
// Use nuxt.js createRoutes bases on pages/
|
||||||
const files = await glob('pages/**/*.vue', { cwd: this.options.srcDir })
|
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 {
|
} else {
|
||||||
templateVars.router.routes = this.options.build.createRoutes(this.options.srcDir)
|
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
|
// let the user extend the routes
|
||||||
this.options.router.extendRoutes(templateVars.router.routes, r)
|
this.options.router.extendRoutes(templateVars.router.routes, r)
|
||||||
}
|
}
|
||||||
// Routes for generate command
|
|
||||||
this.routes = this.flatRoutes(templateVars.router.routes || [])
|
|
||||||
|
|
||||||
// -- Store --
|
// -- Store --
|
||||||
// Add store if needed
|
// Add store if needed
|
||||||
@ -290,258 +282,100 @@ export default class Builder extends Tapable {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async buildFiles () {
|
webpackBuild () {
|
||||||
|
debug('Building files...')
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add middleware for dev
|
||||||
if (this.options.dev) {
|
if (this.options.dev) {
|
||||||
debug('Adding webpack middleware...')
|
this.webpackDev()
|
||||||
this.createWebpackMiddleware()
|
|
||||||
this.webpackWatchAndUpdate()
|
|
||||||
this.watchFiles()
|
|
||||||
} else {
|
|
||||||
debug('Building files...')
|
|
||||||
await this.webpackRunClient()
|
|
||||||
await this.webpackRunServer()
|
|
||||||
this.addAppTemplate()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
createRoutes (files, srcDir) {
|
// Start build
|
||||||
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 this.cleanChildrenRoutes(routes)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = this.cleanChildrenRoutes(route.children, true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
const clientCompiler = webpack(clientConfig)
|
|
||||||
this.clientCompiler = clientCompiler
|
|
||||||
// Add the middleware to the instance context
|
|
||||||
this.webpackDevMiddleware = pify(require('webpack-dev-middleware')(clientCompiler, {
|
|
||||||
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: () => {
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.watchHandler = watchHandler
|
|
||||||
this.webpackServerWatcher = serverCompiler.watch(this.options.watchers.webpack, watchHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
webpackRunClient () {
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const clientConfig = this.getWebpackClientConfig()
|
this.compiler.run((err, multiStats) => {
|
||||||
const clientCompiler = webpack(clientConfig)
|
if (err) {
|
||||||
clientCompiler.run((err, stats) => {
|
return reject(err)
|
||||||
if (err) return reject(err)
|
}
|
||||||
console.log('[nuxt:build:client]\n', stats.toString(this.webpackStats)) // eslint-disable-line no-console
|
for (let _stats of multiStats.stats) {
|
||||||
if (stats.hasErrors()) {
|
console.log(_stats.toString(this.webpackStats)) // eslint-disable-line no-console
|
||||||
return reject(new Error('Webpack build exited with errors'))
|
if (_stats.hasErrors()) {
|
||||||
|
return reject(new Error('Webpack build exited with errors'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
webpackRunServer () {
|
webpackDev () {
|
||||||
return new Promise((resolve, reject) => {
|
debug('Adding webpack middleware...')
|
||||||
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) {
|
// Use MFS for faster builds
|
||||||
// Create bundle renderer to give a fresh context for every request
|
let mfs = new MFS()
|
||||||
this.renderer = createBundleRenderer(bundle, Object.assign({
|
this.compiler.compilers.forEach(compiler => {
|
||||||
clientManifest: manifest,
|
compiler.outputFileSystem = mfs
|
||||||
runInNewContext: false,
|
})
|
||||||
basedir: this.options.rootDir
|
|
||||||
}, this.options.build.ssr))
|
let clientConfig = this.compiler.$client.options
|
||||||
this.renderToString = pify(this.renderer.renderToString)
|
|
||||||
this.renderToStream = this.renderer.renderToStream
|
// 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()
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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(webpackHotMiddleware(this.compiler.$client, {
|
||||||
|
log: false,
|
||||||
|
heartbeat: 2500
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 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.watchFiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
watchFiles () {
|
watchFiles () {
|
||||||
|
@ -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'),
|
dev: (process.env.NODE_ENV !== 'production'),
|
||||||
buildDir: '.nuxt',
|
buildDir: '.nuxt',
|
||||||
build: {
|
build: {
|
||||||
@ -78,6 +117,7 @@ export default {
|
|||||||
scrollBehavior: null
|
scrollBehavior: null
|
||||||
},
|
},
|
||||||
render: {
|
render: {
|
||||||
|
ssr: {},
|
||||||
http2: {
|
http2: {
|
||||||
push: false
|
push: false
|
||||||
},
|
},
|
||||||
|
@ -4,7 +4,7 @@ import _ from 'lodash'
|
|||||||
import { resolve, join, dirname, sep } from 'path'
|
import { resolve, join, dirname, sep } from 'path'
|
||||||
import { minify } from 'html-minifier'
|
import { minify } from 'html-minifier'
|
||||||
import Tapable from 'tappable'
|
import Tapable from 'tappable'
|
||||||
import { isUrl, promisifyRoute, waitFor } from './utils'
|
import { isUrl, promisifyRoute, waitFor, flatRoutes } from './utils'
|
||||||
|
|
||||||
const debug = require('debug')('nuxt:generate')
|
const debug = require('debug')('nuxt:generate')
|
||||||
const copy = pify(fs.copy)
|
const copy = pify(fs.copy)
|
||||||
@ -12,6 +12,8 @@ const remove = pify(fs.remove)
|
|||||||
const writeFile = pify(fs.writeFile)
|
const writeFile = pify(fs.writeFile)
|
||||||
const mkdirp = pify(fs.mkdirp)
|
const mkdirp = pify(fs.mkdirp)
|
||||||
|
|
||||||
|
debug('loaded')
|
||||||
|
|
||||||
export default class Generator extends Tapable {
|
export default class Generator extends Tapable {
|
||||||
constructor (nuxt) {
|
constructor (nuxt) {
|
||||||
super()
|
super()
|
||||||
@ -31,7 +33,7 @@ export default class Generator extends Tapable {
|
|||||||
let distNuxtPath = join(distPath, (isUrl(this.options.build.publicPath) ? '' : this.options.build.publicPath))
|
let distNuxtPath = join(distPath, (isUrl(this.options.build.publicPath) ? '' : this.options.build.publicPath))
|
||||||
|
|
||||||
// Launch build process
|
// Launch build process
|
||||||
await this.nuxt.builder.build()
|
await this.nuxt.build()
|
||||||
|
|
||||||
// Clean destination folder
|
// Clean destination folder
|
||||||
await remove(distPath)
|
await remove(distPath)
|
||||||
@ -78,7 +80,7 @@ export default class Generator extends Tapable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate only index.html for router.mode = 'hash'
|
// 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)
|
routes = decorateWithPayloads(routes)
|
||||||
|
|
||||||
while (routes.length) {
|
while (routes.length) {
|
||||||
|
103
lib/nuxt.js
103
lib/nuxt.js
@ -1,77 +1,29 @@
|
|||||||
import _ from 'lodash'
|
|
||||||
import fs from 'fs-extra'
|
|
||||||
import { resolve, join } from 'path'
|
|
||||||
import Tapable from 'tappable'
|
import Tapable from 'tappable'
|
||||||
import * as Utils from './utils'
|
import * as Utils from './utils'
|
||||||
import Builder from './builder'
|
|
||||||
import Renderer from './renderer'
|
import Renderer from './renderer'
|
||||||
import Generator from './generator'
|
|
||||||
import ModuleContainer from './module-container'
|
import ModuleContainer from './module-container'
|
||||||
import Server from './server'
|
import Server from './server'
|
||||||
import Defaults from './defaults'
|
import defaults from './defaults'
|
||||||
|
|
||||||
export default class Nuxt extends Tapable {
|
export default class Nuxt extends Tapable {
|
||||||
constructor (_options = {}) {
|
constructor (_options = {}) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
// Clone options to prevent unwanted side-effects
|
this.options = defaults(_options)
|
||||||
const options = Object.assign({}, _options)
|
|
||||||
|
|
||||||
// Normalize options
|
this.initialized = false
|
||||||
if (options.loading === true) {
|
this.errorHandler = this.errorHandler.bind(this)
|
||||||
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')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create instance of core components
|
// Create instance of core components
|
||||||
this.moduleContainer = new Nuxt.ModuleContainer(this)
|
this.moduleContainer = new Nuxt.ModuleContainer(this)
|
||||||
this.builder = new Nuxt.Builder(this)
|
|
||||||
this.renderer = new Nuxt.Renderer(this)
|
this.renderer = new Nuxt.Renderer(this)
|
||||||
this.generator = new Nuxt.Generator(this)
|
|
||||||
|
|
||||||
// Backward compatibility
|
// Backward compatibility
|
||||||
this.render = this.renderer.render.bind(this.renderer)
|
this.render = this.renderer.render.bind(this.renderer)
|
||||||
this.renderRoute = this.renderer.renderRoute.bind(this.renderer)
|
this.renderRoute = this.renderer.renderRoute.bind(this.renderer)
|
||||||
this.renderAndGetWindow = this.renderer.renderAndGetWindow.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._init = this.init().catch(this.errorHandler)
|
||||||
this.initialized = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async init () {
|
async init () {
|
||||||
@ -79,29 +31,56 @@ export default class Nuxt extends Tapable {
|
|||||||
return this._init
|
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
|
// Wait for all components to be ready
|
||||||
// Including modules
|
|
||||||
await this.applyPluginsAsync('beforeInit')
|
await this.applyPluginsAsync('beforeInit')
|
||||||
// Including Build
|
|
||||||
await this.applyPluginsAsync('init')
|
await this.applyPluginsAsync('init')
|
||||||
// Extra jobs
|
this.initialized = true
|
||||||
this.applyPluginsAsync('afterInit').catch(this.errorHandler)
|
this.applyPluginsAsync('afterInit').catch(this.errorHandler)
|
||||||
|
|
||||||
this.initialized = true
|
|
||||||
return this
|
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 () {
|
errorHandler () {
|
||||||
// Global error handler
|
|
||||||
// Silent
|
// Silent
|
||||||
if (this.options.errorHandler === false) {
|
if (this.options.errorHandler === false) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Custom eventHandler
|
// Custom errorHandler
|
||||||
if (typeof this.options.errorHandler === 'function') {
|
if (typeof this.options.errorHandler === 'function') {
|
||||||
return this.options.errorHandler.apply(this, arguments)
|
return this.options.errorHandler.apply(this, arguments)
|
||||||
}
|
}
|
||||||
// Default
|
// Default handler
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error.apply(this, arguments)
|
console.error.apply(this, arguments)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
@ -131,6 +110,9 @@ export default class Nuxt extends Tapable {
|
|||||||
if (this.customFilesWatcher) {
|
if (this.customFilesWatcher) {
|
||||||
this.customFilesWatcher.close()
|
this.customFilesWatcher.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
promises.push(this.applyPluginsAsync('close'))
|
||||||
|
|
||||||
return Promise.all(promises).then(() => {
|
return Promise.all(promises).then(() => {
|
||||||
if (typeof callback === 'function') callback()
|
if (typeof callback === 'function') callback()
|
||||||
})
|
})
|
||||||
@ -138,10 +120,7 @@ export default class Nuxt extends Tapable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add core components to Nuxt class
|
// Add core components to Nuxt class
|
||||||
Nuxt.Defaults = Defaults
|
|
||||||
Nuxt.Utils = Utils
|
Nuxt.Utils = Utils
|
||||||
Nuxt.Renderer = Renderer
|
Nuxt.Renderer = Renderer
|
||||||
Nuxt.Builder = Builder
|
|
||||||
Nuxt.ModuleContainer = ModuleContainer
|
Nuxt.ModuleContainer = ModuleContainer
|
||||||
Nuxt.Server = Server
|
Nuxt.Server = Server
|
||||||
Nuxt.Generator = Generator
|
|
||||||
|
152
lib/renderer.js
152
lib/renderer.js
@ -7,8 +7,9 @@ import pify from 'pify'
|
|||||||
import serveStatic from 'serve-static'
|
import serveStatic from 'serve-static'
|
||||||
import compression from 'compression'
|
import compression from 'compression'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { resolve } from 'path'
|
import { resolve, join } from 'path'
|
||||||
import {readFileSync} from 'fs'
|
import fs from 'fs-extra'
|
||||||
|
import { createBundleRenderer } from 'vue-server-renderer'
|
||||||
import { getContext, setAnsiColors, encodeHtml } from './utils'
|
import { getContext, setAnsiColors, encodeHtml } from './utils'
|
||||||
|
|
||||||
const debug = require('debug')('nuxt:render')
|
const debug = require('debug')('nuxt:render')
|
||||||
@ -17,61 +18,149 @@ setAnsiColors(ansiHTML)
|
|||||||
|
|
||||||
let jsdom = null
|
let jsdom = null
|
||||||
|
|
||||||
|
const parseTemplate = templateStr => _.template(templateStr, {
|
||||||
|
interpolate: /{{([\s\S]+?)}}/g
|
||||||
|
})
|
||||||
|
|
||||||
export default class Renderer extends Tapable {
|
export default class Renderer extends Tapable {
|
||||||
constructor (nuxt) {
|
constructor (nuxt) {
|
||||||
super()
|
super()
|
||||||
this.nuxt = nuxt
|
this.nuxt = nuxt
|
||||||
this.options = nuxt.options
|
this.options = nuxt.options
|
||||||
|
|
||||||
this.nuxt.plugin('init', () => {
|
// Will be loaded by createRenderer
|
||||||
// For serving static/ files to /
|
this.bundleRenderer = null
|
||||||
this.serveStatic = pify(serveStatic(resolve(this.options.srcDir, 'static'), this.options.render.static))
|
this.renderToStream = null
|
||||||
|
this.renderToString = null
|
||||||
|
|
||||||
// For serving .nuxt/dist/ files (only when build.publicPath is not an URL)
|
if (nuxt.initialized) {
|
||||||
this.serveStaticNuxt = pify(serveStatic(resolve(this.options.buildDir, 'dist'), {
|
// If nuxt already initialized
|
||||||
maxAge: (this.options.dev ? 0 : '1y') // 1 year in production
|
this._init = this.init().catch(this.nuxt.errorHandler)
|
||||||
}))
|
} else {
|
||||||
|
// Wait for hook
|
||||||
|
this.nuxt.plugin('init', this.init.bind(this))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// gzip middleware for production
|
async init () {
|
||||||
if (!this.options.dev && this.options.render.gzip) {
|
if (this._init) {
|
||||||
this.gzipMiddleware = pify(compression(this.options.render.gzip))
|
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))
|
||||||
|
|
||||||
|
// For serving .nuxt/dist/ files (only when build.publicPath is not an URL)
|
||||||
|
this.serveStaticNuxt = pify(serveStatic(resolve(this.options.buildDir, 'dist'), {
|
||||||
|
maxAge: (this.options.dev ? 0 : '1y') // 1 year in production
|
||||||
|
}))
|
||||||
|
|
||||||
|
// gzip middleware for production
|
||||||
|
if (!this.options.dev && this.options.render.gzip) {
|
||||||
|
this.gzipMiddleware = pify(compression(this.options.render.gzip))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Error template
|
Object.keys(resourceMap).forEach(resourceKey => {
|
||||||
this.errorTemplate = _.template(readFileSync(resolve(__dirname, 'views', 'error.html'), 'utf8'), {
|
let { path, transform } = resourceMap[resourceKey]
|
||||||
interpolate: /{{([\s\S]+?)}}/g
|
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) {
|
async render (req, res) {
|
||||||
/* istanbul ignore if */
|
/* istanbul ignore if */
|
||||||
if (!this.nuxt.builder.renderer || !this.nuxt.builder.appTemplate) {
|
if (!this.bundleRenderer || !this.resources.appTemplate) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve(this.render(req, res))
|
resolve(this.render(req, res))
|
||||||
}, 1000)
|
}, 1000)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get context
|
// Get context
|
||||||
const context = getContext(req, res)
|
const context = getContext(req, res)
|
||||||
res.statusCode = 200
|
res.statusCode = 200
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.options.dev) {
|
// Call webpack middleware only in development
|
||||||
// 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.webpackDevMiddleware(req, res)
|
||||||
await this.nuxt.builder.webpackHotMiddleware(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)
|
await this.gzipMiddleware(req, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If base in req.url, remove it for the middleware and vue-router
|
// 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) {
|
if (this.options.router.base !== '/' && req.url.indexOf(this.options.router.base) === 0) {
|
||||||
// Compatibility with base url for dev server
|
// Compatibility with base url for dev server
|
||||||
req.url = req.url.replace(this.options.router.base, '/')
|
req.url = req.url.replace(this.options.router.base, '/')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve static/ files
|
// Serve static/ files
|
||||||
await this.serveStatic(req, res)
|
await this.serveStatic(req, res)
|
||||||
|
|
||||||
// Serve .nuxt/dist/ files (only for production)
|
// Serve .nuxt/dist/ files (only for production)
|
||||||
if (!this.options.dev && req.url.indexOf(this.options.build.publicPath) === 0) {
|
if (!this.options.dev && req.url.indexOf(this.options.build.publicPath) === 0) {
|
||||||
const url = req.url
|
const url = req.url
|
||||||
@ -80,17 +169,22 @@ export default class Renderer extends Tapable {
|
|||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
req.url = url
|
req.url = url
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.dev && req.url.indexOf(this.options.build.publicPath) === 0 && req.url.includes('.hot-update.json')) {
|
if (this.options.dev && req.url.indexOf(this.options.build.publicPath) === 0 && req.url.includes('.hot-update.json')) {
|
||||||
res.statusCode = 404
|
res.statusCode = 404
|
||||||
return res.end()
|
return res.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
const { html, error, redirected, resourceHints } = await this.renderRoute(req.url, context)
|
const { html, error, redirected, resourceHints } = await this.renderRoute(req.url, context)
|
||||||
|
|
||||||
if (redirected) {
|
if (redirected) {
|
||||||
return html
|
return html
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
res.statusCode = context.nuxt.error.statusCode || 500
|
res.statusCode = context.nuxt.error.statusCode || 500
|
||||||
}
|
}
|
||||||
|
|
||||||
// ETag header
|
// ETag header
|
||||||
if (!error && this.options.render.etag) {
|
if (!error && this.options.render.etag) {
|
||||||
const etag = generateETag(html, 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)
|
res.setHeader('ETag', etag)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP2 push headers
|
// HTTP2 push headers
|
||||||
if (!error && this.options.render.http2.push) {
|
if (!error && this.options.render.http2.push) {
|
||||||
// Parse resourceHints to extract HTTP.2 prefetch/push headers
|
// 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
|
// https://blog.cloudflare.com/http-2-server-push-with-multiple-assets-per-link-header
|
||||||
res.setHeader('Link', pushAssets.join(','))
|
res.setHeader('Link', pushAssets.join(','))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send response
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||||||
res.setHeader('Content-Length', Buffer.byteLength(html))
|
res.setHeader('Content-Length', Buffer.byteLength(html))
|
||||||
res.end(html, 'utf8')
|
res.end(html, 'utf8')
|
||||||
@ -127,11 +224,13 @@ export default class Renderer extends Tapable {
|
|||||||
console.error(err) // eslint-disable-line no-console
|
console.error(err) // eslint-disable-line no-console
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
const html = this.errorTemplate({
|
// Render error template
|
||||||
|
const html = this.resources.errorTemplate({
|
||||||
/* istanbul ignore if */
|
/* istanbul ignore if */
|
||||||
error: err,
|
error: err,
|
||||||
stack: ansiHTML(encodeHtml(err.stack))
|
stack: ansiHTML(encodeHtml(err.stack))
|
||||||
})
|
})
|
||||||
|
// Send response
|
||||||
res.statusCode = 500
|
res.statusCode = 500
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||||||
res.setHeader('Content-Length', Buffer.byteLength(html))
|
res.setHeader('Content-Length', Buffer.byteLength(html))
|
||||||
@ -143,29 +242,34 @@ export default class Renderer extends Tapable {
|
|||||||
async renderRoute (url, context = {}) {
|
async renderRoute (url, context = {}) {
|
||||||
// Log rendered url
|
// Log rendered url
|
||||||
debug(`Rendering url ${url}`)
|
debug(`Rendering url ${url}`)
|
||||||
|
|
||||||
// Add url and isSever to the context
|
// Add url and isSever to the context
|
||||||
context.url = url
|
context.url = url
|
||||||
context.isServer = true
|
context.isServer = true
|
||||||
|
|
||||||
// Call renderToString from the bundleRenderer and generate the HTML (will update the context as well)
|
// 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) {
|
if (!context.nuxt.serverRendered) {
|
||||||
APP = '<div id="__nuxt"></div>'
|
APP = '<div id="__nuxt"></div>'
|
||||||
}
|
}
|
||||||
const m = context.meta.inject()
|
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()
|
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}">`
|
HEAD += `<base href="${this.options.router.base}">`
|
||||||
}
|
}
|
||||||
const resourceHints = context.renderResourceHints()
|
const resourceHints = context.renderResourceHints()
|
||||||
HEAD += resourceHints + context.renderStyles()
|
HEAD += resourceHints + context.renderStyles()
|
||||||
APP += `<script type="text/javascript">window.__NUXT__=${serialize(context.nuxt, { isJSON: true })}</script>`
|
APP += `<script type="text/javascript">window.__NUXT__=${serialize(context.nuxt, { isJSON: true })}</script>`
|
||||||
APP += context.renderScripts()
|
APP += context.renderScripts()
|
||||||
const html = this.nuxt.builder.appTemplate({
|
|
||||||
|
const html = this.resources.appTemplate({
|
||||||
HTML_ATTRS: 'data-n-head-ssr ' + m.htmlAttrs.text(),
|
HTML_ATTRS: 'data-n-head-ssr ' + m.htmlAttrs.text(),
|
||||||
BODY_ATTRS: m.bodyAttrs.text(),
|
BODY_ATTRS: m.bodyAttrs.text(),
|
||||||
HEAD,
|
HEAD,
|
||||||
APP
|
APP
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
html,
|
html,
|
||||||
resourceHints,
|
resourceHints,
|
||||||
|
115
lib/utils.js
115
lib/utils.js
@ -97,3 +97,118 @@ export function r () {
|
|||||||
args = args.map(normalize)
|
args = args.map(normalize)
|
||||||
return wp(resolve.apply(null, args))
|
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)
|
||||||
|
}
|
||||||
|
@ -84,7 +84,6 @@
|
|||||||
"offline-plugin": "^4.8.1",
|
"offline-plugin": "^4.8.1",
|
||||||
"opencollective": "^1.0.3",
|
"opencollective": "^1.0.3",
|
||||||
"pify": "^3.0.0",
|
"pify": "^3.0.0",
|
||||||
"post-compile-webpack-plugin": "^0.1.1",
|
|
||||||
"preload-webpack-plugin": "^1.2.2",
|
"preload-webpack-plugin": "^1.2.2",
|
||||||
"progress-bar-webpack-plugin": "^1.9.3",
|
"progress-bar-webpack-plugin": "^1.9.3",
|
||||||
"script-ext-html-webpack-plugin": "^1.8.1",
|
"script-ext-html-webpack-plugin": "^1.8.1",
|
||||||
|
@ -18,7 +18,7 @@ test.before('Init Nuxt.js', async t => {
|
|||||||
}
|
}
|
||||||
nuxt = new Nuxt(options)
|
nuxt = new Nuxt(options)
|
||||||
await nuxt.build()
|
await nuxt.build()
|
||||||
server = new nuxt.Server(nuxt)
|
server = new Nuxt.Server(nuxt)
|
||||||
server.listen(port, 'localhost')
|
server.listen(port, 'localhost')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ test.before('Init Nuxt.js', async t => {
|
|||||||
}
|
}
|
||||||
nuxt = new Nuxt(options)
|
nuxt = new Nuxt(options)
|
||||||
await nuxt.build()
|
await nuxt.build()
|
||||||
server = new nuxt.Server(nuxt)
|
server = new Nuxt.Server(nuxt)
|
||||||
server.listen(port, 'localhost')
|
server.listen(port, 'localhost')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ test.before('Init Nuxt.js', async t => {
|
|||||||
}
|
}
|
||||||
nuxt = new Nuxt(options)
|
nuxt = new Nuxt(options)
|
||||||
await nuxt.build()
|
await nuxt.build()
|
||||||
server = new nuxt.Server(nuxt)
|
server = new Nuxt.Server(nuxt)
|
||||||
server.listen(port, 'localhost')
|
server.listen(port, 'localhost')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ test.before('Init Nuxt.js', async t => {
|
|||||||
config.dev = false
|
config.dev = false
|
||||||
nuxt = new Nuxt(config)
|
nuxt = new Nuxt(config)
|
||||||
await nuxt.build()
|
await nuxt.build()
|
||||||
server = new nuxt.Server(nuxt)
|
server = new Nuxt.Server(nuxt)
|
||||||
server.listen(port, 'localhost')
|
server.listen(port, 'localhost')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user