From 227391bb969ad258a51ce0d0889705c2a1c59c7f Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 13 Dec 2024 10:55:48 +0530 Subject: [PATCH] feat: add shorthand option for display request props as metadata --- examples/web.ts | 20 ++++++------- package.json | 2 +- src/templates/error_metadata/main.ts | 2 +- src/types.ts | 10 +++++++ src/youch.ts | 42 ++++++++++++++++++++++++++++ tests/youch.spec.ts | 37 ++++++++++++++++++++++++ 6 files changed, 99 insertions(+), 14 deletions(-) diff --git a/examples/web.ts b/examples/web.ts index f097f34..25dbb4b 100644 --- a/examples/web.ts +++ b/examples/web.ts @@ -38,14 +38,6 @@ createServer(async (req, res) => { } catch (error) { const statusCode = error.status ?? 500 const status = HTTP_STATUSES.find((httpStatus) => httpStatus.code === statusCode) - const headers = Object.keys(req.headers).map((key) => { - const value = req.headers[key] - return { - key, - value: key === 'cookie' ? { ...cookie.parse(value as string) } : value, - } - }) - const youch = new Youch() if (error instanceof E_ROUTE_NOT_FOUND) { @@ -59,11 +51,15 @@ createServer(async (req, res) => { }) } - youch.metadata.group('Request', { - headers, + const html = await youch.toHTML(error, { + title: status?.pharse, + cspNonce: 'fooooo', + request: { + url: req.url, + method: req.method, + headers: req.headers, + }, }) - - const html = await youch.toHTML(error, { title: status?.pharse, cspNonce: 'fooooo' }) res.writeHead(statusCode, { 'content-type': 'text/html' }) res.write(html) res.end() diff --git a/package.json b/package.json index 610e458..03e1ef4 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "@types/pg": "^8.11.10", "axios": "^1.7.9", "c8": "^10.1.3", - "cookie": "^1.0.2", "copyfiles": "^2.4.1", "eslint": "^9.16.0", "flydrive": "^1.1.0", @@ -130,6 +129,7 @@ "dependencies": { "@poppinss/dumper": "^0.6.1", "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", "youch-core": "^0.3.0" } } diff --git a/src/templates/error_metadata/main.ts b/src/templates/error_metadata/main.ts index 506fb6c..be26ff5 100644 --- a/src/templates/error_metadata/main.ts +++ b/src/templates/error_metadata/main.ts @@ -63,7 +63,7 @@ export class ErrorMetadata extends BaseComponent { ${ Array.isArray(rows) ? this.#renderRows(rows, cspNonce) - : this.#formatRowValue(rows.value, rows.dump, cspNonce) + : `${this.#formatRowValue(rows.value, rows.dump, cspNonce)}` } ` } diff --git a/src/types.ts b/src/types.ts index c234cc0..7c037a8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -155,6 +155,16 @@ export type YouchHTMLOptions = { * CSP nonce to define on inline style and script tags */ cspNonce?: string + + /** + * Specify the HTTP request properties to be printed as + * metadata under the "Request" group + */ + request?: { + url?: string + method?: string + headers?: Record + } } /** diff --git a/src/youch.ts b/src/youch.ts index bc001a5..29129f0 100644 --- a/src/youch.ts +++ b/src/youch.ts @@ -7,6 +7,7 @@ * file that was distributed with this source code. */ +import cookie from 'cookie' import { ErrorParser } from 'youch-core' import type { Parser, SourceLoader, Transformer, YouchParserOptions } from 'youch-core/types' @@ -51,6 +52,45 @@ export class Youch { return errorParser } + /** + * Defines the request properties as a metadata group + */ + #defineRequestMetadataGroup(request: YouchHTMLOptions['request']) { + if (!request || Object.keys(request).length === 0) { + return + } + + this.metadata.group('Request', { + ...(request.url + ? { + url: { + key: 'URL', + value: request.url, + }, + } + : {}), + ...(request.method + ? { + method: { + key: 'Method', + value: request.method, + }, + } + : {}), + ...(request.headers + ? { + headers: Object.keys(request.headers).map((key) => { + const value = request.headers![key] + return { + key, + value: key === 'cookie' ? { ...cookie.parse(value as string) } : value, + } + }), + } + : {}), + }) + } + /** * Define custom implementation for loading the source code * of a stack frame. @@ -94,6 +134,8 @@ export class Youch { async toHTML(error: unknown, options?: YouchHTMLOptions) { options = { ...options } + this.#defineRequestMetadataGroup(options.request) + const parsedError = await this.#createErrorParser({ offset: options.offset }).parse(error) return this.templates.toHTML({ title: options.title ?? 'An error has occurred', diff --git a/tests/youch.spec.ts b/tests/youch.spec.ts index 5deeaf2..cbedca1 100644 --- a/tests/youch.spec.ts +++ b/tests/youch.spec.ts @@ -7,6 +7,7 @@ * file that was distributed with this source code. */ +import { JSDOM } from 'jsdom' import { test } from '@japa/runner' import { Youch } from '../src/youch.js' import { stripAnsi } from '../src/helpers.js' @@ -51,4 +52,40 @@ test.group('Youch', () => { const output = await new Youch().toHTML(new Error('Something went wrong')) assert.include(output, 'at Object.executor (tests/youch.spec.ts:51:45)') }) + + test('display request URL, method and headers as metadata', async ({ assert }) => { + const html = await new Youch().toHTML(new Error('Something went wrong'), { + request: { + url: '/', + method: 'GET', + headers: { + host: 'localhost:3000', + }, + }, + }) + + const { window } = new JSDOM(html) + const sections: { title: string; contents: string }[] = [] + window.document.querySelectorAll('.card-subtitle').forEach((node) => { + sections.push({ + title: node.textContent!, + contents: node.nextElementSibling!.textContent!.replace(/[\s]+/g, ''), + }) + }) + + assert.deepEqual(sections, [ + { + contents: '/', + title: 'url', + }, + { + contents: 'GET', + title: 'method', + }, + { + contents: `hostlocalhost:3000`, + title: 'headers', + }, + ]) + }) })