diff --git a/src/bin/generate.ts b/src/bin/generate.ts index eb1c3ea8..5668107f 100644 --- a/src/bin/generate.ts +++ b/src/bin/generate.ts @@ -110,6 +110,26 @@ async function main() { } main().catch(error => { + if (typeof error === 'string') { + github.setOutput('error', error); + } else if ('message' in error && typeof error.message === 'string') { + if ('stack' in error && typeof error.stack === 'string') { + if (error.stack.includes(error.message)) { + github.setOutput('error', error.stack); + } else { + github.setOutput('error', error.message + '\n' + error.stack); + } + } else { + github.setOutput('error', error.message); + } + } else { + try { + github.setOutput('error', JSON.stringify(error)); + } catch { + // ignore + } + } + console.error(error); process.exit(1); }); diff --git a/src/lib/walker.ts b/src/lib/walker.ts index f59599e3..6c328064 100644 --- a/src/lib/walker.ts +++ b/src/lib/walker.ts @@ -1,15 +1,24 @@ -import { Root } from 'mdast'; +import { Heading, Root } from 'mdast'; +import markdown from 'remark-parse'; +import stringify from 'remark-stringify'; import { Option, assert } from 'ts-std'; +import unified from 'unified'; import { Node, Parent } from 'unist'; import { VFile } from 'vfile'; + type Handler = (this: Walker, node: Node) => Option | Promise>; function isParent(node: Node): node is Parent { return Array.isArray(node.children); } +const Printer = unified().use(markdown).use(stringify); + export default class Walker { + private lastNode: Option = null; + private lastHeading: Option = null; + constructor(protected options: Options, protected file: VFile) {} [key: string]: unknown; @@ -17,25 +26,90 @@ export default class Walker { async walk(root: Node): Promise { assert(root.type === 'root', `Cannot walk \`${root.type}\` (must be \`root\`)`); - let result = await this.handle(root); + try { + this.lastNode = null; + this.lastHeading = null; - if (Array.isArray(result)) { - assert(result.length === 1, 'Must return a single root node'); - result = result[0]; - } + let result = await this.handle(root); - if (result) { - assert(result.type === 'root', 'Must return a root'); - return result as Root; - } else { - return { - type: 'root', - children: [] - }; + if (Array.isArray(result)) { + assert(result.length === 1, 'Must return a single root node'); + result = result[0]; + } + + if (result) { + assert(result.type === 'root', 'Must return a root'); + return result as Root; + } else { + return { + type: 'root', + children: [] + }; + } + } catch (error) { + let message: string = 'Encounted an error'; + + if (this.file.path) { + message += ` while processing ${this.file.path}`; + } + + if (this.lastNode?.position) { + let { line, column } = this.lastNode.position.start; + message += ` at L${line}:C${column}`; + } + + if (this.lastHeading) { + try { + let heading = Printer.stringify(this.lastHeading); + message += ` (under the section "${heading}")`; + } catch { + // ignore + } + } + + if (typeof error === 'string') { + message += `.\nReason:\n${error}`; + } else if ('message' in error && typeof error.message === 'string') { + if ('stack' in error && typeof error.stack === 'string') { + if (error.stack.includes(error.message)) { + message += `.\nReason:\n${error.stack}`; + } else { + message += `.\nReason:\n${error.message}\n${error.stack}`; + } + } else { + message += `.\nReason:\n${error.message}`; + } + } + + let reason: Error | string; + + if ('stack' in error && typeof error.stack === 'string') { + reason = new Error(message); + + if (error.stack.includes(error.message)) { + reason.stack = error.stack.replace(error.message, message); + } else { + reason.stack = error.stack; + } + } else { + reason = message; + } + + // upstream type for reason is wrong: should be Error | string + this.file.fail(reason as string, this.lastNode?.position); + } finally { + this.lastNode = null; + this.lastHeading = null; } } protected async handle(node: Node): Promise | Node[]> { + this.lastNode = node; + + if (node.type === 'heading') { + this.lastHeading = node as Heading; + } + let maybeHandler = this[node.type]; if (typeof maybeHandler === 'function') {