better SSR error pages

full stack trace and source maps support
This commit is contained in:
Pooya Parsa 2017-08-05 05:28:26 +04:30
parent 6bcfaf8a3a
commit 67bd208c73
7 changed files with 182 additions and 30 deletions

View File

@ -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) {

File diff suppressed because one or more lines are too long

View File

@ -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
},

View File

@ -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('<pre>{{ stack }}</pre>') // 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 = {}) {

View File

@ -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",

View File

@ -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"
},

View File

@ -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"