feat: improve production debugging dx (#7463)

This commit is contained in:
pooya parsa 2020-06-09 21:45:52 +02:00 committed by GitHub
parent 2abbb9957d
commit 796282ceec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 95 additions and 148 deletions

View File

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

View File

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

View 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', () => {

View File

@ -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')
}
}

View File

@ -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()
})
})

View File

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

View File

@ -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'
)
})