mirror of
https://github.com/nuxt/nuxt.git
synced 2025-02-17 06:01:34 +00:00
feat: improve production debugging dx (#7463)
This commit is contained in:
parent
2abbb9957d
commit
796282ceec
@ -567,7 +567,7 @@ exports[`webpack nuxt webpack module.rules 1`] = `
|
||||
\\"loader\\": \\"url-loader\\",
|
||||
\\"options\\": Object {
|
||||
\\"limit\\": 1000,
|
||||
\\"name\\": \\"img/[contenthash:7].[ext]\\",
|
||||
\\"name\\": \\"img/[name].[contenthash:7].[ext]\\",
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -579,7 +579,7 @@ exports[`webpack nuxt webpack module.rules 1`] = `
|
||||
\\"loader\\": \\"url-loader\\",
|
||||
\\"options\\": Object {
|
||||
\\"limit\\": 1000,
|
||||
\\"name\\": \\"fonts/[contenthash:7].[ext]\\",
|
||||
\\"name\\": \\"fonts/[name].[contenthash:7].[ext]\\",
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -590,7 +590,7 @@ exports[`webpack nuxt webpack module.rules 1`] = `
|
||||
Object {
|
||||
\\"loader\\": \\"file-loader\\",
|
||||
\\"options\\": Object {
|
||||
\\"name\\": \\"videos/[contenthash:7].[ext]\\",
|
||||
\\"name\\": \\"videos/[name].[contenthash:7].[ext]\\",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -14,12 +14,12 @@ export default () => ({
|
||||
serverURLPolyfill: 'url',
|
||||
filenames: {
|
||||
// { isDev, isClient, isServer }
|
||||
app: ({ isDev, isModern }) => isDev ? `${isModern ? 'modern-' : ''}[name].js` : '[contenthash].js',
|
||||
chunk: ({ isDev, isModern }) => isDev ? `${isModern ? 'modern-' : ''}[name].js` : '[contenthash].js',
|
||||
css: ({ isDev }) => isDev ? '[name].css' : '[contenthash].css',
|
||||
img: ({ isDev }) => isDev ? '[path][name].[ext]' : 'img/[contenthash:7].[ext]',
|
||||
font: ({ isDev }) => isDev ? '[path][name].[ext]' : 'fonts/[contenthash:7].[ext]',
|
||||
video: ({ isDev }) => isDev ? '[path][name].[ext]' : 'videos/[contenthash:7].[ext]'
|
||||
app: ({ isDev, isModern }) => isDev ? `${isModern ? 'modern-' : ''}[name].js` : '[name].[contenthash:7].js',
|
||||
chunk: ({ isDev, isModern }) => isDev ? `${isModern ? 'modern-' : ''}[name].js` : '[name].[contenthash:7].js',
|
||||
css: ({ isDev }) => isDev ? '[name].css' : '[name].[contenthash:7].css',
|
||||
img: ({ isDev }) => isDev ? '[path][name].[ext]' : 'img/[name].[contenthash:7].[ext]',
|
||||
font: ({ isDev }) => isDev ? '[path][name].[ext]' : 'fonts/[name].[contenthash:7].[ext]',
|
||||
video: ({ isDev }) => isDev ? '[path][name].[ext]' : 'videos/[name].[contenthash:7].[ext]'
|
||||
},
|
||||
loaders: {
|
||||
file: {},
|
||||
|
@ -15,12 +15,12 @@ describe('config: build', () => {
|
||||
test('should return prod filenames', () => {
|
||||
const { filenames } = buildConfig()
|
||||
const env = { isDev: false }
|
||||
expect(filenames.app(env)).toEqual('[contenthash].js')
|
||||
expect(filenames.chunk(env)).toEqual('[contenthash].js')
|
||||
expect(filenames.css(env)).toEqual('[contenthash].css')
|
||||
expect(filenames.img(env)).toEqual('img/[contenthash:7].[ext]')
|
||||
expect(filenames.font(env)).toEqual('fonts/[contenthash:7].[ext]')
|
||||
expect(filenames.video(env)).toEqual('videos/[contenthash:7].[ext]')
|
||||
expect(filenames.app(env)).toEqual('[name].[contenthash:7].js')
|
||||
expect(filenames.chunk(env)).toEqual('[name].[contenthash:7].js')
|
||||
expect(filenames.css(env)).toEqual('[name].[contenthash:7].css')
|
||||
expect(filenames.img(env)).toEqual('img/[name].[contenthash:7].[ext]')
|
||||
expect(filenames.font(env)).toEqual('fonts/[name].[contenthash:7].[ext]')
|
||||
expect(filenames.video(env)).toEqual('videos/[name].[contenthash:7].[ext]')
|
||||
})
|
||||
|
||||
test('should return modern filenames', () => {
|
||||
|
@ -4,20 +4,14 @@ import consola from 'consola'
|
||||
|
||||
import Youch from '@nuxtjs/youch'
|
||||
|
||||
export default ({ resources, options }) => async function errorMiddleware (err, req, res, next) {
|
||||
// ensure statusCode, message and name fields
|
||||
|
||||
const error = {
|
||||
statusCode: err.statusCode || 500,
|
||||
message: err.message || 'Nuxt Server Error',
|
||||
name: !err.name || err.name === 'Error' ? 'NuxtServerError' : err.name,
|
||||
headers: err.headers
|
||||
}
|
||||
export default ({ resources, options }) => async function errorMiddleware (_error, req, res, next) {
|
||||
// Normalize error
|
||||
const error = normalizeError(_error, options)
|
||||
|
||||
const sendResponse = (content, type = 'text/html') => {
|
||||
// Set Headers
|
||||
res.statusCode = error.statusCode
|
||||
res.statusMessage = error.name
|
||||
res.statusMessage = 'RuntimeError'
|
||||
res.setHeader('Content-Type', type + '; charset=utf-8')
|
||||
res.setHeader('Content-Length', Buffer.byteLength(content))
|
||||
res.setHeader('Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate')
|
||||
@ -43,9 +37,10 @@ export default ({ resources, options }) => async function errorMiddleware (err,
|
||||
// Use basic errors when debug mode is disabled
|
||||
if (!options.debug) {
|
||||
// We hide actual errors from end users, so show them on server logs
|
||||
if (err.statusCode !== 404) {
|
||||
consola.error(err)
|
||||
if (error.statusCode !== 404) {
|
||||
consola.error(error)
|
||||
}
|
||||
|
||||
// Json format is compatible with Youch json responses
|
||||
const json = {
|
||||
status: error.statusCode,
|
||||
@ -61,25 +56,11 @@ export default ({ resources, options }) => async function errorMiddleware (err,
|
||||
return
|
||||
}
|
||||
|
||||
const errorFull = err instanceof Error
|
||||
? err
|
||||
: typeof err === 'string'
|
||||
? new Error(err)
|
||||
: new Error(err.message || JSON.stringify(err))
|
||||
|
||||
errorFull.name = error.name
|
||||
errorFull.statusCode = error.statusCode
|
||||
errorFull.stack = err.stack || undefined
|
||||
|
||||
// Show stack trace
|
||||
const youch = new Youch(
|
||||
errorFull,
|
||||
error,
|
||||
req,
|
||||
readSourceFactory({
|
||||
srcDir: options.srcDir,
|
||||
rootDir: options.rootDir,
|
||||
buildDir: options.buildDir
|
||||
}),
|
||||
readSource,
|
||||
options.router.base,
|
||||
true
|
||||
)
|
||||
@ -93,17 +74,21 @@ export default ({ resources, options }) => async function errorMiddleware (err,
|
||||
sendResponse(html)
|
||||
}
|
||||
|
||||
const readSourceFactory = ({ srcDir, rootDir, buildDir }) => async function readSource (frame) {
|
||||
// Remove webpack:/// & query string from the end
|
||||
const sanitizeName = name => name ? name.replace('webpack:///', '').split('?')[0] : null
|
||||
frame.fileName = sanitizeName(frame.fileName)
|
||||
const sanitizeName = name => name ? name.replace('webpack:///', '').split('?')[0] : null
|
||||
|
||||
// Return if fileName is unknown
|
||||
if (!frame.fileName) {
|
||||
return
|
||||
const normalizeError = (_error, { srcDir, rootDir, buildDir }) => {
|
||||
if (typeof _error === 'string') {
|
||||
_error = { message: _error }
|
||||
} else if (!_error) {
|
||||
_error = { message: '<empty>' }
|
||||
}
|
||||
|
||||
// Possible paths for file
|
||||
const error = new Error()
|
||||
error.message = _error.message
|
||||
error.name = _error.name
|
||||
error.statusCode = _error.statusCode || 500
|
||||
error.headers = _error.headers
|
||||
|
||||
const searchPath = [
|
||||
srcDir,
|
||||
rootDir,
|
||||
@ -112,17 +97,34 @@ const readSourceFactory = ({ srcDir, rootDir, buildDir }) => async function read
|
||||
process.cwd()
|
||||
]
|
||||
|
||||
// Scan filesystem for real source
|
||||
for (const pathDir of searchPath) {
|
||||
const fullPath = path.resolve(pathDir, frame.fileName)
|
||||
const source = await fs.readFile(fullPath, 'utf-8').catch(() => null)
|
||||
if (source) {
|
||||
frame.contents = source
|
||||
frame.fullPath = fullPath
|
||||
if (path.isAbsolute(frame.fileName)) {
|
||||
frame.fileName = path.relative(rootDir, fullPath)
|
||||
const findInPaths = (fileName) => {
|
||||
for (const dir of searchPath) {
|
||||
const fullPath = path.resolve(dir, fileName)
|
||||
if (fs.existsSync(fullPath)) {
|
||||
return fullPath
|
||||
}
|
||||
return
|
||||
}
|
||||
return fileName
|
||||
}
|
||||
|
||||
error.stack = (_error.stack || '')
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
const match = line.match(/\(([^)]+)\)|([^\s]+\.[^\s]+):/)
|
||||
if (!match) {
|
||||
return line
|
||||
}
|
||||
const src = match[1] || match[2] || ''
|
||||
return line.replace(src, findInPaths(sanitizeName(src)))
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
async function readSource (frame) {
|
||||
if (fs.existsSync(frame.fileName)) {
|
||||
frame.fullPath = frame.fileName // Youch BW compat
|
||||
frame.contents = await fs.readFile(frame.fileName, 'utf-8')
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,13 @@ const createServerContext = () => ({
|
||||
next: jest.fn()
|
||||
})
|
||||
|
||||
const errorFileName = 'test-error.js'
|
||||
const createError = () => {
|
||||
const err = new Error('Error!')
|
||||
err.stack = `Error!\n at foo (webpack:///${errorFileName}?foo:1)`
|
||||
return err
|
||||
}
|
||||
|
||||
describe('server: errorMiddleware', () => {
|
||||
beforeAll(() => {
|
||||
path.join.mockImplementation((...args) => `join(${args.join(', ')})`)
|
||||
@ -48,26 +55,21 @@ describe('server: errorMiddleware', () => {
|
||||
test('should send html error response', async () => {
|
||||
const params = createParams()
|
||||
const errorMiddleware = createErrorMiddleware(params)
|
||||
const error = new Error()
|
||||
const error = {}
|
||||
error.headers = { 'Custom-Header': 'test' }
|
||||
const ctx = createServerContext()
|
||||
|
||||
await errorMiddleware(error, ctx.req, ctx.res, ctx.next)
|
||||
|
||||
expect(consola.error).toBeCalledWith(error)
|
||||
expect(ctx.res.statusCode).toEqual(500)
|
||||
expect(ctx.res.statusMessage).toEqual('NuxtServerError')
|
||||
expect(ctx.res.statusMessage).toEqual('RuntimeError')
|
||||
expect(ctx.res.setHeader).toBeCalledTimes(4)
|
||||
expect(ctx.res.setHeader).nthCalledWith(1, 'Content-Type', 'text/html; charset=utf-8')
|
||||
expect(ctx.res.setHeader).nthCalledWith(2, 'Content-Length', Buffer.byteLength('error template'))
|
||||
expect(ctx.res.setHeader).nthCalledWith(3, 'Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate')
|
||||
expect(ctx.res.setHeader).nthCalledWith(4, 'Custom-Header', 'test')
|
||||
expect(params.resources.errorTemplate).toBeCalledTimes(1)
|
||||
expect(params.resources.errorTemplate).toBeCalledWith({
|
||||
status: 500,
|
||||
message: 'Nuxt Server Error',
|
||||
name: 'NuxtServerError'
|
||||
})
|
||||
expect(params.resources.errorTemplate).toBeCalledWith({ status: 500 })
|
||||
expect(ctx.res.end).toBeCalledTimes(1)
|
||||
expect(ctx.res.end).toBeCalledWith('error template', 'utf-8')
|
||||
})
|
||||
@ -77,7 +79,6 @@ describe('server: errorMiddleware', () => {
|
||||
const errorMiddleware = createErrorMiddleware(params)
|
||||
const error = {
|
||||
statusCode: 404,
|
||||
name: 'NuxtTestError',
|
||||
message: 'test error'
|
||||
}
|
||||
const ctx = createServerContext()
|
||||
@ -92,7 +93,6 @@ describe('server: errorMiddleware', () => {
|
||||
}, undefined, 2)
|
||||
expect(consola.error).not.toBeCalled()
|
||||
expect(ctx.res.statusCode).toEqual(404)
|
||||
expect(ctx.res.statusMessage).toEqual(error.name)
|
||||
expect(ctx.res.setHeader).toBeCalledTimes(3)
|
||||
expect(ctx.res.setHeader).nthCalledWith(1, 'Content-Type', 'text/json; charset=utf-8')
|
||||
expect(ctx.res.setHeader).nthCalledWith(2, 'Content-Length', Buffer.byteLength(errJson))
|
||||
@ -108,22 +108,13 @@ describe('server: errorMiddleware', () => {
|
||||
const errorMiddleware = createErrorMiddleware(params)
|
||||
const error = new Error('test error')
|
||||
error.statusCode = 503
|
||||
error.name = 'NuxtTestError'
|
||||
const ctx = createServerContext()
|
||||
|
||||
await errorMiddleware(error, ctx.req, ctx.res, ctx.next)
|
||||
|
||||
const errHtml = 'youch html'
|
||||
expect(Youch).toBeCalledTimes(1)
|
||||
expect(Youch).toBeCalledWith(
|
||||
error,
|
||||
ctx.req,
|
||||
expect.any(Function),
|
||||
params.options.router.base,
|
||||
true
|
||||
)
|
||||
|
||||
expect(ctx.res.statusCode).toEqual(503)
|
||||
expect(ctx.res.statusMessage).toEqual(error.name)
|
||||
expect(ctx.res.setHeader).toBeCalledTimes(3)
|
||||
expect(ctx.res.setHeader).nthCalledWith(1, 'Content-Type', 'text/html; charset=utf-8')
|
||||
expect(ctx.res.setHeader).nthCalledWith(2, 'Content-Length', Buffer.byteLength(errHtml))
|
||||
@ -139,7 +130,6 @@ describe('server: errorMiddleware', () => {
|
||||
const errorMiddleware = createErrorMiddleware(params)
|
||||
const error = {
|
||||
statusCode: 404,
|
||||
name: 'NuxtTestError',
|
||||
message: 'test error'
|
||||
}
|
||||
const ctx = createServerContext()
|
||||
@ -148,20 +138,8 @@ describe('server: errorMiddleware', () => {
|
||||
await errorMiddleware(error, ctx.req, ctx.res, ctx.next)
|
||||
|
||||
const errJson = JSON.stringify('youch json', undefined, 2)
|
||||
const errorFull = new Error(error.message)
|
||||
errorFull.name = error.name
|
||||
errorFull.statusCode = error.statusCode
|
||||
errorFull.stack = undefined
|
||||
expect(Youch).toBeCalledTimes(1)
|
||||
expect(Youch).toBeCalledWith(
|
||||
errorFull,
|
||||
ctx.req,
|
||||
expect.any(Function),
|
||||
params.options.router.base,
|
||||
true
|
||||
)
|
||||
|
||||
expect(ctx.res.statusCode).toEqual(404)
|
||||
expect(ctx.res.statusMessage).toEqual(error.name)
|
||||
expect(ctx.res.setHeader).toBeCalledTimes(3)
|
||||
expect(ctx.res.setHeader).nthCalledWith(1, 'Content-Type', 'text/json; charset=utf-8')
|
||||
expect(ctx.res.setHeader).nthCalledWith(2, 'Content-Length', Buffer.byteLength(errJson))
|
||||
@ -175,24 +153,16 @@ describe('server: errorMiddleware', () => {
|
||||
const params = createParams()
|
||||
params.options.debug = true
|
||||
const errorMiddleware = createErrorMiddleware(params)
|
||||
const error = {}
|
||||
const error = createError()
|
||||
const ctx = createServerContext()
|
||||
|
||||
await errorMiddleware(error, ctx.req, ctx.res, ctx.next)
|
||||
|
||||
const frame = { fileName: 'webpack:///test-error.js?desc=test' }
|
||||
const readSource = Youch.mock.calls[0][2]
|
||||
await readSource(frame)
|
||||
|
||||
const fileName = 'test-error.js'
|
||||
expect(frame).toEqual({ fileName })
|
||||
expect(path.resolve).toBeCalledTimes(5)
|
||||
expect(fs.readFile).toBeCalledTimes(5)
|
||||
expect(fs.readFile).nthCalledWith(1, `resolve(${params.options.srcDir}, ${fileName})`, 'utf-8')
|
||||
expect(fs.readFile).nthCalledWith(2, `resolve(${params.options.rootDir}, ${fileName})`, 'utf-8')
|
||||
expect(fs.readFile).nthCalledWith(3, `resolve(join(${params.options.buildDir}, dist, server), ${fileName})`, 'utf-8')
|
||||
expect(fs.readFile).nthCalledWith(4, `resolve(${params.options.buildDir}, ${fileName})`, 'utf-8')
|
||||
expect(fs.readFile).nthCalledWith(5, `resolve(${process.cwd()}, ${fileName})`, 'utf-8')
|
||||
expect(fs.existsSync).nthCalledWith(1, `resolve(${params.options.srcDir}, ${errorFileName})`)
|
||||
expect(fs.existsSync).nthCalledWith(2, `resolve(${params.options.rootDir}, ${errorFileName})`)
|
||||
expect(fs.existsSync).nthCalledWith(3, `resolve(join(${params.options.buildDir}, dist, server), ${errorFileName})`)
|
||||
expect(fs.existsSync).nthCalledWith(4, `resolve(${params.options.buildDir}, ${errorFileName})`)
|
||||
expect(fs.existsSync).nthCalledWith(5, `resolve(${process.cwd()}, ${errorFileName})`)
|
||||
})
|
||||
|
||||
test('should return source content after read source', async () => {
|
||||
@ -204,20 +174,20 @@ describe('server: errorMiddleware', () => {
|
||||
|
||||
await errorMiddleware(error, ctx.req, ctx.res, ctx.next)
|
||||
|
||||
const frame = { fileName: 'webpack:///test-error.js?desc=test' }
|
||||
const frame = { fileName: errorFileName }
|
||||
const readSource = Youch.mock.calls[0][2]
|
||||
fs.existsSync.mockImplementationOnce(() => true)
|
||||
fs.readFile.mockImplementationOnce(() => Promise.resolve('source content'))
|
||||
await readSource(frame)
|
||||
|
||||
const fileName = 'test-error.js'
|
||||
expect(frame).toEqual({
|
||||
fileName,
|
||||
fileName: errorFileName,
|
||||
contents: 'source content',
|
||||
fullPath: `resolve(${params.options.srcDir}, ${fileName})`
|
||||
fullPath: errorFileName
|
||||
})
|
||||
})
|
||||
|
||||
test('should return relative fileName if fileName is absolute path', async () => {
|
||||
test('should ignore if source file not exists', async () => {
|
||||
const params = createParams()
|
||||
params.options.debug = true
|
||||
const errorMiddleware = createErrorMiddleware(params)
|
||||
@ -226,39 +196,11 @@ describe('server: errorMiddleware', () => {
|
||||
|
||||
await errorMiddleware(error, ctx.req, ctx.res, ctx.next)
|
||||
|
||||
const frame = { fileName: 'webpack:///test-error.js?desc=test' }
|
||||
const frame = { fileName: errorFileName }
|
||||
const readSource = Youch.mock.calls[0][2]
|
||||
fs.readFile.mockImplementationOnce(() => Promise.resolve('source content'))
|
||||
path.isAbsolute.mockReturnValueOnce(true)
|
||||
path.relative.mockImplementationOnce((...args) => `relative(${args.join(', ')})`)
|
||||
fs.exists.mockReturnValueOnce(false)
|
||||
await readSource(frame)
|
||||
|
||||
const fullPath = `resolve(${params.options.srcDir}, test-error.js)`
|
||||
expect(frame).toEqual({
|
||||
fileName: `relative(${params.options.rootDir}, ${fullPath})`,
|
||||
contents: 'source content',
|
||||
fullPath
|
||||
})
|
||||
})
|
||||
|
||||
test('should ignore error when reading source', async () => {
|
||||
const params = createParams()
|
||||
params.options.debug = true
|
||||
const errorMiddleware = createErrorMiddleware(params)
|
||||
const error = {}
|
||||
const ctx = createServerContext()
|
||||
|
||||
await errorMiddleware(error, ctx.req, ctx.res, ctx.next)
|
||||
|
||||
const frame = { fileName: 'webpack:///test-error.js?desc=test' }
|
||||
const readSource = Youch.mock.calls[0][2]
|
||||
fs.readFile.mockReturnValueOnce(Promise.reject(new Error('read failed')))
|
||||
await readSource(frame)
|
||||
|
||||
const fileName = 'test-error.js'
|
||||
expect(frame).toEqual({ fileName })
|
||||
expect(path.resolve).toBeCalledTimes(5)
|
||||
expect(fs.readFile).toBeCalledTimes(5)
|
||||
expect(frame).toEqual({ fileName: errorFileName })
|
||||
})
|
||||
|
||||
test('should return if fileName is unknown when read source', async () => {
|
||||
@ -274,6 +216,6 @@ describe('server: errorMiddleware', () => {
|
||||
const readSource = Youch.mock.calls[0][2]
|
||||
await readSource(frame)
|
||||
|
||||
expect(frame.fileName).toBeNull()
|
||||
expect(frame.fileName).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
@ -56,9 +56,11 @@ export default class WebpackServerConfig extends WebpackBaseConfig {
|
||||
}
|
||||
|
||||
optimization () {
|
||||
const { _minifyServer } = this.buildContext.buildOptions
|
||||
|
||||
return {
|
||||
splitChunks: false,
|
||||
minimizer: this.minimizer()
|
||||
minimizer: _minifyServer ? this.minimizer() : []
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,6 +115,7 @@ export default class WebpackServerConfig extends WebpackBaseConfig {
|
||||
}),
|
||||
output: Object.assign({}, config.output, {
|
||||
filename: 'server.js',
|
||||
chunkFilename: '[name].js',
|
||||
libraryTarget: 'commonjs2'
|
||||
}),
|
||||
performance: {
|
||||
|
@ -48,7 +48,7 @@ describe('with-config', () => {
|
||||
test('/ (preload fonts)', async () => {
|
||||
const { html } = await nuxt.server.renderRoute('/')
|
||||
expect(html).toContain(
|
||||
'<link rel="preload" href="/test/orion/fonts/7cf5d7c.woff2" as="font" type="font/woff2" crossorigin'
|
||||
'<link rel="preload" href="/test/orion/fonts/roboto.7cf5d7c.woff2" as="font" type="font/woff2" crossorigin'
|
||||
)
|
||||
})
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user