From a9555d2bdbf314044b39da0f3faece7bdc8de85c Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sun, 17 Nov 2024 06:42:10 +0530 Subject: [PATCH] feat: add cheviron to expandable frames and support for csp nonce --- README.md | 11 ++++++---- example/index.ts | 2 +- src/public/error_stack/style.css | 31 +++++++++++++++++++++++++++ src/templates.ts | 30 +++++++++++++++++++------- src/templates/error_cause/main.ts | 5 ++++- src/templates/error_metadata/main.ts | 25 ++++++++++++++-------- src/templates/error_stack/main.ts | 32 ++++++++++++++++++++++------ src/templates/header/main.ts | 3 ++- src/types.ts | 27 +++++++++++++++++------ src/youch.ts | 1 + 10 files changed, 129 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 2930e26..b3bb054 100644 --- a/README.md +++ b/README.md @@ -58,15 +58,18 @@ Once installed. You can render errors to HTML output using the `youch.render` me In the following example, we use the `hono` framework and pretty print all the errors in development using Youch. You can replace Hono with any other framework of your choice. ```ts +import { Hono } from 'hono' import { Youch } from 'youch' + +const app = new Hono() const IN_DEV = process.env.NODE_ENV === 'development' -app.onError((err, c) => { +app.onError(async (error, c) => { if (IN_DEV) { - const youch = new Youch({ title: 'Something went wrong' }) - const html = await youch.render(err) - return html + const html = await youch.render(error) + return c.html(html) } + return c.text(error.message) }) ``` diff --git a/example/index.ts b/example/index.ts index c4da0e6..be3b48e 100644 --- a/example/index.ts +++ b/example/index.ts @@ -37,7 +37,7 @@ createServer(async (req, res) => { } }) - const youch = new Youch({ title: status?.pharse }) + const youch = new Youch({ title: status?.pharse, cspNonce: 'fooooo' }) if (error instanceof E_ROUTE_NOT_FOUND) { youch.metadata.group('Application', { diff --git a/src/public/error_stack/style.css b/src/public/error_stack/style.css index f28da97..8fb8736 100644 --- a/src/public/error_stack/style.css +++ b/src/public/error_stack/style.css @@ -81,6 +81,37 @@ html.dark { font-family: var(--font-sans); } +.stack-frame-extras { + display: flex; + gap: 6px; + align-items: center; +} + +.stack-frame-toggle-indicator { + border: none; + border-radius: var(--radius); + height: 22px; + width: 22px; + display: flex; + justify-content: center; + align-items: center; + background: none; + color: inherit; + font: inherit; +} +.stack-frame-toggle-indicator:hover { + border: 1px solid var(--switch-border); +} + +.stack-frame-toggle-indicator svg { + width: 16px; + display: block; +} + +.stack-frame.expanded .stack-frame-toggle-indicator svg { + transform: rotate(180deg); +} + .frame-label { padding: 0px 8px; border-radius: 20px; diff --git a/src/templates.ts b/src/templates.ts index f39964b..475ff6c 100644 --- a/src/templates.ts +++ b/src/templates.ts @@ -65,7 +65,7 @@ export class Templates { * Returns a collection of style and script tags to dump * inside the document HEAD. */ - #getStylesAndScripts() { + #getStylesAndScripts(cspNonce?: string) { /** * Keeping injected styles separate from the rest of the * styles and scripts, so that we can append them at @@ -74,16 +74,17 @@ export class Templates { let injectedStyles: string = '' const styles: string[] = [] const scripts: string[] = [] + const cspNonceAttr = cspNonce ? ` nonce="${cspNonce}"` : '' this.#styles.forEach((bucket, name) => { if (name === 'injected') { - injectedStyles = `` + injectedStyles = `` } else { - styles.push(``) + styles.push(``) } }) this.#scripts.forEach((bucket, name) => { - scripts.push(``) + scripts.push(``) }) return { styles: `${styles.join('\n')}\n${injectedStyles}`, scripts: scripts.join('\n') } @@ -156,16 +157,29 @@ export class Templates { /** * Returns the HTML output for the given parsed error */ - async render(props: { title: string; ide?: string; error: ParsedError; metadata: Metadata }) { + async render(props: { + title: string + ide?: string + cspNonce?: string + error: ParsedError + metadata: Metadata + }) { const html = await this.#renderTmpl('layout', { title: props.title, + ide: props.ide, + cspNonce: props.cspNonce, children: async () => { - const header = await this.#renderTmpl('header', undefined) + const header = await this.#renderTmpl('header', props) const info = await this.#renderTmpl('errorInfo', props) const stackTrace = await this.#renderTmpl('errorStack', { ide: process.env.EDITOR ?? 'vscode', sourceCodeRenderer: (error, frame) => { - return this.#renderTmpl('errorStackSource', { error, frame }) + return this.#renderTmpl('errorStackSource', { + error, + frame, + ide: props.ide, + cspNonce: props.cspNonce, + }) }, ...props, }) @@ -175,7 +189,7 @@ export class Templates { }, }) - const { scripts, styles } = this.#getStylesAndScripts() + const { scripts, styles } = this.#getStylesAndScripts(props.cspNonce) return html.replace('', styles).replace('', scripts) } } diff --git a/src/templates/error_cause/main.ts b/src/templates/error_cause/main.ts index acd00c7..a340b32 100644 --- a/src/templates/error_cause/main.ts +++ b/src/templates/error_cause/main.ts @@ -35,7 +35,10 @@ export class ErrorCause extends BaseComponent {
- ${dump(props.error.cause, { styles: themes.cssVariables })} + ${dump(props.error.cause, { + cspNonce: props.cspNonce, + styles: themes.cssVariables, + })}
diff --git a/src/templates/error_metadata/main.ts b/src/templates/error_metadata/main.ts index c34a98d..7a90449 100644 --- a/src/templates/error_metadata/main.ts +++ b/src/templates/error_metadata/main.ts @@ -21,9 +21,9 @@ export class ErrorMetadata extends BaseComponent { /** * Formats the error row value */ - #formatRowValue(value: any, dumpValue?: boolean) { + #formatRowValue(value: any, dumpValue?: boolean, cspNonce?: string) { if (dumpValue === true) { - return dump(value, { styles: themes.cssVariables }) + return dump(value, { styles: themes.cssVariables, cspNonce }) } if (this.#primitives.includes(typeof value) || value === null) { @@ -37,7 +37,7 @@ export class ErrorMetadata extends BaseComponent { * Returns HTML fragment with HTML table containing rows * metadata section rows */ - #renderRows(rows: ErrorMetadataRow[]) { + #renderRows(rows: ErrorMetadataRow[], cspNonce?: string) { return ` ${rows @@ -45,7 +45,7 @@ export class ErrorMetadata extends BaseComponent { return `` }) @@ -57,10 +57,14 @@ export class ErrorMetadata extends BaseComponent { /** * Renders each section with its rows inside a table */ - #renderSection(section: string, rows: ErrorMetadataRow | ErrorMetadataRow[]) { + #renderSection(section: string, rows: ErrorMetadataRow | ErrorMetadataRow[], cspNonce?: string) { return `

${section}

- ${Array.isArray(rows) ? this.#renderRows(rows) : this.#formatRowValue(rows.value, rows.dump)} + ${ + Array.isArray(rows) + ? this.#renderRows(rows, cspNonce) + : this.#formatRowValue(rows.value, rows.dump, cspNonce) + }
` } @@ -69,7 +73,8 @@ export class ErrorMetadata extends BaseComponent { */ #renderGroup( group: string, - sections: { [section: string]: ErrorMetadataRow | ErrorMetadataRow[] } + sections: { [section: string]: ErrorMetadataRow | ErrorMetadataRow[] }, + cspNonce?: string ) { return `
@@ -78,7 +83,7 @@ export class ErrorMetadata extends BaseComponent {
${Object.keys(sections) - .map((section) => this.#renderSection(section, sections[section])) + .map((section) => this.#renderSection(section, sections[section], cspNonce)) .join('\n')}
@@ -96,6 +101,8 @@ export class ErrorMetadata extends BaseComponent { return '' } - return groupsNames.map((group) => this.#renderGroup(group, groups[group])).join('\n') + return groupsNames + .map((group) => this.#renderGroup(group, groups[group], props.cspNonce)) + .join('\n') } } diff --git a/src/templates/error_stack/main.ts b/src/templates/error_stack/main.ts index 9e0b743..c435731 100644 --- a/src/templates/error_stack/main.ts +++ b/src/templates/error_stack/main.ts @@ -14,6 +14,10 @@ import { BaseComponent } from '../../component.js' import { publicDirURL } from '../../public_dir.js' import type { ErrorStackProps } from '../../types.js' +const CHEVIRON = ` + +` + /** * Known editors and their URLs to open the file within * the code editor @@ -108,14 +112,15 @@ export class ErrorStack extends BaseComponent { : '' const loc = `at line ${frame.lineNumber}:${frame.columnNumber}` - if (frame.type === 'native') { - return `
+ if (frame.type !== 'native' && frame.source) { + return `
` + ` } - return `` + ` } /** @@ -130,11 +135,20 @@ export class ErrorStack extends BaseComponent { const id = `frame-${index + 1}` const label = frame.type === 'app' ? 'In App' : '' const expandedClass = expandAtIndex === index ? 'expanded' : '' + const toggleButton = + frame.type !== 'native' && frame.source + ? `` + : '' return `
  • ${this.#renderFrameLocation(frame, id, props.ide)} -
    ${label}
    +
    + ${label} + ${toggleButton} +
    ${await props.sourceCodeRenderer(props.error, frame)} @@ -176,7 +190,11 @@ export class ErrorStack extends BaseComponent {
    - ${dump(props.error.raw, { styles: themes.cssVariables, expand: true })} + ${dump(props.error.raw, { + styles: themes.cssVariables, + expand: true, + cspNonce: props.cspNonce, + })}
    diff --git a/src/templates/header/main.ts b/src/templates/header/main.ts index 6e38733..4b56d3e 100644 --- a/src/templates/header/main.ts +++ b/src/templates/header/main.ts @@ -9,6 +9,7 @@ import { BaseComponent } from '../../component.js' import { publicDirURL } from '../../public_dir.js' +import type { ComponentSharedProps } from '../../types.js' const DARK_MODE_SVG = `` @@ -18,7 +19,7 @@ const LIGHT_MODE_SVG = `
  • ${row.key} - ${this.#formatRowValue(row.value, row.dump)} + ${this.#formatRowValue(row.value, row.dump, cspNonce)}