refactor(cli): better consistency and easier unit testing (#4160)

This commit is contained in:
Pim 2018-10-25 09:43:42 +02:00 committed by Pooya Parsa
parent 9df5f49e07
commit 0669b68c91
27 changed files with 1244 additions and 291 deletions

View File

@ -1,44 +1,3 @@
#!/usr/bin/env node
const consola = require('consola')
const cli = require('../dist/cli.js')
// Global error handler
process.on('unhandledRejection', (err) => {
consola.error(err)
})
// Exit process on fatal errors
consola.add({
log(logObj) {
if (logObj.type === 'fatal') {
process.stderr.write('Nuxt Fatal Error :(\n')
process.exit(1)
}
}
})
const defaultCommand = 'dev'
const commands = new Set([
defaultCommand,
'build',
'start',
'generate'
])
let cmd = process.argv[2]
if (commands.has(cmd)) {
process.argv.splice(2, 1)
} else {
cmd = defaultCommand
}
// Apply default NODE_ENV if not provided
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = cmd === 'dev' ? 'development' : 'production'
}
cli[cmd]().then(m => m.default()).catch((error) => {
consola.fatal(error)
})
require('../dist/cli.js').run()

View File

@ -14,7 +14,8 @@
"dependencies": {
"consola": "^1.4.4",
"esm": "^3.0.84",
"minimist": "^1.2.0"
"minimist": "^1.2.0",
"wrap-ansi": "^4.0.0"
},
"publishConfig": {
"access": "public"

143
packages/cli/src/command.js Normal file
View File

@ -0,0 +1,143 @@
import parseArgs from 'minimist'
import wrapAnsi from 'wrap-ansi'
import { name, version } from '../package.json'
import { loadNuxtConfig, indent, indentLines, foldLines } from './utils'
import { options as Options, defaultOptions as DefaultOptions } from './options'
import * as imports from './imports'
const startSpaces = 6
const optionSpaces = 2
const maxCharsPerLine = 80
export default class NuxtCommand {
constructor({ description, usage, options } = {}) {
this.description = description || ''
this.usage = usage || ''
this.options = Array.from(new Set((options || []).concat(DefaultOptions)))
}
_getMinimistOptions() {
const minimistOptions = {
alias: {},
boolean: [],
string: [],
default: {}
}
for (const name of this.options) {
const option = Options[name]
if (option.alias) {
minimistOptions.alias[option.alias] = name
}
if (option.type) {
minimistOptions[option.type].push(option.alias || name)
}
if (option.default) {
minimistOptions.default[option.alias || name] = option.default
}
}
return minimistOptions
}
getArgv(args) {
const minimistOptions = this._getMinimistOptions()
const argv = parseArgs(args || process.argv.slice(2), minimistOptions)
if (argv.version) {
this.showVersion()
} else if (argv.help) {
this.showHelp()
}
return argv
}
async getNuxtConfig(argv, extraOptions) {
const config = await loadNuxtConfig(argv)
const options = Object.assign(config, extraOptions || {})
for (const name of this.options) {
if (Options[name].handle) {
Options[name].handle(options, argv)
}
}
return options
}
importCore() {
return imports.core()
}
importBuilder() {
return imports.builder()
}
importGenerator() {
return imports.generator()
}
async getNuxt(options) {
const { Nuxt } = await this.importCore()
return new Nuxt(options)
}
async getBuilder(nuxt) {
const { Builder } = await this.importBuilder()
return new Builder(nuxt)
}
async getGenerator(nuxt) {
const { Generator } = await this.importGenerator()
const { Builder } = await this.importBuilder()
return new Generator(nuxt, new Builder(nuxt))
}
_getHelp() {
const options = []
let maxOptionLength = 0
// For consistency Options determines order
for (const name in Options) {
const option = Options[name]
if (this.options.includes(name)) {
let optionHelp = '--'
optionHelp += option.type === 'boolean' && option.default ? 'no-' : ''
optionHelp += name
if (option.alias) {
optionHelp += `, -${option.alias}`
}
maxOptionLength = Math.max(maxOptionLength, optionHelp.length)
options.push([ optionHelp, option.description ])
}
}
const optionStr = options.map(([option, description]) => {
const line = option +
indent(maxOptionLength + optionSpaces - option.length) +
wrapAnsi(description, maxCharsPerLine - startSpaces - maxOptionLength - optionSpaces)
return indentLines(line, startSpaces + maxOptionLength + optionSpaces, startSpaces)
}).join('\n')
const description = foldLines(this.description, maxCharsPerLine, startSpaces)
return `
Description\n${description}
Usage
$ nuxt ${this.usage}
Options\n${optionStr}\n\n`
}
showVersion() {
process.stdout.write(`${name} v${version}\n`)
process.exit(0)
}
showHelp() {
process.stdout.write(this._getHelp())
process.exit(0)
}
}

View File

@ -1,91 +1,46 @@
import parseArgs from 'minimist'
import consola from 'consola'
import { loadNuxtConfig } from '../common/utils'
import NuxtCommand from '../command'
export default async function build() {
const { Nuxt } = await import('@nuxt/core')
const { Builder } = await import('@nuxt/builder')
const { Generator } = await import('@nuxt/generator')
const argv = parseArgs(process.argv.slice(2), {
alias: {
h: 'help',
c: 'config-file',
a: 'analyze',
s: 'spa',
u: 'universal',
q: 'quiet'
},
boolean: ['h', 'a', 's', 'u', 'q'],
string: ['c'],
default: {
c: 'nuxt.config.js'
}
const nuxtCmd = new NuxtCommand({
description: 'Compiles the application for production deployment',
usage: 'build <dir>',
options: [ 'analyze', 'quiet' ]
})
if (argv.help) {
process.stderr.write(`
Description
Compiles the application for production deployment
Usage
$ nuxt build <dir>
Options
--analyze, -a Launch webpack-bundle-analyzer to optimize your bundles.
--spa, -s Launch in SPA mode
--universal, -u Launch in Universal mode (default)
--no-generate Don't generate static version for SPA mode (useful for nuxt start)
--config-file, -c Path to Nuxt.js config file (default: nuxt.config.js)
--quiet, -q Disable output except for errors
--help, -h Displays this message
`)
process.exit(0)
}
const argv = nuxtCmd.getArgv()
const options = await loadNuxtConfig(argv)
// Create production build when calling `nuxt build`
options.dev = false
// Analyze option
options.build = options.build || {}
if (argv.analyze && typeof options.build.analyze !== 'object') {
options.build.analyze = true
}
// Silence output when using --quiet
if (argv.quiet) {
options.build.quiet = !!argv.quiet
}
const nuxt = new Nuxt(options)
const builder = new Builder(nuxt)
// Create production build when calling `nuxt build` (dev: false)
const nuxt = await nuxtCmd.getNuxt(
await nuxtCmd.getNuxtConfig(argv, { dev: false })
)
// Setup hooks
nuxt.hook('error', err => consola.fatal(err))
// Close function
const close = () => {
let builderOrGenerator
if (nuxt.options.mode !== 'spa' || argv.generate === false) {
// Build only
builderOrGenerator = (await nuxtCmd.getBuilder(nuxt)).build()
} else {
// Build + Generate for static deployment
builderOrGenerator = (await nuxtCmd.getGenerator(nuxt)).generate({
build: true
})
}
return builderOrGenerator
.then(() => {
// In analyze mode wait for plugin
// emitting assets and opening browser
if (options.build.analyze === true || typeof options.build.analyze === 'object') {
if (
nuxt.options.build.analyze === true ||
typeof nuxt.options.build.analyze === 'object'
) {
return
}
process.exit(0)
}
if (options.mode !== 'spa' || argv.generate === false) {
// Build only
return builder
.build()
.then(close)
})
.catch(err => consola.fatal(err))
} else {
// Build + Generate for static deployment
return new Generator(nuxt, builder)
.generate({ build: true })
.then(close)
.catch(err => consola.fatal(err))
}
}

View File

@ -1,71 +1,29 @@
import parseArgs from 'minimist'
import consola from 'consola'
import { loadNuxtConfig, runAsyncScript } from '../common/utils'
import NuxtCommand from '../command'
export default async function dev() {
const { Nuxt } = await import('@nuxt/core')
const { Builder } = await import('@nuxt/builder')
const argv = parseArgs(process.argv.slice(2), {
alias: {
h: 'help',
H: 'hostname',
p: 'port',
c: 'config-file',
s: 'spa',
u: 'universal',
v: 'version'
},
boolean: ['h', 's', 'u', 'v'],
string: ['H', 'c'],
default: {
c: 'nuxt.config.js'
}
const nuxtCmd = new NuxtCommand({
description: 'Start the application in development mode (e.g. hot-code reloading, error reporting)',
usage: 'dev <dir> -p <port number> -H <hostname>',
options: [ 'hostname', 'port' ]
})
if (argv.version) {
process.stderr.write('TODO' + '\n')
process.exit(0)
}
if (argv.hostname === '') {
consola.fatal('Provided hostname argument has no value')
}
if (argv.help) {
process.stderr.write(`
Description
Starts the application in development mode (hot-code reloading, error
reporting, etc)
Usage
$ nuxt dev <dir> -p <port number> -H <hostname>
Options
--port, -p A port number on which to start the application
--hostname, -H Hostname on which to start the application
--spa Launch in SPA mode
--universal Launch in Universal mode (default)
--config-file, -c Path to Nuxt.js config file (default: nuxt.config.js)
--help, -h Displays this message
`)
process.exit(0)
}
const config = async () => {
// Force development mode for add hot reloading and watching changes
return Object.assign(await loadNuxtConfig(argv), { dev: true })
}
const argv = nuxtCmd.getArgv()
const errorHandler = (err, instance) => {
instance && instance.builder.watchServer()
consola.error(err)
}
const { Nuxt } = await nuxtCmd.importCore()
const { Builder } = await nuxtCmd.importBuilder()
// Start dev
async function startDev(oldInstance) {
let nuxt, builder
try {
nuxt = new Nuxt(await config())
nuxt = new Nuxt(await nuxtCmd.getNuxtConfig(argv, { dev: true }))
builder = new Builder(nuxt)
nuxt.hook('watch:fileChanged', async (builder, fname) => {
consola.debug(`[${fname}] changed, Rebuilding the app...`)
@ -81,10 +39,10 @@ export default async function dev() {
.then(() => oldInstance && oldInstance.builder.unwatch())
// Start build
.then(() => builder.build())
// Close old nuxt no mater if build successfully
// Close old nuxt no matter if build successfully
.catch((err) => {
oldInstance && oldInstance.nuxt.close()
// Jump to eventHandler
// Jump to errorHandler
throw err
})
.then(() => oldInstance && oldInstance.nuxt.close())
@ -98,5 +56,5 @@ export default async function dev() {
)
}
await runAsyncScript(startDev)
await startDev()
}

View File

@ -1,61 +1,25 @@
import parseArgs from 'minimist'
import consola from 'consola'
import { loadNuxtConfig } from '../common/utils'
import NuxtCommand from '../command'
export default async function generate() {
const { Nuxt } = await import('@nuxt/core')
const { Builder } = await import('@nuxt/builder')
const { Generator } = await import('@nuxt/generator')
const argv = parseArgs(process.argv.slice(2), {
alias: {
h: 'help',
c: 'config-file',
s: 'spa',
u: 'universal'
},
boolean: ['h', 's', 'u', 'build'],
string: ['c'],
default: {
c: 'nuxt.config.js',
build: true
}
const nuxtCmd = new NuxtCommand({
description: 'Generate a static web application (server-rendered)',
usage: 'generate <dir>',
options: [ 'build' ]
})
if (argv.help) {
process.stderr.write(`
Description
Generate a static web application (server-rendered)
Usage
$ nuxt generate <dir>
Options
--spa Launch in SPA mode
--universal Launch in Universal mode (default)
--config-file, -c Path to Nuxt.js config file (default: nuxt.config.js)
--help, -h Displays this message
--no-build Just run generate for faster builds when just dynamic routes changed. Nuxt build is needed before this command.
`)
process.exit(0)
}
const argv = nuxtCmd.getArgv()
const options = await loadNuxtConfig(argv)
const generator = await nuxtCmd.getGenerator(
await nuxtCmd.getNuxt(
await nuxtCmd.getNuxtConfig(argv, { dev: false })
)
)
options.dev = false // Force production mode (no webpack middleware called)
const nuxt = new Nuxt(options)
const builder = new Builder(nuxt)
const generator = new Generator(nuxt, builder)
const generateOptions = {
return generator.generate({
init: true,
build: argv.build
}
return generator
.generate(generateOptions)
.then(() => {
}).then(() => {
process.exit(0)
})
.catch(err => consola.fatal(err))
}).catch(err => consola.fatal(err))
}

View File

@ -0,0 +1,4 @@
export const start = () => import('./start')
export const dev = () => import('./dev')
export const build = () => import('./build')
export const generate = () => import('./generate')

View File

@ -1,59 +1,21 @@
import fs from 'fs'
import path from 'path'
import parseArgs from 'minimist'
import consola from 'consola'
import { loadNuxtConfig } from '../common/utils'
import NuxtCommand from '../command'
export default async function start() {
const { Nuxt } = await import('@nuxt/core')
const argv = parseArgs(process.argv.slice(2), {
alias: {
h: 'help',
H: 'hostname',
p: 'port',
n: 'unix-socket',
c: 'config-file',
s: 'spa',
u: 'universal'
},
boolean: ['h', 's', 'u'],
string: ['H', 'c', 'n'],
default: {
c: 'nuxt.config.js'
}
const nuxtCmd = new NuxtCommand({
description: 'Start the application in production mode (the application should be compiled with `nuxt build` first)',
usage: 'start <dir> -p <port number> -H <hostname>',
options: [ 'hostname', 'port', 'unix-socket' ]
})
if (argv.hostname === '') {
consola.fatal('Provided hostname argument has no value')
}
const argv = nuxtCmd.getArgv()
if (argv.help) {
process.stderr.write(`
Description
Starts the application in production mode.
The application should be compiled with \`nuxt build\` first.
Usage
$ nuxt start <dir> -p <port number> -H <hostname>
Options
--port, -p A port number on which to start the application
--hostname, -H Hostname on which to start the application
--unix-socket, -n Path to a UNIX socket
--spa Launch in SPA mode
--universal Launch in Universal mode (default)
--config-file, -c Path to Nuxt.js config file (default: nuxt.config.js)
--help, -h Displays this message
`)
process.exit(0)
}
const options = await loadNuxtConfig(argv)
// Force production mode (no webpack middleware called)
options.dev = false
const nuxt = new Nuxt(options)
// Create production build when calling `nuxt build`
const nuxt = await nuxtCmd.getNuxt(
await nuxtCmd.getNuxtConfig(argv, { dev: false })
)
// Setup hooks
nuxt.hook('error', err => consola.fatal(err))

View File

@ -0,0 +1,3 @@
export const builder = () => import('@nuxt/builder')
export const generator = () => import('@nuxt/generator')
export const core = () => import('@nuxt/core')

View File

@ -1,5 +1,10 @@
export const start = () => import('./commands/start')
export const dev = () => import('./commands/dev')
import * as _commands from './commands'
import * as _imports from './imports'
export const build = () => import('./commands/build')
export const generate = () => import('./commands/generate')
export const commands = _commands
export const imports = _imports
export { default as setup } from './setup'
export { default as run } from './run'
export { loadNuxtConfig } from './utils'

101
packages/cli/src/options.js Normal file
View File

@ -0,0 +1,101 @@
import consola from 'consola'
export const defaultOptions = [
'spa',
'universal',
'config-file',
'version',
'help'
]
export const options = {
port: {
alias: 'p',
type: 'string',
description: 'Port number on which to start the application',
handle(options, argv) {
if (argv.port) {
options.server.port = +argv.port
}
}
},
hostname: {
alias: 'H',
type: 'string',
description: 'Hostname on which to start the application',
handle(options, argv) {
if (argv.hostname === '') {
consola.fatal('Provided hostname argument has no value')
}
}
},
'unix-socket': {
alias: 'n',
type: 'string',
description: 'Path to a UNIX socket'
},
analyze: {
alias: 'a',
type: 'boolean',
description: 'Launch webpack-bundle-analyzer to optimize your bundles',
handle(options, argv) {
// Analyze option
options.build = options.build || {}
if (argv.analyze && typeof options.build.analyze !== 'object') {
options.build.analyze = true
}
}
},
build: {
type: 'boolean',
default: true,
description: 'Only generate pages for dynamic routes. Nuxt has to be built once before using this option'
},
generate: {
type: 'boolean',
default: true,
description: 'Don\'t generate static version for SPA mode (useful for nuxt start)'
},
spa: {
alias: 's',
type: 'boolean',
description: 'Launch in SPA mode'
},
universal: {
alias: 'u',
type: 'boolean',
description: 'Launch in Universal mode (default)'
},
'config-file': {
alias: 'c',
type: 'string',
default: 'nuxt.config.js',
description: 'Path to Nuxt.js config file (default: nuxt.config.js)'
},
quiet: {
alias: 'q',
type: 'boolean',
description: 'Disable output except for errors',
handle(options, argv) {
// Silence output when using --quiet
options.build = options.build || {}
if (argv.quiet) {
options.build.quiet = !!argv.quiet
}
}
},
verbose: {
alias: 'v',
type: 'boolean',
description: 'Show debug information'
},
version: {
type: 'boolean',
description: 'Display the Nuxt version'
},
help: {
alias: 'h',
type: 'boolean',
description: 'Display this message'
}
}

33
packages/cli/src/run.js Normal file
View File

@ -0,0 +1,33 @@
import consola from 'consola'
import * as commands from './commands'
import setup from './setup'
export default function run() {
const defaultCommand = 'dev'
const cmds = new Set([
defaultCommand,
'build',
'start',
'generate'
])
let cmd = process.argv[2]
if (cmds.has(cmd)) {
process.argv.splice(2, 1)
} else {
cmd = defaultCommand
}
// Setup runtime
setup({
dev: cmd === 'dev'
})
return commands[cmd]() // eslint-disable-line import/namespace
.then(m => m.default())
.catch((error) => {
consola.fatal(error)
})
}

32
packages/cli/src/setup.js Normal file
View File

@ -0,0 +1,32 @@
import consola from 'consola'
let _setup = false
export default function setup({ dev }) {
// Apply default NODE_ENV if not provided
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = dev ? 'development' : 'production'
}
if (_setup) {
return
}
_setup = true
// Global error handler
/* istanbul ignore next */
process.on('unhandledRejection', (err) => {
consola.error(err)
})
// Exit process on fatal errors
/* istanbul ignore next */
consola.add({
log(logObj) {
if (logObj.type === 'fatal') {
process.stderr.write('Nuxt Fatal Error :(\n')
process.exit(1)
}
}
})
}

View File

@ -2,6 +2,7 @@ import path from 'path'
import { existsSync } from 'fs'
import consola from 'consola'
import esm from 'esm'
import wrapAnsi from 'wrap-ansi'
const _require = esm(module, {
cache: false,
@ -33,15 +34,6 @@ const getLatestHost = (argv) => {
return { port, host, socket }
}
export async function runAsyncScript(fn) {
try {
await fn()
} catch (err) {
consola.error(err)
consola.fatal(`Failed to run async Nuxt script!`)
}
}
export async function loadNuxtConfig(argv) {
const rootDir = getRootDir(argv)
const nuxtConfigFile = getNuxtConfigFile(argv)
@ -50,16 +42,17 @@ export async function loadNuxtConfig(argv) {
if (existsSync(nuxtConfigFile)) {
delete require.cache[nuxtConfigFile]
options = _require(nuxtConfigFile)
if (!options) {
options = {}
}
options = _require(nuxtConfigFile) || {}
if (options.default) {
options = options.default
}
if (typeof options === 'function') {
try {
options = await options()
if (options.default) {
options = options.default
}
} catch (error) {
consola.error(error)
consola.fatal('Error while fetching async configuration')
@ -68,7 +61,6 @@ export async function loadNuxtConfig(argv) {
} else if (argv['config-file'] !== 'nuxt.config.js') {
consola.fatal('Could not load config file: ' + argv['config-file'])
}
if (typeof options.rootDir !== 'string') {
options.rootDir = rootDir
}
@ -81,9 +73,33 @@ export async function loadNuxtConfig(argv) {
if (!options.server) {
options.server = {}
}
const { port, host, socket } = getLatestHost(argv)
options.server.port = port || options.server.port || 3000
options.server.host = host || options.server.host || 'localhost'
options.server.socket = socket || options.server.socket
return options
}
export function indent(count, chr = ' ') {
return chr.repeat(count)
}
export function indentLines(string, spaces, firstLineSpaces) {
const lines = Array.isArray(string) ? string : string.split('\n')
let s = ''
if (lines.length) {
const i0 = indent(firstLineSpaces === undefined ? spaces : firstLineSpaces)
s = i0 + lines.shift()
}
if (lines.length) {
const i = indent(spaces)
s += '\n' + lines.map(l => i + l.trim()).join('\n')
}
return s
}
export function foldLines(string, maxCharsPerLine, spaces, firstLineSpaces) {
return indentLines(wrapAnsi(string, maxCharsPerLine), spaces, firstLineSpaces)
}

View File

@ -0,0 +1,9 @@
import { resolve } from 'path'
export default () => {
// delete cache is needed because otherwise Jest will return the same
// object reference as the previous test and then mode will not be
// set correctly. jest.resetModules doesnt work for some reason
delete require.cache[resolve(__dirname, 'nuxt.config.js')]
return import('./nuxt.config.js')
}

View File

@ -0,0 +1 @@
export default () => Promise.reject(new Error('Async Config Error'))

View File

@ -0,0 +1,10 @@
export default {
testOption: true,
rootDir: '/some/path',
mode: 'supercharged',
server: {
host: 'nuxt-host',
port: 3001,
socket: '/var/run/nuxt.sock'
}
}

View File

@ -0,0 +1,62 @@
import { consola, mockGetNuxt, mockGetBuilder, mockGetGenerator } from '../utils'
describe('build', () => {
let build
beforeAll(async () => {
build = await import('../../src/commands/build')
build = build.default
jest.spyOn(process, 'exit').mockImplementation(code => code)
})
afterAll(() => {
process.exit.mockRestore()
})
afterEach(() => {
jest.resetAllMocks()
})
test('is function', () => {
expect(typeof build).toBe('function')
})
test('builds on universal mode', async () => {
mockGetNuxt({
mode: 'universal',
build: {
analyze: true
}
})
const builder = mockGetBuilder(Promise.resolve())
await build()
expect(builder).toHaveBeenCalled()
})
test('generates on spa mode', async () => {
mockGetNuxt({
mode: 'spa',
build: {
analyze: false
}
})
const generate = mockGetGenerator(Promise.resolve())
await build()
expect(generate).toHaveBeenCalled()
expect(process.exit).toHaveBeenCalled()
})
test('catches error', async () => {
mockGetNuxt({ mode: 'universal' })
mockGetBuilder(Promise.reject(new Error('Builder Error')))
await build()
expect(consola.fatal).toHaveBeenCalledWith(new Error('Builder Error'))
})
})

View File

@ -0,0 +1,95 @@
import { readdir } from 'fs'
import { resolve } from 'path'
import { promisify } from 'util'
import { consola } from '../utils'
import { run } from '../../src'
import * as commands from '../../src/commands'
const readDir = promisify(readdir)
consola.add = jest.fn()
const mockCommand = (cmd, p) => {
commands[cmd] = jest.fn().mockImplementationOnce(() => { // eslint-disable-line import/namespace
return Promise.resolve({
default: () => {
return p
}
})
})
}
describe('cli', () => {
afterEach(() => {
jest.resetAllMocks()
})
test('exports for all commands defined', async () => {
const cmds = await readDir(resolve(__dirname, '..', '..', 'src', 'commands'))
for (let cmd of cmds) {
if (cmd === 'index.js') {
continue
}
cmd = cmd.substring(0, cmd.length - 3)
expect(commands[cmd]).toBeDefined() // eslint-disable-line import/namespace
expect(typeof commands[cmd]).toBe('function') // eslint-disable-line import/namespace
}
})
test('calls expected method', async () => {
const argv = process.argv
process.argv = ['', '', 'dev']
mockCommand('dev', Promise.resolve())
await run()
expect(commands.dev).toHaveBeenCalled()
process.argv = argv
})
test('unknown calls default method', async () => {
const argv = process.argv
process.argv = ['', '', 'test']
mockCommand('dev', Promise.resolve())
await run()
expect(commands.dev).toHaveBeenCalled()
process.argv = argv
})
test('sets NODE_ENV=development for dev', async () => {
const nodeEnv = process.env.NODE_ENV
process.env.NODE_ENV = ''
mockCommand('dev', Promise.resolve())
await run()
expect(process.env.NODE_ENV).toBe('development')
process.env.NODE_ENV = nodeEnv
})
test('sets ODE_ENV=production for build', async () => {
const argv = process.argv
const nodeEnv = process.env.NODE_ENV
process.argv = ['', '', 'build']
process.env.NODE_ENV = ''
mockCommand('build', Promise.resolve())
await run()
expect(process.env.NODE_ENV).toBe('production')
process.argv = argv
process.env.NODE_ENV = nodeEnv
})
test('catches fatal error', async () => {
mockCommand('dev', Promise.reject(new Error('Command Error')))
await run()
expect(consola.fatal).toHaveBeenCalledWith(new Error('Command Error'))
})
})

View File

@ -0,0 +1,169 @@
import { consola } from '../utils'
import Command from '../../src/command'
import { options as Options } from '../../src/options'
jest.mock('@nuxt/core')
jest.mock('@nuxt/builder')
jest.mock('@nuxt/generator')
describe('cli/command', () => {
beforeEach(() => {
jest.restoreAllMocks()
})
test('adds default options', () => {
const cmd = new Command()
expect(cmd.options.length).not.toBe(0)
})
test('builds minimist options', () => {
const cmd = new Command({
options: Object.keys(Options)
})
const minimistOptions = cmd._getMinimistOptions()
expect(minimistOptions.string.length).toBe(4)
expect(minimistOptions.boolean.length).toBe(9)
expect(minimistOptions.alias.c).toBe('config-file')
expect(minimistOptions.default.c).toBe(Options['config-file'].default)
})
test('parses args', () => {
const cmd = new Command({
options: Object.keys(Options)
})
let args = ['-c', 'test-file', '-s', '-p', '3001']
let argv = cmd.getArgv(args)
expect(argv['config-file']).toBe(args[1])
expect(argv.spa).toBe(true)
expect(argv.universal).toBe(false)
expect(argv.build).toBe(true)
expect(argv.port).toBe('3001')
args = ['--no-build']
argv = cmd.getArgv(args)
expect(argv.build).toBe(false)
})
test('prints version automatically', () => {
const cmd = new Command()
cmd.showVersion = jest.fn()
const args = ['--version']
cmd.getArgv(args)
expect(cmd.showVersion).toHaveBeenCalledTimes(1)
})
test('prints help automatically', () => {
const cmd = new Command()
cmd.showHelp = jest.fn()
const args = ['-h']
cmd.getArgv(args)
expect(cmd.showHelp).toHaveBeenCalledTimes(1)
})
test('returns nuxt config', async () => {
const cmd = new Command({
options: Object.keys(Options)
})
const args = ['-c', 'test-file', '-a', '-p', '3001', '-q', '-H']
const argv = cmd.getArgv(args)
argv._ = ['.']
const options = await cmd.getNuxtConfig(argv, { testOption: true })
expect(options.testOption).toBe(true)
expect(options.server.port).toBe(3001)
expect(options.build.quiet).toBe(true)
expect(options.build.analyze).toBe(true)
expect(consola.fatal).toHaveBeenCalledWith('Provided hostname argument has no value') // hostname check
})
test('returns Nuxt instance', async () => {
const cmd = new Command()
const nuxt = await cmd.getNuxt()
expect(nuxt.constructor.name).toBe('Nuxt')
expect(typeof nuxt.ready).toBe('function')
})
test('returns Builder instance', async () => {
const cmd = new Command()
const builder = await cmd.getBuilder()
expect(builder.constructor.name).toBe('Builder')
expect(typeof builder.build).toBe('function')
})
test('returns Generator instance', async () => {
const cmd = new Command()
const generator = await cmd.getGenerator()
expect(generator.constructor.name).toBe('Generator')
expect(typeof generator.generate).toBe('function')
})
test('builds help text', () => {
const cmd = new Command({
description: 'a very long description that is longer than 80 chars and ' +
'should wrap to the next line while keeping indentation',
usage: 'this is how you do it',
options: ['build']
})
const expectedText = `
Description
a very long description that is longer than 80 chars and should wrap to the next
line while keeping indentation
Usage
$ nuxt this is how you do it
Options
--no-build Only generate pages for dynamic routes. Nuxt has to be
built once before using this option
--spa, -s Launch in SPA mode
--universal, -u Launch in Universal mode (default)
--config-file, -c Path to Nuxt.js config file (default: nuxt.config.js)
--version Display the Nuxt version
--help, -h Display this message
`
expect(cmd._getHelp()).toBe(expectedText)
})
test('show version prints to stdout and exits', () => {
jest.spyOn(process.stdout, 'write').mockImplementation(() => {})
jest.spyOn(process, 'exit').mockImplementationOnce(code => code)
const cmd = new Command()
cmd.showVersion()
expect(process.stdout.write).toHaveBeenCalled()
expect(process.exit).toHaveBeenCalled()
process.stdout.write.mockRestore()
process.exit.mockRestore()
})
test('show help prints to stdout and exits', () => {
jest.spyOn(process.stdout, 'write').mockImplementation(() => {})
jest.spyOn(process, 'exit').mockImplementationOnce(code => code)
const cmd = new Command()
cmd.showHelp()
expect(process.stdout.write).toHaveBeenCalled()
expect(process.exit).toHaveBeenCalled()
process.stdout.write.mockRestore()
process.exit.mockRestore()
})
})

View File

@ -0,0 +1,114 @@
import { consola } from '../utils'
import { mockNuxt, mockBuilder, mockGetNuxtConfig } from '../utils/mocking'
describe('dev', () => {
let dev
beforeAll(async () => {
dev = await import('../../src/commands/dev')
dev = dev.default
})
afterEach(() => {
jest.resetAllMocks()
})
test('is function', () => {
expect(typeof dev).toBe('function')
})
test('reloads on fileChanged hook', async () => {
const Nuxt = mockNuxt()
const Builder = mockBuilder()
await dev()
expect(consola.error).not.toHaveBeenCalled()
expect(Builder.prototype.build).toHaveBeenCalled()
expect(Nuxt.prototype.listen).toHaveBeenCalled()
expect(Nuxt.prototype.showReady).toHaveBeenCalled()
expect(Builder.prototype.watchServer).toHaveBeenCalled()
jest.resetAllMocks()
const builder = new Builder()
builder.nuxt = new Nuxt()
await Nuxt.fileChangedHook(builder)
expect(consola.debug).toHaveBeenCalled()
expect(Nuxt.prototype.clearHook).toHaveBeenCalled()
expect(Builder.prototype.unwatch).toHaveBeenCalled()
expect(Builder.prototype.build).toHaveBeenCalled()
expect(Nuxt.prototype.close).toHaveBeenCalled()
expect(Nuxt.prototype.listen).toHaveBeenCalled()
expect(Nuxt.prototype.showReady).not.toHaveBeenCalled()
expect(Builder.prototype.watchServer).toHaveBeenCalled()
expect(consola.error).not.toHaveBeenCalled()
})
test('catches build error', async () => {
const Nuxt = mockNuxt()
const Builder = mockBuilder()
await dev()
jest.resetAllMocks()
// Test error on second build so we cover oldInstance stuff
const builder = new Builder()
builder.nuxt = new Nuxt()
Builder.prototype.build = jest.fn().mockImplementationOnce(() => Promise.reject(new Error('Build Error')))
await Nuxt.fileChangedHook(builder)
expect(Nuxt.prototype.close).toHaveBeenCalled()
expect(consola.error).toHaveBeenCalledWith(new Error('Build Error'))
})
test('catches watchServer error', async () => {
const Nuxt = mockNuxt()
const Builder = mockBuilder()
await dev()
jest.resetAllMocks()
const builder = new Builder()
builder.nuxt = new Nuxt()
Builder.prototype.watchServer = jest.fn().mockImplementationOnce(() => Promise.reject(new Error('watchServer Error')))
await Nuxt.fileChangedHook(builder)
expect(consola.error).toHaveBeenCalledWith(new Error('watchServer Error'))
expect(Builder.prototype.watchServer).toHaveBeenCalledTimes(2)
})
test('catches error on hook error', async () => {
const Nuxt = mockNuxt()
const Builder = mockBuilder()
await dev()
jest.resetAllMocks()
mockGetNuxtConfig().mockImplementationOnce(() => {
throw new Error('Config Error')
})
const builder = new Builder()
builder.nuxt = new Nuxt()
await Nuxt.fileChangedHook(builder)
expect(consola.error).toHaveBeenCalledWith(new Error('Config Error'))
expect(Builder.prototype.watchServer).toHaveBeenCalledTimes(1)
})
test('catches error on startDev', async () => {
mockNuxt({
listen: jest.fn().mockImplementation(() => {
throw new Error('Listen Error')
})
})
mockBuilder()
await dev()
expect(consola.error).toHaveBeenCalledWith(new Error('Listen Error'))
})
})

View File

@ -0,0 +1,64 @@
import { consola, mockGetNuxt, mockGetGenerator } from '../utils'
import Command from '../../src/command'
describe('generate', () => {
let generate
beforeAll(async () => {
generate = await import('../../src/commands/generate')
generate = generate.default
jest.spyOn(process, 'exit').mockImplementation(code => code)
})
afterAll(() => {
process.exit.mockRestore()
})
afterEach(() => {
jest.resetAllMocks()
})
test('is function', () => {
expect(typeof generate).toBe('function')
})
test('builds by default', async () => {
mockGetNuxt()
const generator = mockGetGenerator(Promise.resolve())
await generate()
expect(generator).toHaveBeenCalled()
expect(generator.mock.calls[0][0].build).toBe(true)
})
test('doesnt build with no-build', async () => {
mockGetNuxt()
const getArgv = Command.prototype.getArgv
Command.prototype.getArgv = jest.fn().mockImplementationOnce(() => {
return {
'_': ['.'],
rootDir: '.',
'config-file': 'nuxt.config.js',
build: false
}
})
const generator = mockGetGenerator(Promise.resolve())
await generate()
expect(generator).toHaveBeenCalled()
expect(generator.mock.calls[0][0].build).toBe(false)
Command.prototype.getArgv = getArgv
})
test('catches error', async () => {
mockGetNuxt()
mockGetGenerator(Promise.reject(new Error('Generator Error')))
await generate()
expect(consola.fatal).toHaveBeenCalledWith(new Error('Generator Error'))
})
})

View File

@ -0,0 +1,69 @@
import fs from 'fs'
import { consola, mockGetNuxtStart, mockGetNuxtConfig } from '../utils'
describe('start', () => {
let start
beforeAll(async () => {
start = await import('../../src/commands/start')
start = start.default
})
afterEach(() => {
if (fs.existsSync.mockRestore) {
fs.existsSync.mockRestore()
}
jest.resetAllMocks()
})
test('is function', () => {
expect(typeof start).toBe('function')
})
test('starts listening and calls showReady', async () => {
const { listen, showReady } = mockGetNuxtStart()
await start()
expect(listen).toHaveBeenCalled()
expect(showReady).toHaveBeenCalled()
})
test('no error if dist dir exists', async () => {
mockGetNuxtStart()
mockGetNuxtConfig()
jest.spyOn(fs, 'existsSync').mockImplementationOnce(() => true)
await start()
expect(consola.fatal).not.toHaveBeenCalled()
})
test('fatal error if dist dir doesnt exist', async () => {
mockGetNuxtStart()
jest.spyOn(fs, 'existsSync').mockImplementationOnce(() => false)
await start()
expect(consola.fatal).toHaveBeenCalledWith('No build files found, please run `nuxt build` before launching `nuxt start`')
})
test('no error on ssr and server bundle exists', async () => {
mockGetNuxtStart(true)
mockGetNuxtConfig()
jest.spyOn(fs, 'existsSync').mockImplementation(() => true)
await start()
expect(consola.fatal).not.toHaveBeenCalled()
})
test('fatal error on ssr and server bundle doesnt exist', async () => {
mockGetNuxtStart(true)
jest.spyOn(fs, 'existsSync').mockImplementation(() => false)
await start()
expect(consola.fatal).toHaveBeenCalledWith('No SSR build! Please start with `nuxt start --spa` or build using `nuxt build --universal`')
})
})

View File

@ -0,0 +1,111 @@
import { consola } from '../utils'
import * as utils from '../../src/utils'
describe('cli/utils', () => {
afterEach(() => {
jest.resetAllMocks()
})
test('loadNuxtConfig: defaults', async () => {
const argv = {
_: ['.'],
'config-file': 'nuxt.config.js',
universal: true
}
const options = await utils.loadNuxtConfig(argv)
expect(options.rootDir).toBe(process.cwd())
expect(options.mode).toBe('universal')
expect(options.server.host).toBe('localhost')
expect(options.server.port).toBe(3000)
expect(options.server.socket).not.toBeDefined()
})
test('loadNuxtConfig: config-file', async () => {
const argv = {
_: [__dirname],
'config-file': '../fixtures/nuxt.config.js',
spa: true
}
const options = await utils.loadNuxtConfig(argv)
expect(options.testOption).toBe(true)
expect(options.rootDir).toBe('/some/path')
expect(options.mode).toBe('spa')
expect(options.server.host).toBe('nuxt-host')
expect(options.server.port).toBe(3001)
expect(options.server.socket).toBe('/var/run/nuxt.sock')
})
test('loadNuxtConfig: not-existing config-file', async () => {
const argv = {
_: [__dirname],
'config-file': '../fixtures/nuxt.doesnt-exist.js'
}
const options = await utils.loadNuxtConfig(argv)
expect(options.testOption).not.toBeDefined()
expect(consola.fatal).toHaveBeenCalledTimes(1)
expect(consola.fatal).toHaveBeenCalledWith(expect.stringMatching(/Could not load config file/))
})
test('loadNuxtConfig: async config-file', async () => {
const argv = {
_: [__dirname],
'config-file': '../fixtures/nuxt.async-config.js',
hostname: 'async-host',
port: 3002,
'unix-socket': '/var/run/async.sock'
}
const options = await utils.loadNuxtConfig(argv)
expect(options.testOption).toBe(true)
expect(options.mode).toBe('supercharged')
expect(options.server.host).toBe('async-host')
expect(options.server.port).toBe(3002)
expect(options.server.socket).toBe('/var/run/async.sock')
})
test('loadNuxtConfig: async config-file with error', async () => {
const argv = {
_: [__dirname],
'config-file': '../fixtures/nuxt.async-error.js'
}
const options = await utils.loadNuxtConfig(argv)
expect(options.testOption).not.toBeDefined()
expect(consola.error).toHaveBeenCalledTimes(1)
expect(consola.error).toHaveBeenCalledWith(new Error('Async Config Error'))
expect(consola.fatal).toHaveBeenCalledWith('Error while fetching async configuration')
})
test('loadNuxtConfig: server env', async () => {
const env = process.env
process.env.HOST = 'env-host'
process.env.PORT = 3003
process.env.UNIX_SOCKET = '/var/run/env.sock'
const argv = {
_: [__dirname],
'config-file': '../fixtures/nuxt.config.js'
}
const options = await utils.loadNuxtConfig(argv)
expect(options.server.host).toBe('env-host')
expect(options.server.port).toBe('3003')
expect(options.server.socket).toBe('/var/run/env.sock')
process.env = env
})
test('indent', () => {
expect(utils.indent(4)).toBe(' ')
})
test('indent custom char', () => {
expect(utils.indent(4, '-')).toBe('----')
})
})

View File

@ -0,0 +1,8 @@
import consola from 'consola'
export * from './mocking'
jest.mock('consola')
export {
consola
}

View File

@ -0,0 +1,96 @@
import Command from '../../src/command'
export const mockGetNuxt = (options, implementation) => {
Command.prototype.getNuxt = jest.fn().mockImplementationOnce(() => {
return Object.assign({
hook: jest.fn(),
options
}, implementation || {})
})
}
export const mockGetBuilder = (ret) => {
const build = jest.fn().mockImplementationOnce(() => {
return ret
})
Command.prototype.getBuilder = jest.fn().mockImplementationOnce(() => {
return { build }
})
return build
}
export const mockGetGenerator = (ret) => {
const generate = jest.fn()
if (ret) {
generate.mockImplementationOnce(() => {
return ret
})
}
Command.prototype.getGenerator = jest.fn().mockImplementationOnce(() => {
return { generate }
})
return generate
}
export const mockGetNuxtStart = (ssr) => {
const listen = jest.fn().mockImplementationOnce(() => {
return Promise.resolve()
})
const showReady = jest.fn()
mockGetNuxt({
rootDir: '.',
render: {
ssr
}
}, {
listen,
showReady
})
return { listen, showReady }
}
export const mockGetNuxtConfig = () => {
const spy = jest.fn()
Command.prototype.getNuxtConfig = spy
return spy
}
export const mockNuxt = (implementation) => {
const Nuxt = function () {}
Object.assign(Nuxt.prototype, {
hook(type, fn) {
if (type === 'watch:fileChanged') {
Nuxt.fileChangedHook = fn
}
},
clearHook: jest.fn(),
close: jest.fn(),
listen: jest.fn().mockImplementationOnce(() => Promise.resolve()),
showReady: jest.fn().mockImplementationOnce(() => Promise.resolve())
}, implementation || {})
Command.prototype.importCore = jest.fn().mockImplementationOnce(() => {
return { Nuxt }
})
return Nuxt
}
export const mockBuilder = (implementation) => {
const Builder = function () {}
Object.assign(Builder.prototype, {
build: jest.fn().mockImplementationOnce(() => Promise.resolve()),
unwatch: jest.fn().mockImplementationOnce(() => Promise.resolve()),
watchServer: jest.fn().mockImplementationOnce(() => Promise.resolve())
}, implementation || {})
Command.prototype.importBuilder = jest.fn().mockImplementationOnce(() => {
return { Builder }
})
return Builder
}

View File

@ -11088,6 +11088,15 @@ wrap-ansi@^3.0.1:
string-width "^2.1.1"
strip-ansi "^4.0.0"
wrap-ansi@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-4.0.0.tgz#b3570d7c70156159a2d42be5cc942e957f7b1131"
integrity sha512-uMTsj9rDb0/7kk1PbcbCcwvHUxp60fGDB/NNXpVa0Q+ic/e7y5+BwTxKfQ33VYgDppSwi/FBzpetYzo8s6tfbg==
dependencies:
ansi-styles "^3.2.0"
string-width "^2.1.1"
strip-ansi "^4.0.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"