diff --git a/package.json b/package.json index a01f058aa08..0a1f1773e67 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "index.js" ], "dependencies": { + "ansi-to-html": "^0.6.4", "babel-code-frame": "^6.26.0", "babel-core": "^6.25.0", "babel-generator": "^6.25.0", diff --git a/src/Bundler.js b/src/Bundler.js index 83248f190e5..0e8d38bb72c 100644 --- a/src/Bundler.js +++ b/src/Bundler.js @@ -60,7 +60,7 @@ class Bundler extends EventEmitter { this.watcher = null; this.hmr = null; this.bundleHashes = null; - this.errored = false; + this.error = null; this.buildQueue = new PromiseQueue(this.processAsset.bind(this)); this.rebuildTimeout = null; @@ -213,7 +213,7 @@ class Bundler extends EventEmitter { let isInitialBundle = !this.entryAssets; let startTime = Date.now(); this.pending = true; - this.errored = false; + this.error = null; logger.clear(); logger.status(emoji.progress, 'Building...'); @@ -290,7 +290,7 @@ class Bundler extends EventEmitter { this.emit('bundled', this.mainBundle); return this.mainBundle; } catch (err) { - this.errored = true; + this.error = err; logger.error(err); if (this.hmr) { this.hmr.emitError(err); @@ -493,7 +493,7 @@ class Bundler extends EventEmitter { return; } - if (!this.errored) { + if (!this.error) { logger.status(emoji.progress, `Building ${asset.basename}...`); } @@ -713,7 +713,11 @@ class Bundler extends EventEmitter { async serve(port = 1234, https = false) { this.server = await Server.serve(this, port, https); - this.bundle(); + try { + await this.bundle(); + } catch (e) { + // ignore: server can still work with errored bundler + } return this.server; } } diff --git a/src/Server.js b/src/Server.js index cf5f1d6fc1e..08d29dbb483 100644 --- a/src/Server.js +++ b/src/Server.js @@ -5,10 +5,14 @@ const getPort = require('get-port'); const serverErrors = require('./utils/customErrors').serverErrors; const generateCertificate = require('./utils/generateCertificate'); const getCertificate = require('./utils/getCertificate'); +const prettyError = require('./utils/prettyError'); +const AnsiToHtml = require('ansi-to-html'); const logger = require('./Logger'); const path = require('path'); const url = require('url'); +const ansiToHtml = new AnsiToHtml({newline: true}); + serveStatic.mime.define({ 'application/wasm': ['wasm'] }); @@ -45,8 +49,8 @@ function middleware(bundler) { function respond() { let {pathname} = url.parse(req.url); - if (bundler.errored) { - return send500(); + if (bundler.error) { + return send500(bundler.error); } else if ( !pathname.startsWith(bundler.options.publicURL) || path.extname(pathname) === '' @@ -71,10 +75,28 @@ function middleware(bundler) { } } - function send500() { - res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + function send500(error) { + res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.writeHead(500); - res.end('🚨 Build error, check the console for details.'); + let errorMesssge = '

🚨 Build Error

'; + if (process.env.NODE_ENV === 'production') { + errorMesssge += '

Check the console for details.

'; + } else { + const {message, stack} = prettyError(error, {color: true}); + errorMesssge += `

${message}

`; + if (stack) { + errorMesssge += `
${ansiToHtml.toHtml( + stack + )}
`; + } + } + res.end( + [ + ``, + `🚨 Build Error`, + `${errorMesssge}` + ].join('') + ); } function send404() { diff --git a/test/integration/bundler-error-syntax-error/index.html b/test/integration/bundler-error-syntax-error/index.html new file mode 100644 index 00000000000..46839a96b99 --- /dev/null +++ b/test/integration/bundler-error-syntax-error/index.html @@ -0,0 +1,12 @@ + + + + + + + +

Hello world

+ + + + diff --git a/test/integration/bundler-error-syntax-error/index.js b/test/integration/bundler-error-syntax-error/index.js new file mode 100644 index 00000000000..78ffccdd0f8 --- /dev/null +++ b/test/integration/bundler-error-syntax-error/index.js @@ -0,0 +1 @@ +.invalid_js diff --git a/test/server.js b/test/server.js index 40cea6b06ab..53a67c19e53 100644 --- a/test/server.js +++ b/test/server.js @@ -23,14 +23,13 @@ describe('server', function() { rejectUnauthorized: false }, res => { - if (res.statusCode !== 200) { - return reject(new Error('Request failed: ' + res.statusCode)); - } - res.setEncoding('utf8'); let data = ''; res.on('data', c => (data += c)); res.on('end', () => { + if (res.statusCode !== 200) { + return reject(data); + } resolve(data); }); } @@ -85,7 +84,6 @@ describe('server', function() { try { await get('/'); - throw new Error('GET / responded with 200'); } catch (err) { assert.equal(err.message, 'Request failed: 500'); } @@ -94,6 +92,57 @@ describe('server', function() { await get('/'); }); + it('should serve a 500 response with error stack trace when bundler has errors', async function() { + let b = bundler( + __dirname + '/integration/bundler-error-syntax-error/index.html' + ); + + server = await b.serve(0); + let resp; + try { + await get('/'); + } catch (e) { + resp = e; + } + + assert(resp.includes('🚨 Build Error'), 'has title'); + assert(resp.includes('

🚨 Build Error

'), 'has h1'); + assert( + resp.includes('
'), + 'has code frame' + ); + assert(resp.includes('invalid_js'), 'code frame has invalid code'); + }); + + it('should serve a 500 response without stack trace when bundler has errors in production', async function() { + let NODE_ENV = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; + let b = bundler( + __dirname + '/integration/bundler-error-syntax-error/index.html' + ); + + server = await b.serve(0); + let resp; + try { + await get('/'); + } catch (e) { + resp = e; + } + + assert(resp.includes('🚨 Build Error'), 'has title'); + assert(resp.includes('

🚨 Build Error

'), 'has h1'); + assert( + resp.includes('

Check the console for details.

'), + 'has description' + ); + assert( + !resp.includes('
'), + 'do not have code frame' + ); + assert(!resp.includes('invalid_js'), 'source code is not shown'); + process.env.NODE_ENV = NODE_ENV; + }); + it('should support HTTPS', async function() { let b = bundler(__dirname + '/integration/commonjs/index.js'); server = await b.serve(0, true); diff --git a/yarn.lock b/yarn.lock index 202ca80dd7b..6550c5c8f36 100644 --- a/yarn.lock +++ b/yarn.lock @@ -145,6 +145,12 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-to-html@^0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.6.4.tgz#8b14ace87f8b3d25367d03cd5300d60be17cf9e0" + dependencies: + entities "^1.1.1" + any-observable@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b"