mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-27 16:12:12 +00:00
refactor into components
This commit is contained in:
parent
5237764573
commit
8fe9380df9
569
lib/build.js
569
lib/build.js
@ -1,569 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
import _ from 'lodash'
|
|
||||||
import chokidar from 'chokidar'
|
|
||||||
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 { join, resolve, basename, dirname } from 'path'
|
|
||||||
import { isUrl, r, wp } from './utils'
|
|
||||||
import clientWebpackConfig from './webpack/client.config.js'
|
|
||||||
import serverWebpackConfig from './webpack/server.config.js'
|
|
||||||
const debug = require('debug')('nuxt:build')
|
|
||||||
const remove = pify(fs.remove)
|
|
||||||
const readFile = pify(fs.readFile)
|
|
||||||
const utimes = pify(fs.utimes)
|
|
||||||
const writeFile = pify(fs.writeFile)
|
|
||||||
const mkdirp = pify(fs.mkdirp)
|
|
||||||
const glob = pify(require('glob'))
|
|
||||||
|
|
||||||
let webpackStats = 'none'
|
|
||||||
debug.color = 2 // force green color
|
|
||||||
|
|
||||||
const defaults = {
|
|
||||||
analyze: false,
|
|
||||||
extractCSS: false,
|
|
||||||
publicPath: '/_nuxt/',
|
|
||||||
filenames: {
|
|
||||||
css: 'common.[chunkhash].css',
|
|
||||||
manifest: 'manifest.[hash].js',
|
|
||||||
vendor: 'vendor.bundle.[chunkhash].js',
|
|
||||||
app: 'nuxt.bundle.[chunkhash].js'
|
|
||||||
},
|
|
||||||
vendor: [],
|
|
||||||
loaders: [],
|
|
||||||
plugins: [],
|
|
||||||
babel: {},
|
|
||||||
postcss: [],
|
|
||||||
templates: [],
|
|
||||||
watch: []
|
|
||||||
}
|
|
||||||
const defaultsLoaders = [
|
|
||||||
{
|
|
||||||
test: /\.(png|jpe?g|gif|svg)$/,
|
|
||||||
loader: 'url-loader',
|
|
||||||
query: {
|
|
||||||
limit: 1000, // 1KO
|
|
||||||
name: 'img/[name].[hash:7].[ext]'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
|
||||||
loader: 'url-loader',
|
|
||||||
query: {
|
|
||||||
limit: 1000, // 1 KO
|
|
||||||
name: 'fonts/[name].[hash:7].[ext]'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
const defaultsPostcss = [
|
|
||||||
require('autoprefixer')({
|
|
||||||
browsers: ['last 3 versions']
|
|
||||||
})
|
|
||||||
]
|
|
||||||
|
|
||||||
export function options () {
|
|
||||||
// Defaults build options
|
|
||||||
let extraDefaults = {}
|
|
||||||
if (this.options.build && !Array.isArray(this.options.build.loaders)) extraDefaults.loaders = defaultsLoaders
|
|
||||||
if (this.options.build && !Array.isArray(this.options.build.postcss)) extraDefaults.postcss = defaultsPostcss
|
|
||||||
this.options.build = _.defaultsDeep(this.options.build, defaults, extraDefaults)
|
|
||||||
/* istanbul ignore if */
|
|
||||||
if (this.dev && isUrl(this.options.build.publicPath)) {
|
|
||||||
this.options.build.publicPath = defaults.publicPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function production () {
|
|
||||||
// Production, create server-renderer
|
|
||||||
webpackStats = {
|
|
||||||
chunks: false,
|
|
||||||
children: false,
|
|
||||||
modules: false,
|
|
||||||
colors: true
|
|
||||||
}
|
|
||||||
const serverConfig = getWebpackServerConfig.call(this)
|
|
||||||
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)) {
|
|
||||||
const bundle = fs.readFileSync(bundlePath, 'utf8')
|
|
||||||
const manifest = fs.readFileSync(manifestPath, 'utf8')
|
|
||||||
createRenderer.call(this, JSON.parse(bundle), JSON.parse(manifest))
|
|
||||||
addAppTemplate.call(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function build () {
|
|
||||||
// Avoid calling this method multiple times
|
|
||||||
if (this._buildDone) {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
// If building
|
|
||||||
if (this._building) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve(this.build())
|
|
||||||
}, 300)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this._building = true
|
|
||||||
// Wait for Nuxt.js to be ready
|
|
||||||
await this.ready()
|
|
||||||
// Check if pages dir exists and warn if not
|
|
||||||
this._nuxtPages = typeof this.createRoutes !== 'function'
|
|
||||||
if (this._nuxtPages) {
|
|
||||||
if (!fs.existsSync(join(this.srcDir, 'pages'))) {
|
|
||||||
if (fs.existsSync(join(this.srcDir, '..', 'pages'))) {
|
|
||||||
console.error('> No `pages` directory found. Did you mean to run `nuxt` in the parent (`../`) directory?') // eslint-disable-line no-console
|
|
||||||
} else {
|
|
||||||
console.error('> Couldn\'t find a `pages` directory. Please create one under the project root') // eslint-disable-line no-console
|
|
||||||
}
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
debug(`App root: ${this.srcDir}`)
|
|
||||||
debug(`Generating ${this.buildDir} files...`)
|
|
||||||
// Create .nuxt/, .nuxt/components and .nuxt/dist folders
|
|
||||||
await remove(r(this.buildDir))
|
|
||||||
await mkdirp(r(this.buildDir, 'components'))
|
|
||||||
if (!this.dev) {
|
|
||||||
await mkdirp(r(this.buildDir, 'dist'))
|
|
||||||
}
|
|
||||||
// Generate routes and interpret the template files
|
|
||||||
await generateRoutesAndFiles.call(this)
|
|
||||||
// Generate .nuxt/dist/ files
|
|
||||||
await buildFiles.call(this)
|
|
||||||
// Flag to set that building is done
|
|
||||||
this._buildDone = true
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildFiles () {
|
|
||||||
if (this.dev) {
|
|
||||||
debug('Adding webpack middleware...')
|
|
||||||
createWebpackMiddleware.call(this)
|
|
||||||
webpackWatchAndUpdate.call(this)
|
|
||||||
watchFiles.call(this)
|
|
||||||
} else {
|
|
||||||
debug('Building files...')
|
|
||||||
await webpackRunClient.call(this)
|
|
||||||
await webpackRunServer.call(this)
|
|
||||||
addAppTemplate.call(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addAppTemplate () {
|
|
||||||
let templatePath = resolve(this.buildDir, 'dist', 'index.html')
|
|
||||||
if (fs.existsSync(templatePath)) {
|
|
||||||
this.appTemplate = _.template(fs.readFileSync(templatePath, 'utf8'), {
|
|
||||||
interpolate: /{{([\s\S]+?)}}/g
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateRoutesAndFiles () {
|
|
||||||
debug('Generating files...')
|
|
||||||
// -- Templates --
|
|
||||||
let templatesFiles = [
|
|
||||||
'App.vue',
|
|
||||||
'client.js',
|
|
||||||
'index.js',
|
|
||||||
'middleware.js',
|
|
||||||
'router.js',
|
|
||||||
'server.js',
|
|
||||||
'utils.js',
|
|
||||||
'components/nuxt-error.vue',
|
|
||||||
'components/nuxt-loading.vue',
|
|
||||||
'components/nuxt-child.js',
|
|
||||||
'components/nuxt-link.js',
|
|
||||||
'components/nuxt.vue'
|
|
||||||
]
|
|
||||||
const templateVars = {
|
|
||||||
options: this.options,
|
|
||||||
uniqBy: _.uniqBy,
|
|
||||||
isDev: this.dev,
|
|
||||||
router: {
|
|
||||||
mode: this.options.router.mode,
|
|
||||||
base: this.options.router.base,
|
|
||||||
middleware: this.options.router.middleware,
|
|
||||||
linkActiveClass: this.options.router.linkActiveClass,
|
|
||||||
linkExactActiveClass: this.options.router.linkExactActiveClass,
|
|
||||||
scrollBehavior: this.options.router.scrollBehavior
|
|
||||||
},
|
|
||||||
env: this.options.env,
|
|
||||||
head: this.options.head,
|
|
||||||
middleware: fs.existsSync(join(this.srcDir, 'middleware')),
|
|
||||||
store: this.options.store || fs.existsSync(join(this.srcDir, 'store')),
|
|
||||||
css: this.options.css,
|
|
||||||
plugins: this.options.plugins.map((p, i) => {
|
|
||||||
if (typeof p === 'string') p = { src: p }
|
|
||||||
p.src = r(this.srcDir, p.src)
|
|
||||||
return { src: p.src, ssr: (p.ssr !== false), name: `plugin${i}` }
|
|
||||||
}),
|
|
||||||
appPath: './App.vue',
|
|
||||||
layouts: Object.assign({}, this.options.layouts),
|
|
||||||
loading: (typeof this.options.loading === 'string' ? r(this.srcDir, this.options.loading) : this.options.loading),
|
|
||||||
transition: this.options.transition,
|
|
||||||
components: {
|
|
||||||
ErrorPage: this.options.ErrorPage ? r(this.options.ErrorPage) : null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Layouts --
|
|
||||||
if (fs.existsSync(resolve(this.srcDir, 'layouts'))) {
|
|
||||||
const layoutsFiles = await glob('layouts/*.vue', {cwd: this.srcDir})
|
|
||||||
layoutsFiles.forEach((file) => {
|
|
||||||
let name = file.split('/').slice(-1)[0].replace('.vue', '')
|
|
||||||
if (name === 'error') return
|
|
||||||
templateVars.layouts[name] = r(this.srcDir, file)
|
|
||||||
})
|
|
||||||
if (layoutsFiles.includes('layouts/error.vue')) {
|
|
||||||
templateVars.components.ErrorPage = r(this.srcDir, 'layouts/error.vue')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If no default layout, create its folder and add the default folder
|
|
||||||
if (!templateVars.layouts.default) {
|
|
||||||
await mkdirp(r(this.buildDir, 'layouts'))
|
|
||||||
templatesFiles.push('layouts/default.vue')
|
|
||||||
templateVars.layouts.default = r(__dirname, 'app', 'layouts', 'default.vue')
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Routes --
|
|
||||||
debug('Generating routes...')
|
|
||||||
// If user defined a custom method to create routes
|
|
||||||
if (this._nuxtPages) {
|
|
||||||
// Use nuxt.js createRoutes bases on pages/
|
|
||||||
const files = await glob('pages/**/*.vue', {cwd: this.srcDir})
|
|
||||||
templateVars.router.routes = createRoutes(files, this.srcDir)
|
|
||||||
} else {
|
|
||||||
templateVars.router.routes = this.createRoutes(this.srcDir)
|
|
||||||
}
|
|
||||||
// router.extendRoutes method
|
|
||||||
if (typeof this.options.router.extendRoutes === 'function') {
|
|
||||||
// let the user extend the routes
|
|
||||||
this.options.router.extendRoutes.call(this, templateVars.router.routes || [], r)
|
|
||||||
}
|
|
||||||
// Routes for generate command
|
|
||||||
this.routes = flatRoutes(templateVars.router.routes || [])
|
|
||||||
|
|
||||||
// -- Store --
|
|
||||||
// Add store if needed
|
|
||||||
if (this.options.store) {
|
|
||||||
templatesFiles.push('store.js')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve template files
|
|
||||||
const customTemplateFiles = this.options.build.templates.map(t => t.dst || basename(t.src || t))
|
|
||||||
templatesFiles = templatesFiles.map(file => {
|
|
||||||
// Skip if custom file was already provided in build.templates[]
|
|
||||||
if (customTemplateFiles.indexOf(file) !== -1) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Allow override templates using a file with same name in ${srcDir}/app
|
|
||||||
const customPath = r(this.srcDir, 'app', file)
|
|
||||||
const customFileExists = fs.existsSync(customPath)
|
|
||||||
return {
|
|
||||||
src: customFileExists ? customPath : r(__dirname, 'app', file),
|
|
||||||
dst: file,
|
|
||||||
custom: customFileExists
|
|
||||||
}
|
|
||||||
}).filter(i => !!i)
|
|
||||||
|
|
||||||
// -- Custom templates --
|
|
||||||
// Add custom template files
|
|
||||||
templatesFiles = templatesFiles.concat(this.options.build.templates.map(t => {
|
|
||||||
return Object.assign({
|
|
||||||
src: r(this.dir, t.src || t),
|
|
||||||
dst: t.dst || basename(t.src || t),
|
|
||||||
custom: true
|
|
||||||
}, t)
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Interpret and move template files to .nuxt/
|
|
||||||
return Promise.all(templatesFiles.map(async ({ src, dst, options, custom }) => {
|
|
||||||
// Add template to watchers
|
|
||||||
this.options.build.watch.push(src)
|
|
||||||
// Render template to dst
|
|
||||||
const fileContent = await readFile(src, 'utf8')
|
|
||||||
const template = _.template(fileContent, {
|
|
||||||
imports: {
|
|
||||||
serialize,
|
|
||||||
hash,
|
|
||||||
r,
|
|
||||||
wp
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const content = template(Object.assign({}, templateVars, {
|
|
||||||
options: options || {},
|
|
||||||
custom,
|
|
||||||
src,
|
|
||||||
dst
|
|
||||||
}))
|
|
||||||
const path = r(this.buildDir, dst)
|
|
||||||
// Ensure parent dir exits
|
|
||||||
await mkdirp(dirname(path))
|
|
||||||
// Write file
|
|
||||||
await writeFile(path, content, 'utf8')
|
|
||||||
// Fix webpack loop (https://github.com/webpack/watchpack/issues/25#issuecomment-287789288)
|
|
||||||
const dateFS = Date.now() / 1000 - 30
|
|
||||||
return utimes(path, dateFS, dateFS)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
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 }
|
|
||||||
var res = 0
|
|
||||||
var _a = a.path.split('/')
|
|
||||||
var _b = b.path.split('/')
|
|
||||||
for (var i = 0; i < _a.length; i++) {
|
|
||||||
if (res !== 0) { break }
|
|
||||||
var y = (_a[i].indexOf('*') > -1) ? 2 : (_a[i].indexOf(':') > -1 ? 1 : 0)
|
|
||||||
var 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (var 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWebpackClientConfig () {
|
|
||||||
return clientWebpackConfig.call(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWebpackServerConfig () {
|
|
||||||
return serverWebpackConfig.call(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
function createWebpackMiddleware () {
|
|
||||||
const clientConfig = getWebpackClientConfig.call(this)
|
|
||||||
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: 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()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function webpackWatchAndUpdate () {
|
|
||||||
const MFS = require('memory-fs') // <- dependencies of webpack
|
|
||||||
const serverFS = new MFS()
|
|
||||||
const clientFS = this.clientCompiler.outputFileSystem
|
|
||||||
const serverConfig = getWebpackServerConfig.call(this)
|
|
||||||
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')
|
|
||||||
createRenderer.call(this, JSON.parse(bundle), JSON.parse(manifest))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.watchHandler = watchHandler
|
|
||||||
this.webpackServerWatcher = serverCompiler.watch(this.options.watchers.webpack, watchHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
function webpackRunClient () {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const clientConfig = getWebpackClientConfig.call(this)
|
|
||||||
const clientCompiler = webpack(clientConfig)
|
|
||||||
clientCompiler.run((err, stats) => {
|
|
||||||
if (err) return reject(err)
|
|
||||||
console.log('[nuxt:build:client]\n', stats.toString(webpackStats)) // eslint-disable-line no-console
|
|
||||||
if (stats.hasErrors()) return reject(new Error('Webpack build exited with errors'))
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function webpackRunServer () {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const serverConfig = getWebpackServerConfig.call(this)
|
|
||||||
const serverCompiler = webpack(serverConfig)
|
|
||||||
serverCompiler.run((err, stats) => {
|
|
||||||
if (err) return reject(err)
|
|
||||||
console.log('[nuxt:build:server]\n', stats.toString(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 => {
|
|
||||||
createRenderer.call(this, JSON.parse(bundle), JSON.parse(manifest))
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function 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.dir
|
|
||||||
}, this.options.build.ssr))
|
|
||||||
this.renderToString = pify(this.renderer.renderToString)
|
|
||||||
this.renderToStream = this.renderer.renderToStream
|
|
||||||
}
|
|
||||||
|
|
||||||
function watchFiles () {
|
|
||||||
const patterns = [
|
|
||||||
r(this.srcDir, 'layouts'),
|
|
||||||
r(this.srcDir, 'store'),
|
|
||||||
r(this.srcDir, 'middleware'),
|
|
||||||
r(this.srcDir, 'layouts/*.vue'),
|
|
||||||
r(this.srcDir, 'layouts/**/*.vue')
|
|
||||||
]
|
|
||||||
if (this._nuxtPages) {
|
|
||||||
patterns.push(r(this.srcDir, 'pages'))
|
|
||||||
patterns.push(r(this.srcDir, 'pages/*.vue'))
|
|
||||||
patterns.push(r(this.srcDir, 'pages/**/*.vue'))
|
|
||||||
}
|
|
||||||
const options = Object.assign({}, this.options.watchers.chokidar, {
|
|
||||||
ignoreInitial: true
|
|
||||||
})
|
|
||||||
/* istanbul ignore next */
|
|
||||||
const refreshFiles = _.debounce(async () => {
|
|
||||||
await generateRoutesAndFiles.call(this)
|
|
||||||
}, 200)
|
|
||||||
// Watch for internals
|
|
||||||
this.filesWatcher = chokidar.watch(patterns, options)
|
|
||||||
.on('add', refreshFiles)
|
|
||||||
.on('unlink', refreshFiles)
|
|
||||||
// Watch for custom provided files
|
|
||||||
this.customFilesWatcher = chokidar.watch(_.uniq(this.options.build.watch), options)
|
|
||||||
.on('change', refreshFiles)
|
|
||||||
}
|
|
601
lib/builder.js
Normal file
601
lib/builder.js
Normal file
@ -0,0 +1,601 @@
|
|||||||
|
import _ from 'lodash'
|
||||||
|
import chokidar from 'chokidar'
|
||||||
|
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 { join, resolve, basename, dirname } from 'path'
|
||||||
|
import { isUrl, r, wp } from './utils'
|
||||||
|
import clientWebpackConfig from './webpack/client.config.js'
|
||||||
|
import serverWebpackConfig from './webpack/server.config.js'
|
||||||
|
import defaults from './defaults'
|
||||||
|
import Tapable from 'tapable'
|
||||||
|
|
||||||
|
const debug = require('debug')('nuxt:build')
|
||||||
|
debug.color = 2 // Force green color
|
||||||
|
|
||||||
|
const remove = pify(fs.remove)
|
||||||
|
const readFile = pify(fs.readFile)
|
||||||
|
const utimes = pify(fs.utimes)
|
||||||
|
const writeFile = pify(fs.writeFile)
|
||||||
|
const mkdirp = pify(fs.mkdirp)
|
||||||
|
const glob = pify(require('glob'))
|
||||||
|
|
||||||
|
export default class Builder extends Tapable {
|
||||||
|
constructor (nuxt) {
|
||||||
|
super()
|
||||||
|
this.nuxt = nuxt
|
||||||
|
this.options = nuxt.options
|
||||||
|
|
||||||
|
// Add extra loaders only if they are not already provided
|
||||||
|
let extraDefaults = {}
|
||||||
|
if (this.options.build && !Array.isArray(this.options.build.loaders)) {
|
||||||
|
extraDefaults.loaders = defaultsLoaders
|
||||||
|
}
|
||||||
|
if (this.options.build && !Array.isArray(this.options.build.postcss)) {
|
||||||
|
extraDefaults.postcss = defaultsPostcss
|
||||||
|
}
|
||||||
|
this.options.build = _.defaultsDeep(this.options.build, extraDefaults)
|
||||||
|
/* istanbul ignore if */
|
||||||
|
if (this.options.dev && isUrl(this.options.build.publicPath)) {
|
||||||
|
this.options.build.publicPath = defaults.publicPath.publicPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
this.webpackStats = {
|
||||||
|
chunks: false,
|
||||||
|
children: false,
|
||||||
|
modules: false,
|
||||||
|
colors: true
|
||||||
|
}
|
||||||
|
|
||||||
|
this._buildStatus = STATUS.INITIAL
|
||||||
|
}
|
||||||
|
|
||||||
|
ready () {
|
||||||
|
if (this.options.dev) {
|
||||||
|
// Don't await for builder in dev (faster startup)
|
||||||
|
this.build().catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
return Promise.resolve(this)
|
||||||
|
} else {
|
||||||
|
return this.production()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async build () {
|
||||||
|
// Avoid calling this method multiple times
|
||||||
|
if (this._buildStatus === STATUS.BUILD_DONE) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
// If building
|
||||||
|
if (this._buildStatus === STATUS.BUILDING) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(this.build())
|
||||||
|
}, 300)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this._buildStatus = STATUS.BUILDING
|
||||||
|
// 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'))) {
|
||||||
|
if (fs.existsSync(join(this.options.srcDir, '..', 'pages'))) {
|
||||||
|
console.error('> No `pages` directory found. Did you mean to run `nuxt` in the parent (`../`) directory?') // eslint-disable-line no-console
|
||||||
|
} else {
|
||||||
|
console.error('> Couldn\'t find a `pages` directory. Please create one under the project root') // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug(`App root: ${this.options.srcDir}`)
|
||||||
|
debug(`Generating ${this.options.buildDir} files...`)
|
||||||
|
|
||||||
|
// Create .nuxt/, .nuxt/components and .nuxt/dist folders
|
||||||
|
await remove(r(this.options.buildDir))
|
||||||
|
await mkdirp(r(this.options.buildDir, 'components'))
|
||||||
|
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()
|
||||||
|
// Flag to set that building is done
|
||||||
|
this._buildStatus = STATUS.BUILD_DONE
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
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 --
|
||||||
|
let templatesFiles = [
|
||||||
|
'App.vue',
|
||||||
|
'client.js',
|
||||||
|
'index.js',
|
||||||
|
'middleware.js',
|
||||||
|
'router.js',
|
||||||
|
'server.js',
|
||||||
|
'utils.js',
|
||||||
|
'components/nuxt-error.vue',
|
||||||
|
'components/nuxt-loading.vue',
|
||||||
|
'components/nuxt-child.js',
|
||||||
|
'components/nuxt-link.js',
|
||||||
|
'components/nuxt.vue'
|
||||||
|
]
|
||||||
|
const templateVars = {
|
||||||
|
options: this.options,
|
||||||
|
uniqBy: _.uniqBy,
|
||||||
|
isDev: this.options.dev,
|
||||||
|
router: {
|
||||||
|
mode: this.options.router.mode,
|
||||||
|
base: this.options.router.base,
|
||||||
|
middleware: this.options.router.middleware,
|
||||||
|
linkActiveClass: this.options.router.linkActiveClass,
|
||||||
|
linkExactActiveClass: this.options.router.linkExactActiveClass,
|
||||||
|
scrollBehavior: this.options.router.scrollBehavior
|
||||||
|
},
|
||||||
|
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')),
|
||||||
|
css: this.options.css,
|
||||||
|
plugins: this.options.plugins.map((p, i) => {
|
||||||
|
if (typeof p === 'string') p = { src: p }
|
||||||
|
p.src = r(this.options.srcDir, p.src)
|
||||||
|
return { src: p.src, ssr: (p.ssr !== false), name: `plugin${i}` }
|
||||||
|
}),
|
||||||
|
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),
|
||||||
|
transition: this.options.transition,
|
||||||
|
components: {
|
||||||
|
ErrorPage: this.options.ErrorPage ? r(this.options.ErrorPage) : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Layouts --
|
||||||
|
if (fs.existsSync(resolve(this.options.srcDir, 'layouts'))) {
|
||||||
|
const layoutsFiles = await glob('layouts/*.vue', { cwd: this.options.srcDir })
|
||||||
|
layoutsFiles.forEach((file) => {
|
||||||
|
let name = file.split('/').slice(-1)[0].replace('.vue', '')
|
||||||
|
if (name === 'error') return
|
||||||
|
templateVars.layouts[name] = r(this.options.srcDir, file)
|
||||||
|
})
|
||||||
|
if (layoutsFiles.includes('layouts/error.vue')) {
|
||||||
|
templateVars.components.ErrorPage = r(this.options.srcDir, 'layouts/error.vue')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If no default layout, create its folder and add the default folder
|
||||||
|
if (!templateVars.layouts.default) {
|
||||||
|
await mkdirp(r(this.options.buildDir, 'layouts'))
|
||||||
|
templatesFiles.push('layouts/default.vue')
|
||||||
|
templateVars.layouts.default = r(__dirname, 'app', 'layouts', 'default.vue')
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Routes --
|
||||||
|
debug('Generating routes...')
|
||||||
|
// If user defined a custom method to create routes
|
||||||
|
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)
|
||||||
|
} else {
|
||||||
|
templateVars.router.routes = this.options.build.createRoutes(this.options.srcDir)
|
||||||
|
}
|
||||||
|
// router.extendRoutes method
|
||||||
|
if (typeof this.options.router.extendRoutes === 'function') {
|
||||||
|
// let the user extend the routes
|
||||||
|
this.options.router.extendRoutes(this, templateVars.router.routes || [], r)
|
||||||
|
}
|
||||||
|
// Routes for generate command
|
||||||
|
this.routes = this.flatRoutes(templateVars.router.routes || [])
|
||||||
|
|
||||||
|
// -- Store --
|
||||||
|
// Add store if needed
|
||||||
|
if (this.options.store) {
|
||||||
|
templatesFiles.push('store.js')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve template files
|
||||||
|
const customTemplateFiles = this.options.build.templates.map(t => t.dst || basename(t.src || t))
|
||||||
|
|
||||||
|
templatesFiles = templatesFiles.map(file => {
|
||||||
|
// Skip if custom file was already provided in build.templates[]
|
||||||
|
if (customTemplateFiles.indexOf(file) !== -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Allow override templates using a file with same name in ${srcDir}/app
|
||||||
|
const customPath = r(this.options.srcDir, 'app', file)
|
||||||
|
const customFileExists = fs.existsSync(customPath)
|
||||||
|
|
||||||
|
return {
|
||||||
|
src: customFileExists ? customPath : r(__dirname, 'app', file),
|
||||||
|
dst: file,
|
||||||
|
custom: customFileExists
|
||||||
|
}
|
||||||
|
}).filter(i => !!i)
|
||||||
|
|
||||||
|
// -- Custom templates --
|
||||||
|
// Add custom template files
|
||||||
|
templatesFiles = templatesFiles.concat(this.options.build.templates.map(t => {
|
||||||
|
return Object.assign({
|
||||||
|
src: r(this.options.srcDir, t.src || t),
|
||||||
|
dst: t.dst || basename(t.src || t),
|
||||||
|
custom: true
|
||||||
|
}, t)
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Interpret and move template files to .nuxt/
|
||||||
|
return Promise.all(templatesFiles.map(async ({ src, dst, options, custom }) => {
|
||||||
|
// Add template to watchers
|
||||||
|
this.options.build.watch.push(src)
|
||||||
|
// Render template to dst
|
||||||
|
const fileContent = await readFile(src, 'utf8')
|
||||||
|
const template = _.template(fileContent, {
|
||||||
|
imports: {
|
||||||
|
serialize,
|
||||||
|
hash,
|
||||||
|
r,
|
||||||
|
wp
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const content = template(Object.assign({}, templateVars, {
|
||||||
|
options: options || {},
|
||||||
|
custom,
|
||||||
|
src,
|
||||||
|
dst
|
||||||
|
}))
|
||||||
|
const path = r(this.options.buildDir, dst)
|
||||||
|
// Ensure parent dir exits
|
||||||
|
await mkdirp(dirname(path))
|
||||||
|
// Write file
|
||||||
|
await writeFile(path, content, 'utf8')
|
||||||
|
// Fix webpack loop (https://github.com/webpack/watchpack/issues/25#issuecomment-287789288)
|
||||||
|
const dateFS = Date.now() / 1000 - 30
|
||||||
|
return utimes(path, dateFS, dateFS)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async buildFiles () {
|
||||||
|
if (this.options.dev) {
|
||||||
|
debug('Adding webpack middleware...')
|
||||||
|
this.createWebpackMiddleware()
|
||||||
|
this.webpackWatchAndUpdate()
|
||||||
|
this.watchFiles()
|
||||||
|
} else {
|
||||||
|
debug('Building files...')
|
||||||
|
await this.webpackRunClient()
|
||||||
|
await this.webpackRunServer()
|
||||||
|
this.addAppTemplate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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) {
|
||||||
|
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) => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
watchFiles () {
|
||||||
|
const patterns = [
|
||||||
|
r(this.options.srcDir, 'layouts'),
|
||||||
|
r(this.options.srcDir, 'store'),
|
||||||
|
r(this.options.srcDir, 'middleware'),
|
||||||
|
r(this.options.srcDir, 'layouts/*.vue'),
|
||||||
|
r(this.options.srcDir, 'layouts/**/*.vue')
|
||||||
|
]
|
||||||
|
if (this._nuxtPages) {
|
||||||
|
patterns.push(r(this.options.srcDir, 'pages'))
|
||||||
|
patterns.push(r(this.options.srcDir, 'pages/*.vue'))
|
||||||
|
patterns.push(r(this.options.srcDir, 'pages/**/*.vue'))
|
||||||
|
}
|
||||||
|
const options = Object.assign({}, this.options.watchers.chokidar, {
|
||||||
|
ignoreInitial: true
|
||||||
|
})
|
||||||
|
/* istanbul ignore next */
|
||||||
|
const refreshFiles = _.debounce(this.generateRoutesAndFiles, 200)
|
||||||
|
// Watch for internals
|
||||||
|
this.filesWatcher = chokidar.watch(patterns, options)
|
||||||
|
.on('add', refreshFiles)
|
||||||
|
.on('unlink', refreshFiles)
|
||||||
|
// Watch for custom provided files
|
||||||
|
this.customFilesWatcher = chokidar.watch(_.uniq(this.options.build.watch), options)
|
||||||
|
.on('change', refreshFiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultsLoaders = [
|
||||||
|
{
|
||||||
|
test: /\.(png|jpe?g|gif|svg)$/,
|
||||||
|
loader: 'url-loader',
|
||||||
|
query: {
|
||||||
|
limit: 1000, // 1KO
|
||||||
|
name: 'img/[name].[hash:7].[ext]'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||||
|
loader: 'url-loader',
|
||||||
|
query: {
|
||||||
|
limit: 1000, // 1 KO
|
||||||
|
name: 'fonts/[name].[hash:7].[ext]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const defaultsPostcss = [
|
||||||
|
require('autoprefixer')({
|
||||||
|
browsers: ['last 3 versions']
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
const STATUS = {
|
||||||
|
INITIAL: 1,
|
||||||
|
BUILD_DONE: 2,
|
||||||
|
BUILDING: 3
|
||||||
|
}
|
95
lib/defaults.js
Executable file
95
lib/defaults.js
Executable file
@ -0,0 +1,95 @@
|
|||||||
|
export default {
|
||||||
|
dev: (process.env.NODE_ENV !== 'production'),
|
||||||
|
buildDir: '.nuxt',
|
||||||
|
build: {
|
||||||
|
analyze: false,
|
||||||
|
extractCSS: false,
|
||||||
|
publicPath: '/_nuxt/',
|
||||||
|
filenames: {
|
||||||
|
css: 'common.[chunkhash].css',
|
||||||
|
manifest: 'manifest.[hash].js',
|
||||||
|
vendor: 'vendor.bundle.[chunkhash].js',
|
||||||
|
app: 'nuxt.bundle.[chunkhash].js'
|
||||||
|
},
|
||||||
|
vendor: [],
|
||||||
|
loaders: [],
|
||||||
|
plugins: [],
|
||||||
|
babel: {},
|
||||||
|
postcss: [],
|
||||||
|
templates: [],
|
||||||
|
watch: []
|
||||||
|
},
|
||||||
|
generate: {
|
||||||
|
dir: 'dist',
|
||||||
|
routes: [],
|
||||||
|
interval: 0,
|
||||||
|
minify: {
|
||||||
|
collapseBooleanAttributes: true,
|
||||||
|
collapseWhitespace: true,
|
||||||
|
decodeEntities: true,
|
||||||
|
minifyCSS: true,
|
||||||
|
minifyJS: true,
|
||||||
|
processConditionalComments: true,
|
||||||
|
removeAttributeQuotes: false,
|
||||||
|
removeComments: false,
|
||||||
|
removeEmptyAttributes: true,
|
||||||
|
removeOptionalTags: true,
|
||||||
|
removeRedundantAttributes: true,
|
||||||
|
removeScriptTypeAttributes: false,
|
||||||
|
removeStyleLinkTypeAttributes: false,
|
||||||
|
removeTagWhitespace: false,
|
||||||
|
sortAttributes: true,
|
||||||
|
sortClassName: true,
|
||||||
|
trimCustomFragments: true,
|
||||||
|
useShortDoctype: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
env: {},
|
||||||
|
head: {
|
||||||
|
meta: [],
|
||||||
|
link: [],
|
||||||
|
style: [],
|
||||||
|
script: []
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
css: [],
|
||||||
|
modules: [],
|
||||||
|
layouts: {},
|
||||||
|
serverMiddleware: [],
|
||||||
|
ErrorPage: null,
|
||||||
|
loading: {
|
||||||
|
color: 'black',
|
||||||
|
failedColor: 'red',
|
||||||
|
height: '2px',
|
||||||
|
duration: 5000
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
name: 'page',
|
||||||
|
mode: 'out-in'
|
||||||
|
},
|
||||||
|
router: {
|
||||||
|
mode: 'history',
|
||||||
|
base: '/',
|
||||||
|
middleware: [],
|
||||||
|
linkActiveClass: 'nuxt-link-active',
|
||||||
|
linkExactActiveClass: 'nuxt-link-exact-active',
|
||||||
|
extendRoutes: null,
|
||||||
|
scrollBehavior: null
|
||||||
|
},
|
||||||
|
render: {
|
||||||
|
http2: {
|
||||||
|
push: false
|
||||||
|
},
|
||||||
|
static: {},
|
||||||
|
gzip: {
|
||||||
|
threshold: 0
|
||||||
|
},
|
||||||
|
etag: {
|
||||||
|
weak: true // Faster for responses > 5KB
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watchers: {
|
||||||
|
webpack: {},
|
||||||
|
chokidar: {}
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
import fs from 'fs-extra'
|
import fs from 'fs-extra'
|
||||||
import pify from 'pify'
|
import pify from 'pify'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { resolve, join, dirname, sep } from 'path'
|
import { resolve, join, dirname, sep } from 'path'
|
||||||
import { isUrl, promisifyRoute, waitFor } from './utils'
|
import { isUrl, promisifyRoute, waitFor } from './utils'
|
||||||
import { minify } from 'html-minifier'
|
import { minify } from 'html-minifier'
|
||||||
|
import Tapable from 'tapable'
|
||||||
|
|
||||||
const debug = require('debug')('nuxt:generate')
|
const debug = require('debug')('nuxt:generate')
|
||||||
const copy = pify(fs.copy)
|
const copy = pify(fs.copy)
|
||||||
const remove = pify(fs.remove)
|
const remove = pify(fs.remove)
|
||||||
@ -38,7 +38,14 @@ const defaults = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function () {
|
export default class Generator extends Tapable {
|
||||||
|
constructor (nuxt) {
|
||||||
|
super()
|
||||||
|
this.nuxt = nuxt
|
||||||
|
this.options = nuxt.options
|
||||||
|
}
|
||||||
|
|
||||||
|
async generate () {
|
||||||
const s = Date.now()
|
const s = Date.now()
|
||||||
let errors = []
|
let errors = []
|
||||||
/*
|
/*
|
||||||
@ -49,10 +56,10 @@ export default async function () {
|
|||||||
** Set variables
|
** Set variables
|
||||||
*/
|
*/
|
||||||
this.options.generate = _.defaultsDeep(this.options.generate, defaults)
|
this.options.generate = _.defaultsDeep(this.options.generate, defaults)
|
||||||
var srcStaticPath = resolve(this.srcDir, 'static')
|
let srcStaticPath = resolve(this.options.srcDir, 'static')
|
||||||
var srcBuiltPath = resolve(this.buildDir, 'dist')
|
let srcBuiltPath = resolve(this.buildDir, 'dist')
|
||||||
var distPath = resolve(this.dir, this.options.generate.dir)
|
let distPath = resolve(this.options.rootDir, this.options.generate.dir)
|
||||||
var 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
|
||||||
*/
|
*/
|
||||||
@ -63,7 +70,8 @@ export default async function () {
|
|||||||
try {
|
try {
|
||||||
await remove(distPath)
|
await remove(distPath)
|
||||||
debug('Destination folder cleaned')
|
debug('Destination folder cleaned')
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
** Copy static and built files
|
** Copy static and built files
|
||||||
*/
|
*/
|
||||||
@ -75,7 +83,7 @@ export default async function () {
|
|||||||
if (this.options.router.mode !== 'hash') {
|
if (this.options.router.mode !== 'hash') {
|
||||||
// Resolve config.generate.routes promises before generating the routes
|
// Resolve config.generate.routes promises before generating the routes
|
||||||
try {
|
try {
|
||||||
var generateRoutes = await promisifyRoute(this.options.generate.routes || [])
|
let generateRoutes = await promisifyRoute(this.options.generate.routes || [])
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Could not resolve routes') // eslint-disable-line no-console
|
console.error('Could not resolve routes') // eslint-disable-line no-console
|
||||||
console.error(e) // eslint-disable-line no-console
|
console.error(e) // eslint-disable-line no-console
|
||||||
@ -103,6 +111,7 @@ export default async function () {
|
|||||||
})
|
})
|
||||||
return _.values(routeMap)
|
return _.values(routeMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
** Generate only index.html for router.mode = 'hash'
|
** Generate only index.html for router.mode = 'hash'
|
||||||
*/
|
*/
|
||||||
@ -111,7 +120,7 @@ export default async function () {
|
|||||||
|
|
||||||
while (routes.length) {
|
while (routes.length) {
|
||||||
let n = 0
|
let n = 0
|
||||||
await Promise.all(routes.splice(0, 500).map(async ({route, payload}) => {
|
await Promise.all(routes.splice(0, 500).map(async ({ route, payload }) => {
|
||||||
await waitFor(n++ * this.options.generate.interval)
|
await waitFor(n++ * this.options.generate.interval)
|
||||||
let html
|
let html
|
||||||
try {
|
try {
|
||||||
@ -158,4 +167,7 @@ export default async function () {
|
|||||||
})
|
})
|
||||||
console.error('==== Error report ==== \n' + report.join('\n\n')) // eslint-disable-line no-console
|
console.error('==== Error report ==== \n' + report.join('\n\n')) // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,25 +1,23 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { uniq } from 'lodash'
|
import { uniq } from 'lodash'
|
||||||
import hash from 'hash-sum'
|
import hash from 'hash-sum'
|
||||||
import { chainFn, sequence } from './utils'
|
import { chainFn, sequence } from './utils'
|
||||||
|
import Tapable from 'tapable'
|
||||||
|
|
||||||
const debug = require('debug')('nuxt:module')
|
const debug = require('debug')('nuxt:module')
|
||||||
|
|
||||||
class Module {
|
export default class ModuleContainer extends Tapable {
|
||||||
constructor (nuxt) {
|
constructor (nuxt) {
|
||||||
|
super()
|
||||||
this.nuxt = nuxt
|
this.nuxt = nuxt
|
||||||
this.options = nuxt.options
|
this.options = nuxt.options
|
||||||
this.requiredModules = []
|
this.requiredModules = []
|
||||||
this.initing = this.ready()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async ready () {
|
async ready () {
|
||||||
if (this.initing) {
|
if (this._ready) {
|
||||||
await this.initing
|
return this._ready
|
||||||
return this
|
|
||||||
}
|
}
|
||||||
// Install all modules in sequence
|
// Install all modules in sequence
|
||||||
await sequence(this.options.modules, this.addModule.bind(this))
|
await sequence(this.options.modules, this.addModule.bind(this))
|
||||||
@ -61,10 +59,10 @@ class Module {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addPlugin (template) {
|
addPlugin (template) {
|
||||||
const {dst} = this.addTemplate(template)
|
const { dst } = this.addTemplate(template)
|
||||||
// Add to nuxt plugins
|
// Add to nuxt plugins
|
||||||
this.options.plugins.unshift({
|
this.options.plugins.unshift({
|
||||||
src: path.join(this.nuxt.buildDir, dst),
|
src: path.join(this.options.buildDir, dst),
|
||||||
ssr: template.ssr
|
ssr: template.ssr
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -159,5 +157,3 @@ class Module {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Module
|
|
199
lib/nuxt.js
199
lib/nuxt.js
@ -1,150 +1,104 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import compression from 'compression'
|
import compression from 'compression'
|
||||||
import fs from 'fs-extra'
|
import fs from 'fs-extra'
|
||||||
import pify from 'pify'
|
import pify from 'pify'
|
||||||
import Server from './server'
|
import Server from './server'
|
||||||
import Module from './module'
|
import ModuleContainer from './module-container'
|
||||||
import * as build from './build'
|
import Builder from './builder'
|
||||||
import * as render from './render'
|
import Renderer from './renderer'
|
||||||
import generate from './generate'
|
import Generate from './generate'
|
||||||
import serveStatic from 'serve-static'
|
import serveStatic from 'serve-static'
|
||||||
import { resolve, join } from 'path'
|
import { resolve, join } from 'path'
|
||||||
import * as utils from './utils'
|
import defaults from './defaults'
|
||||||
|
import Tapable from 'tapable'
|
||||||
|
|
||||||
class Nuxt {
|
export default class Nuxt extends Tapable {
|
||||||
constructor (options = {}) {
|
constructor (options = {}) {
|
||||||
const defaults = {
|
super()
|
||||||
dev: (process.env.NODE_ENV !== 'production'),
|
|
||||||
buildDir: '.nuxt',
|
// Normalize options
|
||||||
env: {},
|
if (options.loading === true) {
|
||||||
head: {
|
delete options.loading
|
||||||
meta: [],
|
|
||||||
link: [],
|
|
||||||
style: [],
|
|
||||||
script: []
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
css: [],
|
|
||||||
modules: [],
|
|
||||||
layouts: {},
|
|
||||||
serverMiddleware: [],
|
|
||||||
ErrorPage: null,
|
|
||||||
loading: {
|
|
||||||
color: 'black',
|
|
||||||
failedColor: 'red',
|
|
||||||
height: '2px',
|
|
||||||
duration: 5000
|
|
||||||
},
|
|
||||||
transition: {
|
|
||||||
name: 'page',
|
|
||||||
mode: 'out-in'
|
|
||||||
},
|
|
||||||
router: {
|
|
||||||
mode: 'history',
|
|
||||||
base: '/',
|
|
||||||
middleware: [],
|
|
||||||
linkActiveClass: 'nuxt-link-active',
|
|
||||||
linkExactActiveClass: 'nuxt-link-exact-active',
|
|
||||||
extendRoutes: null,
|
|
||||||
scrollBehavior: null
|
|
||||||
},
|
|
||||||
render: {
|
|
||||||
http2: {
|
|
||||||
push: false
|
|
||||||
},
|
|
||||||
static: {},
|
|
||||||
gzip: {
|
|
||||||
threshold: 0
|
|
||||||
},
|
|
||||||
etag: {
|
|
||||||
weak: true // Faster for responses > 5KB
|
|
||||||
}
|
}
|
||||||
},
|
if (options.router && typeof options.router.middleware === 'string') {
|
||||||
watchers: {
|
options.router.middleware = [options.router.middleware]
|
||||||
webpack: {},
|
|
||||||
chokidar: {}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Sanitization
|
|
||||||
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') {
|
if (options.router && typeof options.router.base === 'string') {
|
||||||
this._routerBaseSpecified = true
|
this._routerBaseSpecified = true
|
||||||
}
|
}
|
||||||
if (typeof options.transition === 'string') options.transition = {name: options.transition}
|
if (typeof options.transition === 'string') {
|
||||||
|
options.transition = { name: options.transition }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply defaults
|
||||||
this.options = _.defaultsDeep(options, defaults)
|
this.options = _.defaultsDeep(options, defaults)
|
||||||
// Ready variable
|
|
||||||
this._ready = false
|
// Resolve dirs
|
||||||
// Env variables
|
this.options.rootDir = (typeof options.rootDir === 'string' && options.rootDir ? options.rootDir : process.cwd())
|
||||||
this.dev = this.options.dev
|
this.options.srcDir = (typeof options.srcDir === 'string' && options.srcDir ? resolve(options.rootDir, options.srcDir) : this.options.rootDir)
|
||||||
// Explicit srcDir, rootDir and buildDir
|
this.options.buildDir = join(this.options.rootDir, options.buildDir)
|
||||||
this.dir = (typeof options.rootDir === 'string' && options.rootDir ? options.rootDir : process.cwd())
|
|
||||||
this.srcDir = (typeof options.srcDir === 'string' && options.srcDir ? resolve(this.dir, options.srcDir) : this.dir)
|
|
||||||
this.buildDir = join(this.dir, options.buildDir)
|
|
||||||
options.rootDir = this.dir
|
|
||||||
options.srcDir = this.srcDir
|
|
||||||
options.buildDir = this.buildDir
|
|
||||||
// If store defined, update store options to true
|
|
||||||
if (fs.existsSync(join(this.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.srcDir, 'app.html'))) {
|
|
||||||
this.options.appTemplatePath = join(this.srcDir, 'app.html')
|
|
||||||
}
|
|
||||||
// renderer used by Vue.js (via createBundleRenderer)
|
|
||||||
this.renderer = null
|
|
||||||
// For serving static/ files to /
|
|
||||||
this.serveStatic = pify(serveStatic(resolve(this.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.buildDir, 'dist'), {
|
|
||||||
maxAge: (this.dev ? 0 : '1y') // 1 year in production
|
|
||||||
}))
|
|
||||||
// gzip middleware for production
|
|
||||||
if (!this.dev && this.options.render.gzip) {
|
|
||||||
this.gzipMiddleware = pify(compression(this.options.render.gzip))
|
|
||||||
}
|
|
||||||
// Add this.Server Class
|
|
||||||
this.Server = Server
|
this.Server = Server
|
||||||
// Add this.build
|
this.componentTasks()
|
||||||
build.options.call(this) // Add build options
|
|
||||||
this.build = build.build.bind(this)
|
// Create instance of core components
|
||||||
|
this.builder = new Builder(this)
|
||||||
|
this.renderer = new Renderer(this)
|
||||||
|
this.generate = new Generate(this)
|
||||||
|
this.moduleContainer = new ModuleContainer(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.ready.bind(this)
|
||||||
|
this.dir = options.rootDir
|
||||||
|
this.srcDir = options.srcDir
|
||||||
|
this.buildDir = options.buildDir
|
||||||
|
|
||||||
|
// Wait for all core components be ready
|
||||||
|
this._ready = this.ready().catch(console.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentTasks () {
|
||||||
|
// TODO: This task should move into their own components instead
|
||||||
|
|
||||||
// Error template
|
// Error template
|
||||||
this.errorTemplate = _.template(fs.readFileSync(resolve(__dirname, 'views', 'error.html'), 'utf8'), {
|
this.errorTemplate = _.template(fs.readFileSync(resolve(__dirname, 'views', 'error.html'), 'utf8'), {
|
||||||
interpolate: /{{([\s\S]+?)}}/g
|
interpolate: /{{([\s\S]+?)}}/g
|
||||||
})
|
})
|
||||||
// Add this.render and this.renderRoute
|
|
||||||
this.render = render.render.bind(this)
|
// If store defined, update store options to true
|
||||||
this.renderRoute = render.renderRoute.bind(this)
|
if (fs.existsSync(join(this.options.srcDir, 'store'))) {
|
||||||
this.renderAndGetWindow = render.renderAndGetWindow.bind(this)
|
this.options.store = true
|
||||||
// Add this.generate
|
}
|
||||||
this.generate = generate.bind(this)
|
|
||||||
// Add this.utils (tests purpose)
|
// If app.html is defined, set the template path to the user template
|
||||||
this.utils = utils
|
this.options.appTemplatePath = resolve(__dirname, 'views/app.template.html')
|
||||||
// Add module integration
|
if (fs.existsSync(join(this.options.srcDir, 'app.html'))) {
|
||||||
this.module = new Module(this)
|
this.options.appTemplatePath = join(this.options.srcDir, 'app.html')
|
||||||
// Init nuxt.js
|
}
|
||||||
this._ready = this.ready()
|
|
||||||
// Return nuxt.js instance
|
// For serving static/ files to /
|
||||||
return this
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async ready () {
|
async ready () {
|
||||||
if (this._ready) {
|
if (this._ready) {
|
||||||
await this._ready
|
return this._ready
|
||||||
return this
|
|
||||||
}
|
|
||||||
// Init modules
|
|
||||||
await this.module.ready()
|
|
||||||
// Launch build in development but don't wait for it to be finished
|
|
||||||
if (this.dev) {
|
|
||||||
this.build()
|
|
||||||
} else {
|
|
||||||
build.production.call(this)
|
|
||||||
}
|
}
|
||||||
|
await this.moduleContainer.ready()
|
||||||
|
await this.builder.ready()
|
||||||
|
console.log('Nuxt Ready!')
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,4 +132,3 @@ class Nuxt {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Nuxt
|
|
||||||
|
196
lib/render.js
196
lib/render.js
@ -1,196 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
import ansiHTML from 'ansi-html'
|
|
||||||
import serialize from 'serialize-javascript'
|
|
||||||
import generateETag from 'etag'
|
|
||||||
import fresh from 'fresh'
|
|
||||||
import { getContext, setAnsiColors, encodeHtml } from './utils'
|
|
||||||
|
|
||||||
const debug = require('debug')('nuxt:render')
|
|
||||||
// force blue color
|
|
||||||
debug.color = 4
|
|
||||||
setAnsiColors(ansiHTML)
|
|
||||||
|
|
||||||
export async function render (req, res) {
|
|
||||||
// Wait for nuxt.js to be ready
|
|
||||||
await this.ready()
|
|
||||||
// Check if project is built for production
|
|
||||||
if (!this.renderer && !this.dev) {
|
|
||||||
console.error('> No build files found, please run `nuxt build` before launching `nuxt start`') // eslint-disable-line no-console
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
/* istanbul ignore if */
|
|
||||||
if (!this.renderer || !this.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.dev) {
|
|
||||||
// Call webpack middleware only in development
|
|
||||||
await this.webpackDevMiddleware(req, res)
|
|
||||||
await this.webpackHotMiddleware(req, res)
|
|
||||||
}
|
|
||||||
if (!this.dev && this.options.render.gzip) {
|
|
||||||
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.dev && req.url.indexOf(this.options.build.publicPath) === 0) {
|
|
||||||
const url = req.url
|
|
||||||
req.url = req.url.replace(this.options.build.publicPath, '/')
|
|
||||||
await this.serveStaticNuxt(req, res)
|
|
||||||
/* istanbul ignore next */
|
|
||||||
req.url = url
|
|
||||||
}
|
|
||||||
if (this.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)
|
|
||||||
if (fresh(req.headers, {etag})) {
|
|
||||||
res.statusCode = 304
|
|
||||||
res.end()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
res.setHeader('ETag', etag)
|
|
||||||
}
|
|
||||||
// HTTP2 push headers
|
|
||||||
if (!error && this.options.render.http2.push) {
|
|
||||||
// Parse resourceHints to extract HTTP.2 prefetch/push headers
|
|
||||||
// https://w3c.github.io/preload/#server-push-http-2
|
|
||||||
const regex = /link rel="([^"]*)" href="([^"]*)" as="([^"]*)"/g
|
|
||||||
const pushAssets = []
|
|
||||||
let m
|
|
||||||
while (m = regex.exec(resourceHints)) { // eslint-disable-line no-cond-assign
|
|
||||||
const [_, rel, href, as] = m // eslint-disable-line no-unused-vars
|
|
||||||
if (rel === 'preload') {
|
|
||||||
pushAssets.push(`<${href}>; rel=${rel}; as=${as}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Pass with single Link header
|
|
||||||
// https://blog.cloudflare.com/http-2-server-push-with-multiple-assets-per-link-header
|
|
||||||
res.setHeader('Link', pushAssets.join(','))
|
|
||||||
}
|
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
|
||||||
res.setHeader('Content-Length', Buffer.byteLength(html))
|
|
||||||
res.end(html, 'utf8')
|
|
||||||
return html
|
|
||||||
} catch (err) {
|
|
||||||
if (context.redirected) {
|
|
||||||
console.error(err) // eslint-disable-line no-console
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
const html = this.errorTemplate({
|
|
||||||
/* istanbul ignore if */
|
|
||||||
error: err,
|
|
||||||
stack: ansiHTML(encodeHtml(err.stack))
|
|
||||||
})
|
|
||||||
res.statusCode = 500
|
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
|
||||||
res.setHeader('Content-Length', Buffer.byteLength(html))
|
|
||||||
res.end(html, 'utf8')
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function renderRoute (url, context = {}) {
|
|
||||||
// Wait for modules to be initialized
|
|
||||||
await this.ready()
|
|
||||||
// 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.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) {
|
|
||||||
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.appTemplate({
|
|
||||||
HTML_ATTRS: 'data-n-head-ssr ' + m.htmlAttrs.text(),
|
|
||||||
BODY_ATTRS: m.bodyAttrs.text(),
|
|
||||||
HEAD,
|
|
||||||
APP
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
html,
|
|
||||||
resourceHints,
|
|
||||||
error: context.nuxt.error,
|
|
||||||
redirected: context.redirected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function used to do dom checking via jsdom
|
|
||||||
let jsdom = null
|
|
||||||
export async function renderAndGetWindow (url, opts = {}) {
|
|
||||||
/* istanbul ignore if */
|
|
||||||
if (!jsdom) {
|
|
||||||
try {
|
|
||||||
jsdom = require('jsdom')
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Fail when calling nuxt.renderAndGetWindow(url)') // eslint-disable-line no-console
|
|
||||||
console.error('jsdom module is not installed') // eslint-disable-line no-console
|
|
||||||
console.error('Please install jsdom with: npm install --save-dev jsdom') // eslint-disable-line no-console
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let options = {
|
|
||||||
resources: 'usable', // load subresources (https://github.com/tmpvar/jsdom#loading-subresources)
|
|
||||||
runScripts: 'dangerously',
|
|
||||||
beforeParse (window) {
|
|
||||||
// Mock window.scrollTo
|
|
||||||
window.scrollTo = () => {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (opts.virtualConsole !== false) {
|
|
||||||
options.virtualConsole = new jsdom.VirtualConsole().sendTo(console)
|
|
||||||
}
|
|
||||||
url = url || 'http://localhost:3000'
|
|
||||||
const {window} = await jsdom.JSDOM.fromURL(url, options)
|
|
||||||
// If Nuxt could not be loaded (error from the server-side)
|
|
||||||
const nuxtExists = window.document.body.innerHTML.includes('window.__NUXT__')
|
|
||||||
if (!nuxtExists) {
|
|
||||||
/* istanbul ignore next */
|
|
||||||
let error = new Error('Could not load the nuxt app')
|
|
||||||
/* istanbul ignore next */
|
|
||||||
error.body = window.document.body.innerHTML
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
// Used by nuxt.js to say when the components are loaded and the app ready
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
window._onNuxtLoaded = () => resolve(window)
|
|
||||||
})
|
|
||||||
// Send back window object
|
|
||||||
return window
|
|
||||||
}
|
|
210
lib/renderer.js
Normal file
210
lib/renderer.js
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
import ansiHTML from 'ansi-html'
|
||||||
|
import serialize from 'serialize-javascript'
|
||||||
|
import generateETag from 'etag'
|
||||||
|
import fresh from 'fresh'
|
||||||
|
import { getContext, setAnsiColors, encodeHtml } from './utils'
|
||||||
|
import Tapable from 'tapable'
|
||||||
|
|
||||||
|
const debug = require('debug')('nuxt:render')
|
||||||
|
debug.color = 4 // Force blue color
|
||||||
|
setAnsiColors(ansiHTML)
|
||||||
|
|
||||||
|
let jsdom = null
|
||||||
|
|
||||||
|
export default class Renderer extends Tapable {
|
||||||
|
|
||||||
|
constructor (nuxt) {
|
||||||
|
super()
|
||||||
|
this.nuxt = nuxt
|
||||||
|
this.options = nuxt.options
|
||||||
|
}
|
||||||
|
|
||||||
|
async render (req, res) {
|
||||||
|
// Wait for nuxt.js to be ready
|
||||||
|
await this.nuxt._ready
|
||||||
|
// Check if project is built for production
|
||||||
|
if (!this.nuxt.builder.renderer && !this.options.dev) {
|
||||||
|
console.error('> No build files found, please run `nuxt build` before launching `nuxt start`') // eslint-disable-line no-console
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
/* istanbul ignore if */
|
||||||
|
if (!this.nuxt.builder.renderer || !this.nuxt.builder.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
|
||||||
|
await this.nuxt.builder.webpackDevMiddleware(req, res)
|
||||||
|
await this.nuxt.builder.webpackHotMiddleware(req, res)
|
||||||
|
}
|
||||||
|
if (!this.options.dev && this.options.render.gzip) {
|
||||||
|
await this.nuxt.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.nuxt.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
|
||||||
|
req.url = req.url.replace(this.options.build.publicPath, '/')
|
||||||
|
await this.nuxt.serveStaticNuxt(req, res)
|
||||||
|
/* 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)
|
||||||
|
if (fresh(req.headers, { etag })) {
|
||||||
|
res.statusCode = 304
|
||||||
|
res.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.setHeader('ETag', etag)
|
||||||
|
}
|
||||||
|
// HTTP2 push headers
|
||||||
|
if (!error && this.options.render.http2.push) {
|
||||||
|
// Parse resourceHints to extract HTTP.2 prefetch/push headers
|
||||||
|
// https://w3c.github.io/preload/#server-push-http-2
|
||||||
|
const regex = /link rel="([^"]*)" href="([^"]*)" as="([^"]*)"/g
|
||||||
|
const pushAssets = []
|
||||||
|
let m
|
||||||
|
while (m = regex.exec(resourceHints)) { // eslint-disable-line no-cond-assign
|
||||||
|
const [_, rel, href, as] = m // eslint-disable-line no-unused-vars
|
||||||
|
if (rel === 'preload') {
|
||||||
|
pushAssets.push(`<${href}>; rel=${rel}; as=${as}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Pass with single Link header
|
||||||
|
// https://blog.cloudflare.com/http-2-server-push-with-multiple-assets-per-link-header
|
||||||
|
res.setHeader('Link', pushAssets.join(','))
|
||||||
|
}
|
||||||
|
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||||||
|
res.setHeader('Content-Length', Buffer.byteLength(html))
|
||||||
|
res.end(html, 'utf8')
|
||||||
|
return html
|
||||||
|
} catch (err) {
|
||||||
|
if (context.redirected) {
|
||||||
|
console.error(err) // eslint-disable-line no-console
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
const html = this.nuxt.errorTemplate({
|
||||||
|
/* istanbul ignore if */
|
||||||
|
error: err,
|
||||||
|
stack: ansiHTML(encodeHtml(err.stack))
|
||||||
|
})
|
||||||
|
res.statusCode = 500
|
||||||
|
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||||||
|
res.setHeader('Content-Length', Buffer.byteLength(html))
|
||||||
|
res.end(html, 'utf8')
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async renderRoute (url, context = {}) {
|
||||||
|
// Wait for nuxt.js to be ready
|
||||||
|
await this.nuxt._ready
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
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) {
|
||||||
|
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({
|
||||||
|
HTML_ATTRS: 'data-n-head-ssr ' + m.htmlAttrs.text(),
|
||||||
|
BODY_ATTRS: m.bodyAttrs.text(),
|
||||||
|
HEAD,
|
||||||
|
APP
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
html,
|
||||||
|
resourceHints,
|
||||||
|
error: context.nuxt.error,
|
||||||
|
redirected: context.redirected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async renderAndGetWindow (url, opts = {}) {
|
||||||
|
if (!this.ready) {
|
||||||
|
// Wait for nuxt.js to be ready
|
||||||
|
await this.nuxt._ready
|
||||||
|
this.ready = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore if */
|
||||||
|
if (!jsdom) {
|
||||||
|
try {
|
||||||
|
jsdom = require('jsdom')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fail when calling nuxt.renderAndGetWindow(url)') // eslint-disable-line no-console
|
||||||
|
console.error('jsdom module is not installed') // eslint-disable-line no-console
|
||||||
|
console.error('Please install jsdom with: npm install --save-dev jsdom') // eslint-disable-line no-console
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let options = {
|
||||||
|
resources: 'usable', // load subresources (https://github.com/tmpvar/jsdom#loading-subresources)
|
||||||
|
runScripts: 'dangerously',
|
||||||
|
beforeParse (window) {
|
||||||
|
// Mock window.scrollTo
|
||||||
|
window.scrollTo = () => {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (opts.virtualConsole !== false) {
|
||||||
|
options.virtualConsole = new jsdom.VirtualConsole().sendTo(console)
|
||||||
|
}
|
||||||
|
url = url || 'http://localhost:3000'
|
||||||
|
const { window } = await jsdom.JSDOM.fromURL(url, options)
|
||||||
|
// If Nuxt could not be loaded (error from the server-side)
|
||||||
|
const nuxtExists = window.document.body.innerHTML.includes('window.__NUXT__')
|
||||||
|
if (!nuxtExists) {
|
||||||
|
/* istanbul ignore next */
|
||||||
|
let error = new Error('Could not load the nuxt app')
|
||||||
|
/* istanbul ignore next */
|
||||||
|
error.body = window.document.body.innerHTML
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
// Used by nuxt.js to say when the components are loaded and the app ready
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
window._onNuxtLoaded = () => resolve(window)
|
||||||
|
})
|
||||||
|
// Send back window object
|
||||||
|
return window
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,3 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const http = require('http')
|
const http = require('http')
|
||||||
const connect = require('connect')
|
const connect = require('connect')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
@ -7,13 +5,15 @@ const path = require('path')
|
|||||||
class Server {
|
class Server {
|
||||||
constructor (nuxt) {
|
constructor (nuxt) {
|
||||||
this.nuxt = nuxt
|
this.nuxt = nuxt
|
||||||
|
this.options = nuxt.options
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
this.app = connect()
|
this.app = connect()
|
||||||
this.server = http.createServer(this.app)
|
this.server = http.createServer(this.app)
|
||||||
this.nuxt.ready()
|
this.nuxt.ready()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Add Middleware
|
// Add Middleware
|
||||||
this.nuxt.options.serverMiddleware.forEach(m => {
|
this.options.serverMiddleware.forEach(m => {
|
||||||
this.useMiddleware(m)
|
this.useMiddleware(m)
|
||||||
})
|
})
|
||||||
// Add default render middleware
|
// Add default render middleware
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
'use strict'
|
|
||||||
import { resolve, sep } from 'path'
|
import { resolve, sep } from 'path'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
@ -91,6 +90,7 @@ const normalize = string => string.replace(reqSep, sysSep)
|
|||||||
|
|
||||||
export function r () {
|
export function r () {
|
||||||
let args = Array.from(arguments)
|
let args = Array.from(arguments)
|
||||||
|
|
||||||
if (_.last(args).includes('~')) {
|
if (_.last(args).includes('~')) {
|
||||||
return wp(_.last(args))
|
return wp(_.last(args))
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
import vueLoaderConfig from './vue-loader.config'
|
import vueLoaderConfig from './vue-loader.config'
|
||||||
import { defaults } from 'lodash'
|
import { defaults } from 'lodash'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
@ -15,44 +13,46 @@ import ExtractTextPlugin from 'extract-text-webpack-plugin'
|
|||||||
| webpack config files
|
| webpack config files
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
export default function ({ isClient, isServer }) {
|
export default function webpackBaseConfig ({ isClient, isServer }) {
|
||||||
const nodeModulesDir = join(__dirname, '..', 'node_modules')
|
const nodeModulesDir = join(__dirname, '..', 'node_modules')
|
||||||
let config = {
|
let config = {
|
||||||
devtool: (this.dev ? 'cheap-module-source-map' : false),
|
devtool: (this.options.dev ? 'cheap-module-source-map' : false),
|
||||||
entry: {
|
entry: {
|
||||||
vendor: ['vue', 'vue-router', 'vue-meta']
|
vendor: ['vue', 'vue-router', 'vue-meta']
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
publicPath: (isUrl(this.options.build.publicPath) ? this.options.build.publicPath : urlJoin(this.options.router.base, this.options.build.publicPath))
|
publicPath: (isUrl(this.options.build.publicPath)
|
||||||
|
? this.options.build.publicPath
|
||||||
|
: urlJoin(this.options.router.base, this.options.build.publicPath))
|
||||||
},
|
},
|
||||||
performance: {
|
performance: {
|
||||||
maxEntrypointSize: 300000,
|
maxEntrypointSize: 300000,
|
||||||
maxAssetSize: 300000,
|
maxAssetSize: 300000,
|
||||||
hints: (this.dev ? false : 'warning')
|
hints: (this.options.dev ? false : 'warning')
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.js', '.json', '.vue', '.ts'],
|
extensions: ['.js', '.json', '.vue', '.ts'],
|
||||||
// Disable for now
|
// Disable for now
|
||||||
alias: {
|
alias: {
|
||||||
'~': join(this.srcDir),
|
'~': join(this.options.srcDir),
|
||||||
'static': join(this.srcDir, 'static'), // use in template with <img src="~static/nuxt.png" />
|
'static': join(this.options.srcDir, 'static'), // use in template with <img src="~static/nuxt.png" />
|
||||||
'~static': join(this.srcDir, 'static'),
|
'~static': join(this.options.srcDir, 'static'),
|
||||||
'assets': join(this.srcDir, 'assets'), // use in template with <img src="~assets/nuxt.png" />
|
'assets': join(this.options.srcDir, 'assets'), // use in template with <img src="~assets/nuxt.png" />
|
||||||
'~assets': join(this.srcDir, 'assets'),
|
'~assets': join(this.options.srcDir, 'assets'),
|
||||||
'~plugins': join(this.srcDir, 'plugins'),
|
'~plugins': join(this.options.srcDir, 'plugins'),
|
||||||
'~store': join(this.buildDir, 'store'),
|
'~store': join(this.options.buildDir, 'store'),
|
||||||
'~router': join(this.buildDir, 'router'),
|
'~router': join(this.options.buildDir, 'router'),
|
||||||
'~pages': join(this.srcDir, 'pages'),
|
'~pages': join(this.options.srcDir, 'pages'),
|
||||||
'~components': join(this.srcDir, 'components')
|
'~components': join(this.options.srcDir, 'components')
|
||||||
},
|
},
|
||||||
modules: [
|
modules: [
|
||||||
join(this.dir, 'node_modules'),
|
join(this.options.rootDir, 'node_modules'),
|
||||||
nodeModulesDir
|
nodeModulesDir
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
resolveLoader: {
|
resolveLoader: {
|
||||||
modules: [
|
modules: [
|
||||||
join(this.dir, 'node_modules'),
|
join(this.options.rootDir, 'node_modules'),
|
||||||
nodeModulesDir
|
nodeModulesDir
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -70,7 +70,7 @@ export default function ({ isClient, isServer }) {
|
|||||||
query: defaults(this.options.build.babel, {
|
query: defaults(this.options.build.babel, {
|
||||||
presets: ['vue-app'],
|
presets: ['vue-app'],
|
||||||
babelrc: false,
|
babelrc: false,
|
||||||
cacheDirectory: !!this.dev
|
cacheDirectory: !!this.options.dev
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{ test: /\.css$/, use: styleLoader.call(this, 'css') },
|
{ test: /\.css$/, use: styleLoader.call(this, 'css') },
|
||||||
@ -85,7 +85,7 @@ export default function ({ isClient, isServer }) {
|
|||||||
// CSS extraction
|
// CSS extraction
|
||||||
if (extractStyles.call(this)) {
|
if (extractStyles.call(this)) {
|
||||||
config.plugins.push(
|
config.plugins.push(
|
||||||
new ExtractTextPlugin({filename: this.options.build.filenames.css})
|
new ExtractTextPlugin({ filename: this.options.build.filenames.css })
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Add nuxt build loaders (can be configured in nuxt.config.js)
|
// Add nuxt build loaders (can be configured in nuxt.config.js)
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
import { each, defaults } from 'lodash'
|
import { each, defaults } from 'lodash'
|
||||||
import webpack from 'webpack'
|
import webpack from 'webpack'
|
||||||
import VueSSRClientPlugin from 'vue-server-renderer/client-plugin'
|
import VueSSRClientPlugin from 'vue-server-renderer/client-plugin'
|
||||||
@ -21,11 +19,11 @@ import { resolve } from 'path'
|
|||||||
| In production, will generate public/dist/style.css
|
| In production, will generate public/dist/style.css
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
export default function () {
|
export default function webpackClientConfig() {
|
||||||
let config = base.call(this, { isClient: true })
|
let config = base.call(this, { isClient: true })
|
||||||
|
|
||||||
// Entry
|
// Entry
|
||||||
config.entry.app = resolve(this.buildDir, 'client.js')
|
config.entry.app = resolve(this.options.buildDir, 'client.js')
|
||||||
|
|
||||||
// Add vendors
|
// Add vendors
|
||||||
if (this.options.store) {
|
if (this.options.store) {
|
||||||
@ -34,7 +32,7 @@ export default function () {
|
|||||||
config.entry.vendor = config.entry.vendor.concat(this.options.build.vendor)
|
config.entry.vendor = config.entry.vendor.concat(this.options.build.vendor)
|
||||||
|
|
||||||
// Output
|
// Output
|
||||||
config.output.path = resolve(this.buildDir, 'dist')
|
config.output.path = resolve(this.options.buildDir, 'dist')
|
||||||
config.output.filename = this.options.build.filenames.app
|
config.output.filename = this.options.build.filenames.app
|
||||||
|
|
||||||
// env object defined in nuxt.config.js
|
// env object defined in nuxt.config.js
|
||||||
@ -46,7 +44,7 @@ export default function () {
|
|||||||
config.plugins = (config.plugins || []).concat([
|
config.plugins = (config.plugins || []).concat([
|
||||||
// Strip comments in Vue code
|
// Strip comments in Vue code
|
||||||
new webpack.DefinePlugin(Object.assign(env, {
|
new webpack.DefinePlugin(Object.assign(env, {
|
||||||
'process.env.NODE_ENV': JSON.stringify(env.NODE_ENV || (this.dev ? 'development' : 'production')),
|
'process.env.NODE_ENV': JSON.stringify(env.NODE_ENV || (this.options.dev ? 'development' : 'production')),
|
||||||
'process.BROWSER_BUILD': true,
|
'process.BROWSER_BUILD': true,
|
||||||
'process.SERVER_BUILD': false,
|
'process.SERVER_BUILD': false,
|
||||||
'process.browser': true,
|
'process.browser': true,
|
||||||
@ -87,11 +85,11 @@ export default function () {
|
|||||||
new ProgressBarPlugin()
|
new ProgressBarPlugin()
|
||||||
)
|
)
|
||||||
// Add friendly error plugin
|
// Add friendly error plugin
|
||||||
if (this.dev) {
|
if (this.options.dev) {
|
||||||
config.plugins.push(new FriendlyErrorsWebpackPlugin())
|
config.plugins.push(new FriendlyErrorsWebpackPlugin())
|
||||||
}
|
}
|
||||||
// Production client build
|
// Production client build
|
||||||
if (!this.dev) {
|
if (!this.options.dev) {
|
||||||
config.plugins.push(
|
config.plugins.push(
|
||||||
// This is needed in webpack 2 for minifying CSS
|
// This is needed in webpack 2 for minifying CSS
|
||||||
new webpack.LoaderOptionsPlugin({
|
new webpack.LoaderOptionsPlugin({
|
||||||
@ -109,19 +107,19 @@ export default function () {
|
|||||||
// Extend config
|
// Extend config
|
||||||
if (typeof this.options.build.extend === 'function') {
|
if (typeof this.options.build.extend === 'function') {
|
||||||
this.options.build.extend.call(this, config, {
|
this.options.build.extend.call(this, config, {
|
||||||
dev: this.dev,
|
dev: this.options.dev,
|
||||||
isClient: true
|
isClient: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Offline-plugin integration
|
// Offline-plugin integration
|
||||||
if (!this.dev && this.options.offline) {
|
if (!this.options.dev && this.options.offline) {
|
||||||
const offlineOpts = typeof this.options.offline === 'object' ? this.options.offline : {}
|
const offlineOpts = typeof this.options.offline === 'object' ? this.options.offline : {}
|
||||||
config.plugins.push(
|
config.plugins.push(
|
||||||
new OfflinePlugin(defaults(offlineOpts, {}))
|
new OfflinePlugin(defaults(offlineOpts, {}))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Webpack Bundle Analyzer
|
// Webpack Bundle Analyzer
|
||||||
if (!this.dev && this.options.build.analyze) {
|
if (!this.options.dev && this.options.build.analyze) {
|
||||||
let options = {}
|
let options = {}
|
||||||
if (typeof this.options.build.analyze === 'object') {
|
if (typeof this.options.build.analyze === 'object') {
|
||||||
options = this.options.build.analyze
|
options = this.options.build.analyze
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import ExtractTextPlugin from 'extract-text-webpack-plugin'
|
import ExtractTextPlugin from 'extract-text-webpack-plugin'
|
||||||
|
|
||||||
export function extractStyles () {
|
export function extractStyles () {
|
||||||
return !this.dev && this.options.build.extractCSS
|
return !this.options.dev && this.options.build.extractCSS
|
||||||
}
|
}
|
||||||
|
|
||||||
export function styleLoader (ext, loader = []) {
|
export function styleLoader (ext, loader = []) {
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
import webpack from 'webpack'
|
import webpack from 'webpack'
|
||||||
import VueSSRServerPlugin from 'vue-server-renderer/server-plugin'
|
import VueSSRServerPlugin from 'vue-server-renderer/server-plugin'
|
||||||
import nodeExternals from 'webpack-node-externals'
|
import nodeExternals from 'webpack-node-externals'
|
||||||
@ -12,7 +10,7 @@ import { resolve } from 'path'
|
|||||||
| Webpack Server Config
|
| Webpack Server Config
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
export default function () {
|
export default function webpackServerConfig () {
|
||||||
let config = base.call(this, { isServer: true })
|
let config = base.call(this, { isServer: true })
|
||||||
|
|
||||||
// env object defined in nuxt.config.js
|
// env object defined in nuxt.config.js
|
||||||
@ -23,10 +21,10 @@ export default function () {
|
|||||||
|
|
||||||
config = Object.assign(config, {
|
config = Object.assign(config, {
|
||||||
target: 'node',
|
target: 'node',
|
||||||
devtool: (this.dev ? 'source-map' : false),
|
devtool: (this.options.dev ? 'source-map' : false),
|
||||||
entry: resolve(this.buildDir, 'server.js'),
|
entry: resolve(this.options.buildDir, 'server.js'),
|
||||||
output: Object.assign({}, config.output, {
|
output: Object.assign({}, config.output, {
|
||||||
path: resolve(this.buildDir, 'dist'),
|
path: resolve(this.options.buildDir, 'dist'),
|
||||||
filename: 'server-bundle.js',
|
filename: 'server-bundle.js',
|
||||||
libraryTarget: 'commonjs2'
|
libraryTarget: 'commonjs2'
|
||||||
}),
|
}),
|
||||||
@ -44,7 +42,7 @@ export default function () {
|
|||||||
filename: 'server-bundle.json'
|
filename: 'server-bundle.json'
|
||||||
}),
|
}),
|
||||||
new webpack.DefinePlugin(Object.assign(env, {
|
new webpack.DefinePlugin(Object.assign(env, {
|
||||||
'process.env.NODE_ENV': JSON.stringify(this.dev ? 'development' : 'production'),
|
'process.env.NODE_ENV': JSON.stringify(this.options.dev ? 'development' : 'production'),
|
||||||
'process.BROWSER_BUILD': false, // deprecated
|
'process.BROWSER_BUILD': false, // deprecated
|
||||||
'process.SERVER_BUILD': true, // deprecated
|
'process.SERVER_BUILD': true, // deprecated
|
||||||
'process.browser': false,
|
'process.browser': false,
|
||||||
@ -53,7 +51,7 @@ export default function () {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
// This is needed in webpack 2 for minifying CSS
|
// This is needed in webpack 2 for minifying CSS
|
||||||
if (!this.dev) {
|
if (!this.options.dev) {
|
||||||
config.plugins.push(
|
config.plugins.push(
|
||||||
new webpack.LoaderOptionsPlugin({
|
new webpack.LoaderOptionsPlugin({
|
||||||
minimize: true
|
minimize: true
|
||||||
@ -64,7 +62,7 @@ export default function () {
|
|||||||
// Extend config
|
// Extend config
|
||||||
if (typeof this.options.build.extend === 'function') {
|
if (typeof this.options.build.extend === 'function') {
|
||||||
this.options.build.extend.call(this, config, {
|
this.options.build.extend.call(this, config, {
|
||||||
dev: this.dev,
|
dev: this.options.dev,
|
||||||
isServer: true
|
isServer: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
import { defaults } from 'lodash'
|
import { defaults } from 'lodash'
|
||||||
import { extractStyles, styleLoader } from './helpers'
|
import { extractStyles, styleLoader } from './helpers'
|
||||||
|
|
||||||
@ -7,7 +5,7 @@ export default function ({ isClient }) {
|
|||||||
let babelOptions = JSON.stringify(defaults(this.options.build.babel, {
|
let babelOptions = JSON.stringify(defaults(this.options.build.babel, {
|
||||||
presets: ['vue-app'],
|
presets: ['vue-app'],
|
||||||
babelrc: false,
|
babelrc: false,
|
||||||
cacheDirectory: !!this.dev
|
cacheDirectory: !!this.options.dev
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// https://github.com/vuejs/vue-loader/blob/master/docs/en/configurations
|
// https://github.com/vuejs/vue-loader/blob/master/docs/en/configurations
|
||||||
|
Loading…
Reference in New Issue
Block a user