From fe0516978a650166bf178c0339dd7657c29e2504 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sun, 9 Dec 2018 14:12:22 +0330 Subject: [PATCH] fix: improvements for build and dev stability (#4470) --- examples/hello-world/nuxt.config.js | 0 packages/builder/src/builder.js | 65 ++++++++------ packages/cli/src/command.js | 28 +++--- packages/cli/src/commands/dev.js | 107 +++++++++++----------- packages/cli/src/utils/index.js | 16 ++++ packages/cli/test/unit/dev.test.js | 25 +++--- packages/cli/test/utils/mocking.js | 7 +- packages/common/src/hookable.js | 4 + packages/config/src/config/_common.js | 1 + packages/config/src/options.js | 14 +++ packages/core/src/nuxt.js | 2 + packages/server/src/server.js | 51 +++++++---- packages/vue-renderer/src/renderer.js | 9 ++ packages/webpack/src/builder.js | 125 +++++++++++++++----------- test/unit/fallback.generate.test.js | 7 +- 15 files changed, 274 insertions(+), 187 deletions(-) create mode 100644 examples/hello-world/nuxt.config.js diff --git a/examples/hello-world/nuxt.config.js b/examples/hello-world/nuxt.config.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/builder/src/builder.js b/packages/builder/src/builder.js index 6ebeee904b..7a457a0d7f 100644 --- a/packages/builder/src/builder.js +++ b/packages/builder/src/builder.js @@ -8,14 +8,11 @@ import pify from 'pify' import serialize from 'serialize-javascript' import upath from 'upath' -import concat from 'lodash/concat' import debounce from 'lodash/debounce' -import map from 'lodash/map' import omit from 'lodash/omit' import template from 'lodash/template' import uniq from 'lodash/uniq' import uniqBy from 'lodash/uniqBy' -import values from 'lodash/values' import devalue from '@nuxtjs/devalue' @@ -59,10 +56,11 @@ export default class Builder { this.nuxt.hook('build:done', () => { consola.info('Waiting for file changes') this.watchClient() + this.watchRestart() }) - // Stop watching on nuxt.close() - this.nuxt.hook('close', () => this.unwatch()) + // Close hook + this.nuxt.hook('close', () => this.close()) } if (this.options.build.analyze) { @@ -502,6 +500,7 @@ export default class Builder { watchClient() { const src = this.options.srcDir + let patterns = [ r(src, this.options.dir.layouts), 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}`) ] + if (this._nuxtPages) { patterns.push( r(src, this.options.dir.pages), @@ -516,7 +516,8 @@ export default class Builder { r(src, `${this.options.dir.pages}/**/*.{vue,js}`) ) } - patterns = map(patterns, upath.normalizeSafe) + + patterns = patterns.map(upath.normalizeSafe) const options = this.options.watchers.chokidar /* istanbul ignore next */ @@ -529,43 +530,49 @@ export default class Builder { .on('unlink', refreshFiles) // Watch for custom provided files - let customPatterns = concat( - this.options.build.watch, - ...values(omit(this.options.build.styleResources, ['options'])) - ) - customPatterns = map(uniq(customPatterns), upath.normalizeSafe) + const customPatterns = uniq([ + ...this.options.build.watch, + ...Object.values(omit(this.options.build.styleResources, ['options'])) + ]).map(upath.normalizeSafe) + this.watchers.custom = chokidar .watch(customPatterns, options) .on('change', refreshFiles) } - watchServer() { - const nuxtRestartWatch = concat( - this.options.serverMiddleware - .filter(isString) - .map(this.nuxt.resolver.resolveAlias), - this.options.watch.map(this.nuxt.resolver.resolveAlias), - path.join(this.options.rootDir, 'nuxt.config.js') - ) + watchRestart() { + const nuxtRestartWatch = [ + // Server middleware + ...this.options.serverMiddleware.filter(isString), + // Custom watchers + ...this.options.watch + ].map(this.nuxt.resolver.resolveAlias) this.watchers.restart = chokidar .watch(nuxtRestartWatch, this.options.watchers.chokidar) - .on('change', (_path) => { - this.watchers.restart.close() - const { name, ext } = path.parse(_path) - this.nuxt.callHook('watch:fileChanged', this, `${name}${ext}`) + .on('all', (event, _path) => { + if (['add', 'change', 'unlink'].includes(event) === false) return + this.nuxt.callHook('watch:fileChanged', this, _path) // Legacy + this.nuxt.callHook('watch:restart', { event, path: _path }) }) } - async unwatch() { + unwatch() { for (const watcher in this.watchers) { - if (this.watchers[watcher]) { - this.watchers[watcher].close() - } + this.watchers[watcher].close() } + } - if (this.bundleBuilder.unwatch) { - await this.bundleBuilder.unwatch() + async close() { + if (this.__closed) return + this.__closed = true + + // Unwatch + this.unwatch() + + // Close bundleBuilder + if (typeof this.bundleBuilder.close === 'function') { + await this.bundleBuilder.close() } } } diff --git a/packages/cli/src/command.js b/packages/cli/src/command.js index 0b37fe6825..4083450dd7 100644 --- a/packages/cli/src/command.js +++ b/packages/cli/src/command.js @@ -6,12 +6,8 @@ import * as commands from './commands' import * as imports from './imports' export default class NuxtCommand { - constructor({ name, description, usage, options, run } = {}) { - this.name = name || '' - this.description = description || '' - this.usage = usage || '' - this.options = Object.assign({}, options) - this._run = run + constructor(cmd = { name: '', usage: '', description: '', options: {} }) { + this.cmd = cmd } static async load(name) { @@ -33,7 +29,7 @@ export default class NuxtCommand { } run() { - return this._run(this) + return this.cmd.run(this) } showVersion() { @@ -63,8 +59,8 @@ export default class NuxtCommand { const config = await loadNuxtConfig(argv) const options = Object.assign(config, extraOptions || {}) - for (const name of Object.keys(this.options)) { - this.options[name].prepare && this.options[name].prepare(this, options, argv) + for (const name of Object.keys(this.cmd.options)) { + this.cmd.options[name].prepare && this.cmd.options[name].prepare(this, options, argv) } return options @@ -97,8 +93,8 @@ export default class NuxtCommand { default: {} } - for (const name of Object.keys(this.options)) { - const option = this.options[name] + for (const name of Object.keys(this.cmd.options)) { + const option = this.cmd.options[name] if (option.alias) { minimistOptions.alias[option.alias] = name @@ -118,8 +114,8 @@ export default class NuxtCommand { const options = [] let maxOptionLength = 0 - for (const name in this.options) { - const option = this.options[name] + for (const name in this.cmd.options) { + const option = this.cmd.options[name] let optionHelp = '--' optionHelp += option.type === 'boolean' && option.default ? 'no-' : '' @@ -141,12 +137,12 @@ export default class NuxtCommand { ) }).join('\n') - const usage = foldLines(`Usage: nuxt ${this.usage} [options]`, startSpaces) - const description = foldLines(this.description, startSpaces) + const usage = foldLines(`Usage: nuxt ${this.cmd.usage} [options]`, startSpaces) + const description = foldLines(this.cmd.description, startSpaces) const opts = foldLines(`Options:`, startSpaces) + '\n\n' + _opts 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`) return helpText diff --git a/packages/cli/src/commands/dev.js b/packages/cli/src/commands/dev.js index b8999d2a68..7d6f6f5f86 100644 --- a/packages/cli/src/commands/dev.js +++ b/packages/cli/src/commands/dev.js @@ -1,8 +1,7 @@ import consola from 'consola' import chalk from 'chalk' -import env from 'std-env' import { common, server } from '../options' -import { showBanner } from '../utils' +import { showBanner, eventsMapping, formatPath } from '../utils' export default { name: 'dev', @@ -12,68 +11,68 @@ export default { ...common, ...server }, + async run(cmd) { const argv = cmd.getArgv() + await this.startDev(cmd, argv) + }, - const errorHandler = (err, instance) => { - instance && instance.builder.watchServer() - consola.error(err) + async startDev(cmd, argv) { + try { + await this._startDev(cmd, argv) + } catch (error) { + consola.error(error) } + }, - // Start dev - async function startDev(oldInstance) { - let nuxt, builder + async _startDev(cmd, argv) { + // Load config + const config = await cmd.getNuxtConfig(argv, { dev: true }) - try { - nuxt = await cmd.getNuxt( - await cmd.getNuxtConfig(argv, { dev: true }) - ) - builder = await cmd.getBuilder(nuxt) - } catch (err) { - return errorHandler(err, oldInstance) - } + // Initialize nuxt instance + const nuxt = await cmd.getNuxt(config) - const logChanged = (name) => { - consola.log({ - type: 'change', - icon: chalk.blue.bold(env.windows ? '»' : '↻'), - message: chalk.blue(name) - }) - } + // Setup hooks + nuxt.hook('watch:restart', payload => this.onWatchRestart(payload, { nuxt, builder, cmd, argv })) + nuxt.hook('bundler:change', changedFileName => this.onBundlerChange(changedFileName)) - nuxt.hook('watch:fileChanged', async (builder, name) => { - logChanged(name) - await startDev({ nuxt: builder.nuxt, builder }) - }) + // Start listening + await nuxt.server.listen() - nuxt.hook('bundler:change', (name) => { - logChanged(name) - }) + // Create builder instance + const builder = await cmd.getBuilder(nuxt) - return ( - Promise.resolve() - .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 })) - ) - } + // Start Build + await builder.build() - 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) } } diff --git a/packages/cli/src/utils/index.js b/packages/cli/src/utils/index.js index 5a1eca0f5e..56714eea82 100644 --- a/packages/cli/src/utils/index.js +++ b/packages/cli/src/utils/index.js @@ -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 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') } } + + // Keep _nuxtConfigFile for watching + options._nuxtConfigFile = nuxtConfigFile } else if (argv['config-file'] !== 'nuxt.config.js') { consola.fatal('Could not load config file: ' + argv['config-file']) } @@ -128,3 +137,10 @@ export function normalizeArg(arg, defaultValue) { } return arg } + +export function formatPath(filePath) { + if (!filePath) { + return + } + return filePath.replace(process.cwd() + path.sep, '') +} diff --git a/packages/cli/test/unit/dev.test.js b/packages/cli/test/unit/dev.test.js index 47b16906df..da52ebc2bd 100644 --- a/packages/cli/test/unit/dev.test.js +++ b/packages/cli/test/unit/dev.test.js @@ -13,7 +13,7 @@ describe('dev', () => { expect(typeof dev.run).toBe('function') }) - test('reloads on fileChanged hook', async () => { + test('reloads on fileRestartHook', async () => { const Nuxt = mockNuxt() const Builder = mockBuilder() @@ -23,21 +23,18 @@ describe('dev', () => { expect(Builder.prototype.build).toHaveBeenCalled() expect(Nuxt.prototype.server.listen).toHaveBeenCalled() - expect(Builder.prototype.watchServer).toHaveBeenCalled() + // expect(Builder.prototype.watchRestart).toHaveBeenCalled() jest.clearAllMocks() const builder = new Builder() builder.nuxt = new Nuxt() - await Nuxt.fileChangedHook(builder) + await Nuxt.fileRestartHook(builder) expect(consola.log).toHaveBeenCalled() - expect(Nuxt.prototype.clearHook).toHaveBeenCalled() - expect(Builder.prototype.unwatch).toHaveBeenCalled() expect(Builder.prototype.build).toHaveBeenCalled() expect(Nuxt.prototype.close).toHaveBeenCalled() expect(Nuxt.prototype.server.listen).toHaveBeenCalled() - expect(Builder.prototype.watchServer).toHaveBeenCalled() expect(consola.error).not.toHaveBeenCalled() }) @@ -53,13 +50,13 @@ describe('dev', () => { const builder = new Builder() builder.nuxt = new Nuxt() 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(consola.error).toHaveBeenCalledWith(new Error('Build Error')) }) - test('catches watchServer error', async () => { + test.skip('catches watchRestart error', async () => { const Nuxt = mockNuxt() const Builder = mockBuilder() @@ -68,11 +65,11 @@ describe('dev', () => { const builder = new Builder() builder.nuxt = new Nuxt() - Builder.prototype.watchServer = jest.fn().mockImplementationOnce(() => Promise.reject(new Error('watchServer Error'))) - await Nuxt.fileChangedHook(builder) + Builder.prototype.watchRestart = jest.fn().mockImplementationOnce(() => Promise.reject(new Error('watchRestart Error'))) + await Nuxt.fileRestartHook(builder) - expect(consola.error).toHaveBeenCalledWith(new Error('watchServer Error')) - expect(Builder.prototype.watchServer).toHaveBeenCalledTimes(2) + expect(consola.error).toHaveBeenCalledWith(new Error('watchRestart Error')) + expect(Builder.prototype.watchRestart).toHaveBeenCalledTimes(2) }) test('catches error on hook error', async () => { @@ -87,10 +84,10 @@ describe('dev', () => { }) const builder = new Builder() builder.nuxt = new Nuxt() - await Nuxt.fileChangedHook(builder) + await Nuxt.fileRestartHook(builder) 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 () => { diff --git a/packages/cli/test/utils/mocking.js b/packages/cli/test/utils/mocking.js index 6ecd6a8766..b30e94fbf6 100644 --- a/packages/cli/test/utils/mocking.js +++ b/packages/cli/test/utils/mocking.js @@ -84,12 +84,13 @@ export const mockNuxt = (implementation) => { const Nuxt = function () {} Object.assign(Nuxt.prototype, { hook(type, fn) { - if (type === 'watch:fileChanged') { - Nuxt.fileChangedHook = fn + if (type === 'watch:restart') { + Nuxt.fileRestartHook = fn } }, options: {}, clearHook: jest.fn(), + clearHooks: jest.fn(), close: jest.fn(), ready: jest.fn(), server: { @@ -108,7 +109,7 @@ export const mockBuilder = (implementation) => { Object.assign(Builder.prototype, { build: jest.fn().mockImplementationOnce(() => Promise.resolve()), unwatch: jest.fn().mockImplementationOnce(() => Promise.resolve()), - watchServer: jest.fn().mockImplementationOnce(() => Promise.resolve()) + watchRestart: jest.fn().mockImplementationOnce(() => Promise.resolve()) }, implementation || {}) imports.builder.mockImplementation(() => ({ Builder })) diff --git a/packages/common/src/hookable.js b/packages/common/src/hookable.js index a29f33b65d..1af2a0863e 100644 --- a/packages/common/src/hookable.js +++ b/packages/common/src/hookable.js @@ -45,6 +45,10 @@ export default class Hookable { } } + clearHooks() { + this._hooks = {} + } + flatHooks(configHooks, hooks = {}, parentName) { Object.keys(configHooks).forEach((key) => { const subHook = configHooks[key] diff --git a/packages/config/src/config/_common.js b/packages/config/src/config/_common.js index 5d42e5874d..531589473a 100644 --- a/packages/config/src/config/_common.js +++ b/packages/config/src/config/_common.js @@ -27,6 +27,7 @@ export default () => ({ serverMiddleware: [], // Dirs and extensions + _nuxtConfigFile: undefined, srcDir: undefined, buildDir: '.nuxt', modulesDir: [ diff --git a/packages/config/src/options.js b/packages/config/src/options.js index e43884b424..0d6b0a8ea0 100644 --- a/packages/config/src/options.js +++ b/packages/config/src/options.js @@ -10,8 +10,14 @@ import { guardDir, isNonEmptyString, isPureObject, isUrl } from '@nuxt/common' import { getDefaultNuxtConfig } from './config' export function getNuxtConfig(_options) { + // Prevent duplicate calls + if (_options.__normalized__) { + return _options + } + // Clone options to prevent unwanted side-effects const options = Object.assign({}, _options) + options.__normalized__ = true // Normalize options if (options.loading === true) { @@ -76,6 +82,14 @@ export function getNuxtConfig(_options) { // Resolve 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 guardDir(options, 'rootDir', 'buildDir') diff --git a/packages/core/src/nuxt.js b/packages/core/src/nuxt.js index f4f943b72c..830f9c84f1 100644 --- a/packages/core/src/nuxt.js +++ b/packages/core/src/nuxt.js @@ -79,5 +79,7 @@ export default class Nuxt extends Hookable { if (typeof callback === 'function') { await callback() } + + this.clearHooks() } } diff --git a/packages/server/src/server.js b/packages/server/src/server.js index 13a89a8fb8..70bf695d68 100644 --- a/packages/server/src/server.js +++ b/packages/server/src/server.js @@ -1,4 +1,5 @@ import path from 'path' +import consola from 'consola' import launchMiddleware from 'launch-editor-middleware' import serveStatic from 'serve-static' import servePlaceholder from 'serve-placeholder' @@ -35,6 +36,9 @@ export default class Server { // Create new connect instance this.app = connect() + + // Close hook + this.nuxt.hook('close', () => this.close()) } async ready() { @@ -52,14 +56,6 @@ export default class Server { // Call done hook 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() { @@ -175,16 +171,18 @@ export default class Server { } useMiddleware(middleware) { - // Resolve middleware - if (typeof middleware === 'string') { - middleware = this.nuxt.resolver.requireModule(middleware) - } + let handler = middleware.handler || middleware - // Resolve handler - if (typeof middleware.handler === 'string') { - middleware.handler = this.nuxt.resolver.requireModule(middleware.handler) + // Resolve handler setup as string (path) + if (typeof handler === 'string') { + 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 const path = ( @@ -231,4 +229,25 @@ export default class Server { 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] + } + } } diff --git a/packages/vue-renderer/src/renderer.js b/packages/vue-renderer/src/renderer.js index c7db0ed990..5442b1c998 100644 --- a/packages/vue-renderer/src/renderer.js +++ b/packages/vue-renderer/src/renderer.js @@ -448,4 +448,13 @@ export default class VueRenderer { interpolate: /{{([\s\S]+?)}}/g }) } + + close() { + if (this.__closed) return + this.__closed = true + + for (const key in this.renderer) { + delete this.renderer[key] + } + } } diff --git a/packages/webpack/src/builder.js b/packages/webpack/src/builder.js index 8397170703..6f5b454dea 100644 --- a/packages/webpack/src/builder.js +++ b/packages/webpack/src/builder.js @@ -30,8 +30,10 @@ export class WebpackBundler { // Initialize shared MFS for dev if (this.context.options.dev) { 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) { - return new Promise(async (resolve, reject) => { - const name = compiler.options.name - const { nuxt, options } = this.context + async webpackCompile(compiler) { + const name = compiler.options.name + const { nuxt, options } = this.context - await nuxt.callHook('build:compile', { name, compiler }) + await nuxt.callHook('build:compile', { name, compiler }) - // Load renderer resources after build - compiler.hooks.done.tap('load-resources', async (stats) => { - await nuxt.callHook('build:compiled', { - name, - compiler, - stats - }) - - // Reload renderer if available - await nuxt.callHook('build:resources', this.mfs) - - // Resolve on next tick - process.nextTick(resolve) + // Load renderer resources after build + compiler.hooks.done.tap('load-resources', async (stats) => { + await nuxt.callHook('build:compiled', { + name, + compiler, + stats }) - if (options.dev) { - // --- Dev Build --- - // 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' - } + // Reload renderer + await nuxt.callHook('build:resources', this.mfs) + }) - return reject(err) - } - - resolve() + // --- Dev Build --- + if (options.dev) { + // Client Build, watch is started by dev-middleware + 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) { @@ -229,12 +227,33 @@ export class WebpackBundler { async unwatch() { 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 for (const devMiddleware of Object.values(this.devMiddleware)) { 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() { diff --git a/test/unit/fallback.generate.test.js b/test/unit/fallback.generate.test.js index 2afec697e6..0dc16dc501 100644 --- a/test/unit/fallback.generate.test.js +++ b/test/unit/fallback.generate.test.js @@ -75,8 +75,11 @@ describe('fallback generate', () => { }) test('generate.fallback = true is transformed to /404.html', () => { - nuxt.options.generate.fallback = true - const options = getNuxtConfig(nuxt.options) + const options = getNuxtConfig({ + generate: { + fallback: true + } + }) expect(options.generate.fallback).toBe('404.html') })