diff --git a/bin/nuxt-build b/bin/nuxt-build index 9222db6df2..c5b89eea90 100755 --- a/bin/nuxt-build +++ b/bin/nuxt-build @@ -80,6 +80,38 @@ if (options.mode !== 'spa') { process.exit(1) }) } 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 nuxt.options.generate.minify = false // Generate on spa mode diff --git a/bin/nuxt-generate b/bin/nuxt-generate index 1d9ace2e91..f45b04d613 100755 --- a/bin/nuxt-generate +++ b/bin/nuxt-generate @@ -71,6 +71,38 @@ const generateOptions = { 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) .then(() => { debug('Generate done') diff --git a/lib/builder/generator.js b/lib/builder/generator.js index e9367ba253..7a22714bda 100644 --- a/lib/builder/generator.js +++ b/lib/builder/generator.js @@ -3,9 +3,6 @@ import _ from 'lodash' import { resolve, join, dirname, sep } from 'path' import { minify } from 'html-minifier' import { isUrl, promisifyRoute, waitFor, flatRoutes } from 'utils' -import Debug from 'debug' - -const debug = Debug('nuxt:generate') export default class Generator { constructor(nuxt, builder) { @@ -14,22 +11,33 @@ export default class Generator { this.builder = builder // Set variables - this.generateRoutes = resolve(this.options.srcDir, 'static') + this.staticRoutes = resolve(this.options.srcDir, 'static') this.srcBuiltPath = resolve(this.options.buildDir, 'dist') this.distPath = resolve(this.options.rootDir, this.options.generate.dir) this.distNuxtPath = join(this.distPath, (isUrl(this.options.build.publicPath) ? '' : this.options.build.publicPath)) } 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 await this.nuxt.ready() // Call before hook await this.nuxt.callHook('generate:before', this, this.options.generate) - const s = Date.now() - let errors = [] - // Add flag to set process.static this.builder.forGenerate() @@ -42,7 +50,9 @@ export default class Generator { if (init) { await this.initDist() } + } + async initRoutes() { // Resolve config.generate.routes promises before generating the routes let generateRoutes = [] if (this.options.router.mode !== 'hash') { @@ -61,16 +71,25 @@ export default class Generator { // extendRoutes hook await this.nuxt.callHook('generate:extendRoutes', routes) + return routes + } + + async generateRoutes(routes) { + let errors = [] + // Start generate process while (routes.length) { let n = 0 await Promise.all(routes.splice(0, this.options.generate.concurrency).map(async ({ route, payload }) => { await waitFor(n++ * this.options.generate.interval) await this.generateRoute({ route, payload, errors }) - await this.nuxt.callHook('generate:routeCreated', route) })) } + return errors + } + + async afterGenerate() { const indexPath = join(this.distPath, 'index.html') if (existsSync(indexPath)) { // Copy /index.html to /200.html for surge SPA @@ -80,37 +99,18 @@ export default class Generator { 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() { // Clean destination folder await remove(this.distPath) - debug('Destination folder cleaned') + + await this.nuxt.callHook('generate:distRemoved', this) // Copy static and built files /* istanbul ignore if */ - if (existsSync(this.generateRoutes)) { - await copy(this.generateRoutes, this.distPath) + if (existsSync(this.staticRoutes)) { + await copy(this.staticRoutes, this.distPath) } 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) { @@ -159,16 +159,22 @@ export default class Generator { async generateRoute({ route, payload = {}, errors = [] }) { let html + const pageErrors = [] try { const res = await this.nuxt.renderer.renderRoute(route, { _generate: true, payload }) html = res.html if (res.error) { - errors.push({ type: 'handled', route, error: res.error }) + pageErrors.push({ type: 'handled', route, error: res.error }) } } catch (err) { /* 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) { @@ -176,7 +182,7 @@ export default class Generator { html = minify(html, this.options.generate.minify) } catch (err) /* istanbul ignore next */ { 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 } await this.nuxt.callHook('generate:page', page) - debug('Generate file: ' + page.path) page.path = join(this.distPath, page.path) // Make sure the sub folders are created await mkdirp(dirname(page.path)) 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 } } diff --git a/test/children.patch.test.js b/test/children.patch.test.js index 223c3e4513..a66cfbf6ce 100644 --- a/test/children.patch.test.js +++ b/test/children.patch.test.js @@ -111,7 +111,7 @@ test('Search a country', async t => { 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]') t.is(newCountries.length, 1) t.deepEqual(newCountries, ['Guinea']) diff --git a/test/cli.test.js b/test/cli.test.js new file mode 100644 index 0000000000..2fe743cb2f --- /dev/null +++ b/test/cli.test.js @@ -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('

User: 1

')) + + 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')) +})