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 #!/usr/bin/env node
const consola = require('consola') require('../dist/cli.js').run()
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)
})

View File

@ -14,7 +14,8 @@
"dependencies": { "dependencies": {
"consola": "^1.4.4", "consola": "^1.4.4",
"esm": "^3.0.84", "esm": "^3.0.84",
"minimist": "^1.2.0" "minimist": "^1.2.0",
"wrap-ansi": "^4.0.0"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "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 consola from 'consola'
import NuxtCommand from '../command'
import { loadNuxtConfig } from '../common/utils'
export default async function build() { export default async function build() {
const { Nuxt } = await import('@nuxt/core') const nuxtCmd = new NuxtCommand({
const { Builder } = await import('@nuxt/builder') description: 'Compiles the application for production deployment',
const { Generator } = await import('@nuxt/generator') usage: 'build <dir>',
options: [ 'analyze', 'quiet' ]
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'
}
}) })
if (argv.help) { const argv = nuxtCmd.getArgv()
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 options = await loadNuxtConfig(argv) // Create production build when calling `nuxt build` (dev: false)
const nuxt = await nuxtCmd.getNuxt(
// Create production build when calling `nuxt build` await nuxtCmd.getNuxtConfig(argv, { dev: false })
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)
// Setup hooks // Setup hooks
nuxt.hook('error', err => consola.fatal(err)) nuxt.hook('error', err => consola.fatal(err))
// Close function let builderOrGenerator
const close = () => { 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 // In analyze mode wait for plugin
// emitting assets and opening browser // 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 return
} }
process.exit(0) process.exit(0)
} })
if (options.mode !== 'spa' || argv.generate === false) {
// Build only
return builder
.build()
.then(close)
.catch(err => consola.fatal(err)) .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 consola from 'consola'
import { loadNuxtConfig, runAsyncScript } from '../common/utils' import NuxtCommand from '../command'
export default async function dev() { export default async function dev() {
const { Nuxt } = await import('@nuxt/core') const nuxtCmd = new NuxtCommand({
const { Builder } = await import('@nuxt/builder') description: 'Start the application in development mode (e.g. hot-code reloading, error reporting)',
usage: 'dev <dir> -p <port number> -H <hostname>',
const argv = parseArgs(process.argv.slice(2), { options: [ 'hostname', 'port' ]
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'
}
}) })
if (argv.version) { const argv = nuxtCmd.getArgv()
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 errorHandler = (err, instance) => { const errorHandler = (err, instance) => {
instance && instance.builder.watchServer() instance && instance.builder.watchServer()
consola.error(err) consola.error(err)
} }
const { Nuxt } = await nuxtCmd.importCore()
const { Builder } = await nuxtCmd.importBuilder()
// Start dev // Start dev
async function startDev(oldInstance) { async function startDev(oldInstance) {
let nuxt, builder let nuxt, builder
try { try {
nuxt = new Nuxt(await config()) nuxt = new Nuxt(await nuxtCmd.getNuxtConfig(argv, { dev: true }))
builder = new Builder(nuxt) builder = new Builder(nuxt)
nuxt.hook('watch:fileChanged', async (builder, fname) => { nuxt.hook('watch:fileChanged', async (builder, fname) => {
consola.debug(`[${fname}] changed, Rebuilding the app...`) consola.debug(`[${fname}] changed, Rebuilding the app...`)
@ -81,10 +39,10 @@ export default async function dev() {
.then(() => oldInstance && oldInstance.builder.unwatch()) .then(() => oldInstance && oldInstance.builder.unwatch())
// Start build // Start build
.then(() => builder.build()) .then(() => builder.build())
// Close old nuxt no mater if build successfully // Close old nuxt no matter if build successfully
.catch((err) => { .catch((err) => {
oldInstance && oldInstance.nuxt.close() oldInstance && oldInstance.nuxt.close()
// Jump to eventHandler // Jump to errorHandler
throw err throw err
}) })
.then(() => oldInstance && oldInstance.nuxt.close()) .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 consola from 'consola'
import NuxtCommand from '../command'
import { loadNuxtConfig } from '../common/utils'
export default async function generate() { export default async function generate() {
const { Nuxt } = await import('@nuxt/core') const nuxtCmd = new NuxtCommand({
const { Builder } = await import('@nuxt/builder') description: 'Generate a static web application (server-rendered)',
const { Generator } = await import('@nuxt/generator') usage: 'generate <dir>',
options: [ 'build' ]
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
}
}) })
if (argv.help) { const argv = nuxtCmd.getArgv()
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 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) return generator.generate({
const nuxt = new Nuxt(options)
const builder = new Builder(nuxt)
const generator = new Generator(nuxt, builder)
const generateOptions = {
init: true, init: true,
build: argv.build build: argv.build
} }).then(() => {
return generator
.generate(generateOptions)
.then(() => {
process.exit(0) 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 fs from 'fs'
import path from 'path' import path from 'path'
import parseArgs from 'minimist'
import consola from 'consola' import consola from 'consola'
import NuxtCommand from '../command'
import { loadNuxtConfig } from '../common/utils'
export default async function start() { export default async function start() {
const { Nuxt } = await import('@nuxt/core') const nuxtCmd = new NuxtCommand({
description: 'Start the application in production mode (the application should be compiled with `nuxt build` first)',
const argv = parseArgs(process.argv.slice(2), { usage: 'start <dir> -p <port number> -H <hostname>',
alias: { options: [ 'hostname', 'port', 'unix-socket' ]
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'
}
}) })
if (argv.hostname === '') { const argv = nuxtCmd.getArgv()
consola.fatal('Provided hostname argument has no value')
}
if (argv.help) { // Create production build when calling `nuxt build`
process.stderr.write(` const nuxt = await nuxtCmd.getNuxt(
Description await nuxtCmd.getNuxtConfig(argv, { dev: false })
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)
// Setup hooks // Setup hooks
nuxt.hook('error', err => consola.fatal(err)) 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') import * as _commands from './commands'
export const dev = () => import('./commands/dev') import * as _imports from './imports'
export const build = () => import('./commands/build') export const commands = _commands
export const generate = () => import('./commands/generate') 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 { existsSync } from 'fs'
import consola from 'consola' import consola from 'consola'
import esm from 'esm' import esm from 'esm'
import wrapAnsi from 'wrap-ansi'
const _require = esm(module, { const _require = esm(module, {
cache: false, cache: false,
@ -33,15 +34,6 @@ const getLatestHost = (argv) => {
return { port, host, socket } 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) { export async function loadNuxtConfig(argv) {
const rootDir = getRootDir(argv) const rootDir = getRootDir(argv)
const nuxtConfigFile = getNuxtConfigFile(argv) const nuxtConfigFile = getNuxtConfigFile(argv)
@ -50,16 +42,17 @@ export async function loadNuxtConfig(argv) {
if (existsSync(nuxtConfigFile)) { if (existsSync(nuxtConfigFile)) {
delete require.cache[nuxtConfigFile] delete require.cache[nuxtConfigFile]
options = _require(nuxtConfigFile) options = _require(nuxtConfigFile) || {}
if (!options) {
options = {}
}
if (options.default) { if (options.default) {
options = options.default options = options.default
} }
if (typeof options === 'function') { if (typeof options === 'function') {
try { try {
options = await options() options = await options()
if (options.default) {
options = options.default
}
} catch (error) { } catch (error) {
consola.error(error) consola.error(error)
consola.fatal('Error while fetching async configuration') consola.fatal('Error while fetching async configuration')
@ -68,7 +61,6 @@ export async function loadNuxtConfig(argv) {
} 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'])
} }
if (typeof options.rootDir !== 'string') { if (typeof options.rootDir !== 'string') {
options.rootDir = rootDir options.rootDir = rootDir
} }
@ -81,9 +73,33 @@ export async function loadNuxtConfig(argv) {
if (!options.server) { if (!options.server) {
options.server = {} options.server = {}
} }
const { port, host, socket } = getLatestHost(argv) const { port, host, socket } = getLatestHost(argv)
options.server.port = port || options.server.port || 3000 options.server.port = port || options.server.port || 3000
options.server.host = host || options.server.host || 'localhost' options.server.host = host || options.server.host || 'localhost'
options.server.socket = socket || options.server.socket options.server.socket = socket || options.server.socket
return options 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" string-width "^2.1.1"
strip-ansi "^4.0.0" 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: wrappy@1:
version "1.0.2" version "1.0.2"
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"