refactor generator + cli tests (#2205)

* Rename this.generateRoutes to this.staticRoutes

* Refactor generator to separate logic

* Move routeCreated hook to generateRoute

Add routeFailed hook for unhandled exceptions

Keep page errors separately until page hooks have been called

* Move debug and report statements to hooks

* pageErrors can be a const

Push pageErrors to errors

* fix done hook, errors are 2nd param

* Add generator hooks to nuxt-build for spa mode

* Added a cli integration test for bin/nuxt-(build|start|generate)

* Removed unnecessary waitFor

* Use pify instead util.promisify for v6 compatibility

* Fix windows build

You cant execute .js files directly on Windows/Appveyor so call node with nuxt-*.js file as argument

* Fix windows build (2)

Use correct folder separators in text search

* Fix possible timing quirck in children.path.test
This commit is contained in:
Pim 2017-11-23 23:31:54 +01:00 committed by Pooya Parsa
parent 3764dc73c3
commit 65f4a030f4
5 changed files with 205 additions and 37 deletions

View File

@ -80,6 +80,38 @@ if (options.mode !== 'spa') {
process.exit(1) process.exit(1)
}) })
} else { } else {
const s = Date.now()
nuxt.hook('generate:distRemoved', function() {
debug('Destination folder cleaned')
})
nuxt.hook('generate:distCopied', function() {
debug('Static & build files copied')
})
nuxt.hook('generate:page', function(page) {
debug('Generate file: ' + page.path)
})
nuxt.hook('generate:done', function(generator, errors) {
const duration = Math.round((Date.now() - s) / 100) / 10
debug(`HTML Files generated in ${duration}s`)
if (errors.length) {
const report = errors.map(({ type, route, error }) => {
/* istanbul ignore if */
if (type === 'unhandled') {
return `Route: '${route}'\n${error.stack}`
} else {
return `Route: '${route}' thrown an error: \n` + JSON.stringify(error)
}
})
console.error('==== Error report ==== \n' + report.join('\n\n')) // eslint-disable-line no-console
}
})
// Disable minify to get exact results of nuxt start // Disable minify to get exact results of nuxt start
nuxt.options.generate.minify = false nuxt.options.generate.minify = false
// Generate on spa mode // Generate on spa mode

View File

@ -71,6 +71,38 @@ const generateOptions = {
build: argv['build'] build: argv['build']
} }
const s = Date.now()
nuxt.hook('generate:distRemoved', function() {
debug('Destination folder cleaned')
})
nuxt.hook('generate:distCopied', function() {
debug('Static & build files copied')
})
nuxt.hook('generate:page', function(page) {
debug('Generate file: ' + page.path)
})
nuxt.hook('generate:done', function(generator, errors) {
const duration = Math.round((Date.now() - s) / 100) / 10
debug(`HTML Files generated in ${duration}s`)
if (errors.length) {
const report = errors.map(({ type, route, error }) => {
/* istanbul ignore if */
if (type === 'unhandled') {
return `Route: '${route}'\n${error.stack}`
} else {
return `Route: '${route}' thrown an error: \n` + JSON.stringify(error)
}
})
console.error('==== Error report ==== \n' + report.join('\n\n')) // eslint-disable-line no-console
}
})
generator.generate(generateOptions) generator.generate(generateOptions)
.then(() => { .then(() => {
debug('Generate done') debug('Generate done')

View File

@ -3,9 +3,6 @@ import _ from 'lodash'
import { resolve, join, dirname, sep } from 'path' import { resolve, join, dirname, sep } from 'path'
import { minify } from 'html-minifier' import { minify } from 'html-minifier'
import { isUrl, promisifyRoute, waitFor, flatRoutes } from 'utils' import { isUrl, promisifyRoute, waitFor, flatRoutes } from 'utils'
import Debug from 'debug'
const debug = Debug('nuxt:generate')
export default class Generator { export default class Generator {
constructor(nuxt, builder) { constructor(nuxt, builder) {
@ -14,22 +11,33 @@ export default class Generator {
this.builder = builder this.builder = builder
// Set variables // Set variables
this.generateRoutes = resolve(this.options.srcDir, 'static') this.staticRoutes = resolve(this.options.srcDir, 'static')
this.srcBuiltPath = resolve(this.options.buildDir, 'dist') this.srcBuiltPath = resolve(this.options.buildDir, 'dist')
this.distPath = resolve(this.options.rootDir, this.options.generate.dir) this.distPath = resolve(this.options.rootDir, this.options.generate.dir)
this.distNuxtPath = join(this.distPath, (isUrl(this.options.build.publicPath) ? '' : this.options.build.publicPath)) this.distNuxtPath = join(this.distPath, (isUrl(this.options.build.publicPath) ? '' : this.options.build.publicPath))
} }
async generate({ build = true, init = true } = {}) { async generate({ build = true, init = true } = {}) {
await this.initiate({ build, init })
const routes = await this.initRoutes()
const errors = await this.generateRoutes(routes)
await this.afterGenerate()
// done hook
await this.nuxt.callHook('generate:done', this, errors)
return { errors }
}
async initiate({ build = true, init = true } = {}) {
// Wait for nuxt be ready // Wait for nuxt be ready
await this.nuxt.ready() await this.nuxt.ready()
// Call before hook // Call before hook
await this.nuxt.callHook('generate:before', this, this.options.generate) await this.nuxt.callHook('generate:before', this, this.options.generate)
const s = Date.now()
let errors = []
// Add flag to set process.static // Add flag to set process.static
this.builder.forGenerate() this.builder.forGenerate()
@ -42,7 +50,9 @@ export default class Generator {
if (init) { if (init) {
await this.initDist() await this.initDist()
} }
}
async initRoutes() {
// Resolve config.generate.routes promises before generating the routes // Resolve config.generate.routes promises before generating the routes
let generateRoutes = [] let generateRoutes = []
if (this.options.router.mode !== 'hash') { if (this.options.router.mode !== 'hash') {
@ -61,16 +71,25 @@ export default class Generator {
// extendRoutes hook // extendRoutes hook
await this.nuxt.callHook('generate:extendRoutes', routes) await this.nuxt.callHook('generate:extendRoutes', routes)
return routes
}
async generateRoutes(routes) {
let errors = []
// Start generate process // Start generate process
while (routes.length) { while (routes.length) {
let n = 0 let n = 0
await Promise.all(routes.splice(0, this.options.generate.concurrency).map(async ({ route, payload }) => { await Promise.all(routes.splice(0, this.options.generate.concurrency).map(async ({ route, payload }) => {
await waitFor(n++ * this.options.generate.interval) await waitFor(n++ * this.options.generate.interval)
await this.generateRoute({ route, payload, errors }) await this.generateRoute({ route, payload, errors })
await this.nuxt.callHook('generate:routeCreated', route)
})) }))
} }
return errors
}
async afterGenerate() {
const indexPath = join(this.distPath, 'index.html') const indexPath = join(this.distPath, 'index.html')
if (existsSync(indexPath)) { if (existsSync(indexPath)) {
// Copy /index.html to /200.html for surge SPA // Copy /index.html to /200.html for surge SPA
@ -80,37 +99,18 @@ export default class Generator {
await copy(indexPath, _200Path) await copy(indexPath, _200Path)
} }
} }
const duration = Math.round((Date.now() - s) / 100) / 10
debug(`HTML Files generated in ${duration}s`)
// done hook
await this.nuxt.callHook('generate:done', this, errors)
if (errors.length) {
const report = errors.map(({ type, route, error }) => {
/* istanbul ignore if */
if (type === 'unhandled') {
return `Route: '${route}'\n${error.stack}`
} else {
return `Route: '${route}' thrown an error: \n` + JSON.stringify(error)
}
})
console.error('==== Error report ==== \n' + report.join('\n\n')) // eslint-disable-line no-console
}
return { duration, errors }
} }
async initDist() { async initDist() {
// Clean destination folder // Clean destination folder
await remove(this.distPath) await remove(this.distPath)
debug('Destination folder cleaned')
await this.nuxt.callHook('generate:distRemoved', this)
// Copy static and built files // Copy static and built files
/* istanbul ignore if */ /* istanbul ignore if */
if (existsSync(this.generateRoutes)) { if (existsSync(this.staticRoutes)) {
await copy(this.generateRoutes, this.distPath) await copy(this.staticRoutes, this.distPath)
} }
await copy(this.srcBuiltPath, this.distNuxtPath) await copy(this.srcBuiltPath, this.distNuxtPath)
@ -133,7 +133,7 @@ export default class Generator {
} }
}) })
debug('Static & build files copied') await this.nuxt.callHook('generate:distCopied', this)
} }
decorateWithPayloads(routes, generateRoutes) { decorateWithPayloads(routes, generateRoutes) {
@ -159,16 +159,22 @@ export default class Generator {
async generateRoute({ route, payload = {}, errors = [] }) { async generateRoute({ route, payload = {}, errors = [] }) {
let html let html
const pageErrors = []
try { try {
const res = await this.nuxt.renderer.renderRoute(route, { _generate: true, payload }) const res = await this.nuxt.renderer.renderRoute(route, { _generate: true, payload })
html = res.html html = res.html
if (res.error) { if (res.error) {
errors.push({ type: 'handled', route, error: res.error }) pageErrors.push({ type: 'handled', route, error: res.error })
} }
} catch (err) { } catch (err) {
/* istanbul ignore next */ /* istanbul ignore next */
return errors.push({ type: 'unhandled', route, error: err }) pageErrors.push({ type: 'unhandled', route, error: err })
Array.prototype.push.apply(errors, pageErrors)
await this.nuxt.callHook('generate:routeFailed', { route, errors: pageErrors })
return false
} }
if (this.options.generate.minify) { if (this.options.generate.minify) {
@ -176,7 +182,7 @@ export default class Generator {
html = minify(html, this.options.generate.minify) html = minify(html, this.options.generate.minify)
} catch (err) /* istanbul ignore next */ { } catch (err) /* istanbul ignore next */ {
const minifyErr = new Error(`HTML minification failed. Make sure the route generates valid HTML. Failed HTML:\n ${html}`) const minifyErr = new Error(`HTML minification failed. Make sure the route generates valid HTML. Failed HTML:\n ${html}`)
errors.push({ type: 'unhandled', route, error: minifyErr }) pageErrors.push({ type: 'unhandled', route, error: minifyErr })
} }
} }
@ -194,13 +200,18 @@ export default class Generator {
const page = { route, path, html } const page = { route, path, html }
await this.nuxt.callHook('generate:page', page) await this.nuxt.callHook('generate:page', page)
debug('Generate file: ' + page.path)
page.path = join(this.distPath, page.path) page.path = join(this.distPath, page.path)
// Make sure the sub folders are created // Make sure the sub folders are created
await mkdirp(dirname(page.path)) await mkdirp(dirname(page.path))
await writeFile(page.path, page.html, 'utf8') await writeFile(page.path, page.html, 'utf8')
await this.nuxt.callHook('generate:routeCreated', { route, path: page.path, errors: pageErrors })
if (pageErrors.length) {
Array.prototype.push.apply(errors, pageErrors)
}
return true return true
} }
} }

View File

@ -111,7 +111,7 @@ test('Search a country', async t => {
await page.type('[data-test-search-input]', 'gu') await page.type('[data-test-search-input]', 'gu')
await Utils.waitFor(100) await Utils.waitFor(250)
const newCountries = await page.$$text('[data-test-search-result]') const newCountries = await page.$$text('[data-test-search-result]')
t.is(newCountries.length, 1) t.is(newCountries.length, 1)
t.deepEqual(newCountries, ['Guinea']) t.deepEqual(newCountries, ['Guinea'])

93
test/cli.test.js Normal file
View File

@ -0,0 +1,93 @@
import test from 'ava'
import { resolve, sep } from 'path'
import rp from 'request-promise-native'
import { Utils } from '../index.js'
import pify from 'pify'
import { exec, spawn } from 'child_process'
const execify = pify(exec, { multiArgs: true })
const rootDir = resolve(__dirname, 'fixtures/basic')
const port = 4011
const url = (route) => 'http://localhost:' + port + route
test('bin/nuxt-build', async t => {
const binBuild = resolve(__dirname, '..', 'bin', 'nuxt-build')
const [ stdout, stderr ] = await execify(`node ${binBuild} ${rootDir}`)
t.true(stdout.includes('server-bundle.json'))
t.true(stderr.includes('Building done'))
})
test('bin/nuxt-start', async t => {
const binStart = resolve(__dirname, '..', 'bin', 'nuxt-start')
let stdout = ''
let stderr = ''
let error
let exitCode
const env = process.env
env.PORT = port
const nuxtStart = spawn('node', [binStart, rootDir], { env: env })
nuxtStart.stdout.on('data', (data) => {
stdout += data
})
nuxtStart.stderr.on('data', (data) => {
stderr += data
})
nuxtStart.on('error', (err) => {
error = err
})
nuxtStart.on('close', (code) => {
exitCode = code
})
// Give the process max 10s to start
let iterator = 0
while (!stdout.includes('OPEN') && iterator < 40) {
await Utils.waitFor(250)
iterator++
}
t.is(error, undefined)
t.true(stdout.includes('OPEN'))
const html = await rp(url('/users/1'))
t.true(html.includes('<h1>User: 1</h1>'))
nuxtStart.kill()
// Wait max 10s for the process to be killed
iterator = 0
while (exitCode === undefined && iterator < 40) { // eslint-disable-line no-unmodified-loop-condition
await Utils.waitFor(250)
iterator++
}
if (iterator >= 40) {
t.log(`WARN: we were unable to automatically kill the child process with pid: ${nuxtStart.pid}`)
}
t.is(stderr, '')
t.is(exitCode, null)
})
test('bin/nuxt-generate', async t => {
const binGenerate = resolve(__dirname, '..', 'bin', 'nuxt-generate')
const [ stdout, stderr ] = await execify(`node ${binGenerate} ${rootDir}`)
t.true(stdout.includes('server-bundle.json'))
t.true(stderr.includes('Destination folder cleaned'))
t.true(stderr.includes('Static & build files copied'))
t.true(stderr.includes(`Generate file: ${sep}users${sep}1${sep}index.html`))
t.true(stderr.includes('Error report'))
t.true(stderr.includes('Generate done'))
})