Skip to content

Commit

Permalink
feat: add console printer
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Nov 20, 2024
1 parent 3049bdc commit 46dc9ca
Show file tree
Hide file tree
Showing 13 changed files with 366 additions and 18 deletions.
10 changes: 10 additions & 0 deletions example/console.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { run as axios } from './axios.js'
import { Youch } from '../src/youch.js'

try {
await axios()
} catch (error) {
const youch = new Youch()
const output = await youch.print(error)
console.log(output)
}
File renamed without changes.
9 changes: 7 additions & 2 deletions src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,13 @@ export abstract class BaseComponent<Props = undefined> {
}

/**
* The extending class must implement the render method to
* return the HTML fragment
* The render method is used to output the HTML for the
* web view
*/
abstract render(props: Props): Promise<string>

/**
* The print method is used to output the text for the console
*/
abstract print(props: Props): Promise<string>
}
77 changes: 77 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* youch
*
* (c) Poppinss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import useColors from '@poppinss/colors'
import { Colors } from '@poppinss/colors/types'

const ANSI_REGEX = new RegExp(
[
`[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?(?:\\u0007|\\u001B\\u005C|\\u009C))`,
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))',
].join('|'),
'g'
)

/**
* HTML escape string values so that they can be nested
* inside the pre-tags.
*/
export function htmlEscape(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/\\"/g, '&bsol;&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
}

/**
* Wraps a string value to be on multiple lines after
* a certain characters limit has been hit.
*/
export function wordWrap(
value: string,
options: {
width: number
indent: string
newLine: string
escape?: (value: string) => string
}
) {
const width = options.width
const indent = options.indent
const newLine = `${options.newLine}${indent}`

let regexString = '.{1,' + width + '}'
regexString += '([\\s\u200B]+|$)|[^\\s\u200B]+?([\\s\u200B]+|$)'

const re = new RegExp(regexString, 'g')
const lines = value.match(re) || []
const result = lines
.map(function (line) {
if (line.slice(-1) === '\n') {
line = line.slice(0, line.length - 1)
}
return options.escape ? options.escape(line) : htmlEscape(line)
})
.join(newLine)

return result
}

/**
* Strips ANSI sequences from the string
*/
export function stripAnsi(value: string) {
return value.replace(ANSI_REGEX, '')
}

/**
* ANSI coloring library
*/
export const colors: Colors = useColors.ansi()
42 changes: 42 additions & 0 deletions src/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,20 @@ export class Templates {
return component.render(props)
}

/**
* Print a known template by its name
*/
async #printTmpl<K extends keyof YouchTemplates>(
templateName: K,
props: YouchTemplates[K]['$props']
): Promise<string> {
const component: BaseComponent<any> = this.#knownTemplates[templateName]
if (!component) {
throw new Error(`Invalid template "${templateName}"`)
}
return component.print(props)
}

/**
* Define a custom component to be used in place of the default component.
* Overriding components allows you control the HTML layout, styles and
Expand Down Expand Up @@ -192,4 +206,32 @@ export class Templates {
const { scripts, styles } = this.#getStylesAndScripts(props.cspNonce)
return html.replace('<!-- STYLES -->', styles).replace('<!-- SCRIPTS -->', scripts)
}

/**
* Returns the ANSI output to be printed on the terminal
*/
async print(props: { title: string; error: ParsedError; metadata: Metadata }) {
const ansiOutput = await this.#printTmpl('layout', {
title: props.title,
children: async () => {
const header = await this.#printTmpl('header', {})
const info = await this.#printTmpl('errorInfo', props)
const stackTrace = await this.#printTmpl('errorStack', {
ide: process.env.EDITOR ?? 'vscode',
sourceCodeRenderer: (error, frame) => {
return this.#printTmpl('errorStackSource', {
error,
frame,
})
},
...props,
})
const cause = await this.#printTmpl('errorCause', props)
const metadata = await this.#printTmpl('errorMetadata', props)
return `${header}${info}${stackTrace}${cause}${metadata}`
},
})

return ansiOutput
}
}
31 changes: 31 additions & 0 deletions src/templates/error_cause/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
*/

import { dump, themes } from '@poppinss/dumper/html'
import { dump as dumpCli } from '@poppinss/dumper/console'

import { colors } from '../../helpers.js'
import { BaseComponent } from '../../component.js'
import { publicDirURL } from '../../public_dir.js'
import type { ErrorCauseProps } from '../../types.js'
Expand All @@ -19,6 +21,10 @@ import type { ErrorCauseProps } from '../../types.js'
export class ErrorCause extends BaseComponent<ErrorCauseProps> {
cssFile = new URL('./error_cause/style.css', publicDirURL)

/**
* The render method is used to output the HTML for the
* web view
*/
async render(props: ErrorCause['$props']): Promise<string> {
if (!props.error.cause) {
return ''
Expand All @@ -44,4 +50,29 @@ export class ErrorCause extends BaseComponent<ErrorCauseProps> {
</div>
</section>`
}

/**
* The print method is used to output the text for the console
*/
async print(props: ErrorCauseProps) {
if (!props.error.cause) {
return ''
}

/**
* Relying on "YOUCH_CAUSE" environment variable to decide
* how deep the properties should be displayed
*/
let depth = process.env.YOUCH_CAUSE ? Number(process.env.YOUCH_CAUSE) : 2
if (Number.isNaN(depth)) {
depth = 2
}

return `\n\n${colors.red('[CAUSE]')}\n${dumpCli(props.error.cause, {
depth,
inspectObjectPrototype: false,
inspectStaticMembers: false,
inspectArrayPrototype: false,
})}`
}
}
30 changes: 30 additions & 0 deletions src/templates/error_info/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 { wordWrap, colors } from '../../helpers.js'
import type { ErrorInfoProps } from '../../types.js'

const ERROR_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="24" height="24" fill="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 7v6m0 4.01.01-.011M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z"/></svg>`
Expand All @@ -22,6 +23,10 @@ const HINT_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true
export class ErrorInfo extends BaseComponent<ErrorInfoProps> {
cssFile = new URL('./error_info/style.css', publicDirURL)

/**
* The render method is used to output the HTML for the
* web view
*/
async render(props: ErrorInfoProps): Promise<string> {
return `<section>
<h4 id="error-name">${props.error.name}</h4>
Expand All @@ -46,4 +51,29 @@ export class ErrorInfo extends BaseComponent<ErrorInfoProps> {
</div>
</section>`
}

/**
* The print method is used to output the text for the console
*/
async print(props: ErrorInfoProps) {
const errorMessage = colors.red(
`ℹ ${wordWrap(`${props.error.name}: ${props.error.message}`, {
width: process.stdout.columns,
indent: ' ',
newLine: '\n',
})}`
)

const hint = props.error.hint
? `\n\n${colors.blue('◉')} ${colors.dim().italic(
wordWrap(props.error.hint.replace(/(<([^>]+)>)/gi, ''), {
width: process.stdout.columns,
indent: ' ',
newLine: '\n',
})
)}`
: ''

return `${errorMessage}${hint}`
}
}
10 changes: 9 additions & 1 deletion src/templates/error_metadata/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ export class ErrorMetadata extends BaseComponent<ErrorMetadataProps> {
}

/**
* Renders erorr metadata groups
* The render method is used to output the HTML for the
* web view
*/
async render(props: ErrorMetadataProps): Promise<string> {
const groups = props.metadata.toJSON()
Expand All @@ -105,4 +106,11 @@ export class ErrorMetadata extends BaseComponent<ErrorMetadataProps> {
.map((group) => this.#renderGroup(group, groups[group], props.cspNonce))
.join('\n')
}

/**
* The print method is used to output the text for the console
*/
async print() {
return ''
}
}
Loading

0 comments on commit 46dc9ca

Please sign in to comment.