fix: improvements for build and dev stability (#4470)

This commit is contained in:
Pooya Parsa 2018-12-09 14:12:22 +03:30 committed by GitHub
parent 669ffa51ed
commit fe0516978a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 274 additions and 187 deletions

View File

View File

@ -8,14 +8,11 @@ import pify from 'pify'
import serialize from 'serialize-javascript' import serialize from 'serialize-javascript'
import upath from 'upath' import upath from 'upath'
import concat from 'lodash/concat'
import debounce from 'lodash/debounce' import debounce from 'lodash/debounce'
import map from 'lodash/map'
import omit from 'lodash/omit' import omit from 'lodash/omit'
import template from 'lodash/template' import template from 'lodash/template'
import uniq from 'lodash/uniq' import uniq from 'lodash/uniq'
import uniqBy from 'lodash/uniqBy' import uniqBy from 'lodash/uniqBy'
import values from 'lodash/values'
import devalue from '@nuxtjs/devalue' import devalue from '@nuxtjs/devalue'
@ -59,10 +56,11 @@ export default class Builder {
this.nuxt.hook('build:done', () => { this.nuxt.hook('build:done', () => {
consola.info('Waiting for file changes') consola.info('Waiting for file changes')
this.watchClient() this.watchClient()
this.watchRestart()
}) })
// Stop watching on nuxt.close() // Close hook
this.nuxt.hook('close', () => this.unwatch()) this.nuxt.hook('close', () => this.close())
} }
if (this.options.build.analyze) { if (this.options.build.analyze) {
@ -502,6 +500,7 @@ export default class Builder {
watchClient() { watchClient() {
const src = this.options.srcDir const src = this.options.srcDir
let patterns = [ let patterns = [
r(src, this.options.dir.layouts), r(src, this.options.dir.layouts),
r(src, this.options.dir.store), r(src, this.options.dir.store),
@ -509,6 +508,7 @@ export default class Builder {
r(src, `${this.options.dir.layouts}/*.{vue,js}`), r(src, `${this.options.dir.layouts}/*.{vue,js}`),
r(src, `${this.options.dir.layouts}/**/*.{vue,js}`) r(src, `${this.options.dir.layouts}/**/*.{vue,js}`)
] ]
if (this._nuxtPages) { if (this._nuxtPages) {
patterns.push( patterns.push(
r(src, this.options.dir.pages), r(src, this.options.dir.pages),
@ -516,7 +516,8 @@ export default class Builder {
r(src, `${this.options.dir.pages}/**/*.{vue,js}`) r(src, `${this.options.dir.pages}/**/*.{vue,js}`)
) )
} }
patterns = map(patterns, upath.normalizeSafe)
patterns = patterns.map(upath.normalizeSafe)
const options = this.options.watchers.chokidar const options = this.options.watchers.chokidar
/* istanbul ignore next */ /* istanbul ignore next */
@ -529,43 +530,49 @@ export default class Builder {
.on('unlink', refreshFiles) .on('unlink', refreshFiles)
// Watch for custom provided files // Watch for custom provided files
let customPatterns = concat( const customPatterns = uniq([
this.options.build.watch, ...this.options.build.watch,
...values(omit(this.options.build.styleResources, ['options'])) ...Object.values(omit(this.options.build.styleResources, ['options']))
) ]).map(upath.normalizeSafe)
customPatterns = map(uniq(customPatterns), upath.normalizeSafe)
this.watchers.custom = chokidar this.watchers.custom = chokidar
.watch(customPatterns, options) .watch(customPatterns, options)
.on('change', refreshFiles) .on('change', refreshFiles)
} }
watchServer() { watchRestart() {
const nuxtRestartWatch = concat( const nuxtRestartWatch = [
this.options.serverMiddleware // Server middleware
.filter(isString) ...this.options.serverMiddleware.filter(isString),
.map(this.nuxt.resolver.resolveAlias), // Custom watchers
this.options.watch.map(this.nuxt.resolver.resolveAlias), ...this.options.watch
path.join(this.options.rootDir, 'nuxt.config.js') ].map(this.nuxt.resolver.resolveAlias)
)
this.watchers.restart = chokidar this.watchers.restart = chokidar
.watch(nuxtRestartWatch, this.options.watchers.chokidar) .watch(nuxtRestartWatch, this.options.watchers.chokidar)
.on('change', (_path) => { .on('all', (event, _path) => {
this.watchers.restart.close() if (['add', 'change', 'unlink'].includes(event) === false) return
const { name, ext } = path.parse(_path) this.nuxt.callHook('watch:fileChanged', this, _path) // Legacy
this.nuxt.callHook('watch:fileChanged', this, `${name}${ext}`) this.nuxt.callHook('watch:restart', { event, path: _path })
}) })
} }
async unwatch() { unwatch() {
for (const watcher in this.watchers) { for (const watcher in this.watchers) {
if (this.watchers[watcher]) { this.watchers[watcher].close()
this.watchers[watcher].close()
}
} }
}
if (this.bundleBuilder.unwatch) { async close() {
await this.bundleBuilder.unwatch() if (this.__closed) return
this.__closed = true
// Unwatch
this.unwatch()
// Close bundleBuilder
if (typeof this.bundleBuilder.close === 'function') {
await this.bundleBuilder.close()
} }
} }
} }

View File

@ -6,12 +6,8 @@ import * as commands from './commands'
import * as imports from './imports' import * as imports from './imports'
export default class NuxtCommand { export default class NuxtCommand {
constructor({ name, description, usage, options, run } = {}) { constructor(cmd = { name: '', usage: '', description: '', options: {} }) {
this.name = name || '' this.cmd = cmd
this.description = description || ''
this.usage = usage || ''
this.options = Object.assign({}, options)
this._run = run
} }
static async load(name) { static async load(name) {
@ -33,7 +29,7 @@ export default class NuxtCommand {
} }
run() { run() {
return this._run(this) return this.cmd.run(this)
} }
showVersion() { showVersion() {
@ -63,8 +59,8 @@ export default class NuxtCommand {
const config = await loadNuxtConfig(argv) const config = await loadNuxtConfig(argv)
const options = Object.assign(config, extraOptions || {}) const options = Object.assign(config, extraOptions || {})
for (const name of Object.keys(this.options)) { for (const name of Object.keys(this.cmd.options)) {
this.options[name].prepare && this.options[name].prepare(this, options, argv) this.cmd.options[name].prepare && this.cmd.options[name].prepare(this, options, argv)
} }
return options return options
@ -97,8 +93,8 @@ export default class NuxtCommand {
default: {} default: {}
} }
for (const name of Object.keys(this.options)) { for (const name of Object.keys(this.cmd.options)) {
const option = this.options[name] const option = this.cmd.options[name]
if (option.alias) { if (option.alias) {
minimistOptions.alias[option.alias] = name minimistOptions.alias[option.alias] = name
@ -118,8 +114,8 @@ export default class NuxtCommand {
const options = [] const options = []
let maxOptionLength = 0 let maxOptionLength = 0
for (const name in this.options) { for (const name in this.cmd.options) {
const option = this.options[name] const option = this.cmd.options[name]
let optionHelp = '--' let optionHelp = '--'
optionHelp += option.type === 'boolean' && option.default ? 'no-' : '' optionHelp += option.type === 'boolean' && option.default ? 'no-' : ''
@ -141,12 +137,12 @@ export default class NuxtCommand {
) )
}).join('\n') }).join('\n')
const usage = foldLines(`Usage: nuxt ${this.usage} [options]`, startSpaces) const usage = foldLines(`Usage: nuxt ${this.cmd.usage} [options]`, startSpaces)
const description = foldLines(this.description, startSpaces) const description = foldLines(this.cmd.description, startSpaces)
const opts = foldLines(`Options:`, startSpaces) + '\n\n' + _opts const opts = foldLines(`Options:`, startSpaces) + '\n\n' + _opts
let helpText = colorize(`${usage}\n\n`) let helpText = colorize(`${usage}\n\n`)
if (this.description) helpText += colorize(`${description}\n\n`) if (this.cmd.description) helpText += colorize(`${description}\n\n`)
if (options.length) helpText += colorize(`${opts}\n\n`) if (options.length) helpText += colorize(`${opts}\n\n`)
return helpText return helpText

View File

@ -1,8 +1,7 @@
import consola from 'consola' import consola from 'consola'
import chalk from 'chalk' import chalk from 'chalk'
import env from 'std-env'
import { common, server } from '../options' import { common, server } from '../options'
import { showBanner } from '../utils' import { showBanner, eventsMapping, formatPath } from '../utils'
export default { export default {
name: 'dev', name: 'dev',
@ -12,68 +11,68 @@ export default {
...common, ...common,
...server ...server
}, },
async run(cmd) { async run(cmd) {
const argv = cmd.getArgv() const argv = cmd.getArgv()
await this.startDev(cmd, argv)
},
const errorHandler = (err, instance) => { async startDev(cmd, argv) {
instance && instance.builder.watchServer() try {
consola.error(err) await this._startDev(cmd, argv)
} catch (error) {
consola.error(error)
} }
},
// Start dev async _startDev(cmd, argv) {
async function startDev(oldInstance) { // Load config
let nuxt, builder const config = await cmd.getNuxtConfig(argv, { dev: true })
try { // Initialize nuxt instance
nuxt = await cmd.getNuxt( const nuxt = await cmd.getNuxt(config)
await cmd.getNuxtConfig(argv, { dev: true })
)
builder = await cmd.getBuilder(nuxt)
} catch (err) {
return errorHandler(err, oldInstance)
}
const logChanged = (name) => { // Setup hooks
consola.log({ nuxt.hook('watch:restart', payload => this.onWatchRestart(payload, { nuxt, builder, cmd, argv }))
type: 'change', nuxt.hook('bundler:change', changedFileName => this.onBundlerChange(changedFileName))
icon: chalk.blue.bold(env.windows ? '»' : '↻'),
message: chalk.blue(name)
})
}
nuxt.hook('watch:fileChanged', async (builder, name) => { // Start listening
logChanged(name) await nuxt.server.listen()
await startDev({ nuxt: builder.nuxt, builder })
})
nuxt.hook('bundler:change', (name) => { // Create builder instance
logChanged(name) const builder = await cmd.getBuilder(nuxt)
})
return ( // Start Build
Promise.resolve() await builder.build()
.then(() => oldInstance && oldInstance.nuxt.clearHook('watch:fileChanged'))
.then(() => oldInstance && oldInstance.builder.unwatch())
// Start build
.then(() => builder.build())
// Close old nuxt no matter if build successfully
.catch((err) => {
oldInstance && oldInstance.nuxt.close()
// Jump to errorHandler
throw err
})
.then(() => oldInstance && oldInstance.nuxt.close())
// Start listening
.then(() => nuxt.server.listen())
// Show banner
.then(() => showBanner(nuxt))
// Start watching serverMiddleware changes
.then(() => builder.watchServer())
// Handle errors
.catch(err => errorHandler(err, { builder, nuxt }))
)
}
await startDev() // Show banner after build
showBanner(nuxt)
// Return instance
return nuxt
},
logChanged({ event, path }) {
const { icon, color, action } = eventsMapping[event] || eventsMapping.change
consola.log({
type: event,
icon: chalk[color].bold(icon),
message: `${action} ${chalk.cyan(path)}`
})
},
async onWatchRestart({ event, path }, { nuxt, cmd, argv }) {
this.logChanged({
event,
path: formatPath(path)
})
await nuxt.close()
await this.startDev(cmd, argv)
},
onBundlerChange(changedFileName) {
this.logChanged(changedFileName)
} }
} }

View File

@ -18,6 +18,12 @@ const _require = esm(module, {
} }
}) })
export const eventsMapping = {
add: { icon: '+', color: 'green', action: 'Created' },
change: { icon: env.windows ? '»' : '↻', color: 'blue', action: 'Updated' },
unlink: { icon: '-', color: 'red', action: 'Removed' }
}
const getRootDir = argv => path.resolve(argv._[0] || '.') const getRootDir = argv => path.resolve(argv._[0] || '.')
const getNuxtConfigFile = argv => path.resolve(getRootDir(argv), argv['config-file']) const getNuxtConfigFile = argv => path.resolve(getRootDir(argv), argv['config-file'])
@ -45,6 +51,9 @@ export async function loadNuxtConfig(argv) {
consola.fatal('Error while fetching async configuration') consola.fatal('Error while fetching async configuration')
} }
} }
// Keep _nuxtConfigFile for watching
options._nuxtConfigFile = nuxtConfigFile
} else if (argv['config-file'] !== 'nuxt.config.js') { } else if (argv['config-file'] !== 'nuxt.config.js') {
consola.fatal('Could not load config file: ' + argv['config-file']) consola.fatal('Could not load config file: ' + argv['config-file'])
} }
@ -128,3 +137,10 @@ export function normalizeArg(arg, defaultValue) {
} }
return arg return arg
} }
export function formatPath(filePath) {
if (!filePath) {
return
}
return filePath.replace(process.cwd() + path.sep, '')
}

View File

@ -13,7 +13,7 @@ describe('dev', () => {
expect(typeof dev.run).toBe('function') expect(typeof dev.run).toBe('function')
}) })
test('reloads on fileChanged hook', async () => { test('reloads on fileRestartHook', async () => {
const Nuxt = mockNuxt() const Nuxt = mockNuxt()
const Builder = mockBuilder() const Builder = mockBuilder()
@ -23,21 +23,18 @@ describe('dev', () => {
expect(Builder.prototype.build).toHaveBeenCalled() expect(Builder.prototype.build).toHaveBeenCalled()
expect(Nuxt.prototype.server.listen).toHaveBeenCalled() expect(Nuxt.prototype.server.listen).toHaveBeenCalled()
expect(Builder.prototype.watchServer).toHaveBeenCalled() // expect(Builder.prototype.watchRestart).toHaveBeenCalled()
jest.clearAllMocks() jest.clearAllMocks()
const builder = new Builder() const builder = new Builder()
builder.nuxt = new Nuxt() builder.nuxt = new Nuxt()
await Nuxt.fileChangedHook(builder) await Nuxt.fileRestartHook(builder)
expect(consola.log).toHaveBeenCalled() expect(consola.log).toHaveBeenCalled()
expect(Nuxt.prototype.clearHook).toHaveBeenCalled()
expect(Builder.prototype.unwatch).toHaveBeenCalled()
expect(Builder.prototype.build).toHaveBeenCalled() expect(Builder.prototype.build).toHaveBeenCalled()
expect(Nuxt.prototype.close).toHaveBeenCalled() expect(Nuxt.prototype.close).toHaveBeenCalled()
expect(Nuxt.prototype.server.listen).toHaveBeenCalled() expect(Nuxt.prototype.server.listen).toHaveBeenCalled()
expect(Builder.prototype.watchServer).toHaveBeenCalled()
expect(consola.error).not.toHaveBeenCalled() expect(consola.error).not.toHaveBeenCalled()
}) })
@ -53,13 +50,13 @@ describe('dev', () => {
const builder = new Builder() const builder = new Builder()
builder.nuxt = new Nuxt() builder.nuxt = new Nuxt()
Builder.prototype.build = jest.fn().mockImplementationOnce(() => Promise.reject(new Error('Build Error'))) Builder.prototype.build = jest.fn().mockImplementationOnce(() => Promise.reject(new Error('Build Error')))
await Nuxt.fileChangedHook(builder) await Nuxt.fileRestartHook(builder)
expect(Nuxt.prototype.close).toHaveBeenCalled() expect(Nuxt.prototype.close).toHaveBeenCalled()
expect(consola.error).toHaveBeenCalledWith(new Error('Build Error')) expect(consola.error).toHaveBeenCalledWith(new Error('Build Error'))
}) })
test('catches watchServer error', async () => { test.skip('catches watchRestart error', async () => {
const Nuxt = mockNuxt() const Nuxt = mockNuxt()
const Builder = mockBuilder() const Builder = mockBuilder()
@ -68,11 +65,11 @@ describe('dev', () => {
const builder = new Builder() const builder = new Builder()
builder.nuxt = new Nuxt() builder.nuxt = new Nuxt()
Builder.prototype.watchServer = jest.fn().mockImplementationOnce(() => Promise.reject(new Error('watchServer Error'))) Builder.prototype.watchRestart = jest.fn().mockImplementationOnce(() => Promise.reject(new Error('watchRestart Error')))
await Nuxt.fileChangedHook(builder) await Nuxt.fileRestartHook(builder)
expect(consola.error).toHaveBeenCalledWith(new Error('watchServer Error')) expect(consola.error).toHaveBeenCalledWith(new Error('watchRestart Error'))
expect(Builder.prototype.watchServer).toHaveBeenCalledTimes(2) expect(Builder.prototype.watchRestart).toHaveBeenCalledTimes(2)
}) })
test('catches error on hook error', async () => { test('catches error on hook error', async () => {
@ -87,10 +84,10 @@ describe('dev', () => {
}) })
const builder = new Builder() const builder = new Builder()
builder.nuxt = new Nuxt() builder.nuxt = new Nuxt()
await Nuxt.fileChangedHook(builder) await Nuxt.fileRestartHook(builder)
expect(consola.error).toHaveBeenCalledWith(new Error('Config Error')) expect(consola.error).toHaveBeenCalledWith(new Error('Config Error'))
expect(Builder.prototype.watchServer).toHaveBeenCalledTimes(1) // expect(Builder.prototype.watchRestart).toHaveBeenCalledTimes(1)
}) })
test('catches error on startDev', async () => { test('catches error on startDev', async () => {

View File

@ -84,12 +84,13 @@ export const mockNuxt = (implementation) => {
const Nuxt = function () {} const Nuxt = function () {}
Object.assign(Nuxt.prototype, { Object.assign(Nuxt.prototype, {
hook(type, fn) { hook(type, fn) {
if (type === 'watch:fileChanged') { if (type === 'watch:restart') {
Nuxt.fileChangedHook = fn Nuxt.fileRestartHook = fn
} }
}, },
options: {}, options: {},
clearHook: jest.fn(), clearHook: jest.fn(),
clearHooks: jest.fn(),
close: jest.fn(), close: jest.fn(),
ready: jest.fn(), ready: jest.fn(),
server: { server: {
@ -108,7 +109,7 @@ export const mockBuilder = (implementation) => {
Object.assign(Builder.prototype, { Object.assign(Builder.prototype, {
build: jest.fn().mockImplementationOnce(() => Promise.resolve()), build: jest.fn().mockImplementationOnce(() => Promise.resolve()),
unwatch: jest.fn().mockImplementationOnce(() => Promise.resolve()), unwatch: jest.fn().mockImplementationOnce(() => Promise.resolve()),
watchServer: jest.fn().mockImplementationOnce(() => Promise.resolve()) watchRestart: jest.fn().mockImplementationOnce(() => Promise.resolve())
}, implementation || {}) }, implementation || {})
imports.builder.mockImplementation(() => ({ Builder })) imports.builder.mockImplementation(() => ({ Builder }))

View File

@ -45,6 +45,10 @@ export default class Hookable {
} }
} }
clearHooks() {
this._hooks = {}
}
flatHooks(configHooks, hooks = {}, parentName) { flatHooks(configHooks, hooks = {}, parentName) {
Object.keys(configHooks).forEach((key) => { Object.keys(configHooks).forEach((key) => {
const subHook = configHooks[key] const subHook = configHooks[key]

View File

@ -27,6 +27,7 @@ export default () => ({
serverMiddleware: [], serverMiddleware: [],
// Dirs and extensions // Dirs and extensions
_nuxtConfigFile: undefined,
srcDir: undefined, srcDir: undefined,
buildDir: '.nuxt', buildDir: '.nuxt',
modulesDir: [ modulesDir: [

View File

@ -10,8 +10,14 @@ import { guardDir, isNonEmptyString, isPureObject, isUrl } from '@nuxt/common'
import { getDefaultNuxtConfig } from './config' import { getDefaultNuxtConfig } from './config'
export function getNuxtConfig(_options) { export function getNuxtConfig(_options) {
// Prevent duplicate calls
if (_options.__normalized__) {
return _options
}
// Clone options to prevent unwanted side-effects // Clone options to prevent unwanted side-effects
const options = Object.assign({}, _options) const options = Object.assign({}, _options)
options.__normalized__ = true
// Normalize options // Normalize options
if (options.loading === true) { if (options.loading === true) {
@ -76,6 +82,14 @@ export function getNuxtConfig(_options) {
// Resolve buildDir // Resolve buildDir
options.buildDir = path.resolve(options.rootDir, options.buildDir) options.buildDir = path.resolve(options.rootDir, options.buildDir)
// Default value for _nuxtConfigFile
if (!options._nuxtConfigFile) {
options._nuxtConfigFile = path.resolve(options.rootDir, 'nuxt.config.js')
}
// Watch for _nuxtConfigFile changes
options.watch.push(options._nuxtConfigFile)
// Protect rootDir against buildDir // Protect rootDir against buildDir
guardDir(options, 'rootDir', 'buildDir') guardDir(options, 'rootDir', 'buildDir')

View File

@ -79,5 +79,7 @@ export default class Nuxt extends Hookable {
if (typeof callback === 'function') { if (typeof callback === 'function') {
await callback() await callback()
} }
this.clearHooks()
} }
} }

View File

@ -1,4 +1,5 @@
import path from 'path' import path from 'path'
import consola from 'consola'
import launchMiddleware from 'launch-editor-middleware' import launchMiddleware from 'launch-editor-middleware'
import serveStatic from 'serve-static' import serveStatic from 'serve-static'
import servePlaceholder from 'serve-placeholder' import servePlaceholder from 'serve-placeholder'
@ -35,6 +36,9 @@ export default class Server {
// Create new connect instance // Create new connect instance
this.app = connect() this.app = connect()
// Close hook
this.nuxt.hook('close', () => this.close())
} }
async ready() { async ready() {
@ -52,14 +56,6 @@ export default class Server {
// Call done hook // Call done hook
await this.nuxt.callHook('render:done', this) await this.nuxt.callHook('render:done', this)
// Close all listeners after nuxt close
this.nuxt.hook('close', async () => {
for (const listener of this.listeners) {
await listener.close()
}
this.listeners = []
})
} }
async setupMiddleware() { async setupMiddleware() {
@ -175,16 +171,18 @@ export default class Server {
} }
useMiddleware(middleware) { useMiddleware(middleware) {
// Resolve middleware let handler = middleware.handler || middleware
if (typeof middleware === 'string') {
middleware = this.nuxt.resolver.requireModule(middleware)
}
// Resolve handler // Resolve handler setup as string (path)
if (typeof middleware.handler === 'string') { if (typeof handler === 'string') {
middleware.handler = this.nuxt.resolver.requireModule(middleware.handler) try {
handler = this.nuxt.resolver.requireModule(middleware.handler || middleware)
} catch (err) {
if (!this.options.dev) throw err[0]
// Only warn missing file in development
consola.warn(err[0])
}
} }
const handler = middleware.handler || middleware
// Resolve path // Resolve path
const path = ( const path = (
@ -231,4 +229,25 @@ export default class Server {
await this.nuxt.callHook('listen', listener.server, listener) await this.nuxt.callHook('listen', listener.server, listener)
} }
async close() {
if (this.__closed) return
this.__closed = true
for (const listener of this.listeners) {
await listener.close()
}
this.listeners = []
if (typeof this.renderer.close === 'function') {
await this.renderer.close()
}
this.app.removeAllListeners()
this.app = null
for (const key in this.resources) {
delete this.resources[key]
}
}
} }

View File

@ -448,4 +448,13 @@ export default class VueRenderer {
interpolate: /{{([\s\S]+?)}}/g interpolate: /{{([\s\S]+?)}}/g
}) })
} }
close() {
if (this.__closed) return
this.__closed = true
for (const key in this.renderer) {
delete this.renderer[key]
}
}
} }

View File

@ -30,8 +30,10 @@ export class WebpackBundler {
// Initialize shared MFS for dev // Initialize shared MFS for dev
if (this.context.options.dev) { if (this.context.options.dev) {
this.mfs = new MFS() this.mfs = new MFS()
this.mfs.exists = function (...args) { return Promise.resolve(this.existsSync(...args)) }
this.mfs.readFile = function (...args) { return Promise.resolve(this.readFileSync(...args)) } // TODO: Enable when async FS rquired
// this.mfs.exists = function (...args) { return Promise.resolve(this.existsSync(...args)) }
// this.mfs.readFile = function (...args) { return Promise.resolve(this.readFileSync(...args)) }
} }
} }
@ -121,63 +123,59 @@ export class WebpackBundler {
}) })
} }
webpackCompile(compiler) { async webpackCompile(compiler) {
return new Promise(async (resolve, reject) => { const name = compiler.options.name
const name = compiler.options.name const { nuxt, options } = this.context
const { nuxt, options } = this.context
await nuxt.callHook('build:compile', { name, compiler }) await nuxt.callHook('build:compile', { name, compiler })
// Load renderer resources after build // Load renderer resources after build
compiler.hooks.done.tap('load-resources', async (stats) => { compiler.hooks.done.tap('load-resources', async (stats) => {
await nuxt.callHook('build:compiled', { await nuxt.callHook('build:compiled', {
name, name,
compiler, compiler,
stats stats
})
// Reload renderer if available
await nuxt.callHook('build:resources', this.mfs)
// Resolve on next tick
process.nextTick(resolve)
}) })
if (options.dev) { // Reload renderer
// --- Dev Build --- await nuxt.callHook('build:resources', this.mfs)
// Client Build, watch is started by dev-middleware })
if (['client', 'modern'].includes(name)) {
return this.webpackDev(compiler)
}
// Server, build and watch for changes
this.compilersWatching.push(
compiler.watch(options.watchers.webpack, (err) => {
/* istanbul ignore if */
if (err) return reject(err)
})
)
} else {
// --- Production Build ---
compiler.run((err, stats) => {
/* istanbul ignore next */
if (err) {
return reject(err)
} else if (stats.hasErrors()) {
if (options.build.quiet === true) {
err = stats.toString(options.build.stats)
}
if (!err) {
// actual errors will be printed by webpack itself
err = 'Nuxt Build Error'
}
return reject(err) // --- Dev Build ---
} if (options.dev) {
// Client Build, watch is started by dev-middleware
resolve() if (['client', 'modern'].includes(name)) {
return new Promise((resolve, reject) => {
compiler.hooks.done.tap('nuxt-dev', () => resolve())
this.webpackDev(compiler)
}) })
} }
})
// Server, build and watch for changes
return new Promise((resolve, reject) => {
const watching = compiler.watch(options.watchers.webpack, (err) => {
if (err) {
return reject(err)
}
watching.close = pify(watching.close)
this.compilersWatching.push(watching)
resolve()
})
})
}
// --- Production Build ---
compiler.run = pify(compiler.run)
const stats = await compiler.run()
if (stats.hasErrors()) {
if (options.build.quiet === true) {
return Promise.reject(stats.toString(options.build.stats))
} else {
// Actual error will be printet by webpack
throw new Error('Nuxt Build Error')
}
}
} }
webpackDev(compiler) { webpackDev(compiler) {
@ -229,12 +227,33 @@ export class WebpackBundler {
async unwatch() { async unwatch() {
for (const watching of this.compilersWatching) { for (const watching of this.compilersWatching) {
watching.close() await watching.close()
} }
}
async close() {
if (this.__closed) return
this.__closed = true
// Unwatch
await this.unwatch()
// Stop webpack middleware // Stop webpack middleware
for (const devMiddleware of Object.values(this.devMiddleware)) { for (const devMiddleware of Object.values(this.devMiddleware)) {
await devMiddleware.close() await devMiddleware.close()
} }
// Cleanup MFS
if (this.mfs) {
delete this.mfs.data
delete this.mfs
}
// Cleanup more resources
delete this.compilers
delete this.compilersWatching
delete this.devMiddleware
delete this.hotMiddleware
} }
forGenerate() { forGenerate() {

View File

@ -75,8 +75,11 @@ describe('fallback generate', () => {
}) })
test('generate.fallback = true is transformed to /404.html', () => { test('generate.fallback = true is transformed to /404.html', () => {
nuxt.options.generate.fallback = true const options = getNuxtConfig({
const options = getNuxtConfig(nuxt.options) generate: {
fallback: true
}
})
expect(options.generate.fallback).toBe('404.html') expect(options.generate.fallback).toBe('404.html')
}) })