Skip to content

Commit

Permalink
Surface Bundler error to browser (#1457)
Browse files Browse the repository at this point in the history
  • Loading branch information
mohsen1 authored and devongovett committed Jun 14, 2018
1 parent eda41e1 commit 82a80bb
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 15 deletions.
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -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",
Expand Down
14 changes: 9 additions & 5 deletions src/Bundler.js
Expand Up @@ -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;

Expand Down Expand Up @@ -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...');
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -493,7 +493,7 @@ class Bundler extends EventEmitter {
return;
}

if (!this.errored) {
if (!this.error) {
logger.status(emoji.progress, `Building ${asset.basename}...`);
}

Expand Down Expand Up @@ -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;
}
}
Expand Down
32 changes: 27 additions & 5 deletions src/Server.js
Expand Up @@ -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']
});
Expand Down Expand Up @@ -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) === ''
Expand All @@ -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 = '<h1>🚨 Build Error</h1>';
if (process.env.NODE_ENV === 'production') {
errorMesssge += '<p><b>Check the console for details.</b></p>';
} else {
const {message, stack} = prettyError(error, {color: true});
errorMesssge += `<p><b>${message}</b></p>`;
if (stack) {
errorMesssge += `<div style="background: black; padding: 1rem;">${ansiToHtml.toHtml(
stack
)}</div>`;
}
}
res.end(
[
`<!doctype html>`,
`<head><title>🚨 Build Error</title></head>`,
`<body style="font-family: monospace; white-space: pre;">${errorMesssge}</body>`
].join('')
);
}

function send404() {
Expand Down
12 changes: 12 additions & 0 deletions test/integration/bundler-error-syntax-error/index.html
@@ -0,0 +1,12 @@
<!doctype html>
<html>

<head>
</head>

<body>
<h1>Hello world</h1>
<script src="./index.js"></script>
</body>

</html>
1 change: 1 addition & 0 deletions test/integration/bundler-error-syntax-error/index.js
@@ -0,0 +1 @@
.invalid_js
59 changes: 54 additions & 5 deletions test/server.js
Expand Up @@ -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);
});
}
Expand Down Expand Up @@ -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');
}
Expand All @@ -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('<title>🚨 Build Error</title>'), 'has title');
assert(resp.includes('<h1>🚨 Build Error</h1>'), 'has h1');
assert(
resp.includes('<div style="background: black; padding: 1rem;">'),
'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('<title>🚨 Build Error</title>'), 'has title');
assert(resp.includes('<h1>🚨 Build Error</h1>'), 'has h1');
assert(
resp.includes('<p><b>Check the console for details.</b></p>'),
'has description'
);
assert(
!resp.includes('<div style="background: black; padding: 1rem;">'),
'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);
Expand Down
6 changes: 6 additions & 0 deletions yarn.lock
Expand Up @@ -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"
Expand Down

0 comments on commit 82a80bb

Please sign in to comment.