diff --git a/src/core/before-save.js b/src/core/before-save.js index a2d58c10e7..e79d8a6515 100644 --- a/src/core/before-save.js +++ b/src/core/before-save.js @@ -38,8 +38,7 @@ function performTransformations(transforms, doc) { const nameOrPosition = `\`${fn.name}\`` || `at position ${pos}`; const msg = docLink`Function ${nameOrPosition}\` threw an error during processing of ${"[beforeSave]"}.`; const hint = "See developer console."; - showError(msg, name, { hint }); - console.error(err); + showError(msg, name, { hint, cause: err }); } finally { pos++; } diff --git a/src/core/caniuse.js b/src/core/caniuse.js index 7b4ad3b5a7..5d7f36612c 100644 --- a/src/core/caniuse.js +++ b/src/core/caniuse.js @@ -103,8 +103,7 @@ export async function run(conf) { function handleError(err, options, featureURL) { const msg = `Failed to retrieve feature "${options.feature}".`; const hint = docLink`Please check the feature key on [caniuse.com](https://caniuse.com) and update ${"[caniuse]"}.`; - showError(msg, name, { hint }); - console.error(err); + showError(msg, name, { hint, cause: err }); return html`caniuse.com`; } diff --git a/src/core/contrib.js b/src/core/contrib.js index 0bd6673b89..621dd43a28 100644 --- a/src/core/contrib.js +++ b/src/core/contrib.js @@ -60,8 +60,7 @@ async function showContributors(editors, apiURL) { ); } catch (error) { const msg = "Error loading contributors from GitHub."; - showError(msg, name); - console.error(error); + showError(msg, name, { cause: error }); return null; } } diff --git a/src/core/custom-elements/rs-changelog.js b/src/core/custom-elements/rs-changelog.js index dd477328da..26d77aa908 100644 --- a/src/core/custom-elements/rs-changelog.js +++ b/src/core/custom-elements/rs-changelog.js @@ -34,7 +34,9 @@ export const element = class ChangelogElement extends HTMLElement { ${{ any: fetchCommits(from, to, filter) .then(commits => toHTML(commits)) - .catch(error => showError(error.message, name, { elements: [this] })) + .catch(error => + showError(error.message, name, { elements: [this], cause: error }) + ) .finally(() => { this.dispatchEvent(new CustomEvent("done")); }), @@ -70,8 +72,7 @@ async function fetchCommits(from, to, filter) { commits = commits.filter(filter); } catch (error) { const msg = `Error loading commits from GitHub. ${error.message}`; - console.error(error); - throw new Error(msg); + throw new Error(msg, { cause: error }); } return commits; } diff --git a/src/core/data-include.js b/src/core/data-include.js index 49d4f1222f..55ffa0dbee 100644 --- a/src/core/data-include.js +++ b/src/core/data-include.js @@ -102,8 +102,7 @@ async function runIncludes(root, currentDepth) { } } catch (err) { const msg = `\`data-include\` failed: \`${url}\` (${err.message}).`; - console.error(msg, el, err); - showError(msg, name, { elements: [el] }); + showError(msg, name, { elements: [el], cause: err }); } }); await Promise.all(promisesToInclude); diff --git a/src/core/post-process.js b/src/core/post-process.js index ad4a4ae338..20d0fc53f3 100644 --- a/src/core/post-process.js +++ b/src/core/post-process.js @@ -32,8 +32,7 @@ export async function run(config) { } catch (err) { const msg = `Function ${f.name} threw an error during \`postProcess\`.`; const hint = "See developer console."; - showError(msg, name, { hint }); - console.error(err); + showError(msg, name, { hint, cause: err }); } }); await Promise.all(promises); diff --git a/src/core/pre-process.js b/src/core/pre-process.js index 5eb830a721..b9a389f920 100644 --- a/src/core/pre-process.js +++ b/src/core/pre-process.js @@ -31,8 +31,7 @@ export async function run(config) { } catch (err) { const msg = `Function ${f.name} threw an error during \`preProcess\`.`; const hint = "See developer console."; - showError(msg, name, { hint }); - console.error(err); + showError(msg, name, { hint, cause: err }); } }); await Promise.all(promises); diff --git a/src/core/utils.js b/src/core/utils.js index 373e7fc66b..5cd3fb622e 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -294,8 +294,7 @@ export function runTransforms(content, flist, ...funcArgs) { } catch (e) { const msg = `call to \`${meth}()\` failed with: ${e}.`; const hint = "See developer console for stack trace."; - showWarning(msg, "utils/runTransforms", { hint }); - console.error(e); + showWarning(msg, "utils/runTransforms", { hint, cause: e }); } } } @@ -850,7 +849,7 @@ export class RespecError extends Error { * @param {Parameters[2] & { isWarning: boolean }} options */ constructor(message, plugin, options) { - super(message); + super(message, { ...(options.cause && { cause: options.cause }) }); const name = options.isWarning ? "ReSpecWarning" : "ReSpecError"; Object.assign(this, { message, plugin, name, ...options }); if (options.elements) { @@ -864,7 +863,23 @@ export class RespecError extends Error { const { message, name, stack } = this; // @ts-expect-error https://github.com/microsoft/TypeScript/issues/26792 const { plugin, hint, elements, title, details } = this; - return { message, name, plugin, hint, elements, title, details, stack }; + return { + message, + name, + plugin, + hint, + elements, + title, + details, + stack, + ...(this.cause instanceof Error && { + cause: { + name: this.cause.name, + message: this.cause.message, + stack: this.cause.stack, + }, + }), + }; } } @@ -876,6 +891,7 @@ export class RespecError extends Error { * @param {HTMLElement[]} [options.elements] Offending elements. * @param {string} [options.title] Title attribute for offending elements. Can be a shorter form of the message. * @param {string} [options.details] Any further details/context. + * @param {Error} [options.cause] The error that caused this one. */ export function showError(message, pluginName, options = {}) { const opts = { ...options, isWarning: false }; @@ -890,6 +906,7 @@ export function showError(message, pluginName, options = {}) { * @param {HTMLElement[]} [options.elements] Offending elements. * @param {string} [options.title] Title attribute for offending elements. Can be a shorter form of the message. * @param {string} [options.details] Any further details/context. + * @param {Error} [options.cause] The error that caused this one. */ export function showWarning(message, pluginName, options = {}) { const opts = { ...options, isWarning: true }; diff --git a/src/jsconfig.json b/src/jsconfig.json index 248cbb30e2..d63e4b2e55 100644 --- a/src/jsconfig.json +++ b/src/jsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "target": "es2019", + "target": "es2022", "moduleResolution": "node", "noImplicitThis": true, - "module": "es2020" + "module": "es2022" } } diff --git a/tools/respec2html.js b/tools/respec2html.js index 6aa53ddf66..931217c94b 100755 --- a/tools/respec2html.js +++ b/tools/respec2html.js @@ -90,15 +90,32 @@ class Logger { /** @param {import("./respecDocWriter").ReSpecError} rsError */ _printDetails(rsError) { + const shouldPrintStacktrace = this._shouldPrintStacktrace(rsError); const print = (title, value) => { if (!value) return; - const padWidth = "Plugin".length + 1; // "Plugin" is the longest title + const longestTitle = shouldPrintStacktrace ? "Stacktrace" : "Plugin"; + const padWidth = longestTitle.length + 1; const paddedTitle = `${title}:`.padStart(padWidth); console.error(" ", colors.bold(paddedTitle), this._formatMarkdown(value)); }; print("Count", rsError.elements && String(rsError.elements.length)); print("Plugin", rsError.plugin); print("Hint", rsError.hint); + if (shouldPrintStacktrace) { + let stacktrace = `${rsError.stack}`; + if (rsError.cause) { + stacktrace += `\n ${colors.bold("Caused by:")} ${rsError.cause.stack.split("\n").join("\n ")}`; + } + print("Stacktrace", stacktrace); + } + } + + _shouldPrintStacktrace(rsError) { + return ( + this.verbose && + !!rsError.stack && + (!!rsError.cause?.stack || rsError.plugin === "unknown") + ); } } diff --git a/tools/respecDocWriter.js b/tools/respecDocWriter.js index 3da3da07bd..e0454eb9c0 100644 --- a/tools/respecDocWriter.js +++ b/tools/respecDocWriter.js @@ -310,10 +310,19 @@ function handleConsoleMessages(page, onError, onWarning) { // Old ReSpec versions might report errors as strings. return JSON.stringify({ message: String(obj) }); } else if (obj instanceof Error && !obj.plugin) { + let cause; + if (obj.cause instanceof Error) { + cause = { + name: obj.cause.name, + message: obj.cause.message, + stack: obj.cause.stack, + }; + } return JSON.stringify({ message: obj.message, plugin: "unknown", name: obj.name, + cause, stack: obj.stack?.replace( obj.message, `${obj.message.slice(0, 30)}…`