Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
150 lines
5.1 KiB
Plaintext
150 lines
5.1 KiB
Plaintext
/**
|
|
* Renders the provided audit results to well-formatted and valid HTML.
|
|
*
|
|
* Do note that the rendered result is not an HTML document, it's rather
|
|
* just a component with results.
|
|
*/
|
|
export async function renderAuditResultsToHTML(results) {
|
|
const grouped = {
|
|
total: 0,
|
|
ok: [],
|
|
notice: [],
|
|
warn: [],
|
|
error: [],
|
|
};
|
|
for (const result of results) {
|
|
grouped.total++;
|
|
if (result.status === 'ok') {
|
|
grouped[result.status].push(result);
|
|
}
|
|
else {
|
|
grouped[result.status].push(result);
|
|
}
|
|
}
|
|
let report = '<i>* This report was auto-generated by graphql-http</i>\n';
|
|
report += '\n';
|
|
report += '<h1>GraphQL over HTTP audit report</h1>\n';
|
|
report += '\n';
|
|
report += '<ul>\n';
|
|
report += `<li><b>${grouped.total}</b> audits in total</li>\n`;
|
|
// font-family: monospace helps render native emojis in HTML
|
|
if (grouped.ok.length) {
|
|
report += `<li><span style="font-family: monospace">✅</span> <b>${grouped.ok.length}</b> pass</li>\n`;
|
|
}
|
|
if (grouped.notice.length) {
|
|
report += `<li><span style="font-family: monospace">💡</span> <b>${grouped.notice.length}</b> notices (suggestions)</li>\n`;
|
|
}
|
|
if (grouped.warn.length) {
|
|
report += `<li><span style="font-family: monospace">❗️</span> <b>${grouped.warn.length}</b> warnings (optional)</li>\n`;
|
|
}
|
|
if (grouped.error.length) {
|
|
report += `<li><span style="font-family: monospace">❌</span> <b>${grouped.error.length}</b> errors (required)</li>\n`;
|
|
}
|
|
report += '</ul>\n';
|
|
report += '\n';
|
|
if (grouped.ok.length) {
|
|
report += '<h2>Passing</h2>\n';
|
|
report += '<ol>\n';
|
|
for (const [, result] of grouped.ok.entries()) {
|
|
report += `<li><code>${result.id}</code> ${result.name}</li>\n`;
|
|
}
|
|
report += '</ol>\n';
|
|
report += '\n';
|
|
}
|
|
if (grouped.notice.length) {
|
|
report += `<h2>Notices</h2>\n`;
|
|
report +=
|
|
'The server <i>MAY</i> support these, but are truly optional. These are suggestions following recommended conventions.\n';
|
|
report += '<ol>\n';
|
|
for (const [, result] of grouped.notice.entries()) {
|
|
report += await printAuditFail(result);
|
|
}
|
|
report += '</ol>\n';
|
|
report += '\n';
|
|
}
|
|
if (grouped.warn.length) {
|
|
report += `<h2>Warnings</h2>\n`;
|
|
report += 'The server <i>SHOULD</i> support these, but is not required.\n';
|
|
report += '<ol>\n';
|
|
for (const [, result] of grouped.warn.entries()) {
|
|
report += await printAuditFail(result);
|
|
}
|
|
report += '</ol>\n';
|
|
report += '\n';
|
|
}
|
|
if (grouped.error.length) {
|
|
report += `<h2>Errors</h2>\n`;
|
|
report += 'The server <b>MUST</b> support these.\n';
|
|
report += '<ol>\n';
|
|
for (const [, result] of grouped.error.entries()) {
|
|
report += await printAuditFail(result);
|
|
}
|
|
report += '</ol>\n';
|
|
}
|
|
return report;
|
|
}
|
|
async function printAuditFail(result) {
|
|
var _a;
|
|
let report = '';
|
|
report += `<li><code>${result.id}</code> ${result.name}\n`;
|
|
report += '<details>\n';
|
|
report += `<summary>${truncate(result.reason)}</summary>\n`;
|
|
report += '<pre><code class="lang-json">'; // no "\n" because they count in HTML pre tags
|
|
const res = result.response;
|
|
const headers = {};
|
|
for (const [key, val] of res.headers.entries()) {
|
|
// some headers change on each run, dont report it
|
|
if (key === 'date') {
|
|
headers[key] = '<timestamp>';
|
|
}
|
|
else if (['cf-ray', 'server-timing', 'set-cookie'].includes(key)) {
|
|
headers[key] = '<omitted>';
|
|
}
|
|
else {
|
|
headers[key] = val;
|
|
}
|
|
}
|
|
let text = '', json;
|
|
try {
|
|
text = await res.text();
|
|
json = JSON.parse(text);
|
|
// is json, there shouldnt be nothing to sanitize (hopefully)
|
|
}
|
|
catch (_b) {
|
|
// is not json, avoid rendering html (rest is allowed)
|
|
if ((_a = res.headers.get('content-type')) === null || _a === void 0 ? void 0 : _a.includes('text/html')) {
|
|
text = '<html omitted>';
|
|
}
|
|
}
|
|
const stringified = JSON.stringify({
|
|
status: res.status,
|
|
statusText: res.statusText,
|
|
headers,
|
|
body: json || ((text === null || text === void 0 ? void 0 : text.length) > 5120 ? '<body is too long>' : text) || null,
|
|
}, (_k, v) => {
|
|
if (v != null && typeof v === 'object' && !Array.isArray(v)) {
|
|
// sort object fields for stable stringify
|
|
const acc = {};
|
|
return Object.keys(v)
|
|
.sort()
|
|
.reverse() // body on bottom
|
|
.reduce((acc, k) => {
|
|
acc[k] = v[k];
|
|
return acc;
|
|
}, acc);
|
|
}
|
|
return v;
|
|
}, 2);
|
|
report += stringified + '\n';
|
|
report += '</code></pre>\n';
|
|
report += '</details>\n';
|
|
report += '</li>\n';
|
|
return report;
|
|
}
|
|
function truncate(str, len = 1024) {
|
|
if (str.length > len) {
|
|
return str.substring(0, len) + '...';
|
|
}
|
|
return str;
|
|
}
|