diff --git a/build/start.js b/build/start.js index ad8d8df4ad..d19c594677 100755 --- a/build/start.js +++ b/build/start.js @@ -25,7 +25,7 @@ const excludes = [ ].concat(Object.keys(packageJSON.devDependencies)) // Parse dist/core.js for all external dependencies -const requireRegex = /require\('([-\w]+)'\)/g +const requireRegex = /require\('([-@/\w]+)'\)/g const rawCore = readFileSync(resolve(rootDir, 'dist/core.js')) let match = requireRegex.exec(rawCore) while (match) { diff --git a/lib/app/views/error.html b/lib/app/views/error.html index 497a0e28ed..f35e1b02fe 100644 --- a/lib/app/views/error.html +++ b/lib/app/views/error.html @@ -1,11 +1,45 @@ - - - Nuxt.js Error - - -

Nuxt.js Error:

-
{{ stack }}
- + + Error - {{code }} {{ message }} + + + + + +
+
+
+
+

{{ code }}

+
+              {{ message }}
+            
+
+
+
+ + + \ No newline at end of file diff --git a/lib/common/options.js b/lib/common/options.js index f1b42b4a3e..8e784da5b9 100755 --- a/lib/common/options.js +++ b/lib/common/options.js @@ -48,6 +48,11 @@ export default function Options (_options) { options.store = true } + // Debug errors + if (options.render.debug === undefined) { + options.render.debug = options.dev + } + // Resolve mode let mode = options.mode if (typeof mode === 'function') { @@ -180,6 +185,7 @@ Options.defaults = { bundleRenderer: {}, resourceHints: true, ssr: undefined, + debug: undefined, // Will be set equal to dev http2: { push: false }, diff --git a/lib/core/renderer.js b/lib/core/renderer.js index bd3d350ccf..761ab1c938 100644 --- a/lib/core/renderer.js +++ b/lib/core/renderer.js @@ -10,8 +10,10 @@ import _ from 'lodash' import { join, resolve } from 'path' import fs from 'fs-extra' import { createBundleRenderer } from 'vue-server-renderer' -import { encodeHtml, getContext, setAnsiColors, isUrl } from 'utils' +import { getContext, setAnsiColors, isUrl } from 'utils' import Debug from 'debug' +import Youch from '@nuxtjs/youch' +import { SourceMapConsumer } from 'source-map' import connect from 'connect' import { Options } from 'common' @@ -44,7 +46,7 @@ export default class Renderer extends Tapable { serverBundle: null, ssrTemplate: null, spaTemplate: null, - errorTemplate: parseTemplate('
{{ stack }}
') // Will be loaded on ready + errorTemplate: parseTemplate('Nuxt.js Internal Server Error') } } @@ -54,7 +56,7 @@ export default class Renderer extends Tapable { // Setup nuxt middleware await this.setupMiddleware() - // Load error template + // Load error template for when debug is disabled const errorTemplatePath = resolve(this.options.buildDir, 'views/error.html') if (fs.existsSync(errorTemplatePath)) { this.resources.errorTemplate = parseTemplate(fs.readFileSync(errorTemplatePath, 'utf8')) @@ -211,6 +213,8 @@ export default class Renderer extends Tapable { this.useMiddleware(this.nuxtMiddleware.bind(this)) // Error middleware for errors that occurred in middleware that declared above + // Middleware should exactly take 4 arguments + // https://github.com/senchalabs/connect#error-middleware this.useMiddleware(this.errorMiddleware.bind(this)) } @@ -262,28 +266,116 @@ export default class Renderer extends Tapable { res.end(html, 'utf8') return html } catch (err) { - next(this.errorMiddleware(err, req, res, next, context)) + /* istanbul ignore if */ + if (context && context.redirected) { + console.error(err) // eslint-disable-line no-console + return err + } + + next(err) } } - errorMiddleware (err, req, res, next, context) { - /* istanbul ignore if */ - if (context && context.redirected) { - console.error(err) // eslint-disable-line no-console - return err + errorMiddleware (err, req, res, next) { + const sendResponse = html => { + // Set Headers + res.statusCode = 500 + res.setHeader('Content-Type', 'text/html; charset=utf-8') + res.setHeader('Content-Length', Buffer.byteLength(html)) + + // Send Response + res.end(html, 'utf8') } - // Render error template - const html = this.resources.errorTemplate({ - error: err, - stack: ansiHTML(encodeHtml(err.stack)) - }) - // Send response - res.statusCode = 500 - res.setHeader('Content-Type', 'text/html; charset=utf-8') - res.setHeader('Content-Length', Buffer.byteLength(html)) - res.end(html, 'utf8') - return err + // Use basic errors when debug mode is disabled + if (!this.options.render.debug) { + const html = this.resources.errorTemplate({ + code: err.statusCode || 500, + message: err.message || 'Nuxt Server Error' + }) + sendResponse(html) + return + } + + // Show stack trace + err.name = 'Nuxt Server Error' + err.status = 500 + const youch = new Youch(err, req, this.readSource.bind(this)) + youch.toHTML().then(html => { sendResponse(html) }) + } + + async readSource (frame) { + const serverBundle = this.resources.serverBundle + // Initialize smc cache + if (!serverBundle.$maps) { + serverBundle.$maps = {} + } + + // Remove webpack:/// & query string from the end + const sanitizeName = name => name ? name.replace('webpack:///', '').split('?')[0] : '' + + // SourceMap Support for SSR Bundle + if (serverBundle && serverBundle.maps[frame.fileName]) { + // Read SourceMap object + const smc = serverBundle.$maps[frame.fileName] || new SourceMapConsumer(serverBundle.maps[frame.fileName]) + serverBundle.$maps[frame.fileName] = smc + + // Try to find original position + const { line, column, name, source } = smc.originalPositionFor({ + line: frame.getLineNumber() || 0, + column: frame.getColumnNumber() || 0 + }) + if (line) { + frame.lineNumber = line + } + if (column) { + frame.columnNumber = column + } + if (name) { + frame.functionName = name + } + if (source) { + frame.fileName = sanitizeName(source) + + // Source detected, try to get original source code + const contents = smc.sourceContentFor(source) + if (contents) { + frame.contents = contents + return + } + } + } + + // Return if fileName is still unknown + if (!frame.fileName) { + return + } + + frame.fileName = sanitizeName(frame.fileName) + + // Try to read from SSR bundle files + if (serverBundle && serverBundle.files[frame.fileName]) { + frame.contents = serverBundle.files[frame.fileName] + return + } + + // Possible paths for file + const searchPath = [ + this.options.rootDir, + join(this.options.buildDir, 'dist'), + this.options.srcDir, + this.options.buildDir + ] + + // Scan filesystem + for (let pathDir of searchPath) { + let fullPath = resolve(pathDir, frame.fileName) + let source = await fs.readFile(fullPath, 'utf-8').catch(() => null) + if (source) { + frame.contents = source + return + } + } } async renderRoute (url, context = {}) { diff --git a/package.json b/package.json index 579ae89766..397b8bc8a9 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "npm": ">=3.0.0" }, "dependencies": { + "@nuxtjs/youch": "3.0.1", "ansi-html": "^0.0.7", "autoprefixer": "^7.1.2", "babel-core": "^6.25.0", @@ -100,6 +101,7 @@ "serialize-javascript": "^1.4.0", "serve-static": "^1.12.3", "server-destroy": "^1.0.1", + "source-map": "^0.5.6", "source-map-support": "^0.4.15", "tappable": "^1.1.0", "url-loader": "^0.5.9", diff --git a/start/package.json b/start/package.json index 2c14b8eb2c..8ab5d2026d 100644 --- a/start/package.json +++ b/start/package.json @@ -62,6 +62,8 @@ "compression": "^1.7.0", "fs-extra": "^4.0.1", "vue-server-renderer": "~2.4.2", + "@nuxtjs/youch": "3.0.1", + "source-map": "^0.5.6", "connect": "^3.6.3", "server-destroy": "^1.0.1" }, diff --git a/yarn.lock b/yarn.lock index 2701d883da..8775521e88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -44,6 +44,14 @@ dependencies: arrify "^1.0.1" +"@nuxtjs/youch@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@nuxtjs/youch/-/youch-3.0.1.tgz#332bfea84c91c60798cbb693b5622d40ab782fb0" + dependencies: + cookie "^0.3.1" + mustache "^2.3.0" + stack-trace "0.0.10" + "@types/node@^6.0.46": version "6.0.85" resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.85.tgz#ec02bfe54a61044f2be44f13b389c6a0e8ee05ae" @@ -1722,7 +1730,7 @@ cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" -cookie@0.3.1: +cookie@0.3.1, cookie@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" @@ -4117,6 +4125,10 @@ multimatch@^2.1.0: arrify "^1.0.0" minimatch "^3.0.0" +mustache@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.0.tgz#4028f7778b17708a489930a6e52ac3bca0da41d0" + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -5666,6 +5678,10 @@ sshpk@^1.7.0: jsbn "~0.1.0" tweetnacl "~0.14.0" +stack-trace@0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + stack-utils@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620"