refactor into components

This commit is contained in:
Pooya Parsa 2017-06-11 18:47:36 +04:30
parent 5237764573
commit 8fe9380df9
15 changed files with 1170 additions and 1074 deletions

View File

@ -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
View 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
View 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: {}
}
}

View File

@ -1,11 +1,11 @@
'use strict'
import fs from 'fs-extra' import fs from 'fs-extra'
import pify from 'pify' import pify from 'pify'
import _ from 'lodash' import _ from 'lodash'
import { resolve, join, dirname, sep } from 'path' import { resolve, join, dirname, sep } from 'path'
import { isUrl, promisifyRoute, waitFor } from './utils' import { isUrl, promisifyRoute, waitFor } from './utils'
import { minify } from 'html-minifier' import { minify } from 'html-minifier'
import Tapable from 'tapable'
const debug = require('debug')('nuxt:generate') const debug = require('debug')('nuxt:generate')
const copy = pify(fs.copy) const copy = pify(fs.copy)
const remove = pify(fs.remove) const remove = pify(fs.remove)
@ -38,124 +38,136 @@ const defaults = {
} }
} }
export default async function () { export default class Generator extends Tapable {
const s = Date.now() constructor (nuxt) {
let errors = [] super()
/* this.nuxt = nuxt
** Wait for modules to be initialized this.options = nuxt.options
*/
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)
} }
await copy(srcBuiltPath, distNuxtPath)
debug('Static & build files copied') async generate () {
if (this.options.router.mode !== 'hash') { const s = Date.now()
// Resolve config.generate.routes promises before generating the routes 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 { try {
var generateRoutes = await promisifyRoute(this.options.generate.routes || []) await remove(distPath)
debug('Destination folder cleaned')
} catch (e) { } catch (e) {
console.error('Could not resolve routes') // eslint-disable-line no-console }
console.error(e) // eslint-disable-line no-console /*
process.exit(1) ** Copy static and built files
throw e // eslint-disable-line no-unreachable */
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
}
} }

View File

@ -1,25 +1,23 @@
'use strict'
import path from 'path' import path from 'path'
import fs from 'fs' import fs from 'fs'
import { uniq } from 'lodash' import { uniq } from 'lodash'
import hash from 'hash-sum' import hash from 'hash-sum'
import { chainFn, sequence } from './utils' import { chainFn, sequence } from './utils'
import Tapable from 'tapable'
const debug = require('debug')('nuxt:module') const debug = require('debug')('nuxt:module')
class Module { export default class ModuleContainer extends Tapable {
constructor (nuxt) { constructor (nuxt) {
super()
this.nuxt = nuxt this.nuxt = nuxt
this.options = nuxt.options this.options = nuxt.options
this.requiredModules = [] this.requiredModules = []
this.initing = this.ready()
} }
async ready () { async ready () {
if (this.initing) { if (this._ready) {
await this.initing return this._ready
return this
} }
// Install all modules in sequence // Install all modules in sequence
await sequence(this.options.modules, this.addModule.bind(this)) await sequence(this.options.modules, this.addModule.bind(this))
@ -61,10 +59,10 @@ class Module {
} }
addPlugin (template) { addPlugin (template) {
const {dst} = this.addTemplate(template) const { dst } = this.addTemplate(template)
// Add to nuxt plugins // Add to nuxt plugins
this.options.plugins.unshift({ this.options.plugins.unshift({
src: path.join(this.nuxt.buildDir, dst), src: path.join(this.options.buildDir, dst),
ssr: template.ssr ssr: template.ssr
}) })
} }
@ -159,5 +157,3 @@ class Module {
}) })
} }
} }
export default Module

View File

@ -1,150 +1,104 @@
'use strict'
import _ from 'lodash' import _ from 'lodash'
import compression from 'compression' import compression from 'compression'
import fs from 'fs-extra' import fs from 'fs-extra'
import pify from 'pify' import pify from 'pify'
import Server from './server' import Server from './server'
import Module from './module' import ModuleContainer from './module-container'
import * as build from './build' import Builder from './builder'
import * as render from './render' import Renderer from './renderer'
import generate from './generate' import Generate from './generate'
import serveStatic from 'serve-static' import serveStatic from 'serve-static'
import { resolve, join } from 'path' import { resolve, join } from 'path'
import * as utils from './utils' import defaults from './defaults'
import Tapable from 'tapable'
class Nuxt { export default class Nuxt extends Tapable {
constructor (options = {}) { constructor (options = {}) {
const defaults = { super()
dev: (process.env.NODE_ENV !== 'production'),
buildDir: '.nuxt', // Normalize options
env: {}, if (options.loading === true) {
head: { delete options.loading
meta: [], }
link: [], if (options.router && typeof options.router.middleware === 'string') {
style: [], options.router.middleware = [options.router.middleware]
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: {}
}
} }
// Sanitization
if (options.loading === true) delete options.loading
if (options.router && typeof options.router.middleware === 'string') options.router.middleware = [options.router.middleware]
if (options.router && typeof options.router.base === 'string') { if (options.router && typeof options.router.base === 'string') {
this._routerBaseSpecified = true this._routerBaseSpecified = true
} }
if (typeof options.transition === 'string') options.transition = {name: options.transition} if (typeof options.transition === 'string') {
options.transition = { name: options.transition }
}
// Apply defaults
this.options = _.defaultsDeep(options, defaults) this.options = _.defaultsDeep(options, defaults)
// Ready variable
this._ready = false // Resolve dirs
// Env variables this.options.rootDir = (typeof options.rootDir === 'string' && options.rootDir ? options.rootDir : process.cwd())
this.dev = this.options.dev this.options.srcDir = (typeof options.srcDir === 'string' && options.srcDir ? resolve(options.rootDir, options.srcDir) : this.options.rootDir)
// Explicit srcDir, rootDir and buildDir this.options.buildDir = join(this.options.rootDir, options.buildDir)
this.dir = (typeof options.rootDir === 'string' && options.rootDir ? options.rootDir : process.cwd())
this.srcDir = (typeof options.srcDir === 'string' && options.srcDir ? resolve(this.dir, options.srcDir) : this.dir)
this.buildDir = join(this.dir, options.buildDir)
options.rootDir = this.dir
options.srcDir = this.srcDir
options.buildDir = this.buildDir
// If store defined, update store options to true
if (fs.existsSync(join(this.srcDir, 'store'))) {
this.options.store = true
}
// If app.html is defined, set the template path to the user template
this.options.appTemplatePath = resolve(__dirname, 'views/app.template.html')
if (fs.existsSync(join(this.srcDir, 'app.html'))) {
this.options.appTemplatePath = join(this.srcDir, 'app.html')
}
// renderer used by Vue.js (via createBundleRenderer)
this.renderer = null
// For serving static/ files to /
this.serveStatic = pify(serveStatic(resolve(this.srcDir, 'static'), this.options.render.static))
// For serving .nuxt/dist/ files (only when build.publicPath is not an URL)
this.serveStaticNuxt = pify(serveStatic(resolve(this.buildDir, 'dist'), {
maxAge: (this.dev ? 0 : '1y') // 1 year in production
}))
// gzip middleware for production
if (!this.dev && this.options.render.gzip) {
this.gzipMiddleware = pify(compression(this.options.render.gzip))
}
// Add this.Server Class
this.Server = Server this.Server = Server
// Add this.build this.componentTasks()
build.options.call(this) // Add build options
this.build = build.build.bind(this) // Create instance of core components
this.builder = new Builder(this)
this.renderer = new Renderer(this)
this.generate = new Generate(this)
this.moduleContainer = new ModuleContainer(this)
// Backward compatibility
this.render = this.renderer.render.bind(this.renderer)
this.renderRoute = this.renderer.renderRoute.bind(this.renderer)
this.renderAndGetWindow = this.renderer.renderAndGetWindow.bind(this.renderer)
this.build = this.ready.bind(this)
this.dir = options.rootDir
this.srcDir = options.srcDir
this.buildDir = options.buildDir
// Wait for all core components be ready
this._ready = this.ready().catch(console.error)
}
componentTasks () {
// TODO: This task should move into their own components instead
// Error template // Error template
this.errorTemplate = _.template(fs.readFileSync(resolve(__dirname, 'views', 'error.html'), 'utf8'), { this.errorTemplate = _.template(fs.readFileSync(resolve(__dirname, 'views', 'error.html'), 'utf8'), {
interpolate: /{{([\s\S]+?)}}/g interpolate: /{{([\s\S]+?)}}/g
}) })
// Add this.render and this.renderRoute
this.render = render.render.bind(this) // If store defined, update store options to true
this.renderRoute = render.renderRoute.bind(this) if (fs.existsSync(join(this.options.srcDir, 'store'))) {
this.renderAndGetWindow = render.renderAndGetWindow.bind(this) this.options.store = true
// Add this.generate }
this.generate = generate.bind(this)
// Add this.utils (tests purpose) // If app.html is defined, set the template path to the user template
this.utils = utils this.options.appTemplatePath = resolve(__dirname, 'views/app.template.html')
// Add module integration if (fs.existsSync(join(this.options.srcDir, 'app.html'))) {
this.module = new Module(this) this.options.appTemplatePath = join(this.options.srcDir, 'app.html')
// Init nuxt.js }
this._ready = this.ready()
// Return nuxt.js instance // For serving static/ files to /
return this this.serveStatic = pify(serveStatic(resolve(this.options.srcDir, 'static'), this.options.render.static))
// For serving .nuxt/dist/ files (only when build.publicPath is not an URL)
this.serveStaticNuxt = pify(serveStatic(resolve(this.options.buildDir, 'dist'), {
maxAge: (this.options.dev ? 0 : '1y') // 1 year in production
}))
// gzip middleware for production
if (!this.options.dev && this.options.render.gzip) {
this.gzipMiddleware = pify(compression(this.options.render.gzip))
}
} }
async ready () { async ready () {
if (this._ready) { if (this._ready) {
await this._ready return this._ready
return this
}
// Init modules
await this.module.ready()
// Launch build in development but don't wait for it to be finished
if (this.dev) {
this.build()
} else {
build.production.call(this)
} }
await this.moduleContainer.ready()
await this.builder.ready()
console.log('Nuxt Ready!')
return this return this
} }
@ -178,4 +132,3 @@ class Nuxt {
} }
} }
export default Nuxt

View File

@ -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
View 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
}
}

View File

@ -1,5 +1,3 @@
'use strict'
const http = require('http') const http = require('http')
const connect = require('connect') const connect = require('connect')
const path = require('path') const path = require('path')
@ -7,18 +5,20 @@ const path = require('path')
class Server { class Server {
constructor (nuxt) { constructor (nuxt) {
this.nuxt = nuxt this.nuxt = nuxt
this.options = nuxt.options
// Initialize // Initialize
this.app = connect() this.app = connect()
this.server = http.createServer(this.app) this.server = http.createServer(this.app)
this.nuxt.ready() this.nuxt.ready()
.then(() => { .then(() => {
// Add Middleware // Add Middleware
this.nuxt.options.serverMiddleware.forEach(m => { this.options.serverMiddleware.forEach(m => {
this.useMiddleware(m) this.useMiddleware(m)
})
// Add default render middleware
this.useMiddleware(this.render.bind(this))
}) })
// Add default render middleware
this.useMiddleware(this.render.bind(this))
})
return this return this
} }
@ -49,11 +49,11 @@ class Server {
host = host || '127.0.0.1' host = host || '127.0.0.1'
port = port || 3000 port = port || 3000
this.nuxt.ready() this.nuxt.ready()
.then(() => { .then(() => {
this.server.listen(port, host, () => { this.server.listen(port, host, () => {
console.log('Ready on http://%s:%s', host, port) // eslint-disable-line no-console console.log('Ready on http://%s:%s', host, port) // eslint-disable-line no-console
})
}) })
})
return this return this
} }

View File

@ -1,4 +1,3 @@
'use strict'
import { resolve, sep } from 'path' import { resolve, sep } from 'path'
import _ from 'lodash' import _ from 'lodash'
@ -91,6 +90,7 @@ const normalize = string => string.replace(reqSep, sysSep)
export function r () { export function r () {
let args = Array.from(arguments) let args = Array.from(arguments)
if (_.last(args).includes('~')) { if (_.last(args).includes('~')) {
return wp(_.last(args)) return wp(_.last(args))
} }

View File

@ -1,5 +1,3 @@
'use strict'
import vueLoaderConfig from './vue-loader.config' import vueLoaderConfig from './vue-loader.config'
import { defaults } from 'lodash' import { defaults } from 'lodash'
import { join } from 'path' import { join } from 'path'
@ -15,44 +13,46 @@ import ExtractTextPlugin from 'extract-text-webpack-plugin'
| webpack config files | webpack config files
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
*/ */
export default function ({ isClient, isServer }) { export default function webpackBaseConfig ({ isClient, isServer }) {
const nodeModulesDir = join(__dirname, '..', 'node_modules') const nodeModulesDir = join(__dirname, '..', 'node_modules')
let config = { let config = {
devtool: (this.dev ? 'cheap-module-source-map' : false), devtool: (this.options.dev ? 'cheap-module-source-map' : false),
entry: { entry: {
vendor: ['vue', 'vue-router', 'vue-meta'] vendor: ['vue', 'vue-router', 'vue-meta']
}, },
output: { output: {
publicPath: (isUrl(this.options.build.publicPath) ? this.options.build.publicPath : urlJoin(this.options.router.base, this.options.build.publicPath)) publicPath: (isUrl(this.options.build.publicPath)
? this.options.build.publicPath
: urlJoin(this.options.router.base, this.options.build.publicPath))
}, },
performance: { performance: {
maxEntrypointSize: 300000, maxEntrypointSize: 300000,
maxAssetSize: 300000, maxAssetSize: 300000,
hints: (this.dev ? false : 'warning') hints: (this.options.dev ? false : 'warning')
}, },
resolve: { resolve: {
extensions: ['.js', '.json', '.vue', '.ts'], extensions: ['.js', '.json', '.vue', '.ts'],
// Disable for now // Disable for now
alias: { alias: {
'~': join(this.srcDir), '~': join(this.options.srcDir),
'static': join(this.srcDir, 'static'), // use in template with <img src="~static/nuxt.png" /> 'static': join(this.options.srcDir, 'static'), // use in template with <img src="~static/nuxt.png" />
'~static': join(this.srcDir, 'static'), '~static': join(this.options.srcDir, 'static'),
'assets': join(this.srcDir, 'assets'), // use in template with <img src="~assets/nuxt.png" /> 'assets': join(this.options.srcDir, 'assets'), // use in template with <img src="~assets/nuxt.png" />
'~assets': join(this.srcDir, 'assets'), '~assets': join(this.options.srcDir, 'assets'),
'~plugins': join(this.srcDir, 'plugins'), '~plugins': join(this.options.srcDir, 'plugins'),
'~store': join(this.buildDir, 'store'), '~store': join(this.options.buildDir, 'store'),
'~router': join(this.buildDir, 'router'), '~router': join(this.options.buildDir, 'router'),
'~pages': join(this.srcDir, 'pages'), '~pages': join(this.options.srcDir, 'pages'),
'~components': join(this.srcDir, 'components') '~components': join(this.options.srcDir, 'components')
}, },
modules: [ modules: [
join(this.dir, 'node_modules'), join(this.options.rootDir, 'node_modules'),
nodeModulesDir nodeModulesDir
] ]
}, },
resolveLoader: { resolveLoader: {
modules: [ modules: [
join(this.dir, 'node_modules'), join(this.options.rootDir, 'node_modules'),
nodeModulesDir nodeModulesDir
] ]
}, },
@ -70,7 +70,7 @@ export default function ({ isClient, isServer }) {
query: defaults(this.options.build.babel, { query: defaults(this.options.build.babel, {
presets: ['vue-app'], presets: ['vue-app'],
babelrc: false, babelrc: false,
cacheDirectory: !!this.dev cacheDirectory: !!this.options.dev
}) })
}, },
{ test: /\.css$/, use: styleLoader.call(this, 'css') }, { test: /\.css$/, use: styleLoader.call(this, 'css') },
@ -85,7 +85,7 @@ export default function ({ isClient, isServer }) {
// CSS extraction // CSS extraction
if (extractStyles.call(this)) { if (extractStyles.call(this)) {
config.plugins.push( config.plugins.push(
new ExtractTextPlugin({filename: this.options.build.filenames.css}) new ExtractTextPlugin({ filename: this.options.build.filenames.css })
) )
} }
// Add nuxt build loaders (can be configured in nuxt.config.js) // Add nuxt build loaders (can be configured in nuxt.config.js)

View File

@ -1,5 +1,3 @@
'use strict'
import { each, defaults } from 'lodash' import { each, defaults } from 'lodash'
import webpack from 'webpack' import webpack from 'webpack'
import VueSSRClientPlugin from 'vue-server-renderer/client-plugin' import VueSSRClientPlugin from 'vue-server-renderer/client-plugin'
@ -21,11 +19,11 @@ import { resolve } from 'path'
| In production, will generate public/dist/style.css | In production, will generate public/dist/style.css
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
*/ */
export default function () { export default function webpackClientConfig() {
let config = base.call(this, { isClient: true }) let config = base.call(this, { isClient: true })
// Entry // Entry
config.entry.app = resolve(this.buildDir, 'client.js') config.entry.app = resolve(this.options.buildDir, 'client.js')
// Add vendors // Add vendors
if (this.options.store) { if (this.options.store) {
@ -34,7 +32,7 @@ export default function () {
config.entry.vendor = config.entry.vendor.concat(this.options.build.vendor) config.entry.vendor = config.entry.vendor.concat(this.options.build.vendor)
// Output // Output
config.output.path = resolve(this.buildDir, 'dist') config.output.path = resolve(this.options.buildDir, 'dist')
config.output.filename = this.options.build.filenames.app config.output.filename = this.options.build.filenames.app
// env object defined in nuxt.config.js // env object defined in nuxt.config.js
@ -46,7 +44,7 @@ export default function () {
config.plugins = (config.plugins || []).concat([ config.plugins = (config.plugins || []).concat([
// Strip comments in Vue code // Strip comments in Vue code
new webpack.DefinePlugin(Object.assign(env, { new webpack.DefinePlugin(Object.assign(env, {
'process.env.NODE_ENV': JSON.stringify(env.NODE_ENV || (this.dev ? 'development' : 'production')), 'process.env.NODE_ENV': JSON.stringify(env.NODE_ENV || (this.options.dev ? 'development' : 'production')),
'process.BROWSER_BUILD': true, 'process.BROWSER_BUILD': true,
'process.SERVER_BUILD': false, 'process.SERVER_BUILD': false,
'process.browser': true, 'process.browser': true,
@ -87,11 +85,11 @@ export default function () {
new ProgressBarPlugin() new ProgressBarPlugin()
) )
// Add friendly error plugin // Add friendly error plugin
if (this.dev) { if (this.options.dev) {
config.plugins.push(new FriendlyErrorsWebpackPlugin()) config.plugins.push(new FriendlyErrorsWebpackPlugin())
} }
// Production client build // Production client build
if (!this.dev) { if (!this.options.dev) {
config.plugins.push( config.plugins.push(
// This is needed in webpack 2 for minifying CSS // This is needed in webpack 2 for minifying CSS
new webpack.LoaderOptionsPlugin({ new webpack.LoaderOptionsPlugin({
@ -109,19 +107,19 @@ export default function () {
// Extend config // Extend config
if (typeof this.options.build.extend === 'function') { if (typeof this.options.build.extend === 'function') {
this.options.build.extend.call(this, config, { this.options.build.extend.call(this, config, {
dev: this.dev, dev: this.options.dev,
isClient: true isClient: true
}) })
} }
// Offline-plugin integration // Offline-plugin integration
if (!this.dev && this.options.offline) { if (!this.options.dev && this.options.offline) {
const offlineOpts = typeof this.options.offline === 'object' ? this.options.offline : {} const offlineOpts = typeof this.options.offline === 'object' ? this.options.offline : {}
config.plugins.push( config.plugins.push(
new OfflinePlugin(defaults(offlineOpts, {})) new OfflinePlugin(defaults(offlineOpts, {}))
) )
} }
// Webpack Bundle Analyzer // Webpack Bundle Analyzer
if (!this.dev && this.options.build.analyze) { if (!this.options.dev && this.options.build.analyze) {
let options = {} let options = {}
if (typeof this.options.build.analyze === 'object') { if (typeof this.options.build.analyze === 'object') {
options = this.options.build.analyze options = this.options.build.analyze

View File

@ -1,7 +1,7 @@
import ExtractTextPlugin from 'extract-text-webpack-plugin' import ExtractTextPlugin from 'extract-text-webpack-plugin'
export function extractStyles () { export function extractStyles () {
return !this.dev && this.options.build.extractCSS return !this.options.dev && this.options.build.extractCSS
} }
export function styleLoader (ext, loader = []) { export function styleLoader (ext, loader = []) {

View File

@ -1,5 +1,3 @@
'use strict'
import webpack from 'webpack' import webpack from 'webpack'
import VueSSRServerPlugin from 'vue-server-renderer/server-plugin' import VueSSRServerPlugin from 'vue-server-renderer/server-plugin'
import nodeExternals from 'webpack-node-externals' import nodeExternals from 'webpack-node-externals'
@ -12,7 +10,7 @@ import { resolve } from 'path'
| Webpack Server Config | Webpack Server Config
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
*/ */
export default function () { export default function webpackServerConfig () {
let config = base.call(this, { isServer: true }) let config = base.call(this, { isServer: true })
// env object defined in nuxt.config.js // env object defined in nuxt.config.js
@ -23,10 +21,10 @@ export default function () {
config = Object.assign(config, { config = Object.assign(config, {
target: 'node', target: 'node',
devtool: (this.dev ? 'source-map' : false), devtool: (this.options.dev ? 'source-map' : false),
entry: resolve(this.buildDir, 'server.js'), entry: resolve(this.options.buildDir, 'server.js'),
output: Object.assign({}, config.output, { output: Object.assign({}, config.output, {
path: resolve(this.buildDir, 'dist'), path: resolve(this.options.buildDir, 'dist'),
filename: 'server-bundle.js', filename: 'server-bundle.js',
libraryTarget: 'commonjs2' libraryTarget: 'commonjs2'
}), }),
@ -44,7 +42,7 @@ export default function () {
filename: 'server-bundle.json' filename: 'server-bundle.json'
}), }),
new webpack.DefinePlugin(Object.assign(env, { new webpack.DefinePlugin(Object.assign(env, {
'process.env.NODE_ENV': JSON.stringify(this.dev ? 'development' : 'production'), 'process.env.NODE_ENV': JSON.stringify(this.options.dev ? 'development' : 'production'),
'process.BROWSER_BUILD': false, // deprecated 'process.BROWSER_BUILD': false, // deprecated
'process.SERVER_BUILD': true, // deprecated 'process.SERVER_BUILD': true, // deprecated
'process.browser': false, 'process.browser': false,
@ -53,7 +51,7 @@ export default function () {
]) ])
}) })
// This is needed in webpack 2 for minifying CSS // This is needed in webpack 2 for minifying CSS
if (!this.dev) { if (!this.options.dev) {
config.plugins.push( config.plugins.push(
new webpack.LoaderOptionsPlugin({ new webpack.LoaderOptionsPlugin({
minimize: true minimize: true
@ -64,7 +62,7 @@ export default function () {
// Extend config // Extend config
if (typeof this.options.build.extend === 'function') { if (typeof this.options.build.extend === 'function') {
this.options.build.extend.call(this, config, { this.options.build.extend.call(this, config, {
dev: this.dev, dev: this.options.dev,
isServer: true isServer: true
}) })
} }

View File

@ -1,5 +1,3 @@
'use strict'
import { defaults } from 'lodash' import { defaults } from 'lodash'
import { extractStyles, styleLoader } from './helpers' import { extractStyles, styleLoader } from './helpers'
@ -7,7 +5,7 @@ export default function ({ isClient }) {
let babelOptions = JSON.stringify(defaults(this.options.build.babel, { let babelOptions = JSON.stringify(defaults(this.options.build.babel, {
presets: ['vue-app'], presets: ['vue-app'],
babelrc: false, babelrc: false,
cacheDirectory: !!this.dev cacheDirectory: !!this.options.dev
})) }))
// https://github.com/vuejs/vue-loader/blob/master/docs/en/configurations // https://github.com/vuejs/vue-loader/blob/master/docs/en/configurations