feat: show warning on forced exit (#4958)

This commit is contained in:
Pim 2019-02-06 20:23:42 +01:00 committed by Pooya Parsa
parent f5220cfbbb
commit 5094d9c75d
16 changed files with 159 additions and 55 deletions

View File

@ -3,5 +3,5 @@
require('../dist/cli.js').run() require('../dist/cli.js').run()
.catch((error) => { .catch((error) => {
require('consola').fatal(error) require('consola').fatal(error)
process.exit(2) require('exit')(2)
}) })

View File

@ -18,6 +18,7 @@
"consola": "^2.4.0", "consola": "^2.4.0",
"esm": "^3.2.1", "esm": "^3.2.1",
"execa": "^1.0.0", "execa": "^1.0.0",
"exit": "^0.1.2",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"opener": "1.5.1", "opener": "1.5.1",
"pretty-bytes": "^5.1.0", "pretty-bytes": "^5.1.0",

View File

@ -1,8 +1,9 @@
import minimist from 'minimist' import minimist from 'minimist'
import { name, version } from '../package.json' import { name, version } from '../package.json'
import { loadNuxtConfig } from './utils' import { loadNuxtConfig, forceExit } from './utils'
import { indent, foldLines, startSpaces, optionSpaces, colorize } from './utils/formatting' import { indent, foldLines, colorize } from './utils/formatting'
import { startSpaces, optionSpaces, forceExitTimeout } from './utils/constants'
import * as imports from './imports' import * as imports from './imports'
export default class NuxtCommand { export default class NuxtCommand {
@ -12,6 +13,9 @@ export default class NuxtCommand {
} }
this.cmd = cmd this.cmd = cmd
// If the cmd is a server then dont forcibly exit when the cmd has finished
this.isServer = cmd.isServer !== undefined ? cmd.isServer : Boolean(this.cmd.options.hostname)
this._argv = Array.from(argv) this._argv = Array.from(argv)
this._parsedArgv = null // Lazy evaluate this._parsedArgv = null // Lazy evaluate
} }
@ -42,7 +46,14 @@ export default class NuxtCommand {
return Promise.resolve() return Promise.resolve()
} }
return Promise.resolve(this.cmd.run(this)) const runResolve = Promise.resolve(this.cmd.run(this))
// TODO: For v3 set timeout to 0 when force-exit === true
if (!this.isServer || this.argv['force-exit']) {
runResolve.then(() => forceExit(this.cmd.name, forceExitTimeout))
}
return runResolve
} }
showVersion() { showVersion() {

View File

@ -1,5 +1,6 @@
import chalk from 'chalk' import chalk from 'chalk'
import { indent, foldLines, startSpaces, optionSpaces, colorize } from './utils/formatting' import { indent, foldLines, colorize } from './utils/formatting'
import { startSpaces, optionSpaces } from './utils/constants'
import getCommand from './commands' import getCommand from './commands'
export default async function listCommands() { export default async function listCommands() {

View File

@ -28,6 +28,12 @@ export default {
} }
} }
}, },
// TODO: Change this to default: false in Nuxt v3 (see related todo's)
'force-exit': {
type: 'boolean',
default: true,
description: 'Do not force Nuxt.js to exit after the command has finished (this option has no effect on commands which start a server)'
},
version: { version: {
alias: 'v', alias: 'v',
type: 'boolean', type: 'boolean',

View File

@ -1,6 +1,6 @@
import consola from 'consola' import consola from 'consola'
import chalk from 'chalk' import exit from 'exit'
import boxen from 'boxen' import { fatalBox } from './utils/formatting'
let _setup = false let _setup = false
@ -26,17 +26,9 @@ export default function setup({ dev }) {
consola.addReporter({ consola.addReporter({
log(logObj) { log(logObj) {
if (logObj.type === 'fatal') { if (logObj.type === 'fatal') {
process.stderr.write(boxen([ const errorMessage = String(logObj.args[0])
chalk.red('✖ Nuxt Fatal Error'), process.stderr.write(fatalBox(errorMessage))
'', exit(1)
chalk.white(String(logObj.args[0]))
].join('\n'), {
borderColor: 'red',
borderStyle: 'round',
padding: 1,
margin: 1
}) + '\n')
process.exit(1)
} }
} }
}) })

View File

@ -0,0 +1,8 @@
export const forceExitTimeout = 5
export const startSpaces = 2
export const optionSpaces = 2
// 80% of terminal column width
// this is a fn because console width can have changed since startup
export const maxCharsPerLine = () => (process.stdout.columns || 100) * 80 / 100

View File

@ -1,11 +1,7 @@
import wrapAnsi from 'wrap-ansi' import wrapAnsi from 'wrap-ansi'
import chalk from 'chalk' import chalk from 'chalk'
import boxen from 'boxen'
export const startSpaces = 2 import { maxCharsPerLine } from './constants'
export const optionSpaces = 2
// 80% of terminal column width
export const maxCharsPerLine = (process.stdout.columns || 100) * 80 / 100
export function indent(count, chr = ' ') { export function indent(count, chr = ' ') {
return chr.repeat(count) return chr.repeat(count)
@ -25,8 +21,8 @@ export function indentLines(string, spaces, firstLineSpaces) {
return s return s
} }
export function foldLines(string, spaces, firstLineSpaces, maxCharsPerLine) { export function foldLines(string, spaces, firstLineSpaces, charsPerLine = maxCharsPerLine()) {
return indentLines(wrapAnsi(string, maxCharsPerLine, { trim: false }), spaces, firstLineSpaces) return indentLines(wrapAnsi(string, charsPerLine, { trim: false }), spaces, firstLineSpaces)
} }
export function colorize(text) { export function colorize(text) {
@ -36,3 +32,38 @@ export function colorize(text) {
.replace(/ (-[-\w,]+)/g, m => chalk.bold(m)) .replace(/ (-[-\w,]+)/g, m => chalk.bold(m))
.replace(/`(.+)`/g, (_, m) => chalk.bold.cyan(m)) .replace(/`(.+)`/g, (_, m) => chalk.bold.cyan(m))
} }
export function box(message, title, options) {
return boxen([
title || chalk.white('Nuxt Message'),
'',
chalk.white(foldLines(message, 0, 0, maxCharsPerLine()))
].join('\n'), Object.assign({
borderColor: 'white',
borderStyle: 'round',
padding: 1,
margin: 1
}, options)) + '\n'
}
export function successBox(message, title) {
return box(message, title || chalk.green('✔ Nuxt Success'), {
borderColor: 'green'
})
}
export function warningBox(message, title) {
return box(message, title || chalk.yellow('⚠ Nuxt Warning'), {
borderColor: 'yellow'
})
}
export function errorBox(message, title) {
return box(message, title || chalk.red('✖ Nuxt Error'), {
borderColor: 'red'
})
}
export function fatalBox(message, title) {
return errorBox(message, title || chalk.red('✖ Nuxt Fatal Error'))
}

View File

@ -2,12 +2,13 @@ import path from 'path'
import { existsSync } from 'fs' import { existsSync } from 'fs'
import consola from 'consola' import consola from 'consola'
import esm from 'esm' import esm from 'esm'
import exit from 'exit'
import defaultsDeep from 'lodash/defaultsDeep' import defaultsDeep from 'lodash/defaultsDeep'
import { defaultNuxtConfigFile, getDefaultNuxtConfig } from '@nuxt/config' import { defaultNuxtConfigFile, getDefaultNuxtConfig } from '@nuxt/config'
import boxen from 'boxen'
import chalk from 'chalk' import chalk from 'chalk'
import prettyBytes from 'pretty-bytes' import prettyBytes from 'pretty-bytes'
import env from 'std-env' import env from 'std-env'
import { successBox, warningBox } from './formatting'
export const requireModule = process.env.NUXT_TS ? require : esm(module, { export const requireModule = process.env.NUXT_TS ? require : esm(module, {
cache: false, cache: false,
@ -87,37 +88,30 @@ export function showBanner(nuxt) {
return return
} }
const lines = [] const titleLines = []
const messageLines = []
// Name and version // Name and version
lines.push(`${chalk.green.bold('Nuxt.js')} ${nuxt.constructor.version}`) titleLines.push(`${chalk.green.bold('Nuxt.js')} ${nuxt.constructor.version}`)
// Running mode // Running mode
lines.push(`Running in ${nuxt.options.dev ? chalk.bold.blue('development') : chalk.bold.green('production')} mode (${chalk.bold(nuxt.options.mode)})`) titleLines.push(`Running in ${nuxt.options.dev ? chalk.bold.blue('development') : chalk.bold.green('production')} mode (${chalk.bold(nuxt.options.mode)})`)
// https://nodejs.org/api/process.html#process_process_memoryusage // https://nodejs.org/api/process.html#process_process_memoryusage
const { heapUsed, rss } = process.memoryUsage() const { heapUsed, rss } = process.memoryUsage()
lines.push(`Memory usage: ${chalk.bold(prettyBytes(heapUsed))} (RSS: ${prettyBytes(rss)})`) titleLines.push(`Memory usage: ${chalk.bold(prettyBytes(heapUsed))} (RSS: ${prettyBytes(rss)})`)
// Listeners // Listeners
lines.push('')
for (const listener of nuxt.server.listeners) { for (const listener of nuxt.server.listeners) {
lines.push(chalk.bold('Listening on: ') + chalk.underline.blue(listener.url)) messageLines.push(chalk.bold('Listening on: ') + chalk.underline.blue(listener.url))
} }
// Add custom badge messages // Add custom badge messages
if (nuxt.options.cli.badgeMessages.length) { if (nuxt.options.cli.badgeMessages.length) {
lines.push('', ...nuxt.options.cli.badgeMessages) messageLines.push('', ...nuxt.options.cli.badgeMessages)
} }
const box = boxen(lines.join('\n'), { process.stdout.write(successBox(messageLines.join('\n'), titleLines.join('\n')))
borderColor: 'green',
borderStyle: 'round',
padding: 1,
margin: 1
})
process.stdout.write(box + '\n')
} }
export function formatPath(filePath) { export function formatPath(filePath) {
@ -144,3 +138,23 @@ export function normalizeArg(arg, defaultValue) {
} }
return arg return arg
} }
export function forceExit(cmdName, timeout) {
if (timeout) {
const exitTimeout = setTimeout(() => {
const msg = `The command 'nuxt ${cmdName}' finished but did not exit after ${timeout}s
This is most likely not caused by a bug in Nuxt.js\
Make sure to cleanup all timers and listeners you or your plugins/modules start.
Nuxt.js will now force exit
${chalk.bold('DeprecationWarning: Starting with Nuxt version 3 this will be a fatal error')}`
// TODO: Change this to a fatal error in v3
process.stderr.write(warningBox(msg))
exit(0)
}, timeout * 1000)
exitTimeout.unref()
} else {
exit(0)
}
}

View File

@ -1,22 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`cli/command builds help text 1`] = ` exports[`cli/command builds help text 1`] = `
" Usage: nuxt this is how you do it [options] " Usage: nuxt this is how you do it
[options]
a very long description that should not wrap to the next line because is not longer than the terminal width a very long description that should wrap
to the next line because is not longer
than the terminal width
Options: Options:
--spa, -s Launch in SPA mode --spa, -s Launch in SPA mode
--universal, -u Launch in Universal mode (default) --universal, -u Launch in Universal
--config-file, -c Path to Nuxt.js config file (default: nuxt.config.js) mode (default)
--modern, -m Build/Start app for modern browsers, e.g. server, client and false --config-file, -c Path to Nuxt.js
--version, -v Display the Nuxt version config file (default: nuxt.config.js)
--modern, -m Build/Start app for
modern browsers, e.g. server, client and
false
--no-force-exit Do not force Nuxt.js
to exit after the command has finished
(this option has no effect on commands
which start a server)
--version, -v Display the Nuxt
version
--help, -h Display this message --help, -h Display this message
--port, -p Port number on which to start the application --port, -p Port number on which
--hostname, -H Hostname on which to start the application to start the application
--hostname, -H Hostname on which to
start the application
--unix-socket, -n Path to a UNIX socket --unix-socket, -n Path to a UNIX socket
--foo very long option that is not longer than the terminal width and should not wrap to the next line --foo very long option that
is longer than the terminal width and
should wrap to the next line
" "
`; `;

View File

@ -1,3 +1,4 @@
import * as utils from '../../src/utils/'
import { mockGetNuxt, mockGetBuilder, mockGetGenerator, NuxtCommand } from '../utils' import { mockGetNuxt, mockGetBuilder, mockGetGenerator, NuxtCommand } from '../utils'
describe('build', () => { describe('build', () => {
@ -6,6 +7,7 @@ describe('build', () => {
beforeAll(async () => { beforeAll(async () => {
build = await import('../../src/commands/build').then(m => m.default) build = await import('../../src/commands/build').then(m => m.default)
jest.spyOn(process, 'exit').mockImplementation(code => code) jest.spyOn(process, 'exit').mockImplementation(code => code)
jest.spyOn(utils, 'forceExit').mockImplementation(() => {})
}) })
afterEach(() => jest.resetAllMocks()) afterEach(() => jest.resetAllMocks())

View File

@ -1,9 +1,15 @@
import { run } from '../../src' import { run } from '../../src'
import getCommand from '../../src/commands' import getCommand from '../../src/commands'
import * as utils from '../../src/utils/'
jest.mock('../../src/commands') jest.mock('../../src/commands')
describe('cli', () => { describe('cli', () => {
beforeAll(() => {
// TODO: Below spyOn can be removed in v3 when force-exit is default false
jest.spyOn(utils, 'forceExit').mockImplementation(() => {})
})
afterEach(() => jest.resetAllMocks()) afterEach(() => jest.resetAllMocks())
test('calls expected method', async () => { test('calls expected method', async () => {

View File

@ -1,5 +1,7 @@
import Command from '../../src/command' import Command from '../../src/command'
import { common, server } from '../../src/options' import { common, server } from '../../src/options'
import * as utils from '../../src/utils/'
import * as constants from '../../src/utils/constants'
import { consola } from '../utils' import { consola } from '../utils'
jest.mock('@nuxt/core') jest.mock('@nuxt/core')
@ -19,7 +21,7 @@ describe('cli/command', () => {
const minimistOptions = cmd._getMinimistOptions() const minimistOptions = cmd._getMinimistOptions()
expect(minimistOptions.string.length).toBe(5) expect(minimistOptions.string.length).toBe(5)
expect(minimistOptions.boolean.length).toBe(4) expect(minimistOptions.boolean.length).toBe(5)
expect(minimistOptions.alias.c).toBe('config-file') expect(minimistOptions.alias.c).toBe('config-file')
expect(minimistOptions.default.c).toBe(common['config-file'].default) expect(minimistOptions.default.c).toBe(common['config-file'].default)
}) })
@ -38,6 +40,8 @@ describe('cli/command', () => {
}) })
test('prints version automatically', async () => { test('prints version automatically', async () => {
jest.spyOn(utils, 'forceExit').mockImplementation(() => {})
const cmd = new Command({}, ['--version']) const cmd = new Command({}, ['--version'])
cmd.showVersion = jest.fn() cmd.showVersion = jest.fn()
await cmd.run() await cmd.run()
@ -46,6 +50,8 @@ describe('cli/command', () => {
}) })
test('prints help automatically', async () => { test('prints help automatically', async () => {
jest.spyOn(utils, 'forceExit').mockImplementation(() => {})
const cmd = new Command({ options: allOptions }, ['-h']) const cmd = new Command({ options: allOptions }, ['-h'])
cmd.showHelp = jest.fn() cmd.showHelp = jest.fn()
await cmd.run() await cmd.run()
@ -88,16 +94,18 @@ describe('cli/command', () => {
}) })
test('builds help text', () => { test('builds help text', () => {
jest.spyOn(constants, 'maxCharsPerLine').mockReturnValue(40)
const cmd = new Command({ const cmd = new Command({
description: 'a very long description that should not wrap to the next line because is not longer ' + description: 'a very long description that should wrap to the next line because is not longer ' +
'than the terminal width', 'than the terminal width',
usage: 'this is how you do it', usage: 'this is how you do it',
options: { options: {
...allOptions, ...allOptions,
foo: { foo: {
type: 'boolean', type: 'boolean',
description: 'very long option that is not longer than the terminal width and ' + description: 'very long option that is longer than the terminal width and ' +
'should not wrap to the next line' 'should wrap to the next line'
} }
} }
}) })

View File

@ -1,3 +1,4 @@
import * as utils from '../../src/utils/'
import { consola, mockNuxt, mockBuilder, mockGetNuxtConfig, NuxtCommand } from '../utils' import { consola, mockNuxt, mockBuilder, mockGetNuxtConfig, NuxtCommand } from '../utils'
describe('dev', () => { describe('dev', () => {
@ -5,6 +6,8 @@ describe('dev', () => {
beforeAll(async () => { beforeAll(async () => {
dev = await import('../../src/commands/dev').then(m => m.default) dev = await import('../../src/commands/dev').then(m => m.default)
// TODO: Below spyOn can be removed in v3 when force-exit is default false
jest.spyOn(utils, 'forceExit').mockImplementation(() => {})
}) })
afterEach(() => jest.clearAllMocks()) afterEach(() => jest.clearAllMocks())

View File

@ -1,3 +1,4 @@
import * as utils from '../../src/utils/'
import { mockGetNuxt, mockGetGenerator, NuxtCommand } from '../utils' import { mockGetNuxt, mockGetGenerator, NuxtCommand } from '../utils'
describe('generate', () => { describe('generate', () => {
@ -6,6 +7,7 @@ describe('generate', () => {
beforeAll(async () => { beforeAll(async () => {
generate = await import('../../src/commands/generate').then(m => m.default) generate = await import('../../src/commands/generate').then(m => m.default)
jest.spyOn(process, 'exit').mockImplementation(code => code) jest.spyOn(process, 'exit').mockImplementation(code => code)
jest.spyOn(utils, 'forceExit').mockImplementation(() => {})
}) })
afterEach(() => jest.resetAllMocks()) afterEach(() => jest.resetAllMocks())

View File

@ -1,4 +1,5 @@
import fs from 'fs-extra' import fs from 'fs-extra'
import * as utils from '../../src/utils/'
import { consola, mockGetNuxtStart, mockGetNuxtConfig, NuxtCommand } from '../utils' import { consola, mockGetNuxtStart, mockGetNuxtConfig, NuxtCommand } from '../utils'
describe('start', () => { describe('start', () => {
@ -6,6 +7,8 @@ describe('start', () => {
beforeAll(async () => { beforeAll(async () => {
start = await import('../../src/commands/start').then(m => m.default) start = await import('../../src/commands/start').then(m => m.default)
// TODO: Below spyOn can be removed in v3 when force-exit is default false
jest.spyOn(utils, 'forceExit').mockImplementation(() => {})
}) })
afterEach(() => { afterEach(() => {