mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-27 16:12:12 +00:00
refactor: move renderer middleware into seperate modules
This commit is contained in:
parent
4354dee243
commit
e54beeee1e
136
lib/core/middleware/error.js
Normal file
136
lib/core/middleware/error.js
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
const Youch = require('@nuxtjs/youch')
|
||||||
|
const { SourceMapConsumer } = require('source-map')
|
||||||
|
const { join, resolve } = require('path')
|
||||||
|
const { readFile } = require('fs-extra')
|
||||||
|
|
||||||
|
module.exports = function errorMiddleware(err, req, res, next) {
|
||||||
|
// ensure statusCode, message and name fields
|
||||||
|
err.statusCode = err.statusCode || 500
|
||||||
|
err.message = err.message || 'Nuxt Server Error'
|
||||||
|
err.name = (!err.name || err.name === 'Error') ? 'NuxtServerError' : err.name
|
||||||
|
|
||||||
|
// We hide actual errors from end users, so show them on server logs
|
||||||
|
if (err.statusCode !== 404) {
|
||||||
|
console.error(err) // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendResponse = (content, type = 'text/html') => {
|
||||||
|
// Set Headers
|
||||||
|
res.statusCode = err.statusCode
|
||||||
|
res.statusMessage = err.name
|
||||||
|
res.setHeader('Content-Type', type + '; charset=utf-8')
|
||||||
|
res.setHeader('Content-Length', Buffer.byteLength(content))
|
||||||
|
|
||||||
|
// Send Response
|
||||||
|
res.end(content, 'utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if request accepts JSON
|
||||||
|
const hasReqHeader = (header, includes) => req.headers[header] && req.headers[header].toLowerCase().includes(includes)
|
||||||
|
const isJson = hasReqHeader('accept', 'application/json') || hasReqHeader('user-agent', 'curl/')
|
||||||
|
|
||||||
|
// Use basic errors when debug mode is disabled
|
||||||
|
if (!this.options.debug) {
|
||||||
|
// Json format is compatible with Youch json responses
|
||||||
|
const json = {
|
||||||
|
status: err.statusCode,
|
||||||
|
message: err.message,
|
||||||
|
name: err.name
|
||||||
|
}
|
||||||
|
if (isJson) {
|
||||||
|
sendResponse(JSON.stringify(json, undefined, 2), 'text/json')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const html = this.resources.errorTemplate(json)
|
||||||
|
sendResponse(html)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show stack trace
|
||||||
|
const youch = new Youch(err, req, readSource.bind(this))
|
||||||
|
if (isJson) {
|
||||||
|
youch.toJSON().then(json => { sendResponse(JSON.stringify(json, undefined, 2), 'text/json') })
|
||||||
|
} else {
|
||||||
|
youch.toHTML().then(html => { sendResponse(html) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readSource(frame) {
|
||||||
|
const serverBundle = this.resources.serverBundle
|
||||||
|
|
||||||
|
// 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]) {
|
||||||
|
// Initialize smc cache
|
||||||
|
if (!serverBundle.$maps) {
|
||||||
|
serverBundle.$maps = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
bias: SourceMapConsumer.LEAST_UPPER_BOUND
|
||||||
|
})
|
||||||
|
if (line) {
|
||||||
|
frame.lineNumber = line
|
||||||
|
}
|
||||||
|
/* istanbul ignore if */
|
||||||
|
if (column) {
|
||||||
|
frame.columnNumber = column
|
||||||
|
}
|
||||||
|
/* istanbul ignore if */
|
||||||
|
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 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 real path
|
||||||
|
for (let pathDir of searchPath) {
|
||||||
|
let fullPath = resolve(pathDir, frame.fileName)
|
||||||
|
let source = await readFile(fullPath, 'utf-8').catch(() => null)
|
||||||
|
if (source) {
|
||||||
|
if (!frame.contents) {
|
||||||
|
frame.contents = source
|
||||||
|
}
|
||||||
|
frame.fullPath = fullPath
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
lib/core/middleware/nuxt.js
Normal file
66
lib/core/middleware/nuxt.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
const generateETag = require('etag')
|
||||||
|
const fresh = require('fresh')
|
||||||
|
|
||||||
|
const { getContext } = require('../common/utils')
|
||||||
|
|
||||||
|
module.exports = async function nuxtMiddleware(req, res, next) {
|
||||||
|
// Get context
|
||||||
|
const context = getContext(req, res)
|
||||||
|
|
||||||
|
res.statusCode = 200
|
||||||
|
try {
|
||||||
|
const result = await this.renderRoute(req.url, context)
|
||||||
|
await this.nuxt.callHook('render:route', req.url, result)
|
||||||
|
const { html, error, redirected, resourceHints } = result
|
||||||
|
|
||||||
|
if (redirected) {
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
res.statusCode = context.nuxt.error.statusCode || 500
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add ETag header
|
||||||
|
if (!error && this.options.render.etag) {
|
||||||
|
const etag = generateETag(html, this.options.render.etag)
|
||||||
|
if (fresh(req.headers, { etag })) {
|
||||||
|
res.statusCode = 304
|
||||||
|
res.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.setHeader('ETag', etag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP2 push headers
|
||||||
|
if (!error && this.options.render.http2.push) {
|
||||||
|
// Parse resourceHints to extract HTTP.2 prefetch/push headers
|
||||||
|
// https://w3c.github.io/preload/#server-push-http-2
|
||||||
|
const regex = /link rel="([^"]*)" href="([^"]*)" as="([^"]*)"/g
|
||||||
|
const pushAssets = []
|
||||||
|
let m
|
||||||
|
while (m = regex.exec(resourceHints)) { // eslint-disable-line no-cond-assign
|
||||||
|
const [, rel, href, as] = m
|
||||||
|
if (rel === 'preload') {
|
||||||
|
pushAssets.push(`<${href}>; rel=${rel}; as=${as}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Pass with single Link header
|
||||||
|
// https://blog.cloudflare.com/http-2-server-push-with-multiple-assets-per-link-header
|
||||||
|
res.setHeader('Link', pushAssets.join(','))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send response
|
||||||
|
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||||||
|
res.setHeader('Content-Length', Buffer.byteLength(html))
|
||||||
|
res.end(html, 'utf8')
|
||||||
|
return html
|
||||||
|
} catch (err) {
|
||||||
|
/* istanbul ignore if */
|
||||||
|
if (context && context.redirected) {
|
||||||
|
console.error(err) // eslint-disable-line no-console
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
next(err)
|
||||||
|
}
|
||||||
|
}
|
23
lib/core/middleware/open-in-editor.js
Normal file
23
lib/core/middleware/open-in-editor.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
const openInEditor = require('open-in-editor')
|
||||||
|
|
||||||
|
module.exports = function openInEditorMiddleware(req, res) {
|
||||||
|
// Lazy load open-in-editor
|
||||||
|
const editor = openInEditor.configure(this.options.editor)
|
||||||
|
|
||||||
|
// Parse Query
|
||||||
|
const query = req.url.split('?')[1].split('&').reduce((q, part) => {
|
||||||
|
const s = part.split('=')
|
||||||
|
q[s[0]] = decodeURIComponent(s[1])
|
||||||
|
return q
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('[open in editor]', query.file)
|
||||||
|
|
||||||
|
editor.open(query.file).then(() => {
|
||||||
|
res.end('opened in editor!')
|
||||||
|
}).catch(err => {
|
||||||
|
res.end(err)
|
||||||
|
})
|
||||||
|
}
|
@ -1,21 +1,23 @@
|
|||||||
const ansiHTML = require('ansi-html')
|
const ansiHTML = require('ansi-html')
|
||||||
const serialize = require('serialize-javascript')
|
const serialize = require('serialize-javascript')
|
||||||
const generateETag = require('etag')
|
|
||||||
const fresh = require('fresh')
|
|
||||||
const serveStatic = require('serve-static')
|
const serveStatic = require('serve-static')
|
||||||
const compression = require('compression')
|
const compression = require('compression')
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
const { join, resolve } = require('path')
|
const { join, resolve } = require('path')
|
||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
const { createBundleRenderer } = require('vue-server-renderer')
|
const { createBundleRenderer } = require('vue-server-renderer')
|
||||||
const { getContext, setAnsiColors, isUrl, waitFor } = require('../common/utils')
|
const { setAnsiColors, isUrl, waitFor } = require('../common/utils')
|
||||||
const Debug = require('debug')
|
const Debug = require('debug')
|
||||||
const Youch = require('@nuxtjs/youch')
|
|
||||||
const { SourceMapConsumer } = require('source-map')
|
|
||||||
const connect = require('connect')
|
const connect = require('connect')
|
||||||
const { Options } = require('../common')
|
const { Options } = require('../common')
|
||||||
const MetaRenderer = require('./meta')
|
const MetaRenderer = require('./meta')
|
||||||
|
|
||||||
|
const errorMiddleware = require('./middleware/error')
|
||||||
|
const nuxtMiddleware = require('./middleware/nuxt')
|
||||||
|
const openInEditorMiddleware = require('./middleware/open-in-editor')
|
||||||
|
|
||||||
const debug = Debug('nuxt:render')
|
const debug = Debug('nuxt:render')
|
||||||
debug.color = 4 // Force blue color
|
debug.color = 4 // Force blue color
|
||||||
|
|
||||||
@ -223,28 +225,10 @@ module.exports = class Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// open in editor for debug mode only
|
// open in editor for debug mode only
|
||||||
const _this = this
|
|
||||||
if (this.options.debug && this.options.dev) {
|
if (this.options.debug && this.options.dev) {
|
||||||
this.useMiddleware({
|
this.useMiddleware({
|
||||||
path: '__open-in-editor',
|
path: '__open-in-editor',
|
||||||
handler(req, res) {
|
handler: openInEditorMiddleware.bind(this)
|
||||||
// Lazy load open-in-editor
|
|
||||||
const openInEditor = require('open-in-editor')
|
|
||||||
const editor = openInEditor.configure(_this.options.editor)
|
|
||||||
// Parse Query
|
|
||||||
const query = req.url.split('?')[1].split('&').reduce((q, part) => {
|
|
||||||
const s = part.split('=')
|
|
||||||
q[s[0]] = decodeURIComponent(s[1])
|
|
||||||
return q
|
|
||||||
}, {})
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('[open in editor]', query.file)
|
|
||||||
editor.open(query.file).then(() => {
|
|
||||||
res.end('opened in editor!')
|
|
||||||
}).catch(err => {
|
|
||||||
res.end(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,206 +254,12 @@ module.exports = class Renderer {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Finally use nuxtMiddleware
|
// Finally use nuxtMiddleware
|
||||||
this.useMiddleware(this.nuxtMiddleware.bind(this))
|
this.useMiddleware(nuxtMiddleware.bind(this))
|
||||||
|
|
||||||
// Error middleware for errors that occurred in middleware that declared above
|
// Error middleware for errors that occurred in middleware that declared above
|
||||||
// Middleware should exactly take 4 arguments
|
// Middleware should exactly take 4 arguments
|
||||||
// https://github.com/senchalabs/connect#error-middleware
|
// https://github.com/senchalabs/connect#error-middleware
|
||||||
this.useMiddleware(this.errorMiddleware.bind(this))
|
this.useMiddleware(errorMiddleware.bind(this))
|
||||||
}
|
|
||||||
|
|
||||||
async nuxtMiddleware(req, res, next) {
|
|
||||||
// Get context
|
|
||||||
const context = getContext(req, res)
|
|
||||||
|
|
||||||
res.statusCode = 200
|
|
||||||
try {
|
|
||||||
const result = await this.renderRoute(req.url, context)
|
|
||||||
await this.nuxt.callHook('render:route', req.url, result)
|
|
||||||
const { html, error, redirected, resourceHints } = result
|
|
||||||
|
|
||||||
if (redirected) {
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
res.statusCode = context.nuxt.error.statusCode || 500
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add ETag header
|
|
||||||
if (!error && this.options.render.etag) {
|
|
||||||
const etag = generateETag(html, this.options.render.etag)
|
|
||||||
if (fresh(req.headers, { etag })) {
|
|
||||||
res.statusCode = 304
|
|
||||||
res.end()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
res.setHeader('ETag', etag)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP2 push headers
|
|
||||||
if (!error && this.options.render.http2.push) {
|
|
||||||
// Parse resourceHints to extract HTTP.2 prefetch/push headers
|
|
||||||
// https://w3c.github.io/preload/#server-push-http-2
|
|
||||||
const regex = /link rel="([^"]*)" href="([^"]*)" as="([^"]*)"/g
|
|
||||||
const pushAssets = []
|
|
||||||
let m
|
|
||||||
while (m = regex.exec(resourceHints)) { // eslint-disable-line no-cond-assign
|
|
||||||
const [, rel, href, as] = m
|
|
||||||
if (rel === 'preload') {
|
|
||||||
pushAssets.push(`<${href}>; rel=${rel}; as=${as}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Pass with single Link header
|
|
||||||
// https://blog.cloudflare.com/http-2-server-push-with-multiple-assets-per-link-header
|
|
||||||
res.setHeader('Link', pushAssets.join(','))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send response
|
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
|
||||||
res.setHeader('Content-Length', Buffer.byteLength(html))
|
|
||||||
res.end(html, 'utf8')
|
|
||||||
return html
|
|
||||||
} catch (err) {
|
|
||||||
/* istanbul ignore if */
|
|
||||||
if (context && context.redirected) {
|
|
||||||
console.error(err) // eslint-disable-line no-console
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
next(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
errorMiddleware(err, req, res, next) {
|
|
||||||
// ensure statusCode, message and name fields
|
|
||||||
err.statusCode = err.statusCode || 500
|
|
||||||
err.message = err.message || 'Nuxt Server Error'
|
|
||||||
err.name = (!err.name || err.name === 'Error') ? 'NuxtServerError' : err.name
|
|
||||||
|
|
||||||
// We hide actual errors from end users, so show them on server logs
|
|
||||||
if (err.statusCode !== 404) {
|
|
||||||
console.error(err) // eslint-disable-line no-console
|
|
||||||
}
|
|
||||||
|
|
||||||
const sendResponse = (content, type = 'text/html') => {
|
|
||||||
// Set Headers
|
|
||||||
res.statusCode = err.statusCode
|
|
||||||
res.statusMessage = err.name
|
|
||||||
res.setHeader('Content-Type', type + '; charset=utf-8')
|
|
||||||
res.setHeader('Content-Length', Buffer.byteLength(content))
|
|
||||||
|
|
||||||
// Send Response
|
|
||||||
res.end(content, 'utf-8')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if request accepts JSON
|
|
||||||
const hasReqHeader = (header, includes) => req.headers[header] && req.headers[header].toLowerCase().includes(includes)
|
|
||||||
const isJson = hasReqHeader('accept', 'application/json') || hasReqHeader('user-agent', 'curl/')
|
|
||||||
|
|
||||||
// Use basic errors when debug mode is disabled
|
|
||||||
if (!this.options.debug) {
|
|
||||||
// Json format is compatible with Youch json responses
|
|
||||||
const json = {
|
|
||||||
status: err.statusCode,
|
|
||||||
message: err.message,
|
|
||||||
name: err.name
|
|
||||||
}
|
|
||||||
if (isJson) {
|
|
||||||
sendResponse(JSON.stringify(json, undefined, 2), 'text/json')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const html = this.resources.errorTemplate(json)
|
|
||||||
sendResponse(html)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show stack trace
|
|
||||||
const youch = new Youch(err, req, this.readSource.bind(this))
|
|
||||||
if (isJson) {
|
|
||||||
youch.toJSON().then(json => { sendResponse(JSON.stringify(json, undefined, 2), 'text/json') })
|
|
||||||
} else {
|
|
||||||
youch.toHTML().then(html => { sendResponse(html) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async readSource(frame) {
|
|
||||||
const serverBundle = this.resources.serverBundle
|
|
||||||
|
|
||||||
// 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]) {
|
|
||||||
// Initialize smc cache
|
|
||||||
if (!serverBundle.$maps) {
|
|
||||||
serverBundle.$maps = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
bias: SourceMapConsumer.LEAST_UPPER_BOUND
|
|
||||||
})
|
|
||||||
if (line) {
|
|
||||||
frame.lineNumber = line
|
|
||||||
}
|
|
||||||
/* istanbul ignore if */
|
|
||||||
if (column) {
|
|
||||||
frame.columnNumber = column
|
|
||||||
}
|
|
||||||
/* istanbul ignore if */
|
|
||||||
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 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 real path
|
|
||||||
for (let pathDir of searchPath) {
|
|
||||||
let fullPath = resolve(pathDir, frame.fileName)
|
|
||||||
let source = await fs.readFile(fullPath, 'utf-8').catch(() => null)
|
|
||||||
if (source) {
|
|
||||||
if (!frame.contents) {
|
|
||||||
frame.contents = source
|
|
||||||
}
|
|
||||||
frame.fullPath = fullPath
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderRoute(url, context = {}) {
|
async renderRoute(url, context = {}) {
|
||||||
|
Loading…
Reference in New Issue
Block a user