From 51502abdd8bfa44114756203e0c5c528ed4a7d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 27 Feb 2024 11:10:47 -0500 Subject: [PATCH 1/3] chore: fix header generation and production build crashes (#5100) * chore: use direct path to assetgraph-builder > buildProduction in nps * chore: switch NODE_VERSION to 20 * chore: remove scripts/netlify-headers.js * chore: remove scripts/netlify-headers.js * git checkout master -- netlify.toml --- package-scripts.js | 4 +- scripts/netlify-headers.js | 126 ------------------------------------- 2 files changed, 2 insertions(+), 128 deletions(-) delete mode 100644 scripts/netlify-headers.js diff --git a/package-scripts.js b/package-scripts.js index 6fb0594980..1e4c072973 100644 --- a/package-scripts.js +++ b/package-scripts.js @@ -259,7 +259,7 @@ module.exports = { docs: { default: { script: - 'nps docs.clean && nps docs.api && eleventy && nps docs.linkcheck && node scripts/netlify-headers.js docs/_site >> docs/_site/_headers', + 'nps docs.clean && nps docs.api && eleventy && nps docs.linkcheck', description: 'Build documentation' }, production: { @@ -277,7 +277,7 @@ module.exports = { }, postbuild: { script: - 'buildProduction docs/_site/index.html --outroot docs/_dist --canonicalroot https://mochajs.org/ --optimizeimages --svgo --inlinehtmlimage 9400 --inlinehtmlscript 0 --asyncscripts && cp docs/_headers docs/_dist/_headers && node scripts/netlify-headers.js docs/_dist >> docs/_dist/_headers', + 'node node_modules/assetgraph-builder/bin/buildProduction docs/_site/index.html --outroot docs/_dist --canonicalroot https://mochajs.org/ --optimizeimages --svgo --inlinehtmlimage 9400 --inlinehtmlscript 0 --asyncscripts && cp docs/_headers docs/_dist/_headers', description: 'Post-process docs after build', hiddenFromHelp: true }, diff --git a/scripts/netlify-headers.js b/scripts/netlify-headers.js deleted file mode 100644 index e881423d20..0000000000 --- a/scripts/netlify-headers.js +++ /dev/null @@ -1,126 +0,0 @@ -'use strict'; - -const AssetGraph = require('assetgraph'); - -const dest = process.argv[2]; - -if (!dest) { - console.error('usage: node netlify-headers.js '); - console.error('example: node netlify-headers.js docs/_dist'); - process.exit(1); -} - -const headers = ['Content-Security-Policy']; - -const resourceHintTypeMap = { - HtmlPreloadLink: 'preload', - HtmlPrefetchLink: 'prefetch', - HtmlPreconnectLink: 'preconnect', - HtmlDnsPrefetchLink: 'dns-prefetch' -}; - -function getHeaderForRelation(rel) { - let header = `Link: <${rel.href}>; rel=${resourceHintTypeMap[rel.type]}; as=${ - rel.as - }; type=${rel.to.contentType}`; - - if (rel.as === 'font') { - header = `${header}; crossorigin=anonymous`; - } - - return header; -} - -console.error('Generating optimal netlify headers...'); - -new AssetGraph({root: dest}) - .loadAssets('*.html') - .populate({ - followRelations: {type: 'HtmlAnchor', crossorigin: false} - }) - .queue(function (assetGraph) { - const assets = assetGraph.findAssets({ - type: 'Html', - isInline: false, - isLoaded: true - }); - - const headerMap = {}; - - assets.forEach(function (asset) { - const url = - '/' + - asset.url - .replace(assetGraph.root, '') - .replace(/#.*/, '') - .replace('index.html', ''); - if (!headerMap[url]) { - headerMap[url] = []; - } - - headers.forEach(function (header) { - const node = asset.parseTree.querySelector( - 'meta[http-equiv=' + header + ']' - ); - - if (node) { - headerMap[url].push(`${header}: ${node.getAttribute('content')}`); - - node.parentNode.removeChild(node); - asset.markDirty(); - } - }); - - const firstCssRel = asset.outgoingRelations.filter(r => { - return ( - r.type === 'HtmlStyle' && - r.crossorigin === false && - r.href !== undefined - ); - })[0]; - - if (firstCssRel) { - const header = `Link: <${firstCssRel.href}>; rel=preload; as=style`; - - headerMap[url].push(header); - } - - const resourceHintRelations = asset.outgoingRelations.filter(r => - ['HtmlPreloadLink', 'HtmlPrefetchLink'].includes(r.type) - ); - - resourceHintRelations.forEach(rel => { - headerMap[url].push(getHeaderForRelation(rel)); - - rel.detach(); - }); - - const preconnectRelations = asset.outgoingRelations.filter(r => - ['HtmlPreconnectLink'].includes(r.type) - ); - - preconnectRelations.forEach(rel => { - const header = `Link: <${rel.href}>; rel=preconnect`; - - headerMap[url].push(header); - - rel.detach(); - }); - }); - - console.log('\n## Autogenerated headers:\n'); - - Object.keys(headerMap).forEach(function (url) { - console.log(url); - - const httpHeaders = headerMap[url]; - - httpHeaders.forEach(function (header) { - console.log(` ${header}`); - }); - - console.log(''); - }); - console.error('netlify headers done!'); - }) - .run(); From b88978deb3c12f9b95502828f6ac29ebe8be85ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 27 Feb 2024 11:30:37 -0500 Subject: [PATCH 2/3] chore: bump ESLint ecmaVersion to 2020 (#5104) --- eslint.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.js b/eslint.config.js index 53e788ebed..88fdd14a81 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -12,7 +12,7 @@ module.exports = [ { ...js.configs.recommended, languageOptions: { - ecmaVersion: 2018, + ecmaVersion: 2020, globals: { ...globals.browser, ...globals.node From 37358738260cfae7c244c157aee21654f2b588f2 Mon Sep 17 00:00:00 2001 From: Pelle Wessman Date: Mon, 4 Mar 2024 15:27:18 +0100 Subject: [PATCH 3/3] feat: include `.cause` stacks in the error stack traces (#4829) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Append the cause stacks to the main stack trace It would be great to get the full error stack chain for errors with causes, especially as all current browsers and Node.js >=16 supports it, see eg https://v8.dev/features/error-cause and https://dev.to/voxpelli/pony-cause-1-0-error-causes-2l2o Eg. `pino` merged support for this as well: https://github.com/pinojs/pino-std-serializers/pull/78 * Fix tests * Skip some string concatenation * Don't export needlessly + improve docs * Improved recursive filtering * Added loop protection * Same logic for "message" in cause trail as in top * Apply suggestions from code review Co-authored-by: Josh Goldberg ✨ * Revert "Apply suggestions from code review" This reverts commit 04f700820e91a5b2edad8dd23ac3ee1d89ab2973. --------- Co-authored-by: Josh Goldberg ✨ --- lib/reporters/base.js | 70 ++++++++++++++++++------- lib/runner.js | 21 ++++++-- test/reporters/base.spec.js | 102 ++++++++++++++++++++++++++++++++++++ test/unit/runner.spec.js | 34 ++++++++++++ 4 files changed, 203 insertions(+), 24 deletions(-) diff --git a/lib/reporters/base.js b/lib/reporters/base.js index 40b5996461..5af6e7bd8a 100644 --- a/lib/reporters/base.js +++ b/lib/reporters/base.js @@ -221,6 +221,56 @@ var generateDiff = (exports.generateDiff = function (actual, expected) { } }); +/** + * Traverses err.cause and returns all stack traces + * + * @private + * @param {Error} err + * @param {Set} [seen] + * @return {{ message: string, msg: string, stack: string }} + */ +var getFullErrorStack = function (err, seen) { + if (seen && seen.has(err)) { + return { message: '', msg: '', stack: '' }; + } + + var message; + + if (typeof err.inspect === 'function') { + message = err.inspect() + ''; + } else if (err.message && typeof err.message.toString === 'function') { + message = err.message + ''; + } else { + message = ''; + } + + var msg; + var stack = err.stack || message; + var index = message ? stack.indexOf(message) : -1; + + if (index === -1) { + msg = message; + } else { + index += message.length; + msg = stack.slice(0, index); + // remove msg from stack + stack = stack.slice(index + 1); + + if (err.cause) { + seen = seen || new Set(); + seen.add(err); + const causeStack = getFullErrorStack(err.cause, seen) + stack += '\n Caused by: ' + causeStack.msg + (causeStack.stack ? '\n' + causeStack.stack : ''); + } + } + + return { + message, + msg, + stack + }; +}; + /** * Outputs the given `failures` as a list. * @@ -241,7 +291,6 @@ exports.list = function (failures) { color('error stack', '\n%s\n'); // msg - var msg; var err; if (test.err && test.err.multiple) { if (multipleTest !== test) { @@ -252,25 +301,8 @@ exports.list = function (failures) { } else { err = test.err; } - var message; - if (typeof err.inspect === 'function') { - message = err.inspect() + ''; - } else if (err.message && typeof err.message.toString === 'function') { - message = err.message + ''; - } else { - message = ''; - } - var stack = err.stack || message; - var index = message ? stack.indexOf(message) : -1; - if (index === -1) { - msg = message; - } else { - index += message.length; - msg = stack.slice(0, index); - // remove msg from stack - stack = stack.slice(index + 1); - } + var { message, msg, stack } = getFullErrorStack(err); // uncaught if (err.uncaught) { diff --git a/lib/runner.js b/lib/runner.js index 12807725fb..60a19f0e3f 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -443,11 +443,22 @@ Runner.prototype.fail = function (test, err, force) { err = thrown2Error(err); } - try { - err.stack = - this.fullStackTrace || !err.stack ? err.stack : stackFilter(err.stack); - } catch (ignore) { - // some environments do not take kindly to monkeying with the stack + // Filter the stack traces + if (!this.fullStackTrace) { + const alreadyFiltered = new Set(); + let currentErr = err; + + while (currentErr && currentErr.stack && !alreadyFiltered.has(currentErr)) { + alreadyFiltered.add(currentErr); + + try { + currentErr.stack = stackFilter(currentErr.stack); + } catch (ignore) { + // some environments do not take kindly to monkeying with the stack + } + + currentErr = currentErr.cause; + } } this.emit(constants.EVENT_TEST_FAIL, test, err); diff --git a/test/reporters/base.spec.js b/test/reporters/base.spec.js index 6b30b2ccc7..84778f3693 100644 --- a/test/reporters/base.spec.js +++ b/test/reporters/base.spec.js @@ -491,6 +491,108 @@ describe('Base reporter', function () { expect(errOut, 'to be', '1) test title:\n Error\n foo\n bar'); }); + describe('error causes', function () { + it('should append any error cause trail to stack traces', function () { + var err = { + message: 'Error', + stack: 'Error\nfoo\nbar', + showDiff: false, + cause: { + message: 'Cause1', + stack: 'Cause1\nbar\nfoo', + showDiff: false, + cause: { + message: 'Cause2', + stack: 'Cause2\nabc\nxyz', + showDiff: false + } + } + }; + var test = makeTest(err); + + list([test]); + + var errOut = stdout.join('\n').trim(); + expect( + errOut, + 'to be', + '1) test title:\n Error\n foo\n bar\n Caused by: Cause1\n bar\n foo\n Caused by: Cause2\n abc\n xyz' + ); + }); + + it('should not get stuck in a hypothetical circular error cause trail', function () { + var err1 = { + message: 'Error', + stack: 'Error\nfoo\nbar', + showDiff: false, + }; + var err2 = { + message: 'Cause1', + stack: 'Cause1\nbar\nfoo', + showDiff: false, + cause: err1 + } + err1.cause = err2; + + var test = makeTest(err1); + + list([test]); + + var errOut = stdout.join('\n').trim(); + expect( + errOut, + 'to be', + '1) test title:\n Error\n foo\n bar\n Caused by: Cause1\n bar\n foo\n Caused by: ' + ); + }); + + it("should set an empty cause if neither 'inspect' nor 'message' is set", function () { + var err = { + message: 'Error', + stack: 'Error\nfoo\nbar', + showDiff: false, + cause: { + showDiff: false, + } + }; + + var test = makeTest(err); + + list([test]); + + var errOut = stdout.join('\n').trim(); + expect( + errOut, + 'to be', + '1) test title:\n Error\n foo\n bar\n Caused by:' + ); + }); + + it('should not add cause trail if error does not contain message', function () { + var err = { + message: 'Error', + stack: 'foo\nbar', + showDiff: false, + cause: { + message: 'Cause1', + stack: 'Cause1\nbar\nfoo', + showDiff: false, + cause: { + message: 'Cause2', + stack: 'Cause2\nabc\nxyz', + showDiff: false + } + } + }; + var test = makeTest(err); + + list([test]); + + var errOut = stdout.join('\n').trim(); + expect(errOut, 'to be', '1) test title:\n Error\n foo\n bar'); + }); + }); + it('should list multiple Errors per test', function () { var err = new Error('First Error'); err.multiple = [new Error('Second Error - same test')]; diff --git a/test/unit/runner.spec.js b/test/unit/runner.spec.js index dd96558017..66ca6a0532 100644 --- a/test/unit/runner.spec.js +++ b/test/unit/runner.spec.js @@ -629,6 +629,22 @@ describe('Runner', function () { }); runner.fail(hook, err); }); + + it('should prettify stack-traces in error cause trail', function (done) { + var hook = new Hook(); + hook.parent = suite; + var causeErr = new Error(); + // Fake stack-trace + causeErr.stack = stack.join('\n'); + var err = new Error(); + err.cause = causeErr; + + runner.on(EVENT_TEST_FAIL, function (_hook, _err) { + expect(_err.cause.stack, 'to be', stack.slice(0, 3).join('\n')); + done(); + }); + runner.fail(hook, err); + }); }); describe('long', function () { @@ -647,6 +663,24 @@ describe('Runner', function () { }); runner.fail(hook, err); }); + + it('should display full stack-traces in error cause trail', function (done) { + var hook = new Hook(); + hook.parent = suite; + var causeErr = new Error(); + // Fake stack-trace + causeErr.stack = stack.join('\n'); + var err = new Error(); + err.cause = causeErr; + // Add --stack-trace option + runner.fullStackTrace = true; + + runner.on(EVENT_TEST_FAIL, function (_hook, _err) { + expect(_err.cause.stack, 'to be', stack.join('\n')); + done(); + }); + runner.fail(hook, err); + }); }); describe('ginormous', function () {