Skip to content

Commit

Permalink
feat: add cheviron to expandable frames and support for csp nonce
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Nov 17, 2024
1 parent 98f770b commit a9555d2
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 38 deletions.
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
```

Expand Down
2 changes: 1 addition & 1 deletion example/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down
31 changes: 31 additions & 0 deletions src/public/error_stack/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
30 changes: 22 additions & 8 deletions src/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = `<style id="${name}-styles">${bucket}</style>`
injectedStyles = `<style id="${name}-styles"${cspNonceAttr}>${bucket}</style>`
} else {
styles.push(`<style id="${name}-styles">${bucket}</style>`)
styles.push(`<style id="${name}-styles"${cspNonceAttr}>${bucket}</style>`)
}
})
this.#scripts.forEach((bucket, name) => {
scripts.push(`<script id="${name}-script">${bucket}</script>`)
scripts.push(`<script id="${name}-script"${cspNonceAttr}>${bucket}</script>`)
})

return { styles: `${styles.join('\n')}\n${injectedStyles}`, scripts: scripts.join('\n') }
Expand Down Expand Up @@ -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,
})
Expand All @@ -175,7 +189,7 @@ export class Templates {
},
})

const { scripts, styles } = this.#getStylesAndScripts()
const { scripts, styles } = this.#getStylesAndScripts(props.cspNonce)
return html.replace('<!-- STYLES -->', styles).replace('<!-- SCRIPTS -->', scripts)
}
}
5 changes: 4 additions & 1 deletion src/templates/error_cause/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ export class ErrorCause extends BaseComponent<ErrorCauseProps> {
</div>
<div class="card-body">
<div id="error-cause">
${dump(props.error.cause, { styles: themes.cssVariables })}
${dump(props.error.cause, {
cspNonce: props.cspNonce,
styles: themes.cssVariables,
})}
</div>
</div>
</div>
Expand Down
25 changes: 16 additions & 9 deletions src/templates/error_metadata/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ export class ErrorMetadata extends BaseComponent<ErrorMetadataProps> {
/**
* 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) {
Expand All @@ -37,15 +37,15 @@ export class ErrorMetadata extends BaseComponent<ErrorMetadataProps> {
* Returns HTML fragment with HTML table containing rows
* metadata section rows
*/
#renderRows(rows: ErrorMetadataRow[]) {
#renderRows(rows: ErrorMetadataRow[], cspNonce?: string) {
return `<table class="card-table">
<tbody>
${rows
.map((row) => {
return `<tr>
<td class="table-key">${row.key}</td>
<td class="table-value">
${this.#formatRowValue(row.value, row.dump)}
${this.#formatRowValue(row.value, row.dump, cspNonce)}
</td>
</tr>`
})
Expand All @@ -57,10 +57,14 @@ export class ErrorMetadata extends BaseComponent<ErrorMetadataProps> {
/**
* Renders each section with its rows inside a table
*/
#renderSection(section: string, rows: ErrorMetadataRow | ErrorMetadataRow[]) {
#renderSection(section: string, rows: ErrorMetadataRow | ErrorMetadataRow[], cspNonce?: string) {
return `<div>
<h4 class="card-subtitle">${section}</h4>
${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)
}
</div>`
}

Expand All @@ -69,7 +73,8 @@ export class ErrorMetadata extends BaseComponent<ErrorMetadataProps> {
*/
#renderGroup(
group: string,
sections: { [section: string]: ErrorMetadataRow | ErrorMetadataRow[] }
sections: { [section: string]: ErrorMetadataRow | ErrorMetadataRow[] },
cspNonce?: string
) {
return `<section>
<div class="card">
Expand All @@ -78,7 +83,7 @@ export class ErrorMetadata extends BaseComponent<ErrorMetadataProps> {
</div>
<div class="card-body">
${Object.keys(sections)
.map((section) => this.#renderSection(section, sections[section]))
.map((section) => this.#renderSection(section, sections[section], cspNonce))
.join('\n')}
</div>
</div>
Expand All @@ -96,6 +101,8 @@ export class ErrorMetadata extends BaseComponent<ErrorMetadataProps> {
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')
}
}
32 changes: 25 additions & 7 deletions src/templates/error_stack/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import { BaseComponent } from '../../component.js'
import { publicDirURL } from '../../public_dir.js'
import type { ErrorStackProps } from '../../types.js'

const CHEVIRON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M6 9l6 6l6 -6"></path>
</svg>`

/**
* Known editors and their URLs to open the file within
* the code editor
Expand Down Expand Up @@ -108,14 +112,15 @@ export class ErrorStack extends BaseComponent<ErrorStackProps> {
: ''
const loc = `<span>at line <code>${frame.lineNumber}:${frame.columnNumber}</code></span>`

if (frame.type === 'native') {
return `<div class="stack-frame-location">
if (frame.type !== 'native' && frame.source) {
return `<button class="stack-frame-location" onclick="toggleFrameSource(event, '${id}')">
${fileName} ${functionName} ${loc}
</div>`
</button>`
}
return `<button class="stack-frame-location" onclick="toggleFrameSource(event, '${id}')">

return `<div class="stack-frame-location">
${fileName} ${functionName} ${loc}
</button>`
</div>`
}

/**
Expand All @@ -130,11 +135,20 @@ export class ErrorStack extends BaseComponent<ErrorStackProps> {
const id = `frame-${index + 1}`
const label = frame.type === 'app' ? '<span class="frame-label">In App</span>' : ''
const expandedClass = expandAtIndex === index ? 'expanded' : ''
const toggleButton =
frame.type !== 'native' && frame.source
? `<button class="stack-frame-toggle-indicator" onclick="toggleFrameSource(event, '${id}')">
${CHEVIRON}
</button>`
: ''

return `<li class="stack-frame ${expandedClass} stack-frame-${frame.type}" id="${id}">
<div class="stack-frame-contents">
${this.#renderFrameLocation(frame, id, props.ide)}
<div class="stack-frame-extras">${label}</div>
<div class="stack-frame-extras">
${label}
${toggleButton}
</div>
</div>
<div class="stack-frame-source">
${await props.sourceCodeRenderer(props.error, frame)}
Expand Down Expand Up @@ -176,7 +190,11 @@ export class ErrorStack extends BaseComponent<ErrorStackProps> {
</ul>
</div>
<div id="stack-frames-raw">
${dump(props.error.raw, { styles: themes.cssVariables, expand: true })}
${dump(props.error.raw, {
styles: themes.cssVariables,
expand: true,
cspNonce: props.cspNonce,
})}
</div>
</div>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/templates/header/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="15" height="15" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M0 0h24v24H0z" stroke="none"/><path d="M12 3h.393a7.5 7.5 0 0 0 7.92 12.446A9 9 0 1 1 12 2.992z"/></svg>`

Expand All @@ -18,7 +19,7 @@ const LIGHT_MODE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="tru
* Renders the header for the error page. It contains only the
* theme-switcher for now
*/
export class Header extends BaseComponent {
export class Header extends BaseComponent<ComponentSharedProps> {
cssFile = new URL('./header/style.css', publicDirURL)
scriptFile = new URL('./header/script.js', publicDirURL)

Expand Down
Loading

0 comments on commit a9555d2

Please sign in to comment.