mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-23 22:25: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: {}
|
||||
}
|
||||
}
|
244
lib/generate.js
244
lib/generate.js
@ -1,11 +1,11 @@
|
||||
'use strict'
|
||||
|
||||
import fs from 'fs-extra'
|
||||
import pify from 'pify'
|
||||
import _ from 'lodash'
|
||||
import { resolve, join, dirname, sep } from 'path'
|
||||
import { isUrl, promisifyRoute, waitFor } from './utils'
|
||||
import { minify } from 'html-minifier'
|
||||
import Tapable from 'tapable'
|
||||
|
||||
const debug = require('debug')('nuxt:generate')
|
||||
const copy = pify(fs.copy)
|
||||
const remove = pify(fs.remove)
|
||||
@ -38,124 +38,136 @@ const defaults = {
|
||||
}
|
||||
}
|
||||
|
||||
export default async function () {
|
||||
const s = Date.now()
|
||||
let errors = []
|
||||
/*
|
||||
** Wait for modules to be initialized
|
||||
*/
|
||||
await this.ready()
|
||||
/*
|
||||
** Set variables
|
||||
*/
|
||||
this.options.generate = _.defaultsDeep(this.options.generate, defaults)
|
||||
var srcStaticPath = resolve(this.srcDir, 'static')
|
||||
var srcBuiltPath = resolve(this.buildDir, 'dist')
|
||||
var distPath = resolve(this.dir, this.options.generate.dir)
|
||||
var distNuxtPath = join(distPath, (isUrl(this.options.build.publicPath) ? '' : this.options.build.publicPath))
|
||||
/*
|
||||
** Launch build process
|
||||
*/
|
||||
await this.build()
|
||||
/*
|
||||
** Clean destination folder
|
||||
*/
|
||||
try {
|
||||
await remove(distPath)
|
||||
debug('Destination folder cleaned')
|
||||
} catch (e) {}
|
||||
/*
|
||||
** Copy static and built files
|
||||
*/
|
||||
if (fs.existsSync(srcStaticPath)) {
|
||||
await copy(srcStaticPath, distPath)
|
||||
export default class Generator extends Tapable {
|
||||
constructor (nuxt) {
|
||||
super()
|
||||
this.nuxt = nuxt
|
||||
this.options = nuxt.options
|
||||
}
|
||||
await copy(srcBuiltPath, distNuxtPath)
|
||||
debug('Static & build files copied')
|
||||
if (this.options.router.mode !== 'hash') {
|
||||
// Resolve config.generate.routes promises before generating the routes
|
||||
|
||||
async generate () {
|
||||
const s = Date.now()
|
||||
let errors = []
|
||||
/*
|
||||
** Wait for modules to be initialized
|
||||
*/
|
||||
await this.ready()
|
||||
/*
|
||||
** Set variables
|
||||
*/
|
||||
this.options.generate = _.defaultsDeep(this.options.generate, defaults)
|
||||
let srcStaticPath = resolve(this.options.srcDir, 'static')
|
||||
let srcBuiltPath = resolve(this.buildDir, 'dist')
|
||||
let distPath = resolve(this.options.rootDir, this.options.generate.dir)
|
||||
let distNuxtPath = join(distPath, (isUrl(this.options.build.publicPath) ? '' : this.options.build.publicPath))
|
||||
/*
|
||||
** Launch build process
|
||||
*/
|
||||
await this.build()
|
||||
/*
|
||||
** Clean destination folder
|
||||
*/
|
||||
try {
|
||||
var generateRoutes = await promisifyRoute(this.options.generate.routes || [])
|
||||
await remove(distPath)
|
||||
debug('Destination folder cleaned')
|
||||
} catch (e) {
|
||||
console.error('Could not resolve routes') // eslint-disable-line no-console
|
||||
console.error(e) // eslint-disable-line no-console
|
||||
process.exit(1)
|
||||
throw e // eslint-disable-line no-unreachable
|
||||
}
|
||||
/*
|
||||
** Copy static and built files
|
||||
*/
|
||||
if (fs.existsSync(srcStaticPath)) {
|
||||
await copy(srcStaticPath, distPath)
|
||||
}
|
||||
await copy(srcBuiltPath, distNuxtPath)
|
||||
debug('Static & build files copied')
|
||||
if (this.options.router.mode !== 'hash') {
|
||||
// Resolve config.generate.routes promises before generating the routes
|
||||
try {
|
||||
let generateRoutes = await promisifyRoute(this.options.generate.routes || [])
|
||||
} catch (e) {
|
||||
console.error('Could not resolve routes') // eslint-disable-line no-console
|
||||
console.error(e) // eslint-disable-line no-console
|
||||
process.exit(1)
|
||||
throw e // eslint-disable-line no-unreachable
|
||||
}
|
||||
}
|
||||
function decorateWithPayloads (routes) {
|
||||
let routeMap = {}
|
||||
// Fill routeMap for known routes
|
||||
routes.forEach((route) => {
|
||||
routeMap[route] = {
|
||||
route,
|
||||
payload: null
|
||||
}
|
||||
})
|
||||
// Fill routeMap with given generate.routes
|
||||
generateRoutes.forEach((route) => {
|
||||
// route is either a string or like {route : "/my_route/1"}
|
||||
const path = _.isString(route) ? route : route.route
|
||||
routeMap[path] = {
|
||||
route: path,
|
||||
payload: route.payload || null
|
||||
}
|
||||
})
|
||||
return _.values(routeMap)
|
||||
}
|
||||
|
||||
/*
|
||||
** Generate only index.html for router.mode = 'hash'
|
||||
*/
|
||||
let routes = (this.options.router.mode === 'hash') ? ['/'] : this.routes
|
||||
routes = decorateWithPayloads(routes)
|
||||
|
||||
while (routes.length) {
|
||||
let n = 0
|
||||
await Promise.all(routes.splice(0, 500).map(async ({ route, payload }) => {
|
||||
await waitFor(n++ * this.options.generate.interval)
|
||||
let html
|
||||
try {
|
||||
const res = await this.renderRoute(route, { _generate: true, payload })
|
||||
html = res.html
|
||||
if (res.error) {
|
||||
errors.push({ type: 'handled', route, error: res.error })
|
||||
}
|
||||
} catch (err) {
|
||||
/* istanbul ignore next */
|
||||
return errors.push({ type: 'unhandled', route, error: err })
|
||||
}
|
||||
if (this.options.generate.minify) {
|
||||
try {
|
||||
html = minify(html, this.options.generate.minify)
|
||||
} catch (err) /* istanbul ignore next */ {
|
||||
const minifyErr = new Error(`HTML minification failed. Make sure the route generates valid HTML. Failed HTML:\n ${html}`)
|
||||
errors.push({ type: 'unhandled', route, error: minifyErr })
|
||||
}
|
||||
}
|
||||
let path = join(route, sep, 'index.html') // /about -> /about/index.html
|
||||
debug('Generate file: ' + path)
|
||||
path = join(distPath, path)
|
||||
// Make sure the sub folders are created
|
||||
await mkdirp(dirname(path))
|
||||
await writeFile(path, html, 'utf8')
|
||||
}))
|
||||
}
|
||||
// Add .nojekyll file to let Github Pages add the _nuxt/ folder
|
||||
// https://help.github.com/articles/files-that-start-with-an-underscore-are-missing/
|
||||
const nojekyllPath = resolve(distPath, '.nojekyll')
|
||||
writeFile(nojekyllPath, '')
|
||||
const duration = Math.round((Date.now() - s) / 100) / 10
|
||||
debug(`HTML Files generated in ${duration}s`)
|
||||
|
||||
if (errors.length) {
|
||||
const report = errors.map(({ type, route, error }) => {
|
||||
/* istanbul ignore if */
|
||||
if (type === 'unhandled') {
|
||||
return `Route: '${route}'\n${error.stack}`
|
||||
} else {
|
||||
return `Route: '${route}' thrown an error: \n` + JSON.stringify(error)
|
||||
}
|
||||
})
|
||||
console.error('==== Error report ==== \n' + report.join('\n\n')) // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
function decorateWithPayloads (routes) {
|
||||
let routeMap = {}
|
||||
// Fill routeMap for known routes
|
||||
routes.forEach((route) => {
|
||||
routeMap[route] = {
|
||||
route,
|
||||
payload: null
|
||||
}
|
||||
})
|
||||
// Fill routeMap with given generate.routes
|
||||
generateRoutes.forEach((route) => {
|
||||
// route is either a string or like {route : "/my_route/1"}
|
||||
const path = _.isString(route) ? route : route.route
|
||||
routeMap[path] = {
|
||||
route: path,
|
||||
payload: route.payload || null
|
||||
}
|
||||
})
|
||||
return _.values(routeMap)
|
||||
}
|
||||
/*
|
||||
** Generate only index.html for router.mode = 'hash'
|
||||
*/
|
||||
let routes = (this.options.router.mode === 'hash') ? ['/'] : this.routes
|
||||
routes = decorateWithPayloads(routes)
|
||||
|
||||
while (routes.length) {
|
||||
let n = 0
|
||||
await Promise.all(routes.splice(0, 500).map(async ({route, payload}) => {
|
||||
await waitFor(n++ * this.options.generate.interval)
|
||||
let html
|
||||
try {
|
||||
const res = await this.renderRoute(route, { _generate: true, payload })
|
||||
html = res.html
|
||||
if (res.error) {
|
||||
errors.push({ type: 'handled', route, error: res.error })
|
||||
}
|
||||
} catch (err) {
|
||||
/* istanbul ignore next */
|
||||
return errors.push({ type: 'unhandled', route, error: err })
|
||||
}
|
||||
if (this.options.generate.minify) {
|
||||
try {
|
||||
html = minify(html, this.options.generate.minify)
|
||||
} catch (err) /* istanbul ignore next */ {
|
||||
const minifyErr = new Error(`HTML minification failed. Make sure the route generates valid HTML. Failed HTML:\n ${html}`)
|
||||
errors.push({ type: 'unhandled', route, error: minifyErr })
|
||||
}
|
||||
}
|
||||
let path = join(route, sep, 'index.html') // /about -> /about/index.html
|
||||
debug('Generate file: ' + path)
|
||||
path = join(distPath, path)
|
||||
// Make sure the sub folders are created
|
||||
await mkdirp(dirname(path))
|
||||
await writeFile(path, html, 'utf8')
|
||||
}))
|
||||
}
|
||||
// Add .nojekyll file to let Github Pages add the _nuxt/ folder
|
||||
// https://help.github.com/articles/files-that-start-with-an-underscore-are-missing/
|
||||
const nojekyllPath = resolve(distPath, '.nojekyll')
|
||||
writeFile(nojekyllPath, '')
|
||||
const duration = Math.round((Date.now() - s) / 100) / 10
|
||||
debug(`HTML Files generated in ${duration}s`)
|
||||
|
||||
if (errors.length) {
|
||||
const report = errors.map(({ type, route, error }) => {
|
||||
/* istanbul ignore if */
|
||||
if (type === 'unhandled') {
|
||||
return `Route: '${route}'\n${error.stack}`
|
||||
} else {
|
||||
return `Route: '${route}' thrown an error: \n` + JSON.stringify(error)
|
||||
}
|
||||
})
|
||||
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 fs from 'fs'
|
||||
import { uniq } from 'lodash'
|
||||
import hash from 'hash-sum'
|
||||
import { chainFn, sequence } from './utils'
|
||||
import Tapable from 'tapable'
|
||||
|
||||
const debug = require('debug')('nuxt:module')
|
||||
|
||||
class Module {
|
||||
export default class ModuleContainer extends Tapable {
|
||||
constructor (nuxt) {
|
||||
super()
|
||||
this.nuxt = nuxt
|
||||
this.options = nuxt.options
|
||||
this.requiredModules = []
|
||||
this.initing = this.ready()
|
||||
}
|
||||
|
||||
async ready () {
|
||||
if (this.initing) {
|
||||
await this.initing
|
||||
return this
|
||||
if (this._ready) {
|
||||
return this._ready
|
||||
}
|
||||
// Install all modules in sequence
|
||||
await sequence(this.options.modules, this.addModule.bind(this))
|
||||
@ -61,10 +59,10 @@ class Module {
|
||||
}
|
||||
|
||||
addPlugin (template) {
|
||||
const {dst} = this.addTemplate(template)
|
||||
const { dst } = this.addTemplate(template)
|
||||
// Add to nuxt plugins
|
||||
this.options.plugins.unshift({
|
||||
src: path.join(this.nuxt.buildDir, dst),
|
||||
src: path.join(this.options.buildDir, dst),
|
||||
ssr: template.ssr
|
||||
})
|
||||
}
|
||||
@ -159,5 +157,3 @@ class Module {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default Module
|
201
lib/nuxt.js
201
lib/nuxt.js
@ -1,150 +1,104 @@
|
||||
'use strict'
|
||||
|
||||
import _ from 'lodash'
|
||||
import compression from 'compression'
|
||||
import fs from 'fs-extra'
|
||||
import pify from 'pify'
|
||||
import Server from './server'
|
||||
import Module from './module'
|
||||
import * as build from './build'
|
||||
import * as render from './render'
|
||||
import generate from './generate'
|
||||
import ModuleContainer from './module-container'
|
||||
import Builder from './builder'
|
||||
import Renderer from './renderer'
|
||||
import Generate from './generate'
|
||||
import serveStatic from 'serve-static'
|
||||
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 = {}) {
|
||||
const defaults = {
|
||||
dev: (process.env.NODE_ENV !== 'production'),
|
||||
buildDir: '.nuxt',
|
||||
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: {}
|
||||
}
|
||||
super()
|
||||
|
||||
// Normalize options
|
||||
if (options.loading === true) {
|
||||
delete options.loading
|
||||
}
|
||||
if (options.router && typeof options.router.middleware === 'string') {
|
||||
options.router.middleware = [options.router.middleware]
|
||||
}
|
||||
// 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') {
|
||||
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)
|
||||
// Ready variable
|
||||
this._ready = false
|
||||
// Env variables
|
||||
this.dev = this.options.dev
|
||||
// Explicit srcDir, rootDir and 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
|
||||
|
||||
// Resolve dirs
|
||||
this.options.rootDir = (typeof options.rootDir === 'string' && options.rootDir ? options.rootDir : process.cwd())
|
||||
this.options.srcDir = (typeof options.srcDir === 'string' && options.srcDir ? resolve(options.rootDir, options.srcDir) : this.options.rootDir)
|
||||
this.options.buildDir = join(this.options.rootDir, options.buildDir)
|
||||
|
||||
this.Server = Server
|
||||
// Add this.build
|
||||
build.options.call(this) // Add build options
|
||||
this.build = build.build.bind(this)
|
||||
this.componentTasks()
|
||||
|
||||
// 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
|
||||
this.errorTemplate = _.template(fs.readFileSync(resolve(__dirname, 'views', 'error.html'), 'utf8'), {
|
||||
interpolate: /{{([\s\S]+?)}}/g
|
||||
})
|
||||
// Add this.render and this.renderRoute
|
||||
this.render = render.render.bind(this)
|
||||
this.renderRoute = render.renderRoute.bind(this)
|
||||
this.renderAndGetWindow = render.renderAndGetWindow.bind(this)
|
||||
// Add this.generate
|
||||
this.generate = generate.bind(this)
|
||||
// Add this.utils (tests purpose)
|
||||
this.utils = utils
|
||||
// Add module integration
|
||||
this.module = new Module(this)
|
||||
// Init nuxt.js
|
||||
this._ready = this.ready()
|
||||
// Return nuxt.js instance
|
||||
return this
|
||||
|
||||
// If store defined, update store options to true
|
||||
if (fs.existsSync(join(this.options.srcDir, 'store'))) {
|
||||
this.options.store = true
|
||||
}
|
||||
|
||||
// If app.html is defined, set the template path to the user template
|
||||
this.options.appTemplatePath = resolve(__dirname, 'views/app.template.html')
|
||||
if (fs.existsSync(join(this.options.srcDir, 'app.html'))) {
|
||||
this.options.appTemplatePath = join(this.options.srcDir, 'app.html')
|
||||
}
|
||||
|
||||
// For serving static/ files to /
|
||||
this.serveStatic = pify(serveStatic(resolve(this.options.srcDir, 'static'), this.options.render.static))
|
||||
// For serving .nuxt/dist/ files (only when build.publicPath is not an URL)
|
||||
this.serveStaticNuxt = pify(serveStatic(resolve(this.options.buildDir, 'dist'), {
|
||||
maxAge: (this.options.dev ? 0 : '1y') // 1 year in production
|
||||
}))
|
||||
|
||||
// gzip middleware for production
|
||||
if (!this.options.dev && this.options.render.gzip) {
|
||||
this.gzipMiddleware = pify(compression(this.options.render.gzip))
|
||||
}
|
||||
}
|
||||
|
||||
async ready () {
|
||||
if (this._ready) {
|
||||
await 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)
|
||||
return this._ready
|
||||
}
|
||||
await this.moduleContainer.ready()
|
||||
await this.builder.ready()
|
||||
console.log('Nuxt Ready!')
|
||||
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 connect = require('connect')
|
||||
const path = require('path')
|
||||
@ -7,18 +5,20 @@ const path = require('path')
|
||||
class Server {
|
||||
constructor (nuxt) {
|
||||
this.nuxt = nuxt
|
||||
this.options = nuxt.options
|
||||
|
||||
// Initialize
|
||||
this.app = connect()
|
||||
this.server = http.createServer(this.app)
|
||||
this.nuxt.ready()
|
||||
.then(() => {
|
||||
// Add Middleware
|
||||
this.nuxt.options.serverMiddleware.forEach(m => {
|
||||
this.useMiddleware(m)
|
||||
.then(() => {
|
||||
// Add Middleware
|
||||
this.options.serverMiddleware.forEach(m => {
|
||||
this.useMiddleware(m)
|
||||
})
|
||||
// Add default render middleware
|
||||
this.useMiddleware(this.render.bind(this))
|
||||
})
|
||||
// Add default render middleware
|
||||
this.useMiddleware(this.render.bind(this))
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
@ -49,11 +49,11 @@ class Server {
|
||||
host = host || '127.0.0.1'
|
||||
port = port || 3000
|
||||
this.nuxt.ready()
|
||||
.then(() => {
|
||||
this.server.listen(port, host, () => {
|
||||
console.log('Ready on http://%s:%s', host, port) // eslint-disable-line no-console
|
||||
.then(() => {
|
||||
this.server.listen(port, host, () => {
|
||||
console.log('Ready on http://%s:%s', host, port) // eslint-disable-line no-console
|
||||
})
|
||||
})
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
'use strict'
|
||||
import { resolve, sep } from 'path'
|
||||
import _ from 'lodash'
|
||||
|
||||
@ -91,6 +90,7 @@ const normalize = string => string.replace(reqSep, sysSep)
|
||||
|
||||
export function r () {
|
||||
let args = Array.from(arguments)
|
||||
|
||||
if (_.last(args).includes('~')) {
|
||||
return wp(_.last(args))
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
'use strict'
|
||||
|
||||
import vueLoaderConfig from './vue-loader.config'
|
||||
import { defaults } from 'lodash'
|
||||
import { join } from 'path'
|
||||
@ -15,44 +13,46 @@ import ExtractTextPlugin from 'extract-text-webpack-plugin'
|
||||
| webpack config files
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
export default function ({ isClient, isServer }) {
|
||||
export default function webpackBaseConfig ({ isClient, isServer }) {
|
||||
const nodeModulesDir = join(__dirname, '..', 'node_modules')
|
||||
let config = {
|
||||
devtool: (this.dev ? 'cheap-module-source-map' : false),
|
||||
devtool: (this.options.dev ? 'cheap-module-source-map' : false),
|
||||
entry: {
|
||||
vendor: ['vue', 'vue-router', 'vue-meta']
|
||||
},
|
||||
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: {
|
||||
maxEntrypointSize: 300000,
|
||||
maxAssetSize: 300000,
|
||||
hints: (this.dev ? false : 'warning')
|
||||
hints: (this.options.dev ? false : 'warning')
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.json', '.vue', '.ts'],
|
||||
// Disable for now
|
||||
alias: {
|
||||
'~': join(this.srcDir),
|
||||
'static': join(this.srcDir, 'static'), // use in template with <img src="~static/nuxt.png" />
|
||||
'~static': join(this.srcDir, 'static'),
|
||||
'assets': join(this.srcDir, 'assets'), // use in template with <img src="~assets/nuxt.png" />
|
||||
'~assets': join(this.srcDir, 'assets'),
|
||||
'~plugins': join(this.srcDir, 'plugins'),
|
||||
'~store': join(this.buildDir, 'store'),
|
||||
'~router': join(this.buildDir, 'router'),
|
||||
'~pages': join(this.srcDir, 'pages'),
|
||||
'~components': join(this.srcDir, 'components')
|
||||
'~': join(this.options.srcDir),
|
||||
'static': join(this.options.srcDir, 'static'), // use in template with <img src="~static/nuxt.png" />
|
||||
'~static': join(this.options.srcDir, 'static'),
|
||||
'assets': join(this.options.srcDir, 'assets'), // use in template with <img src="~assets/nuxt.png" />
|
||||
'~assets': join(this.options.srcDir, 'assets'),
|
||||
'~plugins': join(this.options.srcDir, 'plugins'),
|
||||
'~store': join(this.options.buildDir, 'store'),
|
||||
'~router': join(this.options.buildDir, 'router'),
|
||||
'~pages': join(this.options.srcDir, 'pages'),
|
||||
'~components': join(this.options.srcDir, 'components')
|
||||
},
|
||||
modules: [
|
||||
join(this.dir, 'node_modules'),
|
||||
join(this.options.rootDir, 'node_modules'),
|
||||
nodeModulesDir
|
||||
]
|
||||
},
|
||||
resolveLoader: {
|
||||
modules: [
|
||||
join(this.dir, 'node_modules'),
|
||||
join(this.options.rootDir, 'node_modules'),
|
||||
nodeModulesDir
|
||||
]
|
||||
},
|
||||
@ -70,7 +70,7 @@ export default function ({ isClient, isServer }) {
|
||||
query: defaults(this.options.build.babel, {
|
||||
presets: ['vue-app'],
|
||||
babelrc: false,
|
||||
cacheDirectory: !!this.dev
|
||||
cacheDirectory: !!this.options.dev
|
||||
})
|
||||
},
|
||||
{ test: /\.css$/, use: styleLoader.call(this, 'css') },
|
||||
@ -85,7 +85,7 @@ export default function ({ isClient, isServer }) {
|
||||
// CSS extraction
|
||||
if (extractStyles.call(this)) {
|
||||
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)
|
||||
|
@ -1,5 +1,3 @@
|
||||
'use strict'
|
||||
|
||||
import { each, defaults } from 'lodash'
|
||||
import webpack from 'webpack'
|
||||
import VueSSRClientPlugin from 'vue-server-renderer/client-plugin'
|
||||
@ -21,11 +19,11 @@ import { resolve } from 'path'
|
||||
| In production, will generate public/dist/style.css
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
export default function () {
|
||||
export default function webpackClientConfig() {
|
||||
let config = base.call(this, { isClient: true })
|
||||
|
||||
// Entry
|
||||
config.entry.app = resolve(this.buildDir, 'client.js')
|
||||
config.entry.app = resolve(this.options.buildDir, 'client.js')
|
||||
|
||||
// Add vendors
|
||||
if (this.options.store) {
|
||||
@ -34,7 +32,7 @@ export default function () {
|
||||
config.entry.vendor = config.entry.vendor.concat(this.options.build.vendor)
|
||||
|
||||
// Output
|
||||
config.output.path = resolve(this.buildDir, 'dist')
|
||||
config.output.path = resolve(this.options.buildDir, 'dist')
|
||||
config.output.filename = this.options.build.filenames.app
|
||||
|
||||
// env object defined in nuxt.config.js
|
||||
@ -46,7 +44,7 @@ export default function () {
|
||||
config.plugins = (config.plugins || []).concat([
|
||||
// Strip comments in Vue code
|
||||
new webpack.DefinePlugin(Object.assign(env, {
|
||||
'process.env.NODE_ENV': JSON.stringify(env.NODE_ENV || (this.dev ? 'development' : 'production')),
|
||||
'process.env.NODE_ENV': JSON.stringify(env.NODE_ENV || (this.options.dev ? 'development' : 'production')),
|
||||
'process.BROWSER_BUILD': true,
|
||||
'process.SERVER_BUILD': false,
|
||||
'process.browser': true,
|
||||
@ -87,11 +85,11 @@ export default function () {
|
||||
new ProgressBarPlugin()
|
||||
)
|
||||
// Add friendly error plugin
|
||||
if (this.dev) {
|
||||
if (this.options.dev) {
|
||||
config.plugins.push(new FriendlyErrorsWebpackPlugin())
|
||||
}
|
||||
// Production client build
|
||||
if (!this.dev) {
|
||||
if (!this.options.dev) {
|
||||
config.plugins.push(
|
||||
// This is needed in webpack 2 for minifying CSS
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
@ -109,19 +107,19 @@ export default function () {
|
||||
// Extend config
|
||||
if (typeof this.options.build.extend === 'function') {
|
||||
this.options.build.extend.call(this, config, {
|
||||
dev: this.dev,
|
||||
dev: this.options.dev,
|
||||
isClient: true
|
||||
})
|
||||
}
|
||||
// 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 : {}
|
||||
config.plugins.push(
|
||||
new OfflinePlugin(defaults(offlineOpts, {}))
|
||||
)
|
||||
}
|
||||
// Webpack Bundle Analyzer
|
||||
if (!this.dev && this.options.build.analyze) {
|
||||
if (!this.options.dev && this.options.build.analyze) {
|
||||
let options = {}
|
||||
if (typeof this.options.build.analyze === 'object') {
|
||||
options = this.options.build.analyze
|
||||
|
@ -1,7 +1,7 @@
|
||||
import ExtractTextPlugin from 'extract-text-webpack-plugin'
|
||||
|
||||
export function extractStyles () {
|
||||
return !this.dev && this.options.build.extractCSS
|
||||
return !this.options.dev && this.options.build.extractCSS
|
||||
}
|
||||
|
||||
export function styleLoader (ext, loader = []) {
|
||||
|
@ -1,5 +1,3 @@
|
||||
'use strict'
|
||||
|
||||
import webpack from 'webpack'
|
||||
import VueSSRServerPlugin from 'vue-server-renderer/server-plugin'
|
||||
import nodeExternals from 'webpack-node-externals'
|
||||
@ -12,7 +10,7 @@ import { resolve } from 'path'
|
||||
| Webpack Server Config
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
export default function () {
|
||||
export default function webpackServerConfig () {
|
||||
let config = base.call(this, { isServer: true })
|
||||
|
||||
// env object defined in nuxt.config.js
|
||||
@ -23,10 +21,10 @@ export default function () {
|
||||
|
||||
config = Object.assign(config, {
|
||||
target: 'node',
|
||||
devtool: (this.dev ? 'source-map' : false),
|
||||
entry: resolve(this.buildDir, 'server.js'),
|
||||
devtool: (this.options.dev ? 'source-map' : false),
|
||||
entry: resolve(this.options.buildDir, 'server.js'),
|
||||
output: Object.assign({}, config.output, {
|
||||
path: resolve(this.buildDir, 'dist'),
|
||||
path: resolve(this.options.buildDir, 'dist'),
|
||||
filename: 'server-bundle.js',
|
||||
libraryTarget: 'commonjs2'
|
||||
}),
|
||||
@ -44,7 +42,7 @@ export default function () {
|
||||
filename: 'server-bundle.json'
|
||||
}),
|
||||
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.SERVER_BUILD': true, // deprecated
|
||||
'process.browser': false,
|
||||
@ -53,7 +51,7 @@ export default function () {
|
||||
])
|
||||
})
|
||||
// This is needed in webpack 2 for minifying CSS
|
||||
if (!this.dev) {
|
||||
if (!this.options.dev) {
|
||||
config.plugins.push(
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
minimize: true
|
||||
@ -64,7 +62,7 @@ export default function () {
|
||||
// Extend config
|
||||
if (typeof this.options.build.extend === 'function') {
|
||||
this.options.build.extend.call(this, config, {
|
||||
dev: this.dev,
|
||||
dev: this.options.dev,
|
||||
isServer: true
|
||||
})
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
'use strict'
|
||||
|
||||
import { defaults } from 'lodash'
|
||||
import { extractStyles, styleLoader } from './helpers'
|
||||
|
||||
@ -7,7 +5,7 @@ export default function ({ isClient }) {
|
||||
let babelOptions = JSON.stringify(defaults(this.options.build.babel, {
|
||||
presets: ['vue-app'],
|
||||
babelrc: false,
|
||||
cacheDirectory: !!this.dev
|
||||
cacheDirectory: !!this.options.dev
|
||||
}))
|
||||
|
||||
// https://github.com/vuejs/vue-loader/blob/master/docs/en/configurations
|
||||
|
Loading…
Reference in New Issue
Block a user